tirecheck-device-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -0
- package/dist/index.cjs +431 -0
- package/dist/index.d.cts +68 -0
- package/dist/index.d.mts +68 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.mjs +425 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Tirecheck Device SDK
|
|
2
|
+
|
|
3
|
+
## Usage
|
|
4
|
+
|
|
5
|
+
First, install this library as a dependency to your application:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
pnpm add tirecheck-device-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Then, create an instance of this SDK. You will need to provide implementation for basic bluetooth methods on your
|
|
12
|
+
environment - this library is optimized for usage with `cordova-plugin-ble-central` on mobile devices, but you can
|
|
13
|
+
provide any other implementation:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
// tirecheckDeviceSdk.ts
|
|
17
|
+
import { createTirecheckDeviceSdk } from 'tirecheck-device-sdk'
|
|
18
|
+
|
|
19
|
+
export default createTirecheckDeviceSdk({
|
|
20
|
+
// See typescript definitions for more info
|
|
21
|
+
startScanWithOptions: ...
|
|
22
|
+
connect: ...
|
|
23
|
+
write: ...
|
|
24
|
+
read: ...
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
// OR, if you're using `cordova-plugin-ble-central`:
|
|
29
|
+
export default createTirecheckDeviceSdk(window.ble.withPromises)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Example library usage:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
const foundBridges = {}
|
|
36
|
+
|
|
37
|
+
tirecheckDeviceSdk.bluetooth.scanDevices(device => {
|
|
38
|
+
if(device.type === 'bridge') foundBridges[device.id] = device
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
await tirecheckDeviceSdk.bridge.connect(deviceId)
|
|
42
|
+
|
|
43
|
+
await tirecheckDeviceSdk.bridge.readVehicleSchema(deviceId)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Contributing
|
|
47
|
+
|
|
48
|
+
### Getting started
|
|
49
|
+
|
|
50
|
+
1. Install Node.js 20.0+
|
|
51
|
+
2. Clone this repo
|
|
52
|
+
3. Run `corepack enable` (sometimes `sudo corepack enable` is needed)
|
|
53
|
+
4. `pnpm dev`
|
|
54
|
+
|
|
55
|
+
That should open Vitest's UI. Feel free to add new functionality and tests.
|
|
56
|
+
|
|
57
|
+
### Test-driven development
|
|
58
|
+
|
|
59
|
+
For this project, we advice to follow TDD for every feature. That means that you first write failing test, and then add
|
|
60
|
+
implementation so that test passes.
|
|
61
|
+
|
|
62
|
+
Focus on one small improvement at a time - one failing test, then make it pass, then another failing test, etc. Mentally
|
|
63
|
+
this approach is much easier because you don't need to think about system as a whole
|
|
64
|
+
|
|
65
|
+
### Conventions
|
|
66
|
+
|
|
67
|
+
- use `deviceMeta` for storing generic device info and methods for processing advertisement for all supported devices
|
|
68
|
+
- this is to avoid importing full device services before we're connected to them
|
|
69
|
+
- use `devices/*` for exposed logic related to devices after they're connected. That includes methods `connect` and
|
|
70
|
+
`disconnect`.
|
|
71
|
+
- use `services/*` for logic shared between devices. Those files won't be exposed in the build.
|
|
72
|
+
- expose only top-level functions. So, expose `bridge.writeConfiguration` or `bridge.writeAxleSetup` instead of generic
|
|
73
|
+
`bridge.writeMessage`
|
|
74
|
+
- in device services, use verbs `read*` and `write*` for methods that primarily read/write to/from device
|
|
75
|
+
- use `on*` for "subscription" methods - e.g. `tirecheckDeviceSdk.bridge.onMeasurementReceived(callback)`
|
|
76
|
+
- for devices that expose additional info via advertisings, do not report them while advertising data is incomplete.
|
|
77
|
+
- don't use `this` keyword.
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
// Bad
|
|
81
|
+
export default {
|
|
82
|
+
foo() {},
|
|
83
|
+
bar() {
|
|
84
|
+
this.foo()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Good
|
|
89
|
+
export default {
|
|
90
|
+
foo,
|
|
91
|
+
bar() {
|
|
92
|
+
foo()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function foo() {}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Submitting changes
|
|
100
|
+
|
|
101
|
+
If you wish to include a new change, process is as follows:
|
|
102
|
+
|
|
103
|
+
- You need to have a Jira ticket related to your change
|
|
104
|
+
- Create a new branch from latest `main` that includes your name and ticket number from Jira, e.g.
|
|
105
|
+
`leonid-buneev/INF-1234`
|
|
106
|
+
- Commit and push your changes to this new branch.
|
|
107
|
+
- Create a Merge Request in https://tycgitlab.tyrecheck.com/leonid.buneev/tirecheck-device-sdk/-/merge_requests
|
|
108
|
+
- Wait for review
|
|
109
|
+
- After ticket is merged, new SDK version will be published to NPM automatically.
|
|
110
|
+
- Bump version of `tirecheck-device-sdk` in the `package.json` of your app to download latest SDK version.
|
|
111
|
+
|
|
112
|
+
## Support
|
|
113
|
+
|
|
114
|
+
You can contact tirecheck if you need support - admin@tirecheck.com
|
|
115
|
+
|
|
116
|
+
## Roadmap
|
|
117
|
+
|
|
118
|
+
[x] Initial structure
|
|
119
|
+
|
|
120
|
+
[] Full CAN bridge support
|
|
121
|
+
|
|
122
|
+
[] CAN Bridge Firmware Update support
|
|
123
|
+
|
|
124
|
+
[] FlexiGauge
|
|
125
|
+
|
|
126
|
+
[] TPMS Router
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
|
|
5
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
|
|
6
|
+
|
|
7
|
+
const ___default = /*#__PURE__*/_interopDefaultCompat(_);
|
|
8
|
+
|
|
9
|
+
let ble;
|
|
10
|
+
function setBleImplementation(bleImplementation) {
|
|
11
|
+
ble = bleImplementation;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const bridgeTools = {
|
|
15
|
+
// convertBytesToStructure(objStructure: BridgeCommandStructureProperties, payload: number[]) {
|
|
16
|
+
// const numArray = _.clone(payload)
|
|
17
|
+
// const keys = Object.keys(objStructure)
|
|
18
|
+
// const sumWithInitial = keys.reduce((accumulator, currentValue) => accumulator + objStructure[currentValue].size, 0)
|
|
19
|
+
// if (numArray.length !== sumWithInitial) {
|
|
20
|
+
// throw new Error('Cannot convert bytes to object')
|
|
21
|
+
// }
|
|
22
|
+
// const result: any = {}
|
|
23
|
+
// for (const key of keys) {
|
|
24
|
+
// const encodedData: number[] = numArray.splice(0, objStructure[key].size)
|
|
25
|
+
// result[key] = this.decodeData(encodedData, objStructure[key].display)
|
|
26
|
+
// }
|
|
27
|
+
// return result
|
|
28
|
+
// },
|
|
29
|
+
// convertStructureToBytes(objStructure: any, structurizedObj: any) {
|
|
30
|
+
// const keys = Object.keys(objStructure)
|
|
31
|
+
// const result: number[] = []
|
|
32
|
+
// for (const key of keys) {
|
|
33
|
+
// const encoded = this.encodeData(structurizedObj[key], objStructure[key].display)
|
|
34
|
+
// if (encoded.length < objStructure[key].size) {
|
|
35
|
+
// const _padArray = Array.from({ length: objStructure[key].size - encoded.length }, () => 0)
|
|
36
|
+
// encoded.push(..._padArray)
|
|
37
|
+
// }
|
|
38
|
+
// result.push(...encoded)
|
|
39
|
+
// }
|
|
40
|
+
// return result
|
|
41
|
+
// },
|
|
42
|
+
// convertAutoLearnObjectToBinary(data: Record<string, string>): Record<string, string> {
|
|
43
|
+
// const result: Record<string, string> = {}
|
|
44
|
+
// for (const key in data) {
|
|
45
|
+
// if (!key.includes('axle')) continue
|
|
46
|
+
// // Parse the value as hexadecimal (base 16)
|
|
47
|
+
// const value = parseInt(data[key], 16)
|
|
48
|
+
// // Convert to binary and pad with leading zeros to ensure 8 characters
|
|
49
|
+
// const binaryString = value.toString(2).padStart(8, '0')
|
|
50
|
+
// result[key] = binaryString
|
|
51
|
+
// }
|
|
52
|
+
// return result
|
|
53
|
+
// },
|
|
54
|
+
// decodeData(data: number[], displayUnits: undefined | 'ascii' | 'decimal') {
|
|
55
|
+
// const _data = _.clone(data)
|
|
56
|
+
// if (displayUnits === 'ascii') {
|
|
57
|
+
// return _data
|
|
58
|
+
// .filter(d => d)
|
|
59
|
+
// .map(x => String.fromCharCode(x))
|
|
60
|
+
// .join('')
|
|
61
|
+
// }
|
|
62
|
+
// const _reversedData = _data.reverse()
|
|
63
|
+
// if (displayUnits === 'decimal') {
|
|
64
|
+
// return Number(`0x${_reversedData.map(dec => this.decimalToHex(dec)).join('')}`.replace(/\r\n/g, ''))
|
|
65
|
+
// }
|
|
66
|
+
// return _reversedData.map(dec => this.decimalToHex(dec)).join('')
|
|
67
|
+
// },
|
|
68
|
+
// encodeData(data: number | string, displayUnits: undefined | 'ascii' | 'decimal'): number[] {
|
|
69
|
+
// const _data = _.clone(data)
|
|
70
|
+
// if (displayUnits === 'ascii') {
|
|
71
|
+
// return (_data as string).split('').map((v, i) => (_data as string).charCodeAt(i))
|
|
72
|
+
// }
|
|
73
|
+
// if (displayUnits === 'decimal') {
|
|
74
|
+
// return this.hexToDecimalArray(this.decimalToHex(_data as number)).reverse()
|
|
75
|
+
// }
|
|
76
|
+
// return this.hexToDecimalArray(_data as string).reverse()
|
|
77
|
+
// },
|
|
78
|
+
// getBridgeId(device: BluetoothDeviceBridge) {
|
|
79
|
+
// // [244,177, 0, 34,123, 155] => F4B100227B9B
|
|
80
|
+
// return (
|
|
81
|
+
// device.advertisingData.macAddress
|
|
82
|
+
// ?.map(n => this.decimalToHex(n))
|
|
83
|
+
// .reverse()
|
|
84
|
+
// .join('')
|
|
85
|
+
// .toUpperCase() || ''
|
|
86
|
+
// )
|
|
87
|
+
// },
|
|
88
|
+
// pkcs(array: any[], length: number) {
|
|
89
|
+
// const pkcsValue = length - array.length
|
|
90
|
+
// if (pkcsValue > 0) {
|
|
91
|
+
// return [...array, ...new Array(pkcsValue).fill(pkcsValue)]
|
|
92
|
+
// }
|
|
93
|
+
// return array
|
|
94
|
+
// },
|
|
95
|
+
decimalToHex(decimal) {
|
|
96
|
+
const hex = decimal.toString(16);
|
|
97
|
+
return hex.padStart(2, "0");
|
|
98
|
+
},
|
|
99
|
+
// hexToDecimalArray(hex: string): number[] {
|
|
100
|
+
// return hex.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || []
|
|
101
|
+
// },
|
|
102
|
+
getFwVersion(payload) {
|
|
103
|
+
if (payload?.length !== 3) {
|
|
104
|
+
console.warn("Could not process FwVersion ", payload);
|
|
105
|
+
return "";
|
|
106
|
+
}
|
|
107
|
+
return payload.reduce((acc, current) => {
|
|
108
|
+
const value = this.decimalToHex(current);
|
|
109
|
+
acc.push(value[0] === "0" ? value[1] : value);
|
|
110
|
+
return acc;
|
|
111
|
+
}, []).join(".");
|
|
112
|
+
}
|
|
113
|
+
// barToKpaByte(value?: number) {
|
|
114
|
+
// if (!value) return 0
|
|
115
|
+
// return _.round(((value + 1) * 100) / 5.0625)
|
|
116
|
+
// },
|
|
117
|
+
// kpaByteToBar(value?: number, decrementValue = 1) {
|
|
118
|
+
// if (!_.isNumber(value)) {
|
|
119
|
+
// throw new TypeError('Value has to be a number')
|
|
120
|
+
// }
|
|
121
|
+
// const rawBar = (value * 5.0625) / 100
|
|
122
|
+
// return Math.max(rawBar - decrementValue, 0)
|
|
123
|
+
// },
|
|
124
|
+
// getMacAddressIfOtaAndIos(device: BluetoothDeviceBridge) {
|
|
125
|
+
// const uint8 = new Uint8Array(device.advertising.kCBAdvDataLeBluetoothDeviceAddress)
|
|
126
|
+
// const macAddressArray = Array.from(uint8)
|
|
127
|
+
// .reverse()
|
|
128
|
+
// .slice(0, 6)
|
|
129
|
+
// .map(x => this.decimalToHex(x).toUpperCase())
|
|
130
|
+
// return macAddressArray.join('')
|
|
131
|
+
// },
|
|
132
|
+
// isVersionGreaterThan(version: string) {
|
|
133
|
+
// if (version?.length !== 5) {
|
|
134
|
+
// throw new Error('Invalid version format'.t())
|
|
135
|
+
// }
|
|
136
|
+
// if (bluetoothStore?.connectedBridge?.advertisingData?.fwVersion) {
|
|
137
|
+
// return bluetoothStore?.connectedBridge?.advertisingData?.fwVersion > version
|
|
138
|
+
// }
|
|
139
|
+
// return false
|
|
140
|
+
// },
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const bridgeAdvertisingParser = {
|
|
144
|
+
getDeviceInfoFromAdvertising(device) {
|
|
145
|
+
const adversitingType = getAdvertisingType(device);
|
|
146
|
+
if (adversitingType !== "connectable")
|
|
147
|
+
return void 0;
|
|
148
|
+
const advertisingData = getAdvertisingData({ advertising: device.advertising, deviceName: device.name });
|
|
149
|
+
const bridgeId = advertisingData?.macAddress?.map((n) => bridgeTools.decimalToHex(n)).reverse().join("").toUpperCase() || "";
|
|
150
|
+
const vin = String.fromCharCode(...advertisingData?.vinNum || []).split("\0").join("");
|
|
151
|
+
return { id: device.id, name: device.name, bridgeId, vin, advertisingData, device };
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
function getAdvertisingType(device) {
|
|
155
|
+
if (device.advertising?.kCBAdvDataIsConnectable) {
|
|
156
|
+
return "connectable";
|
|
157
|
+
}
|
|
158
|
+
const numberArray = Array.from(new Uint8Array(device.advertising));
|
|
159
|
+
if (___default.isEqual(numberArray.slice(0, 4), [2, 1, 6, 7])) {
|
|
160
|
+
return "connectable";
|
|
161
|
+
}
|
|
162
|
+
if (numberArray[3] === 26) {
|
|
163
|
+
return "krone";
|
|
164
|
+
}
|
|
165
|
+
if (___default.isEqual(numberArray.slice(0, 4), [2, 1, 6, 3])) {
|
|
166
|
+
return "measurement";
|
|
167
|
+
}
|
|
168
|
+
return "unknown";
|
|
169
|
+
}
|
|
170
|
+
function getAdvertisingData({ advertising, deviceName }) {
|
|
171
|
+
const isKrone = deviceName === "030321";
|
|
172
|
+
const uint8 = new Uint8Array(
|
|
173
|
+
advertising.kCBAdvDataIsConnectable ? advertising.kCBAdvDataManufacturerData : advertising
|
|
174
|
+
);
|
|
175
|
+
const adv = Array.from(uint8);
|
|
176
|
+
const advertisingData = advertising.kCBAdvDataIsConnectable ? processIosAdvertising(adv) : processAndroidAdvertising(adv, isKrone);
|
|
177
|
+
if (!advertisingData || !advertisingData.macAddress.length || !advertisingData.randomAdvNumber.length || !advertisingData.vinNum.length) {
|
|
178
|
+
throw new Error("Device not recognized");
|
|
179
|
+
}
|
|
180
|
+
return advertisingData;
|
|
181
|
+
}
|
|
182
|
+
function processAndroidAdvertising(adv, isKrone) {
|
|
183
|
+
const advertisingData = {
|
|
184
|
+
configVersion: isKrone ? 17 : 1,
|
|
185
|
+
macAddress: [],
|
|
186
|
+
randomAdvNumber: [],
|
|
187
|
+
vinNum: [],
|
|
188
|
+
fwVersion: void 0,
|
|
189
|
+
timeFromStart: void 0
|
|
190
|
+
};
|
|
191
|
+
do {
|
|
192
|
+
const length = adv[0];
|
|
193
|
+
const identificator = adv[1];
|
|
194
|
+
const messageType = adv[4];
|
|
195
|
+
const packet = adv.splice(0, length + 1);
|
|
196
|
+
if (!length) {
|
|
197
|
+
adv = [];
|
|
198
|
+
}
|
|
199
|
+
if (messageType === 3 && identificator === 255) {
|
|
200
|
+
advertisingData.macAddress = packet.slice(5, 11);
|
|
201
|
+
advertisingData.vinNum = packet.slice(-17);
|
|
202
|
+
}
|
|
203
|
+
if (messageType === 1 && identificator === 255) {
|
|
204
|
+
advertisingData.randomAdvNumber = packet.slice(-8);
|
|
205
|
+
}
|
|
206
|
+
if (messageType === 4 && identificator === 255) {
|
|
207
|
+
advertisingData.randomAdvNumber = packet.slice(5, 13);
|
|
208
|
+
advertisingData.fwVersion = bridgeTools.getFwVersion(packet.slice(13, 16));
|
|
209
|
+
advertisingData.configVersion = packet[16];
|
|
210
|
+
advertisingData.timeFromStart = packet[17];
|
|
211
|
+
}
|
|
212
|
+
} while (adv.length);
|
|
213
|
+
return advertisingData;
|
|
214
|
+
}
|
|
215
|
+
function processIosAdvertising(adv, isKrone) {
|
|
216
|
+
if (adv.length < 34)
|
|
217
|
+
return void 0;
|
|
218
|
+
if (adv.length === 35) {
|
|
219
|
+
adv.slice(3, 11);
|
|
220
|
+
adv.slice(12, 18);
|
|
221
|
+
adv.slice(18, 35);
|
|
222
|
+
}
|
|
223
|
+
if (adv.length === 40) {
|
|
224
|
+
adv.slice(3, 11);
|
|
225
|
+
bridgeTools.getFwVersion(adv.slice(11, 14));
|
|
226
|
+
adv[14];
|
|
227
|
+
adv[15];
|
|
228
|
+
adv.slice(17, 23);
|
|
229
|
+
adv.slice(23, 40);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const deviceMeta = {
|
|
234
|
+
bridge: {
|
|
235
|
+
nameRegex: /(030303|030321)/,
|
|
236
|
+
characteristic: {
|
|
237
|
+
serviceId: "4880c12c-fdcb-4077-8920-a450d7f9b907",
|
|
238
|
+
// message from device
|
|
239
|
+
characteristicId: "fec26ec4-6d71-4442-9f81-55bc21d658d7"
|
|
240
|
+
},
|
|
241
|
+
capabilities: [],
|
|
242
|
+
manufacturerId: 2978,
|
|
243
|
+
mtu: 256,
|
|
244
|
+
getDeviceInfoFromAdvertising: bridgeAdvertisingParser.getDeviceInfoFromAdvertising
|
|
245
|
+
},
|
|
246
|
+
bridgeOta: {
|
|
247
|
+
nameRegex: /CAN BLE BRDG OTA.*/,
|
|
248
|
+
characteristic: {
|
|
249
|
+
serviceId: "4880c12c-fdcb-4077-8920-a450d7f9b907",
|
|
250
|
+
characteristicId: "fec26ec4-6d71-4442-9f81-55bc21d658d7"
|
|
251
|
+
},
|
|
252
|
+
capabilities: [],
|
|
253
|
+
getDeviceInfoFromAdvertising: () => {
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
flexiGaugeTpms: {
|
|
257
|
+
nameRegex: /Flexi.*/,
|
|
258
|
+
characteristic: {
|
|
259
|
+
serviceId: "4880c12c-fdcb-4077-8920-a450d7f9b907",
|
|
260
|
+
// message from device
|
|
261
|
+
characteristicId: "fec26ec4-6d71-4442-9f81-55bc21d658d6"
|
|
262
|
+
},
|
|
263
|
+
getDeviceInfoFromAdvertising: () => {
|
|
264
|
+
},
|
|
265
|
+
// Do we need it here?
|
|
266
|
+
capabilities: [
|
|
267
|
+
{
|
|
268
|
+
id: "td",
|
|
269
|
+
// processFn: processTreadDepth,
|
|
270
|
+
regex: /^\*TD.*/
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
id: "button",
|
|
274
|
+
// processFn: processButtonPress,
|
|
275
|
+
regex: /^\*[UDLRC] $/
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
id: "tpms",
|
|
279
|
+
// processFn: processTpms,
|
|
280
|
+
regex: /^\*TPMS.*/
|
|
281
|
+
}
|
|
282
|
+
],
|
|
283
|
+
reconnect: true
|
|
284
|
+
// Do we need it here?
|
|
285
|
+
},
|
|
286
|
+
pressureStick: {
|
|
287
|
+
nameRegex: /Pressure Stick.*/,
|
|
288
|
+
characteristic: {
|
|
289
|
+
serviceId: "4880c12c-fdcb-4077-8920-a450d7f9b907",
|
|
290
|
+
characteristicId: "fec26ec4-6d71-4442-9f81-55bc21d658d6"
|
|
291
|
+
},
|
|
292
|
+
getDeviceInfoFromAdvertising: () => {
|
|
293
|
+
},
|
|
294
|
+
capabilities: [
|
|
295
|
+
{
|
|
296
|
+
id: "pressure",
|
|
297
|
+
// processFn: processPressure,
|
|
298
|
+
regex: /P([0-9.]+)mBar/
|
|
299
|
+
}
|
|
300
|
+
// only pressure is needed for initial implementation, uncomment tpms functionality when needed
|
|
301
|
+
// {
|
|
302
|
+
// id: 'tpms',
|
|
303
|
+
// processFn: processTpms,
|
|
304
|
+
// regex: /^\*TPMS/,
|
|
305
|
+
// },
|
|
306
|
+
]
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const checkUnreachableDevicesTimeouts = {};
|
|
311
|
+
const checkConnectedStateIntervals = {};
|
|
312
|
+
const deviceAdvertisingCallbacks = [];
|
|
313
|
+
const deviceUpdateCallbacks = [];
|
|
314
|
+
const deviceUnreachableCallbacks = [];
|
|
315
|
+
const bluetooth = {
|
|
316
|
+
/** Triggered when "scanDevices" detects device supported by SDK */
|
|
317
|
+
onDeviceAdvertising(deviceAdvertisingCallback) {
|
|
318
|
+
deviceAdvertisingCallbacks.push(deviceAdvertisingCallback);
|
|
319
|
+
},
|
|
320
|
+
onDeviceUpdate(deviceUpdateCallback) {
|
|
321
|
+
deviceUpdateCallbacks.push(deviceUpdateCallback);
|
|
322
|
+
},
|
|
323
|
+
onDeviceUnreachable(deviceUnreachableCallback) {
|
|
324
|
+
deviceUnreachableCallbacks.push(deviceUnreachableCallback);
|
|
325
|
+
},
|
|
326
|
+
async scanDevices(services = []) {
|
|
327
|
+
await ble.stopScan();
|
|
328
|
+
ble.startScanWithOptions(
|
|
329
|
+
services,
|
|
330
|
+
{
|
|
331
|
+
reportDuplicates: true,
|
|
332
|
+
matchMode: "aggressive",
|
|
333
|
+
scanMode: "lowLatency",
|
|
334
|
+
numOfMatches: "max",
|
|
335
|
+
callbackType: "all",
|
|
336
|
+
reportDelay: 0
|
|
337
|
+
},
|
|
338
|
+
(device) => {
|
|
339
|
+
const processedDevice = processDevice(device);
|
|
340
|
+
if (!processedDevice)
|
|
341
|
+
return;
|
|
342
|
+
for (const c of deviceAdvertisingCallbacks) {
|
|
343
|
+
c(processedDevice);
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
(e) => console.error("ble.startScanWithOptions error:", e)
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
function processDevice(device) {
|
|
351
|
+
const uint8 = new Uint8Array(device.advertising);
|
|
352
|
+
const adv = Array.from(uint8);
|
|
353
|
+
let name = device.advertising.kCBAdvDataLocalName || device.name;
|
|
354
|
+
if (!name || name === "OTA") {
|
|
355
|
+
name = getDeviceNameFromAdvertising(adv);
|
|
356
|
+
}
|
|
357
|
+
const deviceType = getDeviceTypeFromName(name);
|
|
358
|
+
if (!deviceType) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
let processedDevice;
|
|
362
|
+
try {
|
|
363
|
+
processedDevice = deviceMeta[deviceType].getDeviceInfoFromAdvertising({ ...device, name });
|
|
364
|
+
if (!processedDevice)
|
|
365
|
+
return;
|
|
366
|
+
} catch (e) {
|
|
367
|
+
console.warn("Error processing advertising", e);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
refreshUnreachableTimeouts(processedDevice.id);
|
|
371
|
+
monitorConnectedState(processedDevice.id);
|
|
372
|
+
return processedDevice;
|
|
373
|
+
}
|
|
374
|
+
function getDeviceTypeFromName(name) {
|
|
375
|
+
let deviceType;
|
|
376
|
+
for (const key in deviceMeta) {
|
|
377
|
+
const meta = deviceMeta[key];
|
|
378
|
+
if (meta.nameRegex.test(name)) {
|
|
379
|
+
deviceType = key;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return deviceType;
|
|
383
|
+
}
|
|
384
|
+
function monitorConnectedState(deviceId) {
|
|
385
|
+
if (checkConnectedStateIntervals[deviceId])
|
|
386
|
+
return;
|
|
387
|
+
checkConnectedStateIntervals[deviceId] = setInterval(async () => {
|
|
388
|
+
let isConnected = false;
|
|
389
|
+
try {
|
|
390
|
+
await ble.isConnected(deviceId);
|
|
391
|
+
isConnected = true;
|
|
392
|
+
} catch {
|
|
393
|
+
isConnected = false;
|
|
394
|
+
}
|
|
395
|
+
for (const c of deviceUpdateCallbacks) {
|
|
396
|
+
c({ deviceId, isConnected });
|
|
397
|
+
}
|
|
398
|
+
}, 1e3);
|
|
399
|
+
}
|
|
400
|
+
function refreshUnreachableTimeouts(deviceId) {
|
|
401
|
+
if (checkUnreachableDevicesTimeouts[deviceId]) {
|
|
402
|
+
clearTimeout(checkUnreachableDevicesTimeouts[deviceId]);
|
|
403
|
+
}
|
|
404
|
+
checkUnreachableDevicesTimeouts[deviceId] = setTimeout(() => {
|
|
405
|
+
for (const c of deviceUnreachableCallbacks) {
|
|
406
|
+
c(deviceId);
|
|
407
|
+
}
|
|
408
|
+
if (checkConnectedStateIntervals[deviceId]) {
|
|
409
|
+
clearInterval(checkConnectedStateIntervals[deviceId]);
|
|
410
|
+
delete checkConnectedStateIntervals[deviceId];
|
|
411
|
+
}
|
|
412
|
+
}, 6e4);
|
|
413
|
+
}
|
|
414
|
+
function getDeviceNameFromAdvertising(adv) {
|
|
415
|
+
const advertising = ___default.clone(adv);
|
|
416
|
+
const messageType = advertising[4];
|
|
417
|
+
const packet = advertising.slice(5, 11);
|
|
418
|
+
if (messageType === 9) {
|
|
419
|
+
return packet.map((x) => String.fromCharCode(x)).join("");
|
|
420
|
+
}
|
|
421
|
+
return "";
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function createTirecheckDeviceSdk(bleImplementation) {
|
|
425
|
+
setBleImplementation(bleImplementation);
|
|
426
|
+
return {
|
|
427
|
+
bluetooth
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
exports.createTirecheckDeviceSdk = createTirecheckDeviceSdk;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
interface BleImplementation {
|
|
2
|
+
startScanWithOptions: (services: string[], options: StartScanOptions, success: (data: PeripheralData) => any, failure?: (error: string) => any) => void;
|
|
3
|
+
stopScan: () => Promise<void>;
|
|
4
|
+
/** Returns a resolved promise if the device is connected,
|
|
5
|
+
otherwise returns rejected promise if the device is not connected */
|
|
6
|
+
isConnected: ((deviceId: string) => Promise<void>) & ((deviceId: string, rejectWhenDisconnected: false) => Promise<boolean>);
|
|
7
|
+
}
|
|
8
|
+
interface StartScanOptions {
|
|
9
|
+
scanMode?: 'lowPower' | 'balanced' | 'lowLatency' | 'opportunistic';
|
|
10
|
+
callbackType?: 'all' | 'first' | 'lost';
|
|
11
|
+
matchMode?: 'aggressive' | 'sticky';
|
|
12
|
+
numOfMatches?: 'one' | 'few' | 'max';
|
|
13
|
+
phy?: '1m' | 'coded' | 'all';
|
|
14
|
+
legacy?: boolean;
|
|
15
|
+
reportDelay?: number;
|
|
16
|
+
forceScanFilter?: number;
|
|
17
|
+
reportDuplicates?: boolean;
|
|
18
|
+
/** Scanning duration in seconds */
|
|
19
|
+
duration?: number;
|
|
20
|
+
}
|
|
21
|
+
interface PeripheralData {
|
|
22
|
+
name: string;
|
|
23
|
+
id: string;
|
|
24
|
+
rssi: number;
|
|
25
|
+
advertising: ArrayBuffer | any;
|
|
26
|
+
connectable?: boolean;
|
|
27
|
+
state: PeripheralState;
|
|
28
|
+
}
|
|
29
|
+
type PeripheralState = 'disconnected' | 'disconnecting' | 'connecting' | 'connected';
|
|
30
|
+
|
|
31
|
+
type BleDevice = BleBridge;
|
|
32
|
+
interface BleBridge {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
bridgeId: string;
|
|
36
|
+
vin: string;
|
|
37
|
+
advertisingData: BleBridgeAdvertisingData;
|
|
38
|
+
device: PeripheralData;
|
|
39
|
+
}
|
|
40
|
+
interface BleBridgeAdvertisingData {
|
|
41
|
+
randomAdvNumber: number[];
|
|
42
|
+
vinNum: number[];
|
|
43
|
+
macAddress: number[];
|
|
44
|
+
fwVersion?: string;
|
|
45
|
+
/**
|
|
46
|
+
* 0x01 - TYC New Security with development keys
|
|
47
|
+
* 0x02 - TYC New Security with production keys
|
|
48
|
+
* 0x10 - Krone New Security with development keys
|
|
49
|
+
* 0x11 - Krone New Security with production keys
|
|
50
|
+
* 0x00 - predecessors (old security)
|
|
51
|
+
*/
|
|
52
|
+
configVersion: number;
|
|
53
|
+
timeFromStart?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
declare function createTirecheckDeviceSdk(bleImplementation: BleImplementation): {
|
|
57
|
+
bluetooth: {
|
|
58
|
+
onDeviceAdvertising(deviceAdvertisingCallback: (device: BleDevice) => void): void;
|
|
59
|
+
onDeviceUpdate(deviceUpdateCallback: (update: {
|
|
60
|
+
deviceId: string;
|
|
61
|
+
isConnected: boolean;
|
|
62
|
+
}) => void): void;
|
|
63
|
+
onDeviceUnreachable(deviceUnreachableCallback: (deviceId: string) => void): void;
|
|
64
|
+
scanDevices(services?: string[]): Promise<void>;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export { createTirecheckDeviceSdk };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
interface BleImplementation {
|
|
2
|
+
startScanWithOptions: (services: string[], options: StartScanOptions, success: (data: PeripheralData) => any, failure?: (error: string) => any) => void;
|
|
3
|
+
stopScan: () => Promise<void>;
|
|
4
|
+
/** Returns a resolved promise if the device is connected,
|
|
5
|
+
otherwise returns rejected promise if the device is not connected */
|
|
6
|
+
isConnected: ((deviceId: string) => Promise<void>) & ((deviceId: string, rejectWhenDisconnected: false) => Promise<boolean>);
|
|
7
|
+
}
|
|
8
|
+
interface StartScanOptions {
|
|
9
|
+
scanMode?: 'lowPower' | 'balanced' | 'lowLatency' | 'opportunistic';
|
|
10
|
+
callbackType?: 'all' | 'first' | 'lost';
|
|
11
|
+
matchMode?: 'aggressive' | 'sticky';
|
|
12
|
+
numOfMatches?: 'one' | 'few' | 'max';
|
|
13
|
+
phy?: '1m' | 'coded' | 'all';
|
|
14
|
+
legacy?: boolean;
|
|
15
|
+
reportDelay?: number;
|
|
16
|
+
forceScanFilter?: number;
|
|
17
|
+
reportDuplicates?: boolean;
|
|
18
|
+
/** Scanning duration in seconds */
|
|
19
|
+
duration?: number;
|
|
20
|
+
}
|
|
21
|
+
interface PeripheralData {
|
|
22
|
+
name: string;
|
|
23
|
+
id: string;
|
|
24
|
+
rssi: number;
|
|
25
|
+
advertising: ArrayBuffer | any;
|
|
26
|
+
connectable?: boolean;
|
|
27
|
+
state: PeripheralState;
|
|
28
|
+
}
|
|
29
|
+
type PeripheralState = 'disconnected' | 'disconnecting' | 'connecting' | 'connected';
|
|
30
|
+
|
|
31
|
+
type BleDevice = BleBridge;
|
|
32
|
+
interface BleBridge {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
bridgeId: string;
|
|
36
|
+
vin: string;
|
|
37
|
+
advertisingData: BleBridgeAdvertisingData;
|
|
38
|
+
device: PeripheralData;
|
|
39
|
+
}
|
|
40
|
+
interface BleBridgeAdvertisingData {
|
|
41
|
+
randomAdvNumber: number[];
|
|
42
|
+
vinNum: number[];
|
|
43
|
+
macAddress: number[];
|
|
44
|
+
fwVersion?: string;
|
|
45
|
+
/**
|
|
46
|
+
* 0x01 - TYC New Security with development keys
|
|
47
|
+
* 0x02 - TYC New Security with production keys
|
|
48
|
+
* 0x10 - Krone New Security with development keys
|
|
49
|
+
* 0x11 - Krone New Security with production keys
|
|
50
|
+
* 0x00 - predecessors (old security)
|
|
51
|
+
*/
|
|
52
|
+
configVersion: number;
|
|
53
|
+
timeFromStart?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
declare function createTirecheckDeviceSdk(bleImplementation: BleImplementation): {
|
|
57
|
+
bluetooth: {
|
|
58
|
+
onDeviceAdvertising(deviceAdvertisingCallback: (device: BleDevice) => void): void;
|
|
59
|
+
onDeviceUpdate(deviceUpdateCallback: (update: {
|
|
60
|
+
deviceId: string;
|
|
61
|
+
isConnected: boolean;
|
|
62
|
+
}) => void): void;
|
|
63
|
+
onDeviceUnreachable(deviceUnreachableCallback: (deviceId: string) => void): void;
|
|
64
|
+
scanDevices(services?: string[]): Promise<void>;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export { createTirecheckDeviceSdk };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
interface BleImplementation {
|
|
2
|
+
startScanWithOptions: (services: string[], options: StartScanOptions, success: (data: PeripheralData) => any, failure?: (error: string) => any) => void;
|
|
3
|
+
stopScan: () => Promise<void>;
|
|
4
|
+
/** Returns a resolved promise if the device is connected,
|
|
5
|
+
otherwise returns rejected promise if the device is not connected */
|
|
6
|
+
isConnected: ((deviceId: string) => Promise<void>) & ((deviceId: string, rejectWhenDisconnected: false) => Promise<boolean>);
|
|
7
|
+
}
|
|
8
|
+
interface StartScanOptions {
|
|
9
|
+
scanMode?: 'lowPower' | 'balanced' | 'lowLatency' | 'opportunistic';
|
|
10
|
+
callbackType?: 'all' | 'first' | 'lost';
|
|
11
|
+
matchMode?: 'aggressive' | 'sticky';
|
|
12
|
+
numOfMatches?: 'one' | 'few' | 'max';
|
|
13
|
+
phy?: '1m' | 'coded' | 'all';
|
|
14
|
+
legacy?: boolean;
|
|
15
|
+
reportDelay?: number;
|
|
16
|
+
forceScanFilter?: number;
|
|
17
|
+
reportDuplicates?: boolean;
|
|
18
|
+
/** Scanning duration in seconds */
|
|
19
|
+
duration?: number;
|
|
20
|
+
}
|
|
21
|
+
interface PeripheralData {
|
|
22
|
+
name: string;
|
|
23
|
+
id: string;
|
|
24
|
+
rssi: number;
|
|
25
|
+
advertising: ArrayBuffer | any;
|
|
26
|
+
connectable?: boolean;
|
|
27
|
+
state: PeripheralState;
|
|
28
|
+
}
|
|
29
|
+
type PeripheralState = 'disconnected' | 'disconnecting' | 'connecting' | 'connected';
|
|
30
|
+
|
|
31
|
+
type BleDevice = BleBridge;
|
|
32
|
+
interface BleBridge {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
bridgeId: string;
|
|
36
|
+
vin: string;
|
|
37
|
+
advertisingData: BleBridgeAdvertisingData;
|
|
38
|
+
device: PeripheralData;
|
|
39
|
+
}
|
|
40
|
+
interface BleBridgeAdvertisingData {
|
|
41
|
+
randomAdvNumber: number[];
|
|
42
|
+
vinNum: number[];
|
|
43
|
+
macAddress: number[];
|
|
44
|
+
fwVersion?: string;
|
|
45
|
+
/**
|
|
46
|
+
* 0x01 - TYC New Security with development keys
|
|
47
|
+
* 0x02 - TYC New Security with production keys
|
|
48
|
+
* 0x10 - Krone New Security with development keys
|
|
49
|
+
* 0x11 - Krone New Security with production keys
|
|
50
|
+
* 0x00 - predecessors (old security)
|
|
51
|
+
*/
|
|
52
|
+
configVersion: number;
|
|
53
|
+
timeFromStart?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
declare function createTirecheckDeviceSdk(bleImplementation: BleImplementation): {
|
|
57
|
+
bluetooth: {
|
|
58
|
+
onDeviceAdvertising(deviceAdvertisingCallback: (device: BleDevice) => void): void;
|
|
59
|
+
onDeviceUpdate(deviceUpdateCallback: (update: {
|
|
60
|
+
deviceId: string;
|
|
61
|
+
isConnected: boolean;
|
|
62
|
+
}) => void): void;
|
|
63
|
+
onDeviceUnreachable(deviceUnreachableCallback: (deviceId: string) => void): void;
|
|
64
|
+
scanDevices(services?: string[]): Promise<void>;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export { createTirecheckDeviceSdk };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
|
|
3
|
+
let ble;
|
|
4
|
+
function setBleImplementation(bleImplementation) {
|
|
5
|
+
ble = bleImplementation;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const bridgeTools = {
|
|
9
|
+
// convertBytesToStructure(objStructure: BridgeCommandStructureProperties, payload: number[]) {
|
|
10
|
+
// const numArray = _.clone(payload)
|
|
11
|
+
// const keys = Object.keys(objStructure)
|
|
12
|
+
// const sumWithInitial = keys.reduce((accumulator, currentValue) => accumulator + objStructure[currentValue].size, 0)
|
|
13
|
+
// if (numArray.length !== sumWithInitial) {
|
|
14
|
+
// throw new Error('Cannot convert bytes to object')
|
|
15
|
+
// }
|
|
16
|
+
// const result: any = {}
|
|
17
|
+
// for (const key of keys) {
|
|
18
|
+
// const encodedData: number[] = numArray.splice(0, objStructure[key].size)
|
|
19
|
+
// result[key] = this.decodeData(encodedData, objStructure[key].display)
|
|
20
|
+
// }
|
|
21
|
+
// return result
|
|
22
|
+
// },
|
|
23
|
+
// convertStructureToBytes(objStructure: any, structurizedObj: any) {
|
|
24
|
+
// const keys = Object.keys(objStructure)
|
|
25
|
+
// const result: number[] = []
|
|
26
|
+
// for (const key of keys) {
|
|
27
|
+
// const encoded = this.encodeData(structurizedObj[key], objStructure[key].display)
|
|
28
|
+
// if (encoded.length < objStructure[key].size) {
|
|
29
|
+
// const _padArray = Array.from({ length: objStructure[key].size - encoded.length }, () => 0)
|
|
30
|
+
// encoded.push(..._padArray)
|
|
31
|
+
// }
|
|
32
|
+
// result.push(...encoded)
|
|
33
|
+
// }
|
|
34
|
+
// return result
|
|
35
|
+
// },
|
|
36
|
+
// convertAutoLearnObjectToBinary(data: Record<string, string>): Record<string, string> {
|
|
37
|
+
// const result: Record<string, string> = {}
|
|
38
|
+
// for (const key in data) {
|
|
39
|
+
// if (!key.includes('axle')) continue
|
|
40
|
+
// // Parse the value as hexadecimal (base 16)
|
|
41
|
+
// const value = parseInt(data[key], 16)
|
|
42
|
+
// // Convert to binary and pad with leading zeros to ensure 8 characters
|
|
43
|
+
// const binaryString = value.toString(2).padStart(8, '0')
|
|
44
|
+
// result[key] = binaryString
|
|
45
|
+
// }
|
|
46
|
+
// return result
|
|
47
|
+
// },
|
|
48
|
+
// decodeData(data: number[], displayUnits: undefined | 'ascii' | 'decimal') {
|
|
49
|
+
// const _data = _.clone(data)
|
|
50
|
+
// if (displayUnits === 'ascii') {
|
|
51
|
+
// return _data
|
|
52
|
+
// .filter(d => d)
|
|
53
|
+
// .map(x => String.fromCharCode(x))
|
|
54
|
+
// .join('')
|
|
55
|
+
// }
|
|
56
|
+
// const _reversedData = _data.reverse()
|
|
57
|
+
// if (displayUnits === 'decimal') {
|
|
58
|
+
// return Number(`0x${_reversedData.map(dec => this.decimalToHex(dec)).join('')}`.replace(/\r\n/g, ''))
|
|
59
|
+
// }
|
|
60
|
+
// return _reversedData.map(dec => this.decimalToHex(dec)).join('')
|
|
61
|
+
// },
|
|
62
|
+
// encodeData(data: number | string, displayUnits: undefined | 'ascii' | 'decimal'): number[] {
|
|
63
|
+
// const _data = _.clone(data)
|
|
64
|
+
// if (displayUnits === 'ascii') {
|
|
65
|
+
// return (_data as string).split('').map((v, i) => (_data as string).charCodeAt(i))
|
|
66
|
+
// }
|
|
67
|
+
// if (displayUnits === 'decimal') {
|
|
68
|
+
// return this.hexToDecimalArray(this.decimalToHex(_data as number)).reverse()
|
|
69
|
+
// }
|
|
70
|
+
// return this.hexToDecimalArray(_data as string).reverse()
|
|
71
|
+
// },
|
|
72
|
+
// getBridgeId(device: BluetoothDeviceBridge) {
|
|
73
|
+
// // [244,177, 0, 34,123, 155] => F4B100227B9B
|
|
74
|
+
// return (
|
|
75
|
+
// device.advertisingData.macAddress
|
|
76
|
+
// ?.map(n => this.decimalToHex(n))
|
|
77
|
+
// .reverse()
|
|
78
|
+
// .join('')
|
|
79
|
+
// .toUpperCase() || ''
|
|
80
|
+
// )
|
|
81
|
+
// },
|
|
82
|
+
// pkcs(array: any[], length: number) {
|
|
83
|
+
// const pkcsValue = length - array.length
|
|
84
|
+
// if (pkcsValue > 0) {
|
|
85
|
+
// return [...array, ...new Array(pkcsValue).fill(pkcsValue)]
|
|
86
|
+
// }
|
|
87
|
+
// return array
|
|
88
|
+
// },
|
|
89
|
+
decimalToHex(decimal) {
|
|
90
|
+
const hex = decimal.toString(16);
|
|
91
|
+
return hex.padStart(2, "0");
|
|
92
|
+
},
|
|
93
|
+
// hexToDecimalArray(hex: string): number[] {
|
|
94
|
+
// return hex.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || []
|
|
95
|
+
// },
|
|
96
|
+
getFwVersion(payload) {
|
|
97
|
+
if (payload?.length !== 3) {
|
|
98
|
+
console.warn("Could not process FwVersion ", payload);
|
|
99
|
+
return "";
|
|
100
|
+
}
|
|
101
|
+
return payload.reduce((acc, current) => {
|
|
102
|
+
const value = this.decimalToHex(current);
|
|
103
|
+
acc.push(value[0] === "0" ? value[1] : value);
|
|
104
|
+
return acc;
|
|
105
|
+
}, []).join(".");
|
|
106
|
+
}
|
|
107
|
+
// barToKpaByte(value?: number) {
|
|
108
|
+
// if (!value) return 0
|
|
109
|
+
// return _.round(((value + 1) * 100) / 5.0625)
|
|
110
|
+
// },
|
|
111
|
+
// kpaByteToBar(value?: number, decrementValue = 1) {
|
|
112
|
+
// if (!_.isNumber(value)) {
|
|
113
|
+
// throw new TypeError('Value has to be a number')
|
|
114
|
+
// }
|
|
115
|
+
// const rawBar = (value * 5.0625) / 100
|
|
116
|
+
// return Math.max(rawBar - decrementValue, 0)
|
|
117
|
+
// },
|
|
118
|
+
// getMacAddressIfOtaAndIos(device: BluetoothDeviceBridge) {
|
|
119
|
+
// const uint8 = new Uint8Array(device.advertising.kCBAdvDataLeBluetoothDeviceAddress)
|
|
120
|
+
// const macAddressArray = Array.from(uint8)
|
|
121
|
+
// .reverse()
|
|
122
|
+
// .slice(0, 6)
|
|
123
|
+
// .map(x => this.decimalToHex(x).toUpperCase())
|
|
124
|
+
// return macAddressArray.join('')
|
|
125
|
+
// },
|
|
126
|
+
// isVersionGreaterThan(version: string) {
|
|
127
|
+
// if (version?.length !== 5) {
|
|
128
|
+
// throw new Error('Invalid version format'.t())
|
|
129
|
+
// }
|
|
130
|
+
// if (bluetoothStore?.connectedBridge?.advertisingData?.fwVersion) {
|
|
131
|
+
// return bluetoothStore?.connectedBridge?.advertisingData?.fwVersion > version
|
|
132
|
+
// }
|
|
133
|
+
// return false
|
|
134
|
+
// },
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const bridgeAdvertisingParser = {
|
|
138
|
+
getDeviceInfoFromAdvertising(device) {
|
|
139
|
+
const adversitingType = getAdvertisingType(device);
|
|
140
|
+
if (adversitingType !== "connectable")
|
|
141
|
+
return void 0;
|
|
142
|
+
const advertisingData = getAdvertisingData({ advertising: device.advertising, deviceName: device.name });
|
|
143
|
+
const bridgeId = advertisingData?.macAddress?.map((n) => bridgeTools.decimalToHex(n)).reverse().join("").toUpperCase() || "";
|
|
144
|
+
const vin = String.fromCharCode(...advertisingData?.vinNum || []).split("\0").join("");
|
|
145
|
+
return { id: device.id, name: device.name, bridgeId, vin, advertisingData, device };
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
function getAdvertisingType(device) {
|
|
149
|
+
if (device.advertising?.kCBAdvDataIsConnectable) {
|
|
150
|
+
return "connectable";
|
|
151
|
+
}
|
|
152
|
+
const numberArray = Array.from(new Uint8Array(device.advertising));
|
|
153
|
+
if (_.isEqual(numberArray.slice(0, 4), [2, 1, 6, 7])) {
|
|
154
|
+
return "connectable";
|
|
155
|
+
}
|
|
156
|
+
if (numberArray[3] === 26) {
|
|
157
|
+
return "krone";
|
|
158
|
+
}
|
|
159
|
+
if (_.isEqual(numberArray.slice(0, 4), [2, 1, 6, 3])) {
|
|
160
|
+
return "measurement";
|
|
161
|
+
}
|
|
162
|
+
return "unknown";
|
|
163
|
+
}
|
|
164
|
+
function getAdvertisingData({ advertising, deviceName }) {
|
|
165
|
+
const isKrone = deviceName === "030321";
|
|
166
|
+
const uint8 = new Uint8Array(
|
|
167
|
+
advertising.kCBAdvDataIsConnectable ? advertising.kCBAdvDataManufacturerData : advertising
|
|
168
|
+
);
|
|
169
|
+
const adv = Array.from(uint8);
|
|
170
|
+
const advertisingData = advertising.kCBAdvDataIsConnectable ? processIosAdvertising(adv) : processAndroidAdvertising(adv, isKrone);
|
|
171
|
+
if (!advertisingData || !advertisingData.macAddress.length || !advertisingData.randomAdvNumber.length || !advertisingData.vinNum.length) {
|
|
172
|
+
throw new Error("Device not recognized");
|
|
173
|
+
}
|
|
174
|
+
return advertisingData;
|
|
175
|
+
}
|
|
176
|
+
function processAndroidAdvertising(adv, isKrone) {
|
|
177
|
+
const advertisingData = {
|
|
178
|
+
configVersion: isKrone ? 17 : 1,
|
|
179
|
+
macAddress: [],
|
|
180
|
+
randomAdvNumber: [],
|
|
181
|
+
vinNum: [],
|
|
182
|
+
fwVersion: void 0,
|
|
183
|
+
timeFromStart: void 0
|
|
184
|
+
};
|
|
185
|
+
do {
|
|
186
|
+
const length = adv[0];
|
|
187
|
+
const identificator = adv[1];
|
|
188
|
+
const messageType = adv[4];
|
|
189
|
+
const packet = adv.splice(0, length + 1);
|
|
190
|
+
if (!length) {
|
|
191
|
+
adv = [];
|
|
192
|
+
}
|
|
193
|
+
if (messageType === 3 && identificator === 255) {
|
|
194
|
+
advertisingData.macAddress = packet.slice(5, 11);
|
|
195
|
+
advertisingData.vinNum = packet.slice(-17);
|
|
196
|
+
}
|
|
197
|
+
if (messageType === 1 && identificator === 255) {
|
|
198
|
+
advertisingData.randomAdvNumber = packet.slice(-8);
|
|
199
|
+
}
|
|
200
|
+
if (messageType === 4 && identificator === 255) {
|
|
201
|
+
advertisingData.randomAdvNumber = packet.slice(5, 13);
|
|
202
|
+
advertisingData.fwVersion = bridgeTools.getFwVersion(packet.slice(13, 16));
|
|
203
|
+
advertisingData.configVersion = packet[16];
|
|
204
|
+
advertisingData.timeFromStart = packet[17];
|
|
205
|
+
}
|
|
206
|
+
} while (adv.length);
|
|
207
|
+
return advertisingData;
|
|
208
|
+
}
|
|
209
|
+
function processIosAdvertising(adv, isKrone) {
|
|
210
|
+
if (adv.length < 34)
|
|
211
|
+
return void 0;
|
|
212
|
+
if (adv.length === 35) {
|
|
213
|
+
adv.slice(3, 11);
|
|
214
|
+
adv.slice(12, 18);
|
|
215
|
+
adv.slice(18, 35);
|
|
216
|
+
}
|
|
217
|
+
if (adv.length === 40) {
|
|
218
|
+
adv.slice(3, 11);
|
|
219
|
+
bridgeTools.getFwVersion(adv.slice(11, 14));
|
|
220
|
+
adv[14];
|
|
221
|
+
adv[15];
|
|
222
|
+
adv.slice(17, 23);
|
|
223
|
+
adv.slice(23, 40);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const deviceMeta = {
|
|
228
|
+
bridge: {
|
|
229
|
+
nameRegex: /(030303|030321)/,
|
|
230
|
+
characteristic: {
|
|
231
|
+
serviceId: "4880c12c-fdcb-4077-8920-a450d7f9b907",
|
|
232
|
+
// message from device
|
|
233
|
+
characteristicId: "fec26ec4-6d71-4442-9f81-55bc21d658d7"
|
|
234
|
+
},
|
|
235
|
+
capabilities: [],
|
|
236
|
+
manufacturerId: 2978,
|
|
237
|
+
mtu: 256,
|
|
238
|
+
getDeviceInfoFromAdvertising: bridgeAdvertisingParser.getDeviceInfoFromAdvertising
|
|
239
|
+
},
|
|
240
|
+
bridgeOta: {
|
|
241
|
+
nameRegex: /CAN BLE BRDG OTA.*/,
|
|
242
|
+
characteristic: {
|
|
243
|
+
serviceId: "4880c12c-fdcb-4077-8920-a450d7f9b907",
|
|
244
|
+
characteristicId: "fec26ec4-6d71-4442-9f81-55bc21d658d7"
|
|
245
|
+
},
|
|
246
|
+
capabilities: [],
|
|
247
|
+
getDeviceInfoFromAdvertising: () => {
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
flexiGaugeTpms: {
|
|
251
|
+
nameRegex: /Flexi.*/,
|
|
252
|
+
characteristic: {
|
|
253
|
+
serviceId: "4880c12c-fdcb-4077-8920-a450d7f9b907",
|
|
254
|
+
// message from device
|
|
255
|
+
characteristicId: "fec26ec4-6d71-4442-9f81-55bc21d658d6"
|
|
256
|
+
},
|
|
257
|
+
getDeviceInfoFromAdvertising: () => {
|
|
258
|
+
},
|
|
259
|
+
// Do we need it here?
|
|
260
|
+
capabilities: [
|
|
261
|
+
{
|
|
262
|
+
id: "td",
|
|
263
|
+
// processFn: processTreadDepth,
|
|
264
|
+
regex: /^\*TD.*/
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
id: "button",
|
|
268
|
+
// processFn: processButtonPress,
|
|
269
|
+
regex: /^\*[UDLRC] $/
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
id: "tpms",
|
|
273
|
+
// processFn: processTpms,
|
|
274
|
+
regex: /^\*TPMS.*/
|
|
275
|
+
}
|
|
276
|
+
],
|
|
277
|
+
reconnect: true
|
|
278
|
+
// Do we need it here?
|
|
279
|
+
},
|
|
280
|
+
pressureStick: {
|
|
281
|
+
nameRegex: /Pressure Stick.*/,
|
|
282
|
+
characteristic: {
|
|
283
|
+
serviceId: "4880c12c-fdcb-4077-8920-a450d7f9b907",
|
|
284
|
+
characteristicId: "fec26ec4-6d71-4442-9f81-55bc21d658d6"
|
|
285
|
+
},
|
|
286
|
+
getDeviceInfoFromAdvertising: () => {
|
|
287
|
+
},
|
|
288
|
+
capabilities: [
|
|
289
|
+
{
|
|
290
|
+
id: "pressure",
|
|
291
|
+
// processFn: processPressure,
|
|
292
|
+
regex: /P([0-9.]+)mBar/
|
|
293
|
+
}
|
|
294
|
+
// only pressure is needed for initial implementation, uncomment tpms functionality when needed
|
|
295
|
+
// {
|
|
296
|
+
// id: 'tpms',
|
|
297
|
+
// processFn: processTpms,
|
|
298
|
+
// regex: /^\*TPMS/,
|
|
299
|
+
// },
|
|
300
|
+
]
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const checkUnreachableDevicesTimeouts = {};
|
|
305
|
+
const checkConnectedStateIntervals = {};
|
|
306
|
+
const deviceAdvertisingCallbacks = [];
|
|
307
|
+
const deviceUpdateCallbacks = [];
|
|
308
|
+
const deviceUnreachableCallbacks = [];
|
|
309
|
+
const bluetooth = {
|
|
310
|
+
/** Triggered when "scanDevices" detects device supported by SDK */
|
|
311
|
+
onDeviceAdvertising(deviceAdvertisingCallback) {
|
|
312
|
+
deviceAdvertisingCallbacks.push(deviceAdvertisingCallback);
|
|
313
|
+
},
|
|
314
|
+
onDeviceUpdate(deviceUpdateCallback) {
|
|
315
|
+
deviceUpdateCallbacks.push(deviceUpdateCallback);
|
|
316
|
+
},
|
|
317
|
+
onDeviceUnreachable(deviceUnreachableCallback) {
|
|
318
|
+
deviceUnreachableCallbacks.push(deviceUnreachableCallback);
|
|
319
|
+
},
|
|
320
|
+
async scanDevices(services = []) {
|
|
321
|
+
await ble.stopScan();
|
|
322
|
+
ble.startScanWithOptions(
|
|
323
|
+
services,
|
|
324
|
+
{
|
|
325
|
+
reportDuplicates: true,
|
|
326
|
+
matchMode: "aggressive",
|
|
327
|
+
scanMode: "lowLatency",
|
|
328
|
+
numOfMatches: "max",
|
|
329
|
+
callbackType: "all",
|
|
330
|
+
reportDelay: 0
|
|
331
|
+
},
|
|
332
|
+
(device) => {
|
|
333
|
+
const processedDevice = processDevice(device);
|
|
334
|
+
if (!processedDevice)
|
|
335
|
+
return;
|
|
336
|
+
for (const c of deviceAdvertisingCallbacks) {
|
|
337
|
+
c(processedDevice);
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
(e) => console.error("ble.startScanWithOptions error:", e)
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
function processDevice(device) {
|
|
345
|
+
const uint8 = new Uint8Array(device.advertising);
|
|
346
|
+
const adv = Array.from(uint8);
|
|
347
|
+
let name = device.advertising.kCBAdvDataLocalName || device.name;
|
|
348
|
+
if (!name || name === "OTA") {
|
|
349
|
+
name = getDeviceNameFromAdvertising(adv);
|
|
350
|
+
}
|
|
351
|
+
const deviceType = getDeviceTypeFromName(name);
|
|
352
|
+
if (!deviceType) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
let processedDevice;
|
|
356
|
+
try {
|
|
357
|
+
processedDevice = deviceMeta[deviceType].getDeviceInfoFromAdvertising({ ...device, name });
|
|
358
|
+
if (!processedDevice)
|
|
359
|
+
return;
|
|
360
|
+
} catch (e) {
|
|
361
|
+
console.warn("Error processing advertising", e);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
refreshUnreachableTimeouts(processedDevice.id);
|
|
365
|
+
monitorConnectedState(processedDevice.id);
|
|
366
|
+
return processedDevice;
|
|
367
|
+
}
|
|
368
|
+
function getDeviceTypeFromName(name) {
|
|
369
|
+
let deviceType;
|
|
370
|
+
for (const key in deviceMeta) {
|
|
371
|
+
const meta = deviceMeta[key];
|
|
372
|
+
if (meta.nameRegex.test(name)) {
|
|
373
|
+
deviceType = key;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return deviceType;
|
|
377
|
+
}
|
|
378
|
+
function monitorConnectedState(deviceId) {
|
|
379
|
+
if (checkConnectedStateIntervals[deviceId])
|
|
380
|
+
return;
|
|
381
|
+
checkConnectedStateIntervals[deviceId] = setInterval(async () => {
|
|
382
|
+
let isConnected = false;
|
|
383
|
+
try {
|
|
384
|
+
await ble.isConnected(deviceId);
|
|
385
|
+
isConnected = true;
|
|
386
|
+
} catch {
|
|
387
|
+
isConnected = false;
|
|
388
|
+
}
|
|
389
|
+
for (const c of deviceUpdateCallbacks) {
|
|
390
|
+
c({ deviceId, isConnected });
|
|
391
|
+
}
|
|
392
|
+
}, 1e3);
|
|
393
|
+
}
|
|
394
|
+
function refreshUnreachableTimeouts(deviceId) {
|
|
395
|
+
if (checkUnreachableDevicesTimeouts[deviceId]) {
|
|
396
|
+
clearTimeout(checkUnreachableDevicesTimeouts[deviceId]);
|
|
397
|
+
}
|
|
398
|
+
checkUnreachableDevicesTimeouts[deviceId] = setTimeout(() => {
|
|
399
|
+
for (const c of deviceUnreachableCallbacks) {
|
|
400
|
+
c(deviceId);
|
|
401
|
+
}
|
|
402
|
+
if (checkConnectedStateIntervals[deviceId]) {
|
|
403
|
+
clearInterval(checkConnectedStateIntervals[deviceId]);
|
|
404
|
+
delete checkConnectedStateIntervals[deviceId];
|
|
405
|
+
}
|
|
406
|
+
}, 6e4);
|
|
407
|
+
}
|
|
408
|
+
function getDeviceNameFromAdvertising(adv) {
|
|
409
|
+
const advertising = _.clone(adv);
|
|
410
|
+
const messageType = advertising[4];
|
|
411
|
+
const packet = advertising.slice(5, 11);
|
|
412
|
+
if (messageType === 9) {
|
|
413
|
+
return packet.map((x) => String.fromCharCode(x)).join("");
|
|
414
|
+
}
|
|
415
|
+
return "";
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function createTirecheckDeviceSdk(bleImplementation) {
|
|
419
|
+
setBleImplementation(bleImplementation);
|
|
420
|
+
return {
|
|
421
|
+
bluetooth
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export { createTirecheckDeviceSdk };
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tirecheck-device-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SDK for working with various devices produced by Tirecheck via Bluetooth (CAN Bridge, Routers, Sensors, FlexiGauge, PressureStick, etc)",
|
|
5
|
+
"author": "Leonid Buneev <leonid.buneev@tirecheck.com>",
|
|
6
|
+
"license": "ISC",
|
|
7
|
+
"keywords": [],
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"module": "./dist/index.mjs",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"lodash": "^4.17.21"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@antfu/eslint-config": "^2.25.1",
|
|
19
|
+
"@types/lodash": "^4.17.7",
|
|
20
|
+
"@vitest/ui": "^2.0.5",
|
|
21
|
+
"eslint": "^9.9.0",
|
|
22
|
+
"eslint-plugin-format": "^0.1.2",
|
|
23
|
+
"eslint-plugin-tyrecheck": "^2.64.0",
|
|
24
|
+
"unbuild": "^2.0.0",
|
|
25
|
+
"vitest": "^2.0.5"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "vitest --ui",
|
|
29
|
+
"dev": "pnpm install && vitest --ui",
|
|
30
|
+
"build": "unbuild"
|
|
31
|
+
}
|
|
32
|
+
}
|