Comparing version 0.7.1 to 0.8.0
@@ -0,1 +1,9 @@ | ||
# Version 0.8.0 - 2018/06/27 | ||
- Add BLE GATT Characteristic reading functions | ||
- Add ability to derive Lovense hardware info from device queries (no more name/UUID chasing) | ||
- Namespace devtools CSS rules to fix issue with CSS conflicts in devtools | ||
- Change Signature of CreateSimple*Message functions (breaking change) | ||
- Add IsScanning boolean getter to Client | ||
# Version 0.7.1 - 2018/05/02 | ||
@@ -2,0 +10,0 @@ |
@@ -14,3 +14,3 @@ /// <reference types="node" /> | ||
private ParseIncomingMessage; | ||
private OnReaderLoad(aEvent); | ||
private OnReaderLoad; | ||
} |
@@ -15,2 +15,3 @@ /// <reference types="node" /> | ||
protected _logger: ButtplugLogger; | ||
protected _isScanning: boolean; | ||
protected _messageVersion: number; | ||
@@ -21,2 +22,3 @@ constructor(aClientName?: string); | ||
readonly Devices: Device[]; | ||
readonly IsScanning: boolean; | ||
ConnectWebsocket: (aAddress: string) => Promise<void>; | ||
@@ -23,0 +25,0 @@ ConnectLocal: () => Promise<void>; |
@@ -27,2 +27,3 @@ "use strict"; | ||
this._logger = Logging_1.ButtplugLogger.Logger; | ||
this._isScanning = false; | ||
// TODO This should be set on schema load | ||
@@ -54,2 +55,3 @@ this._messageVersion = 1; | ||
this._logger.Debug(`ButtplugClient: StartScanning called`); | ||
this._isScanning = true; | ||
return yield this.SendMsgExpectOk(new Messages.StartScanning()); | ||
@@ -59,2 +61,3 @@ }); | ||
this._logger.Debug(`ButtplugClient: StopScanning called`); | ||
this._isScanning = false; | ||
return yield this.SendMsgExpectOk(new Messages.StopScanning()); | ||
@@ -173,2 +176,5 @@ }); | ||
} | ||
get IsScanning() { | ||
return this._isScanning; | ||
} | ||
SendDeviceMessage(aDevice, aDeviceMsg) { | ||
@@ -216,2 +222,3 @@ return __awaiter(this, void 0, void 0, function* () { | ||
case "ScanningFinished": | ||
this._isScanning = false; | ||
this.emit("scanningfinished", x); | ||
@@ -218,0 +225,0 @@ break; |
@@ -15,3 +15,3 @@ /// <reference types="node" /> | ||
Debug = 5, | ||
Trace = 6, | ||
Trace = 6 | ||
} | ||
@@ -76,4 +76,4 @@ /** | ||
/** | ||
* Get the maximum log level to output to console. | ||
*/ | ||
* Get the maximum log level to output to console. | ||
*/ | ||
MaximumConsoleLogLevel: ButtplugLogLevel; | ||
@@ -84,4 +84,4 @@ /** | ||
/** | ||
* Get the global maximum log level | ||
*/ | ||
* Get the global maximum log level | ||
*/ | ||
MaximumEventLogLevel: ButtplugLogLevel; | ||
@@ -88,0 +88,0 @@ /** |
@@ -5,3 +5,3 @@ import "reflect-metadata"; | ||
constructor(Id: number); | ||
readonly abstract SchemaVersion: number; | ||
abstract readonly SchemaVersion: number; | ||
DowngradeMessage(): ButtplugMessage; | ||
@@ -56,3 +56,3 @@ /*** | ||
ERROR_MSG = 3, | ||
ERROR_DEVICE = 4, | ||
ERROR_DEVICE = 4 | ||
} | ||
@@ -59,0 +59,0 @@ export declare class Error extends ButtplugSystemMessage { |
@@ -47,2 +47,5 @@ "use strict"; | ||
} | ||
if (speed > 1.0 || speed < 0.0) { | ||
throw new Error("Speed must be 0.0 <= x <= 1.0!"); | ||
} | ||
const commands = []; | ||
@@ -52,3 +55,3 @@ for (let i = 0; i < device.MessageAttributes("VibrateCmd").FeatureCount; ++i) { | ||
} | ||
return new Messages.VibrateCmd(commands); | ||
return new Messages.VibrateCmd(commands, device.Index); | ||
} | ||
@@ -60,2 +63,5 @@ exports.CreateSimpleVibrateCmd = CreateSimpleVibrateCmd; | ||
} | ||
if (position > 1.0 || position < 0.0) { | ||
throw new Error("Position must be 0.0 <= x <= 1.0!"); | ||
} | ||
const commands = []; | ||
@@ -65,3 +71,3 @@ for (let i = 0; i < device.MessageAttributes("LinearCmd").FeatureCount; ++i) { | ||
} | ||
return new Messages.LinearCmd(commands); | ||
return new Messages.LinearCmd(commands, device.Index); | ||
} | ||
@@ -73,2 +79,5 @@ exports.CreateSimpleLinearCmd = CreateSimpleLinearCmd; | ||
} | ||
if (speed > 1.0 || speed < 0.0) { | ||
throw new Error("Speed must be 0.0 <= x <= 1.0!"); | ||
} | ||
const commands = []; | ||
@@ -78,5 +87,5 @@ for (let i = 0; i < device.MessageAttributes("RotateCmd").FeatureCount; ++i) { | ||
} | ||
return new Messages.RotateCmd(commands); | ||
return new Messages.RotateCmd(commands, device.Index); | ||
} | ||
exports.CreateSimpleRotateCmd = CreateSimpleRotateCmd; | ||
//# sourceMappingURL=MessageUtils.js.map |
@@ -8,3 +8,3 @@ export declare class LogPanel { | ||
constructor(); | ||
private addLogMessage(msg); | ||
private addLogMessage; | ||
} |
@@ -5,7 +5,9 @@ import { IBluetoothDeviceImpl } from "./IBluetoothDeviceImpl"; | ||
private _names; | ||
private _namePrefixes; | ||
private _services; | ||
private _characteristics; | ||
private _createFunc; | ||
constructor(_names: string[], _services: string[], _characteristics: object, _createFunc: (aDeviceImpl: IBluetoothDeviceImpl) => Promise<ButtplugBluetoothDevice>); | ||
constructor(_names: string[], _namePrefixes: string[], _services: string[], _characteristics: object, _createFunc: (aDeviceImpl: IBluetoothDeviceImpl) => Promise<ButtplugBluetoothDevice>); | ||
readonly Names: string[]; | ||
readonly NamePrefixes: string[]; | ||
readonly Services: string[]; | ||
@@ -12,0 +14,0 @@ readonly Characteristics: object; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
class BluetoothDeviceInfo { | ||
constructor(_names, _services, _characteristics, _createFunc) { | ||
constructor(_names, _namePrefixes, _services, _characteristics, _createFunc) { | ||
this._names = _names; | ||
this._namePrefixes = _namePrefixes; | ||
this._services = _services; | ||
@@ -13,2 +14,5 @@ this._characteristics = _characteristics; | ||
} | ||
get NamePrefixes() { | ||
return this._namePrefixes; | ||
} | ||
get Services() { | ||
@@ -15,0 +19,0 @@ return this._services; |
@@ -11,10 +11,3 @@ "use strict"; | ||
return [FleshlightLaunch_1.FleshlightLaunch.DeviceInfo, | ||
Lovense_1.LovenseRev1.DeviceInfo, | ||
Lovense_1.LovenseRev2.DeviceInfo, | ||
Lovense_1.LovenseRev3.DeviceInfo, | ||
Lovense_1.LovenseRev4.DeviceInfo, | ||
Lovense_1.LovenseRev5.DeviceInfo, | ||
Lovense_1.LovenseRev6.DeviceInfo, | ||
Lovense_1.LovenseRev7.DeviceInfo, | ||
Lovense_1.LovenseRev8.DeviceInfo, | ||
Lovense_1.Lovense.DeviceInfo, | ||
Maxpro_1.Maxpro.DeviceInfo, | ||
@@ -21,0 +14,0 @@ VorzeA10Cyclone_1.VorzeA10Cyclone.DeviceInfo, |
@@ -9,2 +9,3 @@ import { BluetoothDeviceInfo } from "../BluetoothDeviceInfo"; | ||
constructor(aDeviceImpl: IBluetoothDeviceImpl); | ||
Initialize: () => Promise<void>; | ||
readonly MessageSpecifications: object; | ||
@@ -11,0 +12,0 @@ private HandleStopDeviceCmd; |
@@ -18,2 +18,5 @@ "use strict"; | ||
this._lastPosition = 0; | ||
this.Initialize = () => __awaiter(this, void 0, void 0, function* () { | ||
yield this._deviceImpl.WriteValue("cmd", new Uint8Array([0x00])); | ||
}); | ||
this.HandleStopDeviceCmd = (aMsg) => __awaiter(this, void 0, void 0, function* () { | ||
@@ -52,4 +55,5 @@ return yield this.HandleFleshlightLaunchFW12Cmd(new Messages.FleshlightLaunchFW12Cmd(0, 0, aMsg.DeviceIndex, aMsg.Id)); | ||
// Send initializer byte | ||
yield aDeviceImpl.WriteValue("cmd", new Uint8Array([0x00])); | ||
return new FleshlightLaunch(aDeviceImpl); | ||
const dev = new FleshlightLaunch(aDeviceImpl); | ||
yield dev.Initialize(); | ||
return dev; | ||
}); | ||
@@ -65,3 +69,3 @@ } | ||
} | ||
FleshlightLaunch.DeviceInfo = new BluetoothDeviceInfo_1.BluetoothDeviceInfo(["Launch"], ["88f80580-0000-01e6-aace-0002a5d5c51b"], { cmd: "88f80583-0000-01e6-aace-0002a5d5c51b", | ||
FleshlightLaunch.DeviceInfo = new BluetoothDeviceInfo_1.BluetoothDeviceInfo(["Launch"], [], ["88f80580-0000-01e6-aace-0002a5d5c51b"], { cmd: "88f80583-0000-01e6-aace-0002a5d5c51b", | ||
// rx: "88f80582-0000-01e6-aace-0002a5d5c51b", | ||
@@ -68,0 +72,0 @@ tx: "88f80581-0000-01e6-aace-0002a5d5c51b" }, FleshlightLaunch.CreateInstance); |
@@ -5,33 +5,18 @@ import { BluetoothDeviceInfo } from "../BluetoothDeviceInfo"; | ||
export declare class Lovense extends ButtplugBluetoothDevice { | ||
static readonly DeviceInfo: BluetoothDeviceInfo; | ||
static CreateInstance(aDeviceImpl: IBluetoothDeviceImpl): Promise<ButtplugBluetoothDevice>; | ||
private static _deviceNames; | ||
private _initResolve; | ||
private _initPromise; | ||
private _isClockwise; | ||
private _specs; | ||
constructor(aDeviceImpl: IBluetoothDeviceImpl); | ||
Initialize: () => Promise<void>; | ||
readonly MessageSpecifications: object; | ||
private HandleVibrateCmd; | ||
private ParseDeviceType; | ||
private OnValueChanged; | ||
private HandleStopDeviceCmd; | ||
private HandleSingleMotorVibrateCmd; | ||
private HandleVibrateCmd; | ||
private HandleRotateCmd; | ||
} | ||
export declare class LovenseRev1 { | ||
static readonly DeviceInfo: BluetoothDeviceInfo; | ||
} | ||
export declare class LovenseRev2 { | ||
static readonly DeviceInfo: BluetoothDeviceInfo; | ||
} | ||
export declare class LovenseRev3 { | ||
static readonly DeviceInfo: BluetoothDeviceInfo; | ||
} | ||
export declare class LovenseRev4 { | ||
static readonly DeviceInfo: BluetoothDeviceInfo; | ||
} | ||
export declare class LovenseRev5 { | ||
static readonly DeviceInfo: BluetoothDeviceInfo; | ||
} | ||
export declare class LovenseRev6 { | ||
static readonly DeviceInfo: BluetoothDeviceInfo; | ||
} | ||
export declare class LovenseRev7 { | ||
static readonly DeviceInfo: BluetoothDeviceInfo; | ||
} | ||
export declare class LovenseRev8 { | ||
static readonly DeviceInfo: BluetoothDeviceInfo; | ||
} |
@@ -14,96 +14,137 @@ "use strict"; | ||
const Messages = require("../../../core/Messages"); | ||
const Messages_1 = require("../../../core/Messages"); | ||
class Lovense extends ButtplugBluetoothDevice_1.ButtplugBluetoothDevice { | ||
constructor(aDeviceImpl) { | ||
super(`Lovense ${aDeviceImpl.Name}`, aDeviceImpl); | ||
this.HandleVibrateCmd = (aMsg) => __awaiter(this, void 0, void 0, function* () { | ||
if (aMsg.Speeds.length !== 1) { | ||
return new Messages.Error(`Lovense devices require VibrateCmd to have 1 speed command, ` + | ||
`${aMsg.Speeds.length} sent.`, Messages.ErrorClass.ERROR_DEVICE, aMsg.Id); | ||
this._initPromise = new Promise((res, rej) => { this._initResolve = res; }); | ||
this._isClockwise = false; | ||
this._specs = { | ||
VibrateCmd: { FeatureCount: 1 }, | ||
SingleMotorVibrateCmd: {}, | ||
StopDeviceCmd: {}, | ||
}; | ||
this.Initialize = () => __awaiter(this, void 0, void 0, function* () { | ||
this._deviceImpl.addListener("characteristicvaluechanged", this.OnValueChanged); | ||
yield this._deviceImpl.Subscribe("rx"); | ||
yield this._deviceImpl.WriteString("tx", "DeviceType;"); | ||
yield this._initPromise; | ||
}); | ||
this.OnValueChanged = (aCharacteristic, aValue) => __awaiter(this, void 0, void 0, function* () { | ||
// If we haven't initialized yet, consider this to be the first read, for the device info. | ||
if (this._initResolve !== undefined) { | ||
this.ParseDeviceType(aValue.toString()); | ||
const res = this._initResolve; | ||
this._initResolve = undefined; | ||
res(); | ||
return; | ||
} | ||
return yield this.HandleSingleMotorVibrateCmd(new Messages.SingleMotorVibrateCmd(aMsg.Speeds[0].Speed, aMsg.DeviceIndex, aMsg.Id)); | ||
// TODO Fill in battery/accelerometer/etc reads | ||
}); | ||
this.HandleStopDeviceCmd = (aMsg) => __awaiter(this, void 0, void 0, function* () { | ||
return yield this.HandleSingleMotorVibrateCmd(new Messages.SingleMotorVibrateCmd(0, aMsg.DeviceIndex, aMsg.Id)); | ||
yield this.HandleSingleMotorVibrateCmd(new Messages.SingleMotorVibrateCmd(0, aMsg.DeviceIndex, aMsg.Id)); | ||
if (this._specs.hasOwnProperty("RotateCmd")) { | ||
this.HandleRotateCmd(new Messages.RotateCmd([new Messages_1.RotateSubcommand(0, 0, this._isClockwise)], 0, aMsg.Id)); | ||
} | ||
return new Messages.Ok(aMsg.Id); | ||
}); | ||
this.HandleSingleMotorVibrateCmd = (aMsg) => __awaiter(this, void 0, void 0, function* () { | ||
const speed = Math.floor(20 * aMsg.Speed); | ||
yield this._deviceImpl.WriteValue("tx", Buffer.from("Vibrate:" + speed + ";")); | ||
const speeds = []; | ||
for (let i = 0; i < this._specs.VibrateCmd.FeatureCount; i++) { | ||
speeds.push(new Messages.SpeedSubcommand(i, aMsg.Speed)); | ||
} | ||
return yield this.HandleVibrateCmd(new Messages.VibrateCmd(speeds, aMsg.DeviceIndex, aMsg.Id)); | ||
}); | ||
this.HandleVibrateCmd = (aMsg) => __awaiter(this, void 0, void 0, function* () { | ||
if (aMsg.Speeds.length > this._specs.VibrateCmd.FeatureCount) { | ||
return new Messages.Error(`Lovense devices require VibrateCmd to have at most ` + | ||
`${this._specs.VibrateCmd.FeatureCount} speed commands, ` + | ||
`${aMsg.Speeds.length} sent.`, Messages.ErrorClass.ERROR_DEVICE, aMsg.Id); | ||
} | ||
for (const cmd of aMsg.Speeds) { | ||
const index = this._specs.VibrateCmd.FeatureCount > 1 ? (cmd.Index + 1).toString(10) : ""; | ||
const speed = Math.floor(20 * cmd.Speed); | ||
yield this._deviceImpl.WriteString("tx", `Vibrate${index}:${speed};`); | ||
} | ||
return new Messages.Ok(aMsg.Id); | ||
}); | ||
this.MsgFuncs.set(Messages.StopDeviceCmd.name, this.HandleStopDeviceCmd); | ||
this.MsgFuncs.set(Messages.VibrateCmd.name, this.HandleVibrateCmd); | ||
this.MsgFuncs.set(Messages.SingleMotorVibrateCmd.name, this.HandleSingleMotorVibrateCmd); | ||
this.HandleRotateCmd = (aMsg) => __awaiter(this, void 0, void 0, function* () { | ||
if (aMsg.Rotations.length !== 1) { | ||
return new Messages.Error(`Lovense devices require RotateCmd to have 1 rotate command, ` + | ||
`${aMsg.Rotations.length} sent.`, Messages.ErrorClass.ERROR_DEVICE, aMsg.Id); | ||
} | ||
const rotateCmd = aMsg.Rotations[0]; | ||
if (rotateCmd.Index !== 0) { | ||
return new Messages.Error("Rotation command sent for invalid index."); | ||
} | ||
if (rotateCmd.Clockwise !== this._isClockwise) { | ||
yield this._deviceImpl.WriteString("tx", "RotateChange;"); | ||
} | ||
const speed = Math.floor(20 * rotateCmd.Speed); | ||
yield this._deviceImpl.WriteString("tx", `Rotate:${speed};`); | ||
return new Messages.Ok(aMsg.Id); | ||
}); | ||
} | ||
static CreateInstance(aDeviceImpl) { | ||
return Promise.resolve(new Lovense(aDeviceImpl)); | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const dev = new Lovense(aDeviceImpl); | ||
yield dev.Initialize(); | ||
return dev; | ||
}); | ||
} | ||
get MessageSpecifications() { | ||
return { | ||
VibrateCmd: { FeatureCount: 1 }, | ||
SingleMotorVibrateCmd: {}, | ||
StopDeviceCmd: {}, | ||
}; | ||
return this._specs; | ||
} | ||
ParseDeviceType(aDeviceType) { | ||
// Typescript gets angry if we try to destructure this into consts/lets | ||
// differently or all lets (since deviceVersion never changes and | ||
// deviceAddress isn't used), and I don't wanna deal with assigning to const | ||
// then let, so this works well enough. | ||
let deviceLetter; | ||
let deviceVersion; | ||
let deviceAddress; | ||
[deviceLetter, deviceVersion, deviceAddress] = aDeviceType.split(":"); | ||
if (!Lovense._deviceNames.hasOwnProperty(deviceLetter)) { | ||
deviceLetter = "0"; | ||
} | ||
this._name = `Lovense ${Lovense._deviceNames[deviceLetter]} v${deviceVersion}`; | ||
this.MsgFuncs.set(Messages.StopDeviceCmd.name, this.HandleStopDeviceCmd); | ||
this.MsgFuncs.set(Messages.VibrateCmd.name, this.HandleVibrateCmd); | ||
this.MsgFuncs.set(Messages.SingleMotorVibrateCmd.name, this.HandleSingleMotorVibrateCmd); | ||
if (deviceLetter === "P") { | ||
// Edge has 2 motors | ||
this._specs.VibrateCmd = { FeatureCount: 2 }; | ||
} | ||
else if (deviceLetter === "A" || deviceLetter === "C") { | ||
// Nora has rotation | ||
this._specs.RotateCmd = { FeatureCount: 1 }; | ||
this.MsgFuncs.set(Messages.RotateCmd.name, this.HandleRotateCmd); | ||
} | ||
} | ||
} | ||
Lovense._deviceNames = { "LVS-A011": "Nora", | ||
"LVS-C011": "Nora", | ||
"LVS-B011": "Max", | ||
"LVS-L009": "Ambi", | ||
"LVS-S001": "Lush", | ||
"LVS-S35": "Lush", | ||
"LVS-Z36": "Hush", | ||
"LVS-Z001": "Hush", | ||
"LVS_Z001": "Hush", | ||
"LVS-Hush41": "Hush", | ||
"LVS-Domi37": "Domi", | ||
"LVS-Domi38": "Domi", | ||
"LVS-Domi39": "Domi", | ||
"LVS-Domi40": "Domi", | ||
"LVS-P36": "Edge", | ||
"LVS-Edge37": "Edge", | ||
"LVS-Edge38": "Edge", | ||
"LVS-Edge39": "Edge", | ||
"LVS-Edge40": "Edge", | ||
"LVS-Lush41": "Lush" }; | ||
Lovense.DeviceInfo = (() => { | ||
// Start with the two non-standard UUIDs, which come from the original | ||
// versions of the Max/Nora toys. | ||
const uuids = ["0000fff0-0000-1000-8000-00805f9b34fb", | ||
"6e400001-b5a3-f393-e0a9-e50e24dcca9e"]; | ||
// Future-proofing for possible Lovense UUIDs, based on the pattern of the | ||
// current firmware. | ||
for (let i = 0; i < 16; ++i) { | ||
uuids.push(`5${i.toString(16)}300001-0023-4bd4-bbd5-a6920e4c5653`); | ||
uuids.push(`5${i.toString(16)}300001-0024-4bd4-bbd5-a6920e4c5653`); | ||
} | ||
return new BluetoothDeviceInfo_1.BluetoothDeviceInfo([], ["LVS"], uuids, {}, Lovense.CreateInstance); | ||
})(); | ||
Lovense._deviceNames = { | ||
A: "Nora", | ||
B: "Max", | ||
C: "Nora", | ||
L: "Ambi", | ||
O: "Osci", | ||
P: "Edge", | ||
S: "Lush", | ||
W: "Domi", | ||
Z: "Hush", | ||
0: "Unknown", | ||
}; | ||
exports.Lovense = Lovense; | ||
class LovenseRev1 { | ||
} | ||
LovenseRev1.DeviceInfo = new BluetoothDeviceInfo_1.BluetoothDeviceInfo(["LVS-A011", "LVS-C011", "LVS-B011", "LVS-L009"], ["0000fff0-0000-1000-8000-00805f9b34fb"], { tx: "0000fff2-0000-1000-8000-00805f9b34fb", | ||
}, Lovense.CreateInstance); | ||
exports.LovenseRev1 = LovenseRev1; | ||
class LovenseRev2 { | ||
} | ||
LovenseRev2.DeviceInfo = new BluetoothDeviceInfo_1.BluetoothDeviceInfo(["LVS-S001", "LVS-Z001", "LVS_Z001"], ["6e400001-b5a3-f393-e0a9-e50e24dcca9e"], { tx: "6e400002-b5a3-f393-e0a9-e50e24dcca9e", | ||
}, Lovense.CreateInstance); | ||
exports.LovenseRev2 = LovenseRev2; | ||
class LovenseRev3 { | ||
} | ||
LovenseRev3.DeviceInfo = new BluetoothDeviceInfo_1.BluetoothDeviceInfo(["LVS-P36"], ["50300001-0024-4bd4-bbd5-a6920e4c5653"], { tx: "50300002-0024-4bd4-bbd5-a6920e4c5653", | ||
}, Lovense.CreateInstance); | ||
exports.LovenseRev3 = LovenseRev3; | ||
class LovenseRev4 { | ||
} | ||
LovenseRev4.DeviceInfo = new BluetoothDeviceInfo_1.BluetoothDeviceInfo(["LVS-Domi37", "LVS-Domi38", "LVS-Domi39", "LVS-Domi40"], ["57300001-0023-4bd4-bbd5-a6920e4c5653"], { tx: "57300002-0023-4bd4-bbd5-a6920e4c5653", | ||
}, Lovense.CreateInstance); | ||
exports.LovenseRev4 = LovenseRev4; | ||
class LovenseRev5 { | ||
} | ||
LovenseRev5.DeviceInfo = new BluetoothDeviceInfo_1.BluetoothDeviceInfo(["LVS-Z36"], ["5a300001-0024-4bd4-bbd5-a6920e4c5653"], { tx: "5a300002-0024-4bd4-bbd5-a6920e4c5653", | ||
}, Lovense.CreateInstance); | ||
exports.LovenseRev5 = LovenseRev5; | ||
class LovenseRev6 { | ||
} | ||
LovenseRev6.DeviceInfo = new BluetoothDeviceInfo_1.BluetoothDeviceInfo(["LVS-Edge37", "LVS-Edge38", "LVS-Edge39", "LVS-Edge40"], ["50300001-0023-4bd4-bbd5-a6920e4c5653"], { tx: "50300002-0023-4bd4-bbd5-a6920e4c5653", | ||
}, Lovense.CreateInstance); | ||
exports.LovenseRev6 = LovenseRev6; | ||
class LovenseRev7 { | ||
} | ||
LovenseRev7.DeviceInfo = new BluetoothDeviceInfo_1.BluetoothDeviceInfo(["LVS-S35"], ["53300001-0023-4bd4-bbd5-a6920e4c5653"], { tx: "53300002-0023-4bd4-bbd5-a6920e4c5653", | ||
}, Lovense.CreateInstance); | ||
exports.LovenseRev7 = LovenseRev7; | ||
class LovenseRev8 { | ||
} | ||
LovenseRev8.DeviceInfo = new BluetoothDeviceInfo_1.BluetoothDeviceInfo(["LVS-Hush41"], ["5a300001-0023-4bd4-bbd5-a6920e4c5653"], { tx: "5a300002-0023-4bd4-bbd5-a6920e4c5653", | ||
}, Lovense.CreateInstance); | ||
exports.LovenseRev8 = LovenseRev8; | ||
//# sourceMappingURL=Lovense.js.map |
@@ -53,5 +53,4 @@ "use strict"; | ||
} | ||
Maxpro.DeviceInfo = new BluetoothDeviceInfo_1.BluetoothDeviceInfo(["M2"], ["6e400001-b5a3-f393-e0a9-e50e24dcca9e"], { tx: "6e400002-b5a3-f393-e0a9-e50e24dcca9e", | ||
rx: "6e400003-b5a3-f393-e0a9-e50e24dcca9e" }, Maxpro.CreateInstance); | ||
Maxpro.DeviceInfo = new BluetoothDeviceInfo_1.BluetoothDeviceInfo(["M2"], [], ["6e400001-b5a3-f393-e0a9-e50e24dcca9e"], {}, Maxpro.CreateInstance); | ||
exports.Maxpro = Maxpro; | ||
//# sourceMappingURL=Maxpro.js.map |
@@ -38,4 +38,2 @@ "use strict"; | ||
return __awaiter(this, void 0, void 0, function* () { | ||
// Send initializer byte | ||
aDeviceImpl.WriteValue("cmd", new Uint8Array([0x00])); | ||
return new VorzeA10Cyclone(aDeviceImpl); | ||
@@ -52,4 +50,4 @@ }); | ||
} | ||
VorzeA10Cyclone.DeviceInfo = new BluetoothDeviceInfo_1.BluetoothDeviceInfo(["CycSA"], ["40ee1111-63ec-4b7f-8ce7-712efd55b90e"], { tx: "40ee2222-63ec-4b7f-8ce7-712efd55b90e" }, VorzeA10Cyclone.CreateInstance); | ||
VorzeA10Cyclone.DeviceInfo = new BluetoothDeviceInfo_1.BluetoothDeviceInfo(["CycSA"], [], ["40ee1111-63ec-4b7f-8ce7-712efd55b90e"], {}, VorzeA10Cyclone.CreateInstance); | ||
exports.VorzeA10Cyclone = VorzeA10Cyclone; | ||
//# sourceMappingURL=VorzeA10Cyclone.js.map |
@@ -51,5 +51,4 @@ "use strict"; | ||
WeVibe.DeviceInfo = new BluetoothDeviceInfo_1.BluetoothDeviceInfo(["4 Plus", "Ditto", "Nova", "Wish", | ||
"Pivot", "Verge", "Cougar", "Sync"], ["f000bb03-0451-4000-b000-000000000000"], { tx: "f000c000-0451-4000-b000-000000000000", | ||
rx: "f000b000-0451-4000-b000-000000000000" }, WeVibe.CreateInstance); | ||
"Pivot", "Verge", "Cougar", "Sync"], [], ["f000bb03-0451-4000-b000-000000000000"], {}, WeVibe.CreateInstance); | ||
exports.WeVibe = WeVibe; | ||
//# sourceMappingURL=WeVibe.js.map |
@@ -1,7 +0,12 @@ | ||
export interface IBluetoothDeviceImpl { | ||
/// <reference types="node" /> | ||
import { EventEmitter } from "events"; | ||
export interface IBluetoothDeviceImpl extends EventEmitter { | ||
Name: string; | ||
Id: string; | ||
WriteValue: (aCharacteristic: string, aValue: Uint8Array) => Promise<void>; | ||
WriteString: (aCharacteristic: string, aValue: string) => Promise<void>; | ||
ReadValue: (aCharacteristic: string) => Promise<BufferSource>; | ||
ReadString: (aCharacteristic: string) => Promise<string>; | ||
Subscribe: (aCharacteristic: string) => Promise<void>; | ||
Disconnect: () => Promise<void>; | ||
} |
@@ -0,3 +1,3 @@ | ||
/// <reference types="web-bluetooth" /> | ||
/// <reference types="node" /> | ||
/// <reference types="web-bluetooth" /> | ||
import { IBluetoothDeviceImpl } from "./IBluetoothDeviceImpl"; | ||
@@ -11,5 +11,7 @@ import { BluetoothDeviceInfo } from "./BluetoothDeviceInfo"; | ||
static CreateDevice(aDeviceInfo: BluetoothDeviceInfo, aDevice: BluetoothDevice): Promise<ButtplugBluetoothDevice>; | ||
private _notificationHandlers; | ||
private _logger; | ||
private _server; | ||
private _service; | ||
private _decoder; | ||
private _characteristics; | ||
@@ -22,4 +24,9 @@ constructor(_deviceInfo: BluetoothDeviceInfo, _device: BluetoothDevice); | ||
OnDisconnect: () => void; | ||
WriteString: (aCharacteristic: string, aValue: string) => Promise<void>; | ||
WriteValue: (aCharacteristic: string, aValue: Uint8Array) => Promise<void>; | ||
ReadString: (aCharacteristic: string) => Promise<string>; | ||
ReadValue: (aCharacteristic: string) => Promise<BufferSource>; | ||
Subscribe: (aCharacteristic: string) => Promise<void>; | ||
Unsubscribe: (aCharacteristic: string) => Promise<void>; | ||
protected CharacteristicValueChanged: (aEvent: Event, aCharacteristic: string) => void; | ||
} |
@@ -13,2 +13,3 @@ "use strict"; | ||
const events_1 = require("events"); | ||
const string_decoder_1 = require("string_decoder"); | ||
class WebBluetoothDevice extends events_1.EventEmitter { | ||
@@ -19,3 +20,5 @@ constructor(_deviceInfo, _device) { | ||
this._device = _device; | ||
this._notificationHandlers = new Map(); | ||
this._logger = Logging_1.ButtplugLogger.Logger; | ||
this._decoder = new string_decoder_1.StringDecoder("utf-8"); | ||
this._characteristics = new Map(); | ||
@@ -26,8 +29,44 @@ this.Connect = () => __awaiter(this, void 0, void 0, function* () { | ||
this._server = yield this._device.gatt.connect(); | ||
this._service = yield this._server.getPrimaryService(this._deviceInfo.Services[0]); | ||
// We passed along a list of services we expect to work with all hardware as | ||
// part of the connection filters, so only those services will be found when | ||
// running getPrimaryServices | ||
const services = yield this._server.getPrimaryServices(); | ||
if (services.length === 0) { | ||
this._logger.Error(`Cannot find gatt service to connect to on device ${this._device.name}`); | ||
throw new Error(`Cannot find gatt service to connect to on device ${this._device.name}`); | ||
} | ||
// For now, we assume we're only using one service on each device. This will | ||
// most likely change in the future. | ||
this._service = services[0]; | ||
// If the device info contains characteristic address and identity | ||
// information, use that to try and establish characteristic objects. | ||
for (const name of Object.getOwnPropertyNames(this._deviceInfo.Characteristics)) { | ||
this._characteristics.set(name, yield this._service.getCharacteristic(this._deviceInfo.Characteristics[name])); | ||
} | ||
// If no characteristics are present in the DeviceInfo block, we assume that | ||
// we're connecting to a simple rx/tx service, and can query to figure out | ||
// characteristics. Assume that the characteristics have tx/rx references. | ||
if (this._characteristics.entries.length === 0) { | ||
const characteristics = yield this._service.getCharacteristics(); | ||
for (const char of characteristics) { | ||
if (char.properties.write || | ||
char.properties.writeWithoutResponse || | ||
char.properties.reliableWrite) { | ||
this._characteristics.set("tx", char); | ||
} | ||
else if (char.properties.read || | ||
char.properties.broadcast || | ||
char.properties.notify || | ||
char.properties.indicate) { | ||
this._characteristics.set("rx", char); | ||
} | ||
} | ||
} | ||
// If at this point we still don't have any characteristics, something is | ||
// wrong, error out. | ||
}); | ||
this.Disconnect = () => __awaiter(this, void 0, void 0, function* () { | ||
for (const chr of this._notificationHandlers.keys()) { | ||
this.Unsubscribe(chr); | ||
} | ||
this._server.disconnect(); | ||
@@ -40,5 +79,8 @@ }); | ||
}; | ||
this.WriteString = (aCharacteristic, aValue) => __awaiter(this, void 0, void 0, function* () { | ||
return yield this.WriteValue(aCharacteristic, Buffer.from(aValue)); | ||
}); | ||
this.WriteValue = (aCharacteristic, aValue) => __awaiter(this, void 0, void 0, function* () { | ||
if (!this._characteristics.has(aCharacteristic)) { | ||
return; | ||
throw new Error("Tried to access wrong characteristic!"); | ||
} | ||
@@ -49,2 +91,6 @@ const chr = this._characteristics.get(aCharacteristic); | ||
}); | ||
this.ReadString = (aCharacteristic) => __awaiter(this, void 0, void 0, function* () { | ||
const value = yield this.ReadValue(aCharacteristic); | ||
return this._decoder.end(Buffer.from(value)); | ||
}); | ||
this.ReadValue = (aCharacteristic) => __awaiter(this, void 0, void 0, function* () { | ||
@@ -58,2 +104,35 @@ if (!this._characteristics.has(aCharacteristic)) { | ||
}); | ||
this.Subscribe = (aCharacteristic) => __awaiter(this, void 0, void 0, function* () { | ||
if (!this._characteristics.has(aCharacteristic)) { | ||
throw new Error("Tried to access wrong characteristic!"); | ||
} | ||
if (this._notificationHandlers.has(aCharacteristic)) { | ||
throw new Error("Already listening on this characteristic!"); | ||
} | ||
const chr = this._characteristics.get(aCharacteristic); | ||
this._logger.Trace(`WebBluetoothDevice: ${this.constructor.name} subscribing to updates from ${chr.uuid}`); | ||
yield chr.startNotifications(); | ||
this._notificationHandlers.set(aCharacteristic, (event) => { | ||
this.CharacteristicValueChanged(event, aCharacteristic); | ||
}); | ||
chr.addEventListener("characteristicvaluechanged", this._notificationHandlers.get(aCharacteristic)); | ||
}); | ||
this.Unsubscribe = (aCharacteristic) => __awaiter(this, void 0, void 0, function* () { | ||
if (!this._characteristics.has(aCharacteristic)) { | ||
throw new Error("Tried to access wrong characteristic!"); | ||
} | ||
if (!this._notificationHandlers.has(aCharacteristic)) { | ||
throw new Error("Not listening on this characteristic!"); | ||
} | ||
const chr = this._characteristics.get(aCharacteristic); | ||
this._logger.Trace(`WebBluetoothDevice: ${this.constructor.name} unsubscribing to updates from ${chr.uuid}`); | ||
chr.removeEventListener("characteristicvaluechanged", this._notificationHandlers.get(aCharacteristic)); | ||
this._notificationHandlers.delete(aCharacteristic); | ||
yield chr.stopNotifications(); | ||
}); | ||
this.CharacteristicValueChanged = (aEvent, aCharacteristic) => { | ||
// For some reason this EventTarget doesn't have a value prop? | ||
const eventValue = aEvent.target.value; | ||
this.emit("characteristicvaluechanged", aCharacteristic, Buffer.from(eventValue.buffer)); | ||
}; | ||
} | ||
@@ -60,0 +139,0 @@ static CreateDevice(aDeviceInfo, aDevice) { |
@@ -11,2 +11,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const Logging_1 = require("../../core/Logging"); | ||
const BluetoothDevices_1 = require("./BluetoothDevices"); | ||
@@ -31,2 +32,8 @@ const events_1 = require("events"); | ||
} | ||
for (const namePrefix of di.NamePrefixes) { | ||
if (aDevice.name.indexOf(namePrefix) !== -1) { | ||
deviceInfo = di; | ||
break; | ||
} | ||
} | ||
} | ||
@@ -54,4 +61,8 @@ if (deviceInfo === null) { | ||
} | ||
for (const deviceNamePrefix of deviceInfo.NamePrefixes) { | ||
filters.filters.push({ namePrefix: deviceNamePrefix }); | ||
} | ||
filters.optionalServices = [...filters.optionalServices, ...deviceInfo.Services]; | ||
} | ||
Logging_1.ButtplugLogger.Logger.Trace("Bluetooth filter set: " + filters); | ||
// At some point, we should use navigator.bluetooth.getAvailability() to | ||
@@ -58,0 +69,0 @@ // check whether we have a radio to use. However, no browser currently |
@@ -10,3 +10,3 @@ /// <reference types="node" /> | ||
constructor(_name: string, _id: string); | ||
readonly abstract MessageSpecifications: object; | ||
abstract readonly MessageSpecifications: object; | ||
abstract Disconnect(): any; | ||
@@ -13,0 +13,0 @@ readonly Name: string; |
@@ -40,8 +40,12 @@ "use strict"; | ||
it("should convert lovense commands properly", () => __awaiter(this, void 0, void 0, function* () { | ||
yield SetupDevice(Lovense_1.LovenseRev5.DeviceInfo); | ||
yield SetupDevice(Lovense_1.Lovense.DeviceInfo); | ||
utils_1.SetupLovenseTestDevice(mockBT); | ||
yield bp.StartScanning(); | ||
yield bp.StopScanning(); | ||
expect(bp.Devices[0].Name).toEqual("Lovense Domi v01"); | ||
jest.spyOn(mockBT.txChar, "writeValue"); | ||
yield expect(bp.SendDeviceMessage(bp.Devices[0], new index_1.VibrateCmd([new index_1.SpeedSubcommand(0, 1), | ||
new index_1.SpeedSubcommand(0, 2)]))).rejects.toThrow(); | ||
new index_1.SpeedSubcommand(1, 1)]))) | ||
.rejects | ||
.toHaveProperty("ErrorCode", index_1.ErrorClass.ERROR_DEVICE); | ||
yield bp.SendDeviceMessage(bp.Devices[0], new index_1.VibrateCmd([new index_1.SpeedSubcommand(0, 1)])); | ||
@@ -54,2 +58,44 @@ expect(mockBT.txChar.writeValue).toBeCalledWith(Buffer.from("Vibrate:20;")); | ||
})); | ||
it("should convert lovense edge vibrate commands properly", () => __awaiter(this, void 0, void 0, function* () { | ||
yield SetupDevice(Lovense_1.Lovense.DeviceInfo); | ||
utils_1.SetupLovenseTestDevice(mockBT, "P"); | ||
yield bp.StartScanning(); | ||
yield bp.StopScanning(); | ||
expect(bp.Devices[0].Name).toEqual("Lovense Edge v01"); | ||
jest.spyOn(mockBT.txChar, "writeValue"); | ||
yield expect(bp.SendDeviceMessage(bp.Devices[0], new index_1.VibrateCmd([new index_1.SpeedSubcommand(0, 1), | ||
new index_1.SpeedSubcommand(1, 1), | ||
new index_1.SpeedSubcommand(2, 1)]))) | ||
.rejects | ||
.toHaveProperty("ErrorCode", index_1.ErrorClass.ERROR_DEVICE); | ||
yield bp.SendDeviceMessage(bp.Devices[0], new index_1.VibrateCmd([new index_1.SpeedSubcommand(0, 1), new index_1.SpeedSubcommand(1, .5)])); | ||
expect(mockBT.txChar.writeValue).toBeCalledWith(Buffer.from("Vibrate1:20;")); | ||
expect(mockBT.txChar.writeValue).toBeCalledWith(Buffer.from("Vibrate2:10;")); | ||
yield bp.SendDeviceMessage(bp.Devices[0], new index_1.SingleMotorVibrateCmd(.5)); | ||
expect(mockBT.txChar.writeValue).toBeCalledWith(Buffer.from("Vibrate1:10;")); | ||
expect(mockBT.txChar.writeValue).toBeCalledWith(Buffer.from("Vibrate2:10;")); | ||
yield bp.StopAllDevices(); | ||
expect(mockBT.txChar.writeValue).toBeCalledWith(Buffer.from("Vibrate1:0;")); | ||
expect(mockBT.txChar.writeValue).toBeCalledWith(Buffer.from("Vibrate2:0;")); | ||
})); | ||
it("should convert lovense nora rotate commands properly", () => __awaiter(this, void 0, void 0, function* () { | ||
yield SetupDevice(Lovense_1.Lovense.DeviceInfo); | ||
utils_1.SetupLovenseTestDevice(mockBT, "A"); | ||
yield bp.StartScanning(); | ||
yield bp.StopScanning(); | ||
expect(bp.Devices[0].Name).toEqual("Lovense Nora v01"); | ||
jest.spyOn(mockBT.txChar, "writeValue"); | ||
yield expect(bp.SendDeviceMessage(bp.Devices[0], new index_1.RotateCmd([new index_1.RotateSubcommand(0, 1, true), | ||
new index_1.RotateSubcommand(1, 1, true)]))) | ||
.rejects | ||
.toHaveProperty("ErrorCode", index_1.ErrorClass.ERROR_DEVICE); | ||
yield bp.SendDeviceMessage(bp.Devices[0], new index_1.RotateCmd([new index_1.RotateSubcommand(0, 1, false)])); | ||
expect(mockBT.txChar.writeValue).toBeCalledWith(Buffer.from("Rotate:20;")); | ||
yield bp.SendDeviceMessage(bp.Devices[0], new index_1.RotateCmd([new index_1.RotateSubcommand(0, 0.5, true)])); | ||
expect(mockBT.txChar.writeValue).toBeCalledWith(Buffer.from("RotateChange;")); | ||
expect(mockBT.txChar.writeValue).toBeCalledWith(Buffer.from("Rotate:10;")); | ||
yield bp.StopAllDevices(); | ||
expect(mockBT.txChar.writeValue).toBeCalledWith(Buffer.from("Vibrate:0;")); | ||
expect(mockBT.txChar.writeValue).toBeCalledWith(Buffer.from("Rotate:0;")); | ||
})); | ||
it("should convert wevibe commands properly", () => __awaiter(this, void 0, void 0, function* () { | ||
@@ -61,3 +107,5 @@ yield SetupDevice(WeVibe_1.WeVibe.DeviceInfo); | ||
yield expect(bp.SendDeviceMessage(bp.Devices[0], new index_1.VibrateCmd([new index_1.SpeedSubcommand(0, 1), | ||
new index_1.SpeedSubcommand(1, 1)]))).rejects.toThrow(); | ||
new index_1.SpeedSubcommand(1, 1)]))) | ||
.rejects | ||
.toHaveProperty("ErrorCode", index_1.ErrorClass.ERROR_DEVICE); | ||
yield bp.SendDeviceMessage(bp.Devices[0], new index_1.VibrateCmd([new index_1.SpeedSubcommand(0, 1)])); | ||
@@ -76,3 +124,5 @@ expect(mockBT.txChar.writeValue).toBeCalledWith(new Uint8Array([0x0f, 0x03, 0x00, 0xff, 0x00, 0x03, 0x00, 0x00])); | ||
yield expect(bp.SendDeviceMessage(bp.Devices[0], new index_1.LinearCmd([new index_1.VectorSubcommand(0, 1, 1), | ||
new index_1.VectorSubcommand(1, 1, 1)]))).rejects.toThrow(); | ||
new index_1.VectorSubcommand(1, 1, 1)]))) | ||
.rejects | ||
.toHaveProperty("ErrorCode", index_1.ErrorClass.ERROR_DEVICE); | ||
yield bp.SendDeviceMessage(bp.Devices[0], new index_1.FleshlightLaunchFW12Cmd(99, 99)); | ||
@@ -93,3 +143,5 @@ expect(mockBT.txChar.writeValue).toBeCalledWith(new Uint8Array([99, 99])); | ||
yield expect(bp.SendDeviceMessage(bp.Devices[0], new index_1.RotateCmd([new index_1.RotateSubcommand(0, 1, true), | ||
new index_1.RotateSubcommand(1, 1, false)]))).rejects.toThrow(); | ||
new index_1.RotateSubcommand(1, 1, false)]))) | ||
.rejects | ||
.toHaveProperty("ErrorCode", index_1.ErrorClass.ERROR_DEVICE); | ||
yield bp.SendDeviceMessage(bp.Devices[0], new index_1.RotateCmd([new index_1.RotateSubcommand(0, 1, true)])); | ||
@@ -96,0 +148,0 @@ expect(mockBT.txChar.writeValue).toBeCalledWith(new Uint8Array([0x01, 0x01, (100 | (0x80)) & 0xff])); |
@@ -107,3 +107,5 @@ "use strict"; | ||
} | ||
yield expect(bp.SendDeviceMessage(bp.Devices[0], new Messages.KiirooCmd("2"))).rejects.toThrow(); | ||
yield expect(bp.SendDeviceMessage(bp.Devices[0], new Messages.KiirooCmd("2"))) | ||
.rejects | ||
.toHaveProperty("ErrorCode", Messages.ErrorClass.ERROR_DEVICE); | ||
res(); | ||
@@ -117,3 +119,5 @@ })); | ||
bp.on("scanningfinished", (x) => __awaiter(this, void 0, void 0, function* () { | ||
yield (expect(bp.SendDeviceMessage(bp.Devices[0], new Messages.SingleMotorVibrateCmd(50))).rejects.toThrow()); | ||
yield expect(bp.SendDeviceMessage(bp.Devices[0], new Messages.SingleMotorVibrateCmd(50))) | ||
.rejects | ||
.toHaveProperty("ErrorCode", Messages.ErrorClass.ERROR_DEVICE); | ||
res(); | ||
@@ -145,3 +149,5 @@ })); | ||
yield bplocal.ConnectLocal(); | ||
yield expect(bplocal.StartScanning()).rejects.toThrow(); | ||
yield expect(bplocal.StartScanning()) | ||
.rejects | ||
.toHaveProperty("ErrorCode", Messages.ErrorClass.ERROR_DEVICE); | ||
bplocal.Disconnect(); | ||
@@ -148,0 +154,0 @@ })); |
"use strict"; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
@@ -14,3 +6,2 @@ const Messages = require("../src/core/Messages"); | ||
const utils_1 = require("./utils"); | ||
const Messages_1 = require("../src/core/Messages"); | ||
utils_1.SetupTestSuite(); | ||
@@ -71,24 +62,3 @@ describe("Message", () => { | ||
}); | ||
it("CreateSimple*Cmd tests", () => __awaiter(this, void 0, void 0, function* () { | ||
let res; | ||
let rej; | ||
const p = new Promise((resolve, reject) => { res = resolve; rej = reject; }); | ||
const connector = yield utils_1.SetupTestServer(); | ||
connector.Client.on("scanningfinished", () => { | ||
expect(MessageUtils_1.CreateSimpleVibrateCmd(connector.Client.Devices[0], 0.5)) | ||
.toEqual(new Messages.VibrateCmd([new Messages_1.SpeedSubcommand(0, 0.5), | ||
new Messages_1.SpeedSubcommand(1, 0.5)])); | ||
expect(() => MessageUtils_1.CreateSimpleVibrateCmd(connector.Client.Devices[1], 0.5)).toThrow(); | ||
expect(MessageUtils_1.CreateSimpleLinearCmd(connector.Client.Devices[1], 0.5, 100)) | ||
.toEqual(new Messages.LinearCmd([new Messages_1.VectorSubcommand(0, 0.5, 100)])); | ||
expect(() => MessageUtils_1.CreateSimpleLinearCmd(connector.Client.Devices[0], 0.5, 100)).toThrow(); | ||
expect(MessageUtils_1.CreateSimpleRotateCmd(connector.Client.Devices[2], 0.5, true)) | ||
.toEqual(new Messages.RotateCmd([new Messages_1.RotateSubcommand(0, 0.5, true)])); | ||
expect(() => MessageUtils_1.CreateSimpleRotateCmd(connector.Client.Devices[0], 0.5, true)).toThrow(); | ||
res(); | ||
}); | ||
yield connector.Client.StartScanning(); | ||
return p; | ||
})); | ||
}); | ||
//# sourceMappingURL=test-messages.js.map |
@@ -26,4 +26,5 @@ "use strict"; | ||
p = new Promise((resolve, reject) => { res = resolve; rej = reject; }); | ||
// Mock an actual buttplug (Lovense Hush)! | ||
mockBT = utils_1.MakeMockWebBluetoothDevice(Lovense_1.LovenseRev5.DeviceInfo); | ||
// We assume we're using a lovense device for all tests here so set it up. | ||
mockBT = utils_1.MakeMockWebBluetoothDevice(Lovense_1.Lovense.DeviceInfo); | ||
utils_1.SetupLovenseTestDevice(mockBT); | ||
const g = global; | ||
@@ -70,6 +71,16 @@ g.navigator = g.navigator || {}; | ||
}; | ||
yield expect(bp.StartScanning()).rejects.toThrow(); | ||
// Make sure we at least have the right error code. Id and message may vary. | ||
yield expect(bp.StartScanning()).rejects.toHaveProperty("ErrorCode", index_1.ErrorClass.ERROR_DEVICE); | ||
return p; | ||
})); | ||
it("should subscribe on connect for lovense device, unsubscribe on disconnect", () => __awaiter(this, void 0, void 0, function* () { | ||
jest.spyOn(mockBT.rxChar, "startNotifications"); | ||
jest.spyOn(mockBT.rxChar, "stopNotifications"); | ||
yield bp.StartScanning(); | ||
yield bp.StopScanning(); | ||
expect(mockBT.rxChar.startNotifications).toBeCalled(); | ||
yield bp.Disconnect(); | ||
expect(mockBT.rxChar.stopNotifications).toBeCalled(); | ||
})); | ||
}); | ||
//# sourceMappingURL=test-webbluetooth.js.map |
@@ -12,2 +12,3 @@ /// <reference types="node" /> | ||
} | ||
export declare function SetupLovenseTestDevice(mockBT: WebBluetoothMockObject, deviceLetter?: string): void; | ||
export declare function SetupTestSuite(): void; | ||
@@ -19,5 +20,6 @@ export declare class WebBluetoothMockObject { | ||
txChar: CharacteristicMock; | ||
constructor(device: DeviceMock, gatt: GattMock, service: PrimaryServiceMock, txChar: CharacteristicMock); | ||
rxChar: CharacteristicMock; | ||
constructor(device: DeviceMock, gatt: GattMock, service: PrimaryServiceMock, txChar: CharacteristicMock, rxChar: CharacteristicMock); | ||
} | ||
export declare function MakeMockWebBluetoothDevice(deviceInfo: BluetoothDeviceInfo): WebBluetoothMockObject; | ||
export declare function SetupTestServer(): Promise<any>; |
@@ -42,7 +42,24 @@ "use strict"; | ||
exports.BPTestClient = BPTestClient; | ||
function SetupLovenseTestDevice(mockBT, deviceLetter = "W") { | ||
const oldWrite = mockBT.txChar.writeValue; | ||
mockBT.txChar.writeValue = () => __awaiter(this, void 0, void 0, function* () { | ||
const infoBuf = Buffer.from(`${deviceLetter}:01:000000000000`); | ||
const arrBuf = new ArrayBuffer(infoBuf.length); | ||
// If we don't convert and load into a view, the buffer conversion later | ||
// won't work. | ||
const view = new Uint8Array(arrBuf); | ||
for (let i = 0; i < infoBuf.length; ++i) { | ||
view[i] = infoBuf[i]; | ||
} | ||
mockBT.rxChar.value = new DataView(arrBuf); | ||
mockBT.rxChar.dispatchEvent(new CustomEvent("characteristicvaluechanged")); | ||
mockBT.txChar.writeValue = oldWrite; | ||
}); | ||
} | ||
exports.SetupLovenseTestDevice = SetupLovenseTestDevice; | ||
function SetupTestSuite() { | ||
// None of our tests should take very long. | ||
jest.setTimeout(1000); | ||
process.on("unhandledRejection", (error) => { | ||
throw new Error(`Unhandled Promise rejection! ${error}`); | ||
process.on("unhandledRejection", (reason, p) => { | ||
throw new Error(`Unhandled Promise rejection!\n---\n${reason.stack}\n---\n`); | ||
}); | ||
@@ -52,3 +69,3 @@ } | ||
class WebBluetoothMockObject { | ||
constructor(device, gatt, service, txChar) { | ||
constructor(device, gatt, service, txChar, rxChar) { | ||
this.device = device; | ||
@@ -58,2 +75,3 @@ this.gatt = gatt; | ||
this.txChar = txChar; | ||
this.rxChar = rxChar; | ||
} | ||
@@ -63,7 +81,39 @@ } | ||
function MakeMockWebBluetoothDevice(deviceInfo) { | ||
const device = new web_bluetooth_mock_1.DeviceMock(deviceInfo.Names[0], [deviceInfo.Services[0]]); | ||
let name; | ||
if (deviceInfo.Names.length > 0) { | ||
name = deviceInfo.Names[0]; | ||
} | ||
else if (deviceInfo.NamePrefixes.length > 0) { | ||
name = deviceInfo.NamePrefixes[0] + "-test"; | ||
} | ||
else { | ||
throw new Error("Cannot create mock device!"); | ||
} | ||
const device = new web_bluetooth_mock_1.DeviceMock(name, [deviceInfo.Services[0]]); | ||
const gatt = device.gatt; | ||
const service = device.getServiceMock(deviceInfo.Services[0]); | ||
const tx = service.getCharacteristicMock(deviceInfo.Characteristics.tx); | ||
return new WebBluetoothMockObject(device, gatt, service, tx); | ||
let tx; | ||
if (Object.keys(deviceInfo.Characteristics).indexOf("tx") !== -1) { | ||
tx = service.getCharacteristicMock(deviceInfo.Characteristics.tx); | ||
} | ||
else { | ||
// In this case, we are expected to query devices and find rx/tx | ||
// characteristics. Since this is a test and we have no devices, we can't do | ||
// that. Just make one up. | ||
tx = service.getCharacteristicMock("55555555-5555-5555-5555-555555555555"); | ||
tx.properties.write = true; | ||
tx.properties.writeWithoutResponse = true; | ||
} | ||
let rx; | ||
if (Object.keys(deviceInfo.Characteristics).indexOf("rx") !== -1) { | ||
rx = service.getCharacteristicMock(deviceInfo.Characteristics.rx); | ||
} | ||
else { | ||
// In this case, we are expected to query devices and find rx/tx | ||
// characteristics. Since this is a test and we have no devices, we can't do | ||
// that. Just make one up. | ||
rx = service.getCharacteristicMock("55555556-5555-5555-5555-555555555555"); | ||
rx.properties.notify = true; | ||
} | ||
return new WebBluetoothMockObject(device, gatt, service, tx, rx); | ||
} | ||
@@ -70,0 +120,0 @@ exports.MakeMockWebBluetoothDevice = MakeMockWebBluetoothDevice; |
{ | ||
"name": "buttplug", | ||
"version": "0.7.1", | ||
"version": "0.8.0", | ||
"description": "Javascript library for creating or accessing Buttplug Intimate Hardware Protocol servers/clients, for node or web", | ||
@@ -38,19 +38,19 @@ "repository": { | ||
"@tweenjs/tween.js": "^17.2.0", | ||
"ajv": "^6.4.0", | ||
"ajv": "^6.5.1", | ||
"class-transformer": "^0.1.9", | ||
"jspanel4": "^4.0.0-beta.5.1", | ||
"jspanel4": "^4.0.0", | ||
"reflect-metadata": "^0.1.12" | ||
}, | ||
"devDependencies": { | ||
"@types/jest": "^22.2.3", | ||
"@types/node": "^10.0.2", | ||
"@types/jest": "^23.1.3", | ||
"@types/node": "^10.5.0", | ||
"@types/tween.js": "^16.9.0", | ||
"@types/web-bluetooth": "^0.0.4", | ||
"codecov": "^3.0.1", | ||
"codecov": "^3.0.2", | ||
"copyfiles": "^2.0.0", | ||
"css-loader": "^0.28.11", | ||
"fork-ts-checker-webpack-plugin": "^0.4.1", | ||
"fork-ts-checker-webpack-plugin": "^0.4.2", | ||
"html-loader": "^0.5.5", | ||
"istanbul": "^0.4.5", | ||
"jest": "^22.4.3", | ||
"jest": "^23.2.0", | ||
"mock-socket": "^7.1.0", | ||
@@ -60,15 +60,15 @@ "style-loader": "^0.21.0", | ||
"trash-cli": "^1.4.0", | ||
"ts-jest": "^22.4.4", | ||
"ts-jest": "^22.4.6", | ||
"ts-loader": "^4.x", | ||
"ts-node": "^6.0.2", | ||
"tslint": "^5.9.1", | ||
"ts-node": "^7.0.0", | ||
"tslint": "^5.10.0", | ||
"typedoc": "^0.11.1", | ||
"typescript": "^2.8.3", | ||
"uglifyjs-webpack-plugin": "^1.2.5", | ||
"typescript": "^2.9.2", | ||
"uglifyjs-webpack-plugin": "^1.2.7", | ||
"url-loader": "^1.0.1", | ||
"web-bluetooth-mock": "^1.0.2", | ||
"webpack": "^4.6.0", | ||
"webpack-cli": "^2.1.2", | ||
"webpack-merge": "^4.1.2", | ||
"yarn": "^1.6.0" | ||
"webpack": "^4.12.2", | ||
"webpack-cli": "^3.0.8", | ||
"webpack-merge": "^4.1.3", | ||
"yarn": "^1.7.0" | ||
}, | ||
@@ -75,0 +75,0 @@ "jest": { |
# buttplug-js | ||
[![npm](https://img.shields.io/npm/v/buttplug.svg)](https://npmjs.com/package/buttplug) [![Build Status](https://travis-ci.org/metafetish/buttplug-js.svg?branch=master)](https://travis-ci.org/metafetish/buttplug-js) [![codecov](https://codecov.io/gh/metafetish/buttplug-js/branch/master/graph/badge.svg)](https://codecov.io/gh/metafetish/buttplug-js) [![Patreon donate button](https://img.shields.io/badge/patreon-donate-yellow.svg)](https://www.patreon.com/qdot) | ||
[![npm](https://img.shields.io/npm/v/buttplug.svg)](https://npmjs.com/package/buttplug) | ||
[![Build Status](https://travis-ci.org/buttplugio/buttplug-js.svg?branch=master)](https://travis-ci.org/buttplugio/buttplug-js) | ||
[![codecov](https://codecov.io/gh/buttplugio/buttplug-js/branch/master/graph/badge.svg)](https://codecov.io/gh/buttplugio/buttplug-js) | ||
[![Patreon donate button](https://img.shields.io/badge/patreon-donate-yellow.svg)](https://www.patreon.com/qdot) | ||
[![Discourse Forum](https://img.shields.io/badge/discourse-forum-blue.svg)](https://metafetish.club) | ||
[![Discord](https://img.shields.io/discord/353303527587708932.svg?logo=discord)](https://discord.gg/t9g9RuD) | ||
[![Twitter](https://img.shields.io/twitter/follow/buttplugio.svg?style=social&logo=twitter)](https://twitter.com/buttplugio) | ||
buttplug-js is a core implementation in Typescript for the Buttplug | ||
@@ -81,9 +88,16 @@ Sex Toy Control Protocol and Server Architecture. It contains the | ||
buttplug-js API Documentation is available at | ||
[https://metafetish.github.io/buttplug-js](https://metafetish.github.io/buttplug-js). | ||
[https://buttplug-js.docs.buttplug.io](https://buttplug-js.docs.buttplug.io). | ||
The documentation is rebuilt on every commit/merge to master. | ||
The documentation is rebuilt on every commit/merge to release, and | ||
reflects the state of the latest released version on NPM. If you are | ||
using a dev version of the library (i.e. from the master branch of | ||
this repo), you can generate documentation by running | ||
``` | ||
yarn build:doc | ||
``` | ||
We highly recommend reading through the Buttplug Protocol Spec to get | ||
an idea of what messages are available. The Protocol Spec is available | ||
at [https://metafetish.github.io/buttplug](https://metafetish.github.io/buttplug). | ||
at [https://buttplug-spec.docs.buttplug.io](https://buttplug-spec.docs.buttplug.io). | ||
@@ -137,9 +151,8 @@ ## Usage Example | ||
- [buttplug-noble-device-manager](https://github.com/metafetish/buttplug-noble-device-manager) - | ||
Noble device manager for native Bluetooth Device management. | ||
- [buttplug-node-websockets](https://github.com/metafetish/buttplug-node-websockets) - | ||
- [buttplug-node-bluetoothle-manager](https://github.com/buttplugio/buttplug-node-bluetoothle-manager) - | ||
Node device manager for native Bluetooth Device management. | ||
- [buttplug-node-websockets](https://github.com/buttplugio/buttplug-node-websockets) - | ||
Websocket client and server for native node applications. | ||
- [vue-buttplug-material-component](https://github.com/metafetish/vue-buttplug-material-component) - | ||
Vue.js component using [Vue Material | ||
Design](https://vuematerial.io). Provides a simple interface for | ||
- [vue-buttplug-material-component](https://github.com/buttplugio/vue-buttplug-material-component) - | ||
Vue.js component using [Vuetify](https://vuetifyjs.com). Provides a simple interface for | ||
Buttplug server connection, device management, etc. | ||
@@ -153,3 +166,3 @@ | ||
with sex toy synchronization features via Buttplug. | ||
- [buttplug-twine](https://github.com/metafetish/buttplug-twine) - | ||
- [buttplug-twine](https://github.com/buttplugio/buttplug-twine) - | ||
Buttplug bindings for the [Twine Interaction Fiction Game | ||
@@ -163,3 +176,3 @@ Engine](http://twinery.org). Allows developers to easily include | ||
Copyright (c) 2017, Metafetish | ||
Copyright (c) 2017-2018, Nonpolynomial Labs LLC | ||
All rights reserved. | ||
@@ -166,0 +179,0 @@ |
@@ -20,2 +20,3 @@ "use strict"; | ||
protected _logger = ButtplugLogger.Logger; | ||
protected _isScanning = false; | ||
// TODO This should be set on schema load | ||
@@ -49,2 +50,6 @@ protected _messageVersion: number = 1; | ||
public get IsScanning(): boolean { | ||
return this._isScanning; | ||
} | ||
public ConnectWebsocket = async (aAddress: string) => { | ||
@@ -78,2 +83,3 @@ this._logger.Info(`ButtplugClient: Connecting to ${aAddress}`); | ||
this._logger.Debug(`ButtplugClient: StartScanning called`); | ||
this._isScanning = true; | ||
return await this.SendMsgExpectOk(new Messages.StartScanning()); | ||
@@ -84,2 +90,3 @@ } | ||
this._logger.Debug(`ButtplugClient: StopScanning called`); | ||
this._isScanning = false; | ||
return await this.SendMsgExpectOk(new Messages.StopScanning()); | ||
@@ -130,22 +137,23 @@ } | ||
switch (x.Type) { | ||
case "Log": | ||
this.emit("log", x); | ||
break; | ||
case "DeviceAdded": | ||
const addedMsg = x as Messages.DeviceAdded; | ||
const addedDevice = Device.fromMsg(addedMsg); | ||
this._devices.set(addedMsg.DeviceIndex, addedDevice); | ||
this.emit("deviceadded", addedDevice); | ||
break; | ||
case "DeviceRemoved": | ||
const removedMsg = x as Messages.DeviceRemoved; | ||
if (this._devices.has(removedMsg.DeviceIndex)) { | ||
const removedDevice = this._devices.get(removedMsg.DeviceIndex); | ||
this._devices.delete(removedMsg.DeviceIndex); | ||
this.emit("deviceremoved", removedDevice); | ||
} | ||
break; | ||
case "ScanningFinished": | ||
this.emit("scanningfinished", x); | ||
break; | ||
case "Log": | ||
this.emit("log", x); | ||
break; | ||
case "DeviceAdded": | ||
const addedMsg = x as Messages.DeviceAdded; | ||
const addedDevice = Device.fromMsg(addedMsg); | ||
this._devices.set(addedMsg.DeviceIndex, addedDevice); | ||
this.emit("deviceadded", addedDevice); | ||
break; | ||
case "DeviceRemoved": | ||
const removedMsg = x as Messages.DeviceRemoved; | ||
if (this._devices.has(removedMsg.DeviceIndex)) { | ||
const removedDevice = this._devices.get(removedMsg.DeviceIndex); | ||
this._devices.delete(removedMsg.DeviceIndex); | ||
this.emit("deviceremoved", removedDevice); | ||
} | ||
break; | ||
case "ScanningFinished": | ||
this._isScanning = false; | ||
this.emit("scanningfinished", x); | ||
break; | ||
} | ||
@@ -159,35 +167,35 @@ } | ||
switch (msg.Type) { | ||
case "ServerInfo": { | ||
const serverinfo = msg as Messages.ServerInfo; | ||
this._logger.Info(`ButtplugClient: Connected to Server ${serverinfo.ServerName}`); | ||
// TODO: maybe store server name, do something with message template version? | ||
const ping = serverinfo.MaxPingTime; | ||
if (serverinfo.MessageVersion < this._messageVersion) { | ||
// Disconnect and throw an exception explaining the version mismatch problem. | ||
case "ServerInfo": { | ||
const serverinfo = msg as Messages.ServerInfo; | ||
this._logger.Info(`ButtplugClient: Connected to Server ${serverinfo.ServerName}`); | ||
// TODO: maybe store server name, do something with message template version? | ||
const ping = serverinfo.MaxPingTime; | ||
if (serverinfo.MessageVersion < this._messageVersion) { | ||
// Disconnect and throw an exception explaining the version mismatch problem. | ||
this._connector!.Disconnect(); | ||
throw new Error("Server protocol version is older than client protocol version. Please update server."); | ||
} | ||
if (ping > 0) { | ||
this._pingTimer = setInterval(async () => { | ||
// If we've disconnected, stop trying to ping the server. | ||
if (!this.Connected) { | ||
await this.ShutdownConnection(); | ||
return; | ||
} | ||
this.SendMessage(new Messages.Ping(this._counter)); | ||
} , Math.round(ping / 2)); | ||
} | ||
await this.RequestDeviceList(); | ||
return true; | ||
} | ||
case "Error": { | ||
const err = msg as Messages.Error; | ||
this._logger.Error(`ButtplugClient: Cannot connect to server. ${err.ErrorMessage}`); | ||
// Disconnect and throw an exception with the error message we got back. | ||
// This will usually only error out if we have a version mismatch that the | ||
// server has detected. | ||
this._connector!.Disconnect(); | ||
throw new Error("Server protocol version is older than client protocol version. Please update server."); | ||
throw new Error((msg as Messages.Error).ErrorMessage); | ||
} | ||
if (ping > 0) { | ||
this._pingTimer = setInterval(async () => { | ||
// If we've disconnected, stop trying to ping the server. | ||
if (!this.Connected) { | ||
await this.ShutdownConnection(); | ||
return; | ||
} | ||
this.SendMessage(new Messages.Ping(this._counter)); | ||
} , Math.round(ping / 2)); | ||
} | ||
await this.RequestDeviceList(); | ||
return true; | ||
} | ||
case "Error": { | ||
const err = msg as Messages.Error; | ||
this._logger.Error(`ButtplugClient: Cannot connect to server. ${err.ErrorMessage}`); | ||
// Disconnect and throw an exception with the error message we got back. | ||
// This will usually only error out if we have a version mismatch that the | ||
// server has detected. | ||
this._connector!.Disconnect(); | ||
throw new Error((msg as Messages.Error).ErrorMessage); | ||
} | ||
} | ||
return false; | ||
@@ -245,8 +253,8 @@ } | ||
switch (msg.Type) { | ||
case "Ok": | ||
res(); | ||
break; | ||
default: | ||
rej(msg); | ||
break; | ||
case "Ok": | ||
res(); | ||
break; | ||
default: | ||
rej(msg); | ||
break; | ||
} | ||
@@ -253,0 +261,0 @@ return p; |
@@ -50,2 +50,5 @@ "use strict"; | ||
} | ||
if (speed > 1.0 || speed < 0.0) { | ||
throw new Error("Speed must be 0.0 <= x <= 1.0!"); | ||
} | ||
const commands: Messages.SpeedSubcommand[] = []; | ||
@@ -55,3 +58,3 @@ for (let i = 0; i < device.MessageAttributes("VibrateCmd").FeatureCount; ++i) { | ||
} | ||
return new Messages.VibrateCmd(commands); | ||
return new Messages.VibrateCmd(commands, device.Index); | ||
} | ||
@@ -63,2 +66,5 @@ | ||
} | ||
if (position > 1.0 || position < 0.0) { | ||
throw new Error("Position must be 0.0 <= x <= 1.0!"); | ||
} | ||
const commands: Messages.VectorSubcommand[] = []; | ||
@@ -68,3 +74,3 @@ for (let i = 0; i < device.MessageAttributes("LinearCmd").FeatureCount; ++i) { | ||
} | ||
return new Messages.LinearCmd(commands); | ||
return new Messages.LinearCmd(commands, device.Index); | ||
} | ||
@@ -76,2 +82,5 @@ | ||
} | ||
if (speed > 1.0 || speed < 0.0) { | ||
throw new Error("Speed must be 0.0 <= x <= 1.0!"); | ||
} | ||
const commands: Messages.RotateSubcommand[] = []; | ||
@@ -81,3 +90,3 @@ for (let i = 0; i < device.MessageAttributes("RotateCmd").FeatureCount; ++i) { | ||
} | ||
return new Messages.RotateCmd(commands); | ||
return new Messages.RotateCmd(commands, device.Index); | ||
} |
@@ -6,2 +6,3 @@ import { IBluetoothDeviceImpl } from "./IBluetoothDeviceImpl"; | ||
constructor(private _names: string[], | ||
private _namePrefixes: string[], | ||
private _services: string[], | ||
@@ -16,2 +17,6 @@ private _characteristics: object, | ||
public get NamePrefixes() { | ||
return this._namePrefixes; | ||
} | ||
public get Services() { | ||
@@ -18,0 +23,0 @@ return this._services; |
import { BluetoothDeviceInfo } from "./BluetoothDeviceInfo"; | ||
import { FleshlightLaunch } from "./devices/FleshlightLaunch"; | ||
import { LovenseRev1, LovenseRev2, LovenseRev3, LovenseRev4, | ||
LovenseRev5, LovenseRev6, LovenseRev7, LovenseRev8 } from "./devices/Lovense"; | ||
import { Lovense } from "./devices/Lovense"; | ||
import { VorzeA10Cyclone } from "./devices/VorzeA10Cyclone"; | ||
@@ -12,10 +11,3 @@ import { WeVibe } from "./devices/WeVibe"; | ||
return [FleshlightLaunch.DeviceInfo, | ||
LovenseRev1.DeviceInfo, | ||
LovenseRev2.DeviceInfo, | ||
LovenseRev3.DeviceInfo, | ||
LovenseRev4.DeviceInfo, | ||
LovenseRev5.DeviceInfo, | ||
LovenseRev6.DeviceInfo, | ||
LovenseRev7.DeviceInfo, | ||
LovenseRev8.DeviceInfo, | ||
Lovense.DeviceInfo, | ||
Maxpro.DeviceInfo, | ||
@@ -22,0 +14,0 @@ VorzeA10Cyclone.DeviceInfo, |
@@ -8,2 +8,3 @@ import { BluetoothDeviceInfo } from "../BluetoothDeviceInfo"; | ||
public static readonly DeviceInfo = new BluetoothDeviceInfo(["Launch"], | ||
[], | ||
["88f80580-0000-01e6-aace-0002a5d5c51b"], | ||
@@ -17,4 +18,5 @@ { cmd: "88f80583-0000-01e6-aace-0002a5d5c51b", | ||
// Send initializer byte | ||
await aDeviceImpl.WriteValue("cmd", new Uint8Array([0x00])); | ||
return new FleshlightLaunch(aDeviceImpl); | ||
const dev = new FleshlightLaunch(aDeviceImpl); | ||
await dev.Initialize(); | ||
return dev; | ||
} | ||
@@ -31,2 +33,7 @@ | ||
public Initialize = | ||
async (): Promise<void> => { | ||
await this._deviceImpl.WriteValue("cmd", new Uint8Array([0x00])); | ||
} | ||
public get MessageSpecifications(): object { | ||
@@ -33,0 +40,0 @@ return { |
@@ -5,58 +5,116 @@ import { BluetoothDeviceInfo } from "../BluetoothDeviceInfo"; | ||
import * as Messages from "../../../core/Messages"; | ||
import * as MessageUtils from "../../../core/MessageUtils"; | ||
import { RotateSubcommand } from "../../../core/Messages"; | ||
export class Lovense extends ButtplugBluetoothDevice { | ||
public static CreateInstance(aDeviceImpl: IBluetoothDeviceImpl): Promise<ButtplugBluetoothDevice> { | ||
return Promise.resolve(new Lovense(aDeviceImpl)); | ||
public static readonly DeviceInfo = (() => { | ||
// Start with the two non-standard UUIDs, which come from the original | ||
// versions of the Max/Nora toys. | ||
const uuids: string[] = ["0000fff0-0000-1000-8000-00805f9b34fb", | ||
"6e400001-b5a3-f393-e0a9-e50e24dcca9e"]; | ||
// Future-proofing for possible Lovense UUIDs, based on the pattern of the | ||
// current firmware. | ||
for (let i = 0; i < 16; ++i) { | ||
uuids.push(`5${i.toString(16)}300001-0023-4bd4-bbd5-a6920e4c5653`); | ||
uuids.push(`5${i.toString(16)}300001-0024-4bd4-bbd5-a6920e4c5653`); | ||
} | ||
return new BluetoothDeviceInfo([], | ||
["LVS"], | ||
uuids, | ||
{}, | ||
Lovense.CreateInstance); | ||
})(); | ||
public static async CreateInstance(aDeviceImpl: IBluetoothDeviceImpl): Promise<ButtplugBluetoothDevice> { | ||
const dev = new Lovense(aDeviceImpl); | ||
await dev.Initialize(); | ||
return dev; | ||
} | ||
private static _deviceNames = { "LVS-A011": "Nora", | ||
"LVS-C011": "Nora", | ||
"LVS-B011": "Max", | ||
"LVS-L009": "Ambi", | ||
"LVS-S001": "Lush", | ||
"LVS-S35": "Lush", | ||
"LVS-Z36": "Hush", | ||
"LVS-Z001": "Hush", | ||
"LVS_Z001": "Hush", | ||
"LVS-Hush41": "Hush", | ||
"LVS-Domi37": "Domi", | ||
"LVS-Domi38": "Domi", | ||
"LVS-Domi39": "Domi", | ||
"LVS-Domi40": "Domi", | ||
"LVS-P36": "Edge", | ||
"LVS-Edge37": "Edge", | ||
"LVS-Edge38": "Edge", | ||
"LVS-Edge39": "Edge", | ||
"LVS-Edge40": "Edge", | ||
"LVS-Lush41": "Lush"}; | ||
private static _deviceNames = { | ||
A: "Nora", | ||
B: "Max", | ||
C: "Nora", | ||
L: "Ambi", | ||
O: "Osci", | ||
P: "Edge", | ||
S: "Lush", | ||
W: "Domi", | ||
Z: "Hush", | ||
0: "Unknown", | ||
}; | ||
private _initResolve: (() => void) | undefined; | ||
private _initPromise = new Promise((res, rej) => { this._initResolve = res; }); | ||
private _isClockwise = false; | ||
private _specs: any = { | ||
VibrateCmd: { FeatureCount: 1 }, | ||
SingleMotorVibrateCmd: {}, | ||
StopDeviceCmd: {}, | ||
}; | ||
public constructor(aDeviceImpl: IBluetoothDeviceImpl) { | ||
super(`Lovense ${aDeviceImpl.Name}`, aDeviceImpl); | ||
} | ||
public Initialize = async (): Promise<void> => { | ||
this._deviceImpl.addListener("characteristicvaluechanged", this.OnValueChanged); | ||
await this._deviceImpl.Subscribe("rx"); | ||
await this._deviceImpl.WriteString("tx", "DeviceType;"); | ||
await this._initPromise; | ||
} | ||
public get MessageSpecifications(): object { | ||
return this._specs; | ||
} | ||
private ParseDeviceType(aDeviceType: string) { | ||
// Typescript gets angry if we try to destructure this into consts/lets | ||
// differently or all lets (since deviceVersion never changes and | ||
// deviceAddress isn't used), and I don't wanna deal with assigning to const | ||
// then let, so this works well enough. | ||
let deviceLetter; | ||
let deviceVersion; | ||
let deviceAddress; | ||
[deviceLetter, deviceVersion, deviceAddress] = aDeviceType.split(":"); | ||
if (!Lovense._deviceNames.hasOwnProperty(deviceLetter)) { | ||
deviceLetter = "0"; | ||
} | ||
this._name = `Lovense ${Lovense._deviceNames[deviceLetter]} v${deviceVersion}`; | ||
this.MsgFuncs.set(Messages.StopDeviceCmd.name, this.HandleStopDeviceCmd); | ||
this.MsgFuncs.set(Messages.VibrateCmd.name, this.HandleVibrateCmd); | ||
this.MsgFuncs.set(Messages.SingleMotorVibrateCmd.name, this.HandleSingleMotorVibrateCmd); | ||
} | ||
public get MessageSpecifications(): object { | ||
return { | ||
VibrateCmd: { FeatureCount: 1 }, | ||
SingleMotorVibrateCmd: {}, | ||
StopDeviceCmd: {}, | ||
}; | ||
if (deviceLetter === "P") { | ||
// Edge has 2 motors | ||
this._specs.VibrateCmd = { FeatureCount: 2 }; | ||
} else if (deviceLetter === "A" || deviceLetter === "C") { | ||
// Nora has rotation | ||
this._specs.RotateCmd = { FeatureCount: 1 }; | ||
this.MsgFuncs.set(Messages.RotateCmd.name, this.HandleRotateCmd); | ||
} | ||
} | ||
private HandleVibrateCmd = async (aMsg: Messages.VibrateCmd): Promise<Messages.ButtplugMessage> => { | ||
if (aMsg.Speeds.length !== 1) { | ||
return new Messages.Error(`Lovense devices require VibrateCmd to have 1 speed command, ` + | ||
`${aMsg.Speeds.length} sent.`, | ||
Messages.ErrorClass.ERROR_DEVICE, | ||
aMsg.Id); | ||
private OnValueChanged = async (aCharacteristic: string, aValue: Buffer) => { | ||
// If we haven't initialized yet, consider this to be the first read, for the device info. | ||
if (this._initResolve !== undefined) { | ||
this.ParseDeviceType(aValue.toString()); | ||
const res = this._initResolve; | ||
this._initResolve = undefined; | ||
res(); | ||
return; | ||
} | ||
return await this.HandleSingleMotorVibrateCmd(new Messages.SingleMotorVibrateCmd(aMsg.Speeds[0].Speed, | ||
aMsg.DeviceIndex, | ||
aMsg.Id)); | ||
// TODO Fill in battery/accelerometer/etc reads | ||
} | ||
private HandleStopDeviceCmd = async (aMsg: Messages.StopDeviceCmd): Promise<Messages.ButtplugMessage> => { | ||
return await this.HandleSingleMotorVibrateCmd(new Messages.SingleMotorVibrateCmd(0, aMsg.DeviceIndex, aMsg.Id)); | ||
await this.HandleSingleMotorVibrateCmd(new Messages.SingleMotorVibrateCmd(0, aMsg.DeviceIndex, aMsg.Id)); | ||
if (this._specs.hasOwnProperty("RotateCmd")) { | ||
this.HandleRotateCmd(new Messages.RotateCmd([new RotateSubcommand(0, 0, this._isClockwise)], 0, aMsg.Id)); | ||
} | ||
return new Messages.Ok(aMsg.Id); | ||
} | ||
@@ -66,78 +124,44 @@ | ||
async (aMsg: Messages.SingleMotorVibrateCmd): Promise<Messages.ButtplugMessage> => { | ||
const speed = Math.floor(20 * aMsg.Speed); | ||
await this._deviceImpl.WriteValue("tx", Buffer.from("Vibrate:" + speed + ";")); | ||
return new Messages.Ok(aMsg.Id); | ||
const speeds: Messages.SpeedSubcommand[] = []; | ||
for (let i = 0; i < this._specs.VibrateCmd.FeatureCount; i++) { | ||
speeds.push(new Messages.SpeedSubcommand(i, aMsg.Speed)); | ||
} | ||
return await this.HandleVibrateCmd(new Messages.VibrateCmd(speeds, aMsg.DeviceIndex, aMsg.Id)); | ||
} | ||
} | ||
export class LovenseRev1 { | ||
public static readonly DeviceInfo = new BluetoothDeviceInfo(["LVS-A011", "LVS-C011", "LVS-B011", "LVS-L009"], | ||
["0000fff0-0000-1000-8000-00805f9b34fb"], | ||
{tx: "0000fff2-0000-1000-8000-00805f9b34fb", | ||
// rx: "0000fff1-0000-1000-8000-00805f9b34fb" | ||
}, | ||
Lovense.CreateInstance); | ||
} | ||
private HandleVibrateCmd = async (aMsg: Messages.VibrateCmd): Promise<Messages.ButtplugMessage> => { | ||
if (aMsg.Speeds.length > this._specs.VibrateCmd.FeatureCount) { | ||
return new Messages.Error(`Lovense devices require VibrateCmd to have at most ` + | ||
`${this._specs.VibrateCmd.FeatureCount} speed commands, ` + | ||
`${aMsg.Speeds.length} sent.`, | ||
Messages.ErrorClass.ERROR_DEVICE, | ||
aMsg.Id); | ||
} | ||
for (const cmd of aMsg.Speeds) { | ||
const index = this._specs.VibrateCmd.FeatureCount > 1 ? (cmd.Index + 1).toString(10) : ""; | ||
const speed = Math.floor(20 * cmd.Speed); | ||
await this._deviceImpl.WriteString("tx", `Vibrate${index}:${speed};`); | ||
} | ||
return new Messages.Ok(aMsg.Id); | ||
} | ||
export class LovenseRev2 { | ||
public static readonly DeviceInfo = new BluetoothDeviceInfo(["LVS-S001", "LVS-Z001", "LVS_Z001"], | ||
["6e400001-b5a3-f393-e0a9-e50e24dcca9e"], | ||
{tx: "6e400002-b5a3-f393-e0a9-e50e24dcca9e", | ||
// rx: "6e400003-b5a3-f393-e0a9-e50e24dcca9e" | ||
}, | ||
Lovense.CreateInstance); | ||
} | ||
private HandleRotateCmd = async (aMsg: Messages.RotateCmd): Promise<Messages.ButtplugMessage> => { | ||
if (aMsg.Rotations.length !== 1) { | ||
return new Messages.Error(`Lovense devices require RotateCmd to have 1 rotate command, ` + | ||
`${aMsg.Rotations.length} sent.`, | ||
Messages.ErrorClass.ERROR_DEVICE, | ||
aMsg.Id); | ||
} | ||
const rotateCmd = aMsg.Rotations[0]; | ||
if (rotateCmd.Index !== 0) { | ||
return new Messages.Error("Rotation command sent for invalid index."); | ||
} | ||
if (rotateCmd.Clockwise !== this._isClockwise) { | ||
await this._deviceImpl.WriteString("tx", "RotateChange;"); | ||
} | ||
const speed = Math.floor(20 * rotateCmd.Speed); | ||
await this._deviceImpl.WriteString("tx", `Rotate:${speed};`); | ||
return new Messages.Ok(aMsg.Id); | ||
} | ||
export class LovenseRev3 { | ||
public static readonly DeviceInfo = new BluetoothDeviceInfo(["LVS-P36"], | ||
["50300001-0024-4bd4-bbd5-a6920e4c5653"], | ||
{tx: "50300002-0024-4bd4-bbd5-a6920e4c5653", | ||
// rx: "50300003-0024-4bd4-bbd5-a6920e4c5653" | ||
}, | ||
Lovense.CreateInstance); | ||
} | ||
export class LovenseRev4 { | ||
public static readonly DeviceInfo = new BluetoothDeviceInfo(["LVS-Domi37", "LVS-Domi38", "LVS-Domi39", "LVS-Domi40"], | ||
["57300001-0023-4bd4-bbd5-a6920e4c5653"], | ||
{tx: "57300002-0023-4bd4-bbd5-a6920e4c5653", | ||
// rx: "57300003-0023-4bd4-bbd5-a6920e4c5653" | ||
}, | ||
Lovense.CreateInstance); | ||
} | ||
export class LovenseRev5 { | ||
public static readonly DeviceInfo = new BluetoothDeviceInfo(["LVS-Z36"], | ||
["5a300001-0024-4bd4-bbd5-a6920e4c5653"], | ||
{tx: "5a300002-0024-4bd4-bbd5-a6920e4c5653", | ||
// rx: "57300003-0023-4bd4-bbd5-a6920e4c5653" | ||
}, | ||
Lovense.CreateInstance); | ||
} | ||
export class LovenseRev6 { | ||
public static readonly DeviceInfo = new BluetoothDeviceInfo(["LVS-Edge37", "LVS-Edge38", "LVS-Edge39", "LVS-Edge40"], | ||
["50300001-0023-4bd4-bbd5-a6920e4c5653"], | ||
{tx: "50300002-0023-4bd4-bbd5-a6920e4c5653", | ||
// rx: "57300003-0023-4bd4-bbd5-a6920e4c5653" | ||
}, | ||
Lovense.CreateInstance); | ||
} | ||
export class LovenseRev7 { | ||
public static readonly DeviceInfo = new BluetoothDeviceInfo(["LVS-S35"], | ||
["53300001-0023-4bd4-bbd5-a6920e4c5653"], | ||
{tx: "53300002-0023-4bd4-bbd5-a6920e4c5653", | ||
// rx: "53300003-0023-4bd4-bbd5-a6920e4c5653" | ||
}, | ||
Lovense.CreateInstance); | ||
} | ||
export class LovenseRev8 { | ||
public static readonly DeviceInfo = new BluetoothDeviceInfo(["LVS-Hush41"], | ||
["5a300001-0023-4bd4-bbd5-a6920e4c5653"], | ||
{tx: "5a300002-0023-4bd4-bbd5-a6920e4c5653", | ||
// rx: "57300003-0023-4bd4-bbd5-a6920e4c5653" | ||
}, | ||
Lovense.CreateInstance); | ||
} |
@@ -9,5 +9,5 @@ | ||
public static readonly DeviceInfo = new BluetoothDeviceInfo(["M2"], | ||
[], | ||
["6e400001-b5a3-f393-e0a9-e50e24dcca9e"], | ||
{ tx: "6e400002-b5a3-f393-e0a9-e50e24dcca9e", | ||
rx: "6e400003-b5a3-f393-e0a9-e50e24dcca9e"}, | ||
{}, | ||
Maxpro.CreateInstance); | ||
@@ -14,0 +14,0 @@ |
@@ -8,9 +8,8 @@ import { BluetoothDeviceInfo } from "../BluetoothDeviceInfo"; | ||
public static readonly DeviceInfo = new BluetoothDeviceInfo(["CycSA"], | ||
[], | ||
["40ee1111-63ec-4b7f-8ce7-712efd55b90e"], | ||
{ tx: "40ee2222-63ec-4b7f-8ce7-712efd55b90e"}, | ||
{}, | ||
VorzeA10Cyclone.CreateInstance); | ||
public static async CreateInstance(aDeviceImpl: IBluetoothDeviceImpl): Promise<ButtplugBluetoothDevice> { | ||
// Send initializer byte | ||
aDeviceImpl.WriteValue("cmd", new Uint8Array([0x00])); | ||
return new VorzeA10Cyclone(aDeviceImpl); | ||
@@ -17,0 +16,0 @@ } |
@@ -9,5 +9,5 @@ import { BluetoothDeviceInfo } from "../BluetoothDeviceInfo"; | ||
"Pivot", "Verge", "Cougar", "Sync"], | ||
[], | ||
["f000bb03-0451-4000-b000-000000000000"], | ||
{ tx: "f000c000-0451-4000-b000-000000000000", | ||
rx: "f000b000-0451-4000-b000-000000000000"}, | ||
{}, | ||
WeVibe.CreateInstance); | ||
@@ -14,0 +14,0 @@ |
@@ -1,7 +0,12 @@ | ||
export interface IBluetoothDeviceImpl { | ||
import { EventEmitter } from "events"; | ||
export interface IBluetoothDeviceImpl extends EventEmitter { | ||
Name: string; | ||
Id: string; | ||
WriteValue: (aCharacteristic: string, aValue: Uint8Array) => Promise<void>; | ||
WriteString: (aCharacteristic: string, aValue: string) => Promise<void>; | ||
ReadValue: (aCharacteristic: string) => Promise<BufferSource>; | ||
ReadString: (aCharacteristic: string) => Promise<string>; | ||
Subscribe: (aCharacteristic: string) => Promise<void>; | ||
Disconnect: () => Promise<void>; | ||
} |
@@ -6,2 +6,3 @@ import { ButtplugLogger } from "../../core/Logging"; | ||
import { EventEmitter } from "events"; | ||
import { StringDecoder } from "string_decoder"; | ||
@@ -24,5 +25,7 @@ export class WebBluetoothDevice extends EventEmitter implements IBluetoothDeviceImpl { | ||
private _notificationHandlers = new Map<string, (Event) => void>(); | ||
private _logger = ButtplugLogger.Logger; | ||
private _server: BluetoothRemoteGATTServer; | ||
private _service: BluetoothRemoteGATTService; | ||
private _decoder = new StringDecoder("utf-8"); | ||
private _characteristics: Map<string, BluetoothRemoteGATTCharacteristic> = | ||
@@ -48,9 +51,49 @@ new Map<string, BluetoothRemoteGATTCharacteristic>(); | ||
this._server = await this._device.gatt!.connect(); | ||
this._service = await this._server.getPrimaryService(this._deviceInfo.Services[0]); | ||
// We passed along a list of services we expect to work with all hardware as | ||
// part of the connection filters, so only those services will be found when | ||
// running getPrimaryServices | ||
const services = await this._server.getPrimaryServices(); | ||
if (services.length === 0) { | ||
this._logger.Error(`Cannot find gatt service to connect to on device ${this._device.name}`); | ||
throw new Error(`Cannot find gatt service to connect to on device ${this._device.name}`); | ||
} | ||
// For now, we assume we're only using one service on each device. This will | ||
// most likely change in the future. | ||
this._service = services[0]; | ||
// If the device info contains characteristic address and identity | ||
// information, use that to try and establish characteristic objects. | ||
for (const name of Object.getOwnPropertyNames(this._deviceInfo.Characteristics)) { | ||
this._characteristics.set(name, await this._service.getCharacteristic(this._deviceInfo.Characteristics[name])); | ||
} | ||
} | ||
// If no characteristics are present in the DeviceInfo block, we assume that | ||
// we're connecting to a simple rx/tx service, and can query to figure out | ||
// characteristics. Assume that the characteristics have tx/rx references. | ||
if (this._characteristics.entries.length === 0) { | ||
const characteristics = await this._service.getCharacteristics(); | ||
for (const char of characteristics) { | ||
if (char.properties.write || | ||
char.properties.writeWithoutResponse || | ||
char.properties.reliableWrite) { | ||
this._characteristics.set("tx", char); | ||
} else if (char.properties.read || | ||
char.properties.broadcast || | ||
char.properties.notify || | ||
char.properties.indicate) { | ||
this._characteristics.set("rx", char); | ||
} | ||
} | ||
} | ||
// If at this point we still don't have any characteristics, something is | ||
// wrong, error out. | ||
} | ||
public Disconnect = async (): Promise<void> => { | ||
for (const chr of this._notificationHandlers.keys()) { | ||
this.Unsubscribe(chr); | ||
} | ||
this._server.disconnect(); | ||
@@ -65,5 +108,9 @@ } | ||
public WriteString = async (aCharacteristic: string, aValue: string): Promise<void> => { | ||
return await this.WriteValue(aCharacteristic, Buffer.from(aValue)); | ||
} | ||
public WriteValue = async (aCharacteristic: string, aValue: Uint8Array): Promise<void> => { | ||
if (!this._characteristics.has(aCharacteristic)) { | ||
return; | ||
throw new Error("Tried to access wrong characteristic!"); | ||
} | ||
@@ -75,2 +122,7 @@ const chr = this._characteristics.get(aCharacteristic)!; | ||
public ReadString = async (aCharacteristic: string): Promise<string> => { | ||
const value = await this.ReadValue(aCharacteristic); | ||
return this._decoder.end(Buffer.from(value as ArrayBuffer)); | ||
} | ||
public ReadValue = async (aCharacteristic: string): Promise<BufferSource> => { | ||
@@ -84,2 +136,38 @@ if (!this._characteristics.has(aCharacteristic)) { | ||
} | ||
public Subscribe = async (aCharacteristic: string): Promise<void> => { | ||
if (!this._characteristics.has(aCharacteristic)) { | ||
throw new Error("Tried to access wrong characteristic!"); | ||
} | ||
if (this._notificationHandlers.has(aCharacteristic)) { | ||
throw new Error("Already listening on this characteristic!"); | ||
} | ||
const chr = this._characteristics.get(aCharacteristic)!; | ||
this._logger.Trace(`WebBluetoothDevice: ${this.constructor.name} subscribing to updates from ${chr.uuid}`); | ||
await chr.startNotifications(); | ||
this._notificationHandlers.set(aCharacteristic, (event: Event) => { | ||
this.CharacteristicValueChanged(event, aCharacteristic); | ||
}); | ||
chr.addEventListener("characteristicvaluechanged", this._notificationHandlers.get(aCharacteristic)!); | ||
} | ||
public Unsubscribe = async (aCharacteristic: string): Promise<void> => { | ||
if (!this._characteristics.has(aCharacteristic)) { | ||
throw new Error("Tried to access wrong characteristic!"); | ||
} | ||
if (!this._notificationHandlers.has(aCharacteristic)) { | ||
throw new Error("Not listening on this characteristic!"); | ||
} | ||
const chr = this._characteristics.get(aCharacteristic)!; | ||
this._logger.Trace(`WebBluetoothDevice: ${this.constructor.name} unsubscribing to updates from ${chr.uuid}`); | ||
chr.removeEventListener("characteristicvaluechanged", this._notificationHandlers.get(aCharacteristic)!); | ||
this._notificationHandlers.delete(aCharacteristic); | ||
await chr.stopNotifications(); | ||
} | ||
protected CharacteristicValueChanged = (aEvent: Event, aCharacteristic: string) => { | ||
// For some reason this EventTarget doesn't have a value prop? | ||
const eventValue = (aEvent.target! as BluetoothRemoteGATTCharacteristic).value; | ||
this.emit("characteristicvaluechanged", aCharacteristic, Buffer.from(eventValue!.buffer)); | ||
} | ||
} |
@@ -22,5 +22,10 @@ import { ButtplugLogger } from "../../core/Logging"; | ||
} | ||
for (const deviceNamePrefix of deviceInfo.NamePrefixes) { | ||
filters.filters.push({namePrefix: deviceNamePrefix}); | ||
} | ||
filters.optionalServices = [...filters.optionalServices, ...deviceInfo.Services]; | ||
} | ||
ButtplugLogger.Logger.Trace("Bluetooth filter set: " + filters); | ||
// At some point, we should use navigator.bluetooth.getAvailability() to | ||
@@ -75,2 +80,8 @@ // check whether we have a radio to use. However, no browser currently | ||
} | ||
for (const namePrefix of di.NamePrefixes) { | ||
if (aDevice.name!.indexOf(namePrefix) !== -1) { | ||
deviceInfo = di; | ||
break; | ||
} | ||
} | ||
} | ||
@@ -77,0 +88,0 @@ if (deviceInfo === null) { |
import { WebBluetoothMock, DeviceMock, CharacteristicMock, PrimaryServiceMock, GattMock } from "web-bluetooth-mock"; | ||
import { ButtplugLogger, ButtplugLogLevel } from "../src/core/Logging"; | ||
import { ButtplugClient } from "../src/client/Client"; | ||
import { BPTestClient, SetupTestSuite, WebBluetoothMockObject, MakeMockWebBluetoothDevice } from "./utils"; | ||
import { BPTestClient, SetupTestSuite, WebBluetoothMockObject, MakeMockWebBluetoothDevice, | ||
SetupLovenseTestDevice } from "./utils"; | ||
import { VibrateCmd, RotateCmd, SpeedSubcommand, LinearCmd, VectorSubcommand, FleshlightLaunchFW12Cmd, | ||
DeviceInfo, BluetoothDeviceInfo, SingleMotorVibrateCmd, RotateSubcommand, | ||
VorzeA10CycloneCmd } from "../src/index"; | ||
import { LovenseRev5 } from "../src/server/bluetooth/devices/Lovense"; | ||
VorzeA10CycloneCmd, ErrorClass } from "../src/index"; | ||
import { Lovense } from "../src/server/bluetooth/devices/Lovense"; | ||
import { WeVibe } from "../src/server/bluetooth/devices/WeVibe"; | ||
@@ -19,3 +20,3 @@ import { FleshlightLaunch } from "../src/server/bluetooth/devices/FleshlightLaunch"; | ||
let rej; | ||
let bp; | ||
let bp: ButtplugClient; | ||
let mockBT: WebBluetoothMockObject; | ||
@@ -39,8 +40,13 @@ let bluetooth: WebBluetoothMock; | ||
it("should convert lovense commands properly", async () => { | ||
await SetupDevice(LovenseRev5.DeviceInfo); | ||
await SetupDevice(Lovense.DeviceInfo); | ||
SetupLovenseTestDevice(mockBT); | ||
await bp.StartScanning(); | ||
await bp.StopScanning(); | ||
expect(bp.Devices[0].Name).toEqual("Lovense Domi v01"); | ||
jest.spyOn(mockBT.txChar, "writeValue"); | ||
await expect(bp.SendDeviceMessage(bp.Devices[0], new VibrateCmd([new SpeedSubcommand(0, 1), | ||
new SpeedSubcommand(0, 2)]))).rejects.toThrow(); | ||
await expect(bp.SendDeviceMessage(bp.Devices[0], | ||
new VibrateCmd([new SpeedSubcommand(0, 1), | ||
new SpeedSubcommand(1, 1)]))) | ||
.rejects | ||
.toHaveProperty("ErrorCode", ErrorClass.ERROR_DEVICE); | ||
await bp.SendDeviceMessage(bp.Devices[0], new VibrateCmd([new SpeedSubcommand(0, 1)])); | ||
@@ -54,2 +60,48 @@ expect(mockBT.txChar.writeValue).toBeCalledWith(Buffer.from("Vibrate:20;")); | ||
it("should convert lovense edge vibrate commands properly", async () => { | ||
await SetupDevice(Lovense.DeviceInfo); | ||
SetupLovenseTestDevice(mockBT, "P"); | ||
await bp.StartScanning(); | ||
await bp.StopScanning(); | ||
expect(bp.Devices[0].Name).toEqual("Lovense Edge v01"); | ||
jest.spyOn(mockBT.txChar, "writeValue"); | ||
await expect(bp.SendDeviceMessage(bp.Devices[0], | ||
new VibrateCmd([new SpeedSubcommand(0, 1), | ||
new SpeedSubcommand(1, 1), | ||
new SpeedSubcommand(2, 1)]))) | ||
.rejects | ||
.toHaveProperty("ErrorCode", ErrorClass.ERROR_DEVICE); | ||
await bp.SendDeviceMessage(bp.Devices[0], new VibrateCmd([new SpeedSubcommand(0, 1), new SpeedSubcommand(1, .5)])); | ||
expect(mockBT.txChar.writeValue).toBeCalledWith(Buffer.from("Vibrate1:20;")); | ||
expect(mockBT.txChar.writeValue).toBeCalledWith(Buffer.from("Vibrate2:10;")); | ||
await bp.SendDeviceMessage(bp.Devices[0], new SingleMotorVibrateCmd(.5)); | ||
expect(mockBT.txChar.writeValue).toBeCalledWith(Buffer.from("Vibrate1:10;")); | ||
expect(mockBT.txChar.writeValue).toBeCalledWith(Buffer.from("Vibrate2:10;")); | ||
await bp.StopAllDevices(); | ||
expect(mockBT.txChar.writeValue).toBeCalledWith(Buffer.from("Vibrate1:0;")); | ||
expect(mockBT.txChar.writeValue).toBeCalledWith(Buffer.from("Vibrate2:0;")); | ||
}); | ||
it("should convert lovense nora rotate commands properly", async () => { | ||
await SetupDevice(Lovense.DeviceInfo); | ||
SetupLovenseTestDevice(mockBT, "A"); | ||
await bp.StartScanning(); | ||
await bp.StopScanning(); | ||
expect(bp.Devices[0].Name).toEqual("Lovense Nora v01"); | ||
jest.spyOn(mockBT.txChar, "writeValue"); | ||
await expect(bp.SendDeviceMessage(bp.Devices[0], | ||
new RotateCmd([new RotateSubcommand(0, 1, true), | ||
new RotateSubcommand(1, 1, true)]))) | ||
.rejects | ||
.toHaveProperty("ErrorCode", ErrorClass.ERROR_DEVICE); | ||
await bp.SendDeviceMessage(bp.Devices[0], new RotateCmd([new RotateSubcommand(0, 1, false)])); | ||
expect(mockBT.txChar.writeValue).toBeCalledWith(Buffer.from("Rotate:20;")); | ||
await bp.SendDeviceMessage(bp.Devices[0], new RotateCmd([new RotateSubcommand(0, 0.5, true)])); | ||
expect(mockBT.txChar.writeValue).toBeCalledWith(Buffer.from("RotateChange;")); | ||
expect(mockBT.txChar.writeValue).toBeCalledWith(Buffer.from("Rotate:10;")); | ||
await bp.StopAllDevices(); | ||
expect(mockBT.txChar.writeValue).toBeCalledWith(Buffer.from("Vibrate:0;")); | ||
expect(mockBT.txChar.writeValue).toBeCalledWith(Buffer.from("Rotate:0;")); | ||
}); | ||
it("should convert wevibe commands properly", async () => { | ||
@@ -60,4 +112,7 @@ await SetupDevice(WeVibe.DeviceInfo); | ||
jest.spyOn(mockBT.txChar, "writeValue"); | ||
await expect(bp.SendDeviceMessage(bp.Devices[0], new VibrateCmd([new SpeedSubcommand(0, 1), | ||
new SpeedSubcommand(1, 1)]))).rejects.toThrow(); | ||
await expect(bp.SendDeviceMessage(bp.Devices[0], | ||
new VibrateCmd([new SpeedSubcommand(0, 1), | ||
new SpeedSubcommand(1, 1)]))) | ||
.rejects | ||
.toHaveProperty("ErrorCode", ErrorClass.ERROR_DEVICE); | ||
await bp.SendDeviceMessage(bp.Devices[0], new VibrateCmd([new SpeedSubcommand(0, 1)])); | ||
@@ -78,3 +133,5 @@ expect(mockBT.txChar.writeValue).toBeCalledWith(new Uint8Array([0x0f, 0x03, 0x00, 0xff, 0x00, 0x03, 0x00, 0x00])); | ||
new LinearCmd([new VectorSubcommand(0, 1, 1), | ||
new VectorSubcommand(1, 1, 1)]))).rejects.toThrow(); | ||
new VectorSubcommand(1, 1, 1)]))) | ||
.rejects | ||
.toHaveProperty("ErrorCode", ErrorClass.ERROR_DEVICE); | ||
await bp.SendDeviceMessage(bp.Devices[0], new FleshlightLaunchFW12Cmd(99, 99)); | ||
@@ -97,3 +154,5 @@ expect(mockBT.txChar.writeValue).toBeCalledWith(new Uint8Array([99, 99])); | ||
new RotateCmd([new RotateSubcommand(0, 1, true), | ||
new RotateSubcommand(1, 1, false)]))).rejects.toThrow(); | ||
new RotateSubcommand(1, 1, false)]))) | ||
.rejects | ||
.toHaveProperty("ErrorCode", ErrorClass.ERROR_DEVICE); | ||
await bp.SendDeviceMessage(bp.Devices[0], new RotateCmd([new RotateSubcommand(0, 1, true)])); | ||
@@ -100,0 +159,0 @@ expect(mockBT.txChar.writeValue).toBeCalledWith(new Uint8Array([0x01, 0x01, (100 | (0x80)) & 0xff])); |
@@ -107,3 +107,5 @@ import { Device, ButtplugClient, FromJSON, ButtplugLogger, CheckMessage, | ||
await expect(bp.SendDeviceMessage(bp.Devices[0], new Messages.KiirooCmd("2"))).rejects.toThrow(); | ||
await expect(bp.SendDeviceMessage(bp.Devices[0], new Messages.KiirooCmd("2"))) | ||
.rejects | ||
.toHaveProperty("ErrorCode", Messages.ErrorClass.ERROR_DEVICE); | ||
res(); | ||
@@ -118,3 +120,5 @@ }); | ||
bp.on("scanningfinished", async (x) => { | ||
await (expect(bp.SendDeviceMessage(bp.Devices[0], new Messages.SingleMotorVibrateCmd(50))).rejects.toThrow()); | ||
await expect(bp.SendDeviceMessage(bp.Devices[0], new Messages.SingleMotorVibrateCmd(50))) | ||
.rejects | ||
.toHaveProperty("ErrorCode", Messages.ErrorClass.ERROR_DEVICE); | ||
res(); | ||
@@ -149,5 +153,7 @@ }); | ||
await bplocal.ConnectLocal(); | ||
await expect(bplocal.StartScanning()).rejects.toThrow(); | ||
await expect(bplocal.StartScanning()) | ||
.rejects | ||
.toHaveProperty("ErrorCode", Messages.ErrorClass.ERROR_DEVICE); | ||
bplocal.Disconnect(); | ||
}); | ||
}); |
@@ -81,24 +81,2 @@ import * as Messages from "../src/core/Messages"; | ||
it("CreateSimple*Cmd tests", | ||
async () => { | ||
let res; | ||
let rej; | ||
const p = new Promise((resolve, reject) => { res = resolve; rej = reject; }); | ||
const connector = await SetupTestServer(); | ||
connector.Client.on("scanningfinished", () => { | ||
expect(CreateSimpleVibrateCmd(connector.Client.Devices[0], 0.5)) | ||
.toEqual(new Messages.VibrateCmd([new SpeedSubcommand(0, 0.5), | ||
new SpeedSubcommand(1, 0.5)])); | ||
expect(() => CreateSimpleVibrateCmd(connector.Client.Devices[1], 0.5)).toThrow(); | ||
expect(CreateSimpleLinearCmd(connector.Client.Devices[1], 0.5, 100)) | ||
.toEqual(new Messages.LinearCmd([new VectorSubcommand(0, 0.5, 100)])); | ||
expect(() => CreateSimpleLinearCmd(connector.Client.Devices[0], 0.5, 100)).toThrow(); | ||
expect(CreateSimpleRotateCmd(connector.Client.Devices[2], 0.5, true)) | ||
.toEqual(new Messages.RotateCmd([new RotateSubcommand(0, 0.5, true)])); | ||
expect(() => CreateSimpleRotateCmd(connector.Client.Devices[0], 0.5, true)).toThrow(); | ||
res(); | ||
}); | ||
await connector.Client.StartScanning(); | ||
return p; | ||
}); | ||
}); |
import { WebBluetoothMock, DeviceMock, CharacteristicMock, PrimaryServiceMock, GattMock } from "web-bluetooth-mock"; | ||
import { ButtplugLogger, ButtplugLogLevel } from "../src/core/Logging"; | ||
import { ButtplugClient } from "../src/client/Client"; | ||
import { BPTestClient, SetupTestSuite, WebBluetoothMockObject, MakeMockWebBluetoothDevice } from "./utils"; | ||
import { VibrateCmd, SpeedSubcommand } from "../src/index"; | ||
import { LovenseRev5 } from "../src/server/bluetooth/devices/Lovense"; | ||
import { BPTestClient, SetupTestSuite, WebBluetoothMockObject, MakeMockWebBluetoothDevice, | ||
SetupLovenseTestDevice } from "./utils"; | ||
import { VibrateCmd, SpeedSubcommand, ErrorClass } from "../src/index"; | ||
import { Lovense } from "../src/server/bluetooth/devices/Lovense"; | ||
@@ -14,3 +15,3 @@ SetupTestSuite(); | ||
let rej; | ||
let bp; | ||
let bp: ButtplugClient; | ||
let mockBT: WebBluetoothMockObject; | ||
@@ -21,4 +22,5 @@ let bluetooth: WebBluetoothMock; | ||
p = new Promise((resolve, reject) => { res = resolve; rej = reject; }); | ||
// Mock an actual buttplug (Lovense Hush)! | ||
mockBT = MakeMockWebBluetoothDevice(LovenseRev5.DeviceInfo); | ||
// We assume we're using a lovense device for all tests here so set it up. | ||
mockBT = MakeMockWebBluetoothDevice(Lovense.DeviceInfo); | ||
SetupLovenseTestDevice(mockBT); | ||
const g = global as any; | ||
@@ -70,6 +72,16 @@ g.navigator = g.navigator || {}; | ||
}; | ||
await expect(bp.StartScanning()).rejects.toThrow(); | ||
// Make sure we at least have the right error code. Id and message may vary. | ||
await expect(bp.StartScanning()).rejects.toHaveProperty("ErrorCode", ErrorClass.ERROR_DEVICE); | ||
return p; | ||
}); | ||
it("should subscribe on connect for lovense device, unsubscribe on disconnect", async () => { | ||
jest.spyOn(mockBT.rxChar, "startNotifications"); | ||
jest.spyOn(mockBT.rxChar, "stopNotifications"); | ||
await bp.StartScanning(); | ||
await bp.StopScanning(); | ||
expect(mockBT.rxChar.startNotifications).toBeCalled(); | ||
await bp.Disconnect(); | ||
expect(mockBT.rxChar.stopNotifications).toBeCalled(); | ||
}); | ||
}); |
@@ -32,7 +32,24 @@ import { ButtplugClient, CheckMessage, ButtplugServer, ButtplugEmbeddedServerConnector } from "../src/index"; | ||
export function SetupLovenseTestDevice(mockBT: WebBluetoothMockObject, deviceLetter: string = "W") { | ||
const oldWrite = mockBT.txChar.writeValue; | ||
mockBT.txChar.writeValue = async (): Promise<void> => { | ||
const infoBuf = Buffer.from(`${deviceLetter}:01:000000000000`); | ||
const arrBuf = new ArrayBuffer(infoBuf.length); | ||
// If we don't convert and load into a view, the buffer conversion later | ||
// won't work. | ||
const view = new Uint8Array(arrBuf); | ||
for (let i = 0; i < infoBuf.length; ++i) { | ||
view[i] = infoBuf[i]; | ||
} | ||
mockBT.rxChar.value = new DataView(arrBuf); | ||
mockBT.rxChar.dispatchEvent(new CustomEvent("characteristicvaluechanged")); | ||
mockBT.txChar.writeValue = oldWrite; | ||
}; | ||
} | ||
export function SetupTestSuite() { | ||
// None of our tests should take very long. | ||
jest.setTimeout(1000); | ||
process.on("unhandledRejection", (error) => { | ||
throw new Error(`Unhandled Promise rejection! ${error}`); | ||
process.on("unhandledRejection", (reason, p) => { | ||
throw new Error(`Unhandled Promise rejection!\n---\n${reason.stack}\n---\n`); | ||
}); | ||
@@ -45,3 +62,4 @@ } | ||
public service: PrimaryServiceMock, | ||
public txChar: CharacteristicMock) { | ||
public txChar: CharacteristicMock, | ||
public rxChar: CharacteristicMock) { | ||
} | ||
@@ -51,7 +69,35 @@ } | ||
export function MakeMockWebBluetoothDevice(deviceInfo: BluetoothDeviceInfo): WebBluetoothMockObject { | ||
const device = new DeviceMock(deviceInfo.Names[0], [deviceInfo.Services[0]]); | ||
let name: string; | ||
if (deviceInfo.Names.length > 0) { | ||
name = deviceInfo.Names[0]; | ||
} else if (deviceInfo.NamePrefixes.length > 0) { | ||
name = deviceInfo.NamePrefixes[0] + "-test"; | ||
} else { | ||
throw new Error("Cannot create mock device!"); | ||
} | ||
const device = new DeviceMock(name, [deviceInfo.Services[0]]); | ||
const gatt = device.gatt; | ||
const service = device.getServiceMock(deviceInfo.Services[0]); | ||
const tx = service.getCharacteristicMock((deviceInfo.Characteristics as any).tx); | ||
return new WebBluetoothMockObject(device, gatt, service, tx); | ||
let tx: CharacteristicMock; | ||
if (Object.keys(deviceInfo.Characteristics).indexOf("tx") !== -1) { | ||
tx = service.getCharacteristicMock((deviceInfo.Characteristics as any).tx); | ||
} else { | ||
// In this case, we are expected to query devices and find rx/tx | ||
// characteristics. Since this is a test and we have no devices, we can't do | ||
// that. Just make one up. | ||
tx = service.getCharacteristicMock("55555555-5555-5555-5555-555555555555"); | ||
tx.properties.write = true; | ||
tx.properties.writeWithoutResponse = true; | ||
} | ||
let rx: CharacteristicMock; | ||
if (Object.keys(deviceInfo.Characteristics).indexOf("rx") !== -1) { | ||
rx = service.getCharacteristicMock((deviceInfo.Characteristics as any).rx); | ||
} else { | ||
// In this case, we are expected to query devices and find rx/tx | ||
// characteristics. Since this is a test and we have no devices, we can't do | ||
// that. Just make one up. | ||
rx = service.getCharacteristicMock("55555556-5555-5555-5555-555555555555"); | ||
rx.properties.notify = true; | ||
} | ||
return new WebBluetoothMockObject(device, gatt, service, tx, rx); | ||
} | ||
@@ -58,0 +104,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
4138289
217
26724
237
Updatedajv@^6.5.1
Updatedjspanel4@^4.0.0