UNPKG

ttlock-sdk-js

Version:

JavaScript port of the TTLock Android SDK

427 lines (426 loc) 17.9 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.TTBluetoothDevice = void 0; const CommandEnvelope_1 = require("../api/CommandEnvelope"); const Lock_1 = require("../constant/Lock"); const timingUtil_1 = require("../util/timingUtil"); const TTDevice_1 = require("./TTDevice"); const CRLF = "0d0a"; const MTU = 20; class TTBluetoothDevice extends TTDevice_1.TTDevice { constructor(scanner) { super(); this.connected = false; this.incomingDataBuffer = Buffer.from([]); this.waitingForResponse = false; this.responses = []; this.scanner = scanner; } static createFromDevice(device, scanner) { const bDevice = new TTBluetoothDevice(scanner); bDevice.updateFromDevice(device); return bDevice; } updateFromDevice(device) { if (typeof device != "undefined") { if (typeof this.device != "undefined") { this.device.removeAllListeners(); } this.device = device; this.device.on("connected", this.onDeviceConnected.bind(this)); this.device.on("disconnected", this.onDeviceDisconnected.bind(this)); } if (typeof this.device != "undefined") { this.id = this.device.id; this.name = this.device.name; this.rssi = this.device.rssi; if (this.device.manufacturerData.length >= 15) { this.parseManufacturerData(this.device.manufacturerData); } } this.emit("updated"); } async connect() { if (typeof this.device != "undefined" && this.device.connectable) { // stop scan await this.scanner.stopScan(); if (await this.device.connect()) { // TODO: something happens here (disconnect) and it's stuck in limbo console.log("BLE Device reading basic info"); await this.readBasicInfo(); console.log("BLE Device read basic info"); const subscribed = await this.subscribe(); console.log("BLE Device subscribed"); if (!subscribed) { await this.device.disconnect(); return false; } else { this.connected = true; this.emit("connected"); return true; } } else { console.log("Connect failed"); } } else { console.log("Missing device or not connectable"); } return false; } async onDeviceConnected() { // await this.readBasicInfo(); // await this.subscribe(); // this.connected = true; // this.emit("connected"); // console.log("TTBluetoothDevice connected", this.device?.id); } async onDeviceDisconnected() { this.connected = false; // console.log("TTBluetoothDevice disconnected", this.device?.id); this.emit("disconnected"); } async readBasicInfo() { if (typeof this.device != "undefined") { console.log("BLE Device discover services start"); await this.device.discoverServices(); console.log("BLE Device discover services end"); // update some basic information let service; if (this.device.services.has("1800")) { service = this.device.services.get("1800"); if (typeof service != "undefined") { console.log("BLE Device read characteristics start"); await service.readCharacteristics(); console.log("BLE Device read characteristics end"); this.putCharacteristicValue(service, "2a00", "name"); } } if (this.device.services.has("180a")) { service = this.device.services.get("180a"); if (typeof service != "undefined") { console.log("BLE Device read characteristics start"); await service.readCharacteristics(); console.log("BLE Device read characteristics end"); this.putCharacteristicValue(service, "2a29", "manufacturer"); this.putCharacteristicValue(service, "2a24", "model"); this.putCharacteristicValue(service, "2a27", "hardware"); this.putCharacteristicValue(service, "2a26", "firmware"); } } } } async subscribe() { if (typeof this.device != "undefined") { let service; if (this.device.services.has("1910")) { service = this.device.services.get("1910"); } if (typeof service != "undefined") { await service.readCharacteristics(); if (service.characteristics.has("fff4")) { const characteristic = service.characteristics.get("fff4"); if (typeof characteristic != "undefined") { await characteristic.subscribe(); characteristic.on("dataRead", this.onIncomingData.bind(this)); // does not seem to be required // await characteristic.discoverDescriptors(); // const descriptor = characteristic.descriptors.get("2902"); // if (typeof descriptor != "undefined") { // console.log("Subscribing to descriptor notifications"); // await descriptor.writeValue(Buffer.from([0x01, 0x00])); // BE // // await descriptor.writeValue(Buffer.from([0x00, 0x01])); // LE // } return true; } } } } return false; } async sendCommand(command, waitForResponse = true, ignoreCrc = false) { var _a; if (this.waitingForResponse) { throw new Error("Command already in progress"); } if (this.responses.length > 0) { // should this be an error ? throw new Error("Unprocessed responses"); } const commandData = command.buildCommandBuffer(); if (commandData) { let data = Buffer.concat([ commandData, Buffer.from(CRLF, "hex") ]); // write with 20 bytes MTU const service = (_a = this.device) === null || _a === void 0 ? void 0 : _a.services.get("1910"); if (typeof service != undefined) { const characteristic = service === null || service === void 0 ? void 0 : service.characteristics.get("fff2"); if (typeof characteristic != "undefined") { if (waitForResponse) { let retry = 0; let crcs = []; let response; this.waitingForResponse = true; do { if (retry > 0) { // wait a bit before retry // console.log("Sleeping a bit"); await timingUtil_1.sleep(200); } const written = await this.writeCharacteristic(characteristic, data); if (!written) { this.waitingForResponse = false; // make sure we clear response buffer as a response could still have been // received between writing packets (before lock disconnects, on unstable network) this.responses = []; throw new Error("Unable to send data to lock"); } // wait for a response // console.log("Waiting for response"); let cycles = 0; while (this.responses.length == 0 && this.connected) { cycles++; await timingUtil_1.sleep(5); } // console.log("Waited for a response for", cycles, "=", cycles * 5, "ms"); if (!this.connected) { this.waitingForResponse = false; this.responses = []; throw new Error("Disconnected while waiting for response"); } response = this.responses.pop(); if (typeof response != "undefined") { crcs.push(response.getCrc()); } retry++; } while (typeof response == "undefined" || (!response.isCrcOk() && !ignoreCrc && retry <= 2)); this.waitingForResponse = false; if (!response.isCrcOk() && !ignoreCrc) { // check if all CRCs match and auto-ignore bad CRC if (crcs.length > 1) { for (let i = 1; i < crcs.length; i++) { if (crcs[i - 1] != crcs[i]) { throw new Error("Malformed response, bad CRC"); } } } else { throw new Error("Malformed response, bad CRC"); } } return response; } else { await this.writeCharacteristic(characteristic, data); } } } } } /** * * @param timeout Timeout to wait in ms */ async waitForResponse(timeout = 10000) { if (this.waitingForResponse) { throw new Error("Command already in progress"); } let response; this.waitingForResponse = true; console.log("Waiting for response"); let cycles = 0; const sleepPerCycle = 100; while (this.responses.length == 0 && cycles * sleepPerCycle < timeout) { cycles++; await timingUtil_1.sleep(sleepPerCycle); } console.log("Waited for a response for", cycles, "=", cycles * sleepPerCycle, "ms"); if (this.responses.length > 0) { response = this.responses.pop(); } this.waitingForResponse = false; return response; } async writeCharacteristic(characteristic, data) { if (process.env.TTLOCK_DEBUG_COMM == "1") { console.log("Sending command:", data.toString("hex")); } let index = 0; do { const remaining = data.length - index; const written = await characteristic.write(data.subarray(index, index + Math.min(MTU, remaining)), true); if (!written) { return false; } // await sleep(10); index += MTU; } while (index < data.length); return true; } onIncomingData(data) { this.incomingDataBuffer = Buffer.concat([this.incomingDataBuffer, data]); this.readDeviceResponse(); } readDeviceResponse() { if (this.incomingDataBuffer.length >= 2) { // check for CRLF at the end of data const ending = this.incomingDataBuffer.subarray(this.incomingDataBuffer.length - 2); if (ending.toString("hex") == CRLF) { // we have a command response if (process.env.TTLOCK_DEBUG_COMM == "1") { console.log("Received response:", this.incomingDataBuffer.toString("hex")); } try { const command = CommandEnvelope_1.CommandEnvelope.createFromRawData(this.incomingDataBuffer.subarray(0, this.incomingDataBuffer.length - 2)); if (this.waitingForResponse) { this.responses.push(command); } else { // discard unsolicited messages if CRC is not ok if (command.isCrcOk()) { this.emit("dataReceived", command); } } } catch (error) { // TODO: in case of a malformed response we should notify the waiting cycle and stop waiting console.error(error); } this.incomingDataBuffer = Buffer.from([]); } } } putCharacteristicValue(service, uuid, property) { const value = service.characteristics.get(uuid); if (typeof value != "undefined" && typeof value.lastValue != "undefined") { Reflect.set(this, property, value.lastValue.toString()); } } async disconnect() { var _a; if (await ((_a = this.device) === null || _a === void 0 ? void 0 : _a.disconnect())) { this.connected = false; } } parseManufacturerData(manufacturerData) { // TODO: check offset is within the limits of the Buffer // console.log(manufacturerData, manufacturerData.length) if (manufacturerData.length < 15) { throw new Error("Invalid manufacturer data length:" + manufacturerData.length.toString()); } var offset = 0; this.protocolType = manufacturerData.readInt8(offset++); this.protocolVersion = manufacturerData.readInt8(offset++); if (this.protocolType == 18 && this.protocolVersion == 25) { this.isDfuMode = true; return; } if (this.protocolType == -1 && this.protocolVersion == -1) { this.isDfuMode = true; return; } if (this.protocolType == 52 && this.protocolVersion == 18) { this.isWristband = true; } if (this.protocolType == 5 && this.protocolVersion == 3) { this.scene = manufacturerData.readInt8(offset++); } else { offset = 4; this.protocolType = manufacturerData.readInt8(offset++); this.protocolVersion = manufacturerData.readInt8(offset++); offset = 7; this.scene = manufacturerData.readInt8(offset++); } if (this.protocolType < 5 || Lock_1.LockVersion.getLockType(this) == Lock_1.LockType.LOCK_TYPE_V2S) { this.isRoomLock = true; return; } if (this.scene <= 3) { this.isRoomLock = true; } else { switch (this.scene) { case 4: { this.isGlassLock = true; break; } case 5: case 11: { this.isSafeLock = true; break; } case 6: { this.isBicycleLock = true; break; } case 7: { this.isLockcar = true; break; } case 8: { this.isPadLock = true; break; } case 9: { this.isCyLinder = true; break; } case 10: { if (this.protocolType == 5 && this.protocolVersion == 3) { this.isRemoteControlDevice = true; break; } break; } } } const params = manufacturerData.readInt8(offset); this.isUnlock = ((params & 0x1) == 0x1); this.hasEvents = ((params & 0x2) == 0x2); this.isSettingMode = ((params & 0x4) != 0x0); if (Lock_1.LockVersion.getLockType(this) == Lock_1.LockType.LOCK_TYPE_V3 || Lock_1.LockVersion.getLockType(this) == Lock_1.LockType.LOCK_TYPE_V3_CAR) { this.isTouch = ((params && 0x8) != 0x0); } else if (Lock_1.LockVersion.getLockType(this) == Lock_1.LockType.LOCK_TYPE_CAR) { this.isTouch = false; this.isLockcar = true; } if (this.isLockcar) { if (this.isUnlock) { if ((params & 0x10) == 0x10) { this.parkStatus = 3; } else { this.parkStatus = 2; } } else if ((params & 0x10) == 0x10) { this.parkStatus = 1; } else { this.parkStatus = 0; } } offset++; this.batteryCapacity = manufacturerData.readInt8(offset); // offset += 3 + 4; // Offset in original SDK is + 3, but in scans it's actually +4 offset = manufacturerData.length - 6; // let's just get the last 6 bytes const macBuf = manufacturerData.slice(offset, offset + 6); var macArr = []; macBuf.forEach((m) => { let hexByte = m.toString(16); if (hexByte.length < 2) { hexByte = "0" + hexByte; } macArr.push(hexByte); }); macArr.reverse(); this.address = macArr.join(':').toUpperCase(); } } exports.TTBluetoothDevice = TTBluetoothDevice;