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 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;
@@ -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 };
@@ -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 };
@@ -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
+ }