bt-sensors-plugin-sk 1.3.3 → 1.3.4

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/AI-DEV.md ADDED
@@ -0,0 +1,784 @@
1
+ # AI-Assisted Development Guide for Bluetooth Battery Sensors
2
+
3
+ This guide documents the AI-assisted development process used to create Bluetooth battery sensor implementations for the SignalK bt-sensors-plugin-sk. Follow this process to add support for new Bluetooth battery devices.
4
+
5
+ ## Overview
6
+
7
+ This plugin architecture allows adding new Bluetooth battery sensors by creating sensor class files that inherit from [`BTSensor.js`](BTSensor.js:1). Each sensor class handles device-specific Bluetooth communication and data parsing, then maps the data to SignalK paths.
8
+
9
+ ## Prerequisites
10
+
11
+ Before starting, gather the following information about your Bluetooth battery:
12
+
13
+ ### 1. Device Documentation
14
+ - Manufacturer's name and device model
15
+ - Bluetooth specifications (BLE services, characteristics, UUIDs)
16
+ - Data packet format and protocol documentation
17
+ - Available metrics (voltage, current, SOC, temperature, etc.)
18
+
19
+ ### 2. Development Tools
20
+ - Access to the physical device for testing
21
+ - **Android device with Bluetooth debugging enabled** (CRITICAL - see section below)
22
+ - Bluetooth scanning tools (`bluetoothctl`, nRF Connect app, etc.)
23
+ - **Vendor's mobile app** for the battery (if available)
24
+ - Sample data packets (advertising data, characteristic reads)
25
+
26
+ ### 3. Technical Details
27
+ - Service UUIDs the device advertises
28
+ - Characteristic UUIDs for reading data
29
+ - Data encoding format (byte order, scaling factors, units)
30
+ - Any CRC/checksum algorithms used
31
+ - Whether device uses advertising data or GATT connection
32
+
33
+ ## Critical First Step: Capture Real Bluetooth Data
34
+
35
+ ### Using Android Bluetooth HCI Snoop Log (ESSENTIAL)
36
+
37
+ **This is the most important step** - Before writing any code, you need to capture actual Bluetooth communication from the device. The most effective method is using Android's built-in Bluetooth debugging with the vendor's app.
38
+
39
+ **Why this matters:**
40
+ - Reveals the exact protocol the device uses
41
+ - Shows actual commands and responses
42
+ - Eliminates guesswork about byte layouts
43
+ - Provides test data for validation
44
+
45
+ **Step-by-step process:**
46
+
47
+ 1. **Enable Developer Options on Android:**
48
+ - Go to Settings → About Phone
49
+ - Tap "Build Number" 7 times
50
+ - Go back to Settings → Developer Options
51
+
52
+ 2. **Enable Bluetooth HCI Snoop Log:**
53
+ - In Developer Options, enable "Bluetooth HCI snoop log"
54
+ - This captures ALL Bluetooth traffic to a file
55
+
56
+ 3. **Use the Vendor's App:**
57
+ - Install the battery manufacturer's official app
58
+ - Connect to your battery
59
+ - Navigate through all screens showing battery data
60
+ - Trigger all functions (refresh, settings, etc.)
61
+ - Let it run for 30-60 seconds
62
+
63
+ 4. **Extract the Log File:**
64
+ ```bash
65
+ # Pull the Bluetooth log from Android device via ADB
66
+ adb pull /sdcard/Android/data/btsnoop_hci.log
67
+
68
+ # Or from newer Android versions:
69
+ adb bugreport
70
+ # Then extract: FS/data/misc/bluetooth/logs/btsnoop_hci.log
71
+ ```
72
+
73
+ 5. **Analyze with Wireshark:**
74
+ - Open btsnoop_hci.log in Wireshark
75
+ - Filter for your device's MAC address: `bluetooth.addr == XX:XX:XX:XX:XX:XX`
76
+ - Look for:
77
+ - **ATT (Attribute Protocol) packets** - GATT read/write/notify operations
78
+ - **Advertising packets** - Manufacturer/Service data broadcasts
79
+ - **Command/response patterns** - Protocol structure
80
+
81
+ **What to look for in Wireshark:**
82
+
83
+ For GATT devices:
84
+ - `ATT Write Request` - Commands sent by app to device
85
+ - `ATT Handle Value Notification` - Data pushed from device
86
+ - `ATT Read Response` - Data read from characteristics
87
+ - Note the characteristic handles (0x00XX) and UUIDs
88
+
89
+ For Advertising devices:
90
+ - `Advertising Data` packets
91
+ - Manufacturer Data field with ID and hex bytes
92
+ - Service Data field with UUID and hex bytes
93
+
94
+ **Real Example - HSC14F Battery:**
95
+
96
+ ```
97
+ Wireshark Filter: bluetooth.addr == AA:BB:CC:DD:EE:FF
98
+
99
+ Frame 142: ATT Write Request, Handle: 0x000e
100
+ Data: A5 5A 00 FF 01 03
101
+ (This is the "read battery info" command)
102
+
103
+ Frame 145: ATT Handle Value Notification, Handle: 0x000b
104
+ Data: A5 5A 00 01 01 0C E4 10 0E 55 19 [...]
105
+ (Device's response with battery data)
106
+
107
+ Vendor app displayed at this moment:
108
+ - Voltage: 13.2V (bytes 6-7: 0x0CE4 = 3300 * 0.01 / 2 cells)
109
+ - Current: 10.5A (bytes 8-9: 0x100E = 4110 * 0.01 / 4)
110
+ - SOC: 85% (byte 10: 0x55 = 85)
111
+ ```
112
+
113
+ **Prompt to AI after capturing:**
114
+
115
+ ```
116
+ I've captured Bluetooth HCI snoop log from my [DEVICE_NAME] using the vendor's
117
+ Android app. Here's what Wireshark shows:
118
+
119
+ [FOR GATT DEVICES:]
120
+ Service UUID: 0000ffe0-0000-1000-8000-00805f9b34fb
121
+ Characteristic UUID: 0000ffe1-0000-1000-8000-00805f9b34fb
122
+
123
+ Write Request to device:
124
+ A5 5A 00 FF 01 03
125
+
126
+ Notification Response:
127
+ A5 5A 00 01 01 0C E4 10 0E 55 19 00 01 02 [...]
128
+
129
+ When this packet arrived, the vendor app displayed:
130
+ - Voltage: 13.2V
131
+ - Current: 10.5A
132
+ - SOC: 85%
133
+ - Temperature: 25°C
134
+
135
+ Please help me:
136
+ 1. Understand the protocol structure
137
+ 2. Map hex bytes to displayed values
138
+ 3. Determine byte positions, endianness, and scaling
139
+ 4. Create the sensor class implementation
140
+
141
+ [FOR ADVERTISING DEVICES:]
142
+ Advertising Data shows:
143
+ Manufacturer Data (0x1234): AB CD 0C E4 10 0E 55 01 F4
144
+
145
+ When this was captured, device display showed:
146
+ - Voltage: 13.2V
147
+ - Current: 10.5A
148
+ - SOC: 85%
149
+
150
+ Please decode this packet format.
151
+ ```
152
+
153
+ ### Alternative: Using nRF Connect (If No Vendor App Available)
154
+
155
+ If there's no vendor app:
156
+
157
+ 1. **Install nRF Connect** (Nordic Semiconductor app for Android/iOS)
158
+ 2. **Scan and connect** to your battery
159
+ 3. **Explore all services and characteristics**
160
+ 4. **Read values** from readable characteristics
161
+ 5. **Enable notifications** on notifiable characteristics
162
+ 6. **Screenshot hex values** and note what they represent
163
+
164
+ **Prompt to AI:**
165
+ ```
166
+ Using nRF Connect, I found on [DEVICE_NAME]:
167
+
168
+ Service: 0000ffe0-0000-1000-8000-00805f9b34fb
169
+ Characteristics:
170
+ - 0000ffe1-... (Read, Notify)
171
+ Current value: A5 5A 00 01 01 0C E4 10 0E 55
172
+
173
+ Battery label shows: 13.2V, 10.5A, 85%
174
+
175
+ How do I decode this to get those values?
176
+ ```
177
+
178
+ ## Step-by-Step AI-Assisted Development Process
179
+
180
+ ### Step 1: Analyze Existing Similar Implementations
181
+
182
+ **Prompt to AI:**
183
+ ```
184
+ I want to add support for a [DEVICE_NAME] Bluetooth battery to the bt-sensors-plugin-sk.
185
+ Please review the sensor_classes directory and identify existing battery sensor
186
+ implementations that are similar. Show me the key patterns used for:
187
+ - Device identification (matchManufacturerData or identify methods)
188
+ - Bluetooth service/characteristic UUIDs
189
+ - Data packet parsing
190
+ - SignalK path mapping
191
+ - GATT vs advertising data approaches
192
+
193
+ Focus on these example files:
194
+ - sensor_classes/VictronSmartLithium.js
195
+ - sensor_classes/JBDBMS.js
196
+ - sensor_classes/RenogyBattery.js
197
+ ```
198
+
199
+ **What AI should examine:**
200
+ - [`VictronSmartLithium.js`](sensor_classes/VictronSmartLithium.js:1) - Advertising data approach with encryption
201
+ - [`JBDBMS.js`](sensor_classes/JBDBMS.js:1) - GATT connection with command/response protocol
202
+ - [`RenogyBattery.js`](sensor_classes/RenogyBattery.js:1) - GATT with Modbus-style registers
203
+ - [`BTSensor.js`](BTSensor.js:1) - Base class showing required methods
204
+
205
+ ### Step 2: Determine Communication Method
206
+
207
+ **Prompt to AI:**
208
+ ```
209
+ My device [DOES/DOES NOT] require a GATT connection. It provides data via:
210
+ - [X] Advertising packets (manufacturer data or service data)
211
+ - [ ] GATT characteristic notifications
212
+ - [ ] GATT characteristic reads (polling)
213
+
214
+ Here's the Bluetooth scan data I captured:
215
+ [Paste output from bluetoothctl or nRF Connect]
216
+
217
+ Which approach should I use: advertising-based like VictronSmartLithium,
218
+ or GATT-based like JBDBMS or RenogyBattery?
219
+ ```
220
+
221
+ ### Step 3: Create Initial Sensor Class File
222
+
223
+ **Prompt to AI:**
224
+ ```
225
+ Create a new sensor class file for [DEVICE_NAME] based on the following specifications:
226
+
227
+ Device: [DEVICE_NAME]
228
+ Manufacturer: [MANUFACTURER]
229
+ Communication method: [Advertising/GATT]
230
+
231
+ [IF ADVERTISING:]
232
+ Manufacturer ID: [0xXXXX]
233
+ The device advertises data in manufacturer data with this format:
234
+ [Describe byte layout]
235
+
236
+ [IF GATT:]
237
+ Service UUID: [UUID]
238
+ Characteristics:
239
+ - Read: [UUID] - [description]
240
+ - Notify: [UUID] - [description]
241
+ - Write: [UUID] - [description if applicable]
242
+
243
+ The device provides these metrics:
244
+ - Voltage (range: X-Y V)
245
+ - Current (range: -X to +Y A)
246
+ - State of Charge (0-100%)
247
+ - Temperature (range: X-Y °C)
248
+ - [Other metrics...]
249
+
250
+ Please create sensor_classes/[DeviceName].js following the pattern used in
251
+ [VictronSmartLithium.js/JBDBMS.js/RenogyBattery.js], including:
252
+ 1. Class definition extending BTSensor
253
+ 2. Static Domain property set to BTSensor.SensorDomains.electrical
254
+ 3. Static identify() method for device identification
255
+ 4. Static ImageFile property
256
+ 5. initSchema() method with metadata
257
+ 6. Data parsing methods
258
+ 7. SignalK path defaults
259
+ ```
260
+
261
+ **Expected AI actions:**
262
+ - Creates new file in `sensor_classes/`
263
+ - Implements basic class structure
264
+ - Sets up UUID constants (if GATT)
265
+ - Defines device identification logic
266
+ - Creates default SignalK path mappings
267
+
268
+ ### Step 4: Implement Device Identification
269
+
270
+ **For Advertising-based Devices:**
271
+
272
+ **Prompt to AI:**
273
+ ```
274
+ I have Bluetooth advertising data from [DEVICE_NAME]:
275
+ Manufacturer Data: [paste hex dump, e.g., "AB CD 01 02 03 04..."]
276
+ or
277
+ Service Data (UUID [UUID]): [paste hex dump]
278
+
279
+ The manufacturer ID should be: [0xXXXX]
280
+
281
+ Please implement the static identify(device) method to correctly identify this device.
282
+ Use the pattern from VictronSensor.js which checks manufacturer ID and additional
283
+ identifying bytes if needed.
284
+ ```
285
+
286
+ **For GATT Devices:**
287
+
288
+ **Prompt to AI:**
289
+ ```
290
+ My GATT device advertises the service UUID: [UUID]
291
+
292
+ Please implement the static identify(device) method that:
293
+ 1. Checks if the device advertises this service UUID
294
+ 2. Returns the class if matched, null otherwise
295
+
296
+ Use this pattern:
297
+ static async identify(device){
298
+ const serviceUUIDs = await BTSensor.getDeviceProp(device, 'UUIDs')
299
+ if (serviceUUIDs && serviceUUIDs.includes('[UUID]'))
300
+ return this
301
+ return null
302
+ }
303
+ ```
304
+
305
+ ### Step 5: Implement Data Parsing (Advertising Method)
306
+
307
+ **Prompt to AI:**
308
+ ```
309
+ The [DEVICE_NAME] sends data in manufacturer data packets with this format:
310
+
311
+ Byte 0-1: Manufacturer ID (0xXXXX, little-endian)
312
+ Byte 2-3: Voltage (0.01V units, unsigned 16-bit LE)
313
+ Byte 4-5: Current (0.01A units, signed 16-bit LE, positive=charging)
314
+ Byte 6: State of Charge (%, unsigned 8-bit, 0-100)
315
+ Byte 7-8: Temperature (0.1°C units, signed 16-bit LE)
316
+ [Continue for all fields...]
317
+
318
+ Sample packet: [provide actual hex dump, e.g., "CD AB 10 0D E8 03 64 5A 01"]
319
+
320
+ Please implement the propertiesChanged() method that:
321
+ 1. Calls super.propertiesChanged(props) first
322
+ 2. Extracts manufacturer data
323
+ 3. Parses the buffer using the read functions defined in initSchema()
324
+ 4. Calls emitValuesFrom(buffer) to emit all values
325
+
326
+ Also update initSchema() with the correct read functions for each path using:
327
+ .read = (buffer) => { return buffer.readUInt16LE(2) / 100 } // for voltage at byte 2
328
+ ```
329
+
330
+ **Common Buffer Reading Methods:**
331
+ - `buffer.readUInt16LE(offset)` - Unsigned 16-bit little-endian
332
+ - `buffer.readInt16LE(offset)` - Signed 16-bit little-endian
333
+ - `buffer.readUInt8(offset)` - Unsigned 8-bit
334
+ - `buffer.readInt8(offset)` - Signed 8-bit
335
+ - `buffer.readUInt32LE(offset)` - Unsigned 32-bit little-endian
336
+ - Use `BE` suffix for big-endian variants
337
+
338
+ ### Step 6: Implement GATT Communication (GATT Method)
339
+
340
+ **Prompt to AI:**
341
+ ```
342
+ My GATT device uses this protocol:
343
+ Service UUID: [UUID]
344
+ Notify Characteristic: [UUID] - Sends battery data
345
+ Write Characteristic: [UUID] - Accepts commands
346
+ Read Characteristic: [UUID] - Returns data on request
347
+
348
+ Command format: [describe, e.g., "0xDD 0xA5 CMD 0x00 0xFF CHECKSUM 0x77"]
349
+ Response format: [describe structure]
350
+
351
+ Please implement:
352
+ 1. initSchema() that connects to the device and sets up characteristics
353
+ 2. initGATTConnection() override if needed
354
+ 3. initGATTNotifications() or initGATTInterval() depending on polling vs notifications
355
+ 4. emitGATT() method that requests data and parses responses
356
+ 5. Helper methods like sendReadFunctionRequest() if needed
357
+
358
+ Follow the pattern from JBDBMS.js which uses command/response protocol.
359
+ ```
360
+
361
+ **For devices that need polling:**
362
+
363
+ **Prompt to AI:**
364
+ ```
365
+ Add GATT parameters to support polling:
366
+ - In initSchema(), call this.addGATTParameter() for pollFreq
367
+ - Set hasGATT() to return true
368
+ - Set usingGATT() to return this.useGATT
369
+ - Implement initGATTInterval() to poll at specified frequency
370
+ - Implement emitGATT() to read and emit all values
371
+ ```
372
+
373
+ ### Step 7: Define SignalK Path Mappings
374
+
375
+ **Prompt to AI:**
376
+ ```
377
+ Please implement proper SignalK path defaults in initSchema() for this battery.
378
+ Map the parsed values to these SignalK paths:
379
+
380
+ Use addDefaultPath() for standard paths:
381
+ - Voltage → electrical.batteries.voltage
382
+ - Current → electrical.batteries.current
383
+ - SOC → electrical.batteries.capacity.stateOfCharge
384
+ - Temperature → electrical.batteries.temperature
385
+ - Remaining capacity → electrical.batteries.capacity.remaining
386
+ - Cycles → electrical.batteries.cycles
387
+
388
+ Use addMetadatum() for device-specific paths:
389
+ - Cell voltages → electrical.batteries.{batteryID}.cell[N].voltage
390
+ - Protection status → electrical.batteries.{batteryID}.protectionStatus
391
+ - [Other custom metrics]
392
+
393
+ Add parameter for battery ID:
394
+ this.addDefaultParam("batteryID").default="house"
395
+
396
+ Ensure all paths use proper template variables like {batteryID} where appropriate.
397
+ ```
398
+
399
+ **SignalK Path Reference:**
400
+ ```
401
+ electrical.batteries.{id}.voltage // V
402
+ electrical.batteries.{id}.current // A (positive = charging)
403
+ electrical.batteries.{id}.temperature // K (Kelvin)
404
+ electrical.batteries.{id}.capacity.stateOfCharge // ratio (0-1)
405
+ electrical.batteries.{id}.capacity.remaining // C (coulombs/amp-hours)
406
+ electrical.batteries.{id}.capacity.actual // C (total capacity)
407
+ electrical.batteries.{id}.cycles // count
408
+ electrical.batteries.{id}.lifetimeDischarge // C
409
+ electrical.batteries.{id}.lifetimeRecharge // C
410
+ ```
411
+
412
+ ### Step 8: Add Device Image and Description
413
+
414
+ **Prompt to AI:**
415
+ ```
416
+ I have an image for [DEVICE_NAME] saved as: public/images/[DeviceName].webp
417
+
418
+ Please update the sensor class to include:
419
+ 1. static ImageFile = "[DeviceName].webp"
420
+ 2. static Description = "[Brief description of device]"
421
+ 3. static Manufacturer = "[Manufacturer Name]"
422
+
423
+ Follow the pattern from other sensor classes.
424
+ ```
425
+
426
+ ### Step 9: Register the Sensor Class
427
+
428
+ **Prompt to AI:**
429
+ ```
430
+ Please help me register the new [DeviceName] sensor class in the plugin:
431
+ 1. Check classLoader.js to see the pattern for importing and registering classes
432
+ 2. Add the appropriate require() statement
433
+ 3. Add it to the class map
434
+ 4. Ensure it's in the correct order for identification
435
+
436
+ Show me the exact changes needed to classLoader.js.
437
+ ```
438
+
439
+ ### Step 10: Test with Real Device
440
+
441
+ **Prompt to AI:**
442
+ ```
443
+ I'm testing with the actual [DEVICE_NAME] device. Here's what I'm seeing:
444
+
445
+ Debug output:
446
+ [Paste logs, errors, or unexpected behavior]
447
+
448
+ Expected values from device display:
449
+ - Voltage: [X]V
450
+ - Current: [X]A
451
+ - SOC: [X]%
452
+ - Temperature: [X]°C
453
+
454
+ Actual parsed values:
455
+ - Voltage: [Y]V
456
+ - Current: [Y]A
457
+ - SOC: [Y]%
458
+ - Temperature: [Y]°C
459
+
460
+ Please help debug:
461
+ 1. Are the byte offsets correct?
462
+ 2. Is the byte order (endianness) correct?
463
+ 3. Are scaling factors correct?
464
+ 4. Are signed vs unsigned interpretations correct?
465
+ ```
466
+
467
+ **Common Issues & Fixes:**
468
+ - **Values 256x too large/small**: Wrong endianness (LE vs BE)
469
+ - **Negative values where shouldn't be**: Using UInt instead of Int
470
+ - **Values in wrong range**: Incorrect scaling factor
471
+ - **Values always same**: Wrong byte offset or not updating
472
+
473
+ ### Step 11: Add Error Handling
474
+
475
+ **Prompt to AI:**
476
+ ```
477
+ Please add robust error handling to the [DeviceName] class for:
478
+
479
+ For advertising-based devices:
480
+ 1. Check manufacturer data exists before parsing
481
+ 2. Validate buffer length before reading
482
+ 3. Handle out-of-range values gracefully
483
+ 4. Add try-catch in propertiesChanged()
484
+
485
+ For GATT devices:
486
+ 1. Add timeouts for response waits
487
+ 2. Validate checksums if protocol uses them
488
+ 3. Handle disconnections gracefully
489
+ 4. Add retry logic for failed reads
490
+ 5. Implement deactivateGATT() cleanup
491
+
492
+ Follow the error handling patterns in JBDBMS.js and BTSensor.js.
493
+ ```
494
+
495
+ ### Step 12: Documentation and Testing
496
+
497
+ **Prompt to AI:**
498
+ ```
499
+ Please add comprehensive documentation to the [DeviceName] class:
500
+
501
+ 1. Add JSDoc comments for all methods explaining:
502
+ - Parameters and return values
503
+ - Protocol details
504
+ - Data format specifics
505
+
506
+ 2. Add a comment block at the top documenting the data format:
507
+ /*
508
+ [DeviceName] Data Format
509
+ Byte X-Y: [Field] (units, format, range)
510
+ ...
511
+ */
512
+
513
+ 3. Create a _test() usage example showing how to test parsing with sample data
514
+
515
+ 4. Document any device-specific quirks or limitations
516
+ ```
517
+
518
+ ## Complete Example Workflow
519
+
520
+ Here's a condensed example conversation for adding an "AcmeBattery BT-100":
521
+
522
+ ```
523
+ User: I want to add support for the Acme BT-100 Bluetooth battery. It uses
524
+ advertising data with manufacturer ID 0x1234. Here's a sample packet:
525
+ 34 12 10 0D E8 03 64 5A 01
526
+
527
+ AI: [Analyzes structure, creates initial class file]
528
+
529
+ User: The packet format is:
530
+ Bytes 0-1: Manufacturer ID (0x1234)
531
+ Bytes 2-3: Voltage in 0.01V (little-endian, so 0x0D10 = 3344 = 33.44V)
532
+ Bytes 4-5: Current in 0.01A (signed LE, 0x03E8 = 1000 = 10.00A)
533
+ Byte 6: SOC (0x64 = 100 = 100%)
534
+ Bytes 7-8: Temp in 0.1°C (signed LE, 0x015A = 346 = 34.6°C)
535
+
536
+ AI: [Implements propertiesChanged() and read functions with correct parsing]
537
+
538
+ User: Testing shows voltage correct but current is backwards (negative when charging)
539
+
540
+ AI: [Fixes current sign interpretation]
541
+
542
+ User: Perfect! Now add the image reference and register it.
543
+
544
+ AI: [Updates ImageFile, modifies classLoader.js]
545
+ ```
546
+
547
+ ## Bluetooth Scanning Commands
548
+
549
+ ### Using bluetoothctl
550
+ ```bash
551
+ # Start scanning
552
+ bluetoothctl
553
+ scan on
554
+
555
+ # Wait for device to appear, then:
556
+ info [MAC_ADDRESS]
557
+
558
+ # Look for:
559
+ # - ManufacturerData: key [ID] value [hex bytes]
560
+ # - ServiceData: key [UUID] value [hex bytes]
561
+ # - UUIDs: [list of service UUIDs]
562
+ ```
563
+
564
+ ### Using hcitool/hcidump
565
+ ```bash
566
+ # Terminal 1: Start scan
567
+ sudo hcitool lescan
568
+
569
+ # Terminal 2: Capture advertising data
570
+ sudo hcidump --raw
571
+ ```
572
+
573
+ ## Testing Your Implementation
574
+
575
+ ### Static Testing (No Device Required)
576
+ ```javascript
577
+ const MyBattery = require('./sensor_classes/MyBattery.js')
578
+
579
+ // Test with sample packet
580
+ const sampleData = "34 12 10 0D E8 03 64 5A 01"
581
+ MyBattery._test(sampleData)
582
+
583
+ // Should output:
584
+ // voltage=33.44
585
+ // current=10.00
586
+ // SOC=1.00
587
+ // temperature=307.75
588
+ ```
589
+
590
+ ### Live Testing Checklist
591
+ - [ ] Device correctly identified during scan
592
+ - [ ] Connection establishes (if GATT)
593
+ - [ ] All metrics parse with correct values
594
+ - [ ] **Values match vendor app exactly** (critical!)
595
+ - [ ] **Parsed values match HCI snoop captured data**
596
+ - [ ] Values also match device's physical display (if available)
597
+ - [ ] Reconnection works after going out of range
598
+ - [ ] SignalK paths appear in data browser
599
+ - [ ] Units are correct (Kelvin for temp, etc.)
600
+ - [ ] No memory leaks over extended operation
601
+
602
+ ### Validating Against Captured HCI Data
603
+
604
+ After implementing your sensor class, verify it parses the same packets you captured:
605
+
606
+ **Extract specific packets from your capture:**
607
+ ```bash
608
+ # Use tshark to extract hex data from your captured log
609
+ tshark -r btsnoop_hci.log -Y "btatt && bluetooth.addr == XX:XX:XX:XX:XX:XX" \
610
+ -T fields -e btatt.value
611
+ ```
612
+
613
+ **Test with captured data:**
614
+ ```javascript
615
+ const MyBattery = require('./sensor_classes/MyBattery.js')
616
+
617
+ // Use actual packet from your HCI capture
618
+ const capturedPacket = "A5 5A 00 01 01 0C E4 10 0E 55 19 00 01 02"
619
+ MyBattery._test(capturedPacket)
620
+
621
+ // Output should match what vendor app showed:
622
+ // voltage=13.2
623
+ // current=10.5
624
+ // SOC=0.85
625
+ // temperature=298.15
626
+ ```
627
+
628
+ **Prompt to AI if values don't match:**
629
+ ```
630
+ My implementation parses this HCI-captured packet:
631
+ A5 5A 00 01 01 0C E4 10 0E 55
632
+
633
+ And produces:
634
+ - voltage=33.0V (WRONG)
635
+ - current=10.5A (CORRECT)
636
+
637
+ But the vendor app showed voltage=13.2V when this packet was sent.
638
+
639
+ Bytes 6-7 are: 0C E4 (hex)
640
+ 0x0CE4 = 3300 decimal
641
+
642
+ The device is a 2-cell battery. Help me debug the voltage parsing.
643
+ ```
644
+
645
+ **AI will likely identify:** You need to divide by number of cells, or the scaling factor is different, or byte order is reversed.
646
+
647
+ ## Common Data Parsing Patterns
648
+
649
+ ### Temperature Conversions
650
+ ```javascript
651
+ // Celsius to Kelvin
652
+ .read = (buffer) => { return buffer.readInt16LE(offset) / 10 + 273.15 }
653
+
654
+ // Handle "not available" values
655
+ .read = (buffer) => {
656
+ const val = buffer.readInt8(offset)
657
+ return val === 0x7F ? NaN : val + 273.15
658
+ }
659
+ ```
660
+
661
+ ### Current Direction
662
+ ```javascript
663
+ // Positive = charging, negative = discharging
664
+ .read = (buffer) => { return buffer.readInt16LE(offset) / 100 }
665
+ ```
666
+
667
+ ### State of Charge
668
+ ```javascript
669
+ // Percent to ratio (0-1)
670
+ .read = (buffer) => { return buffer.readUInt8(offset) / 100 }
671
+ ```
672
+
673
+ ### Bit Flags
674
+ ```javascript
675
+ .read = (buffer) => {
676
+ const byte = buffer.readUInt8(offset)
677
+ return {
678
+ charging: (byte & 0x01) !== 0,
679
+ discharging: (byte & 0x02) !== 0,
680
+ balancing: (byte & 0x04) !== 0,
681
+ fault: (byte & 0x80) !== 0
682
+ }
683
+ }
684
+ ```
685
+
686
+ ## Advanced Topics
687
+
688
+ ### Encryption
689
+ Some devices (like Victron) encrypt advertising data. If your device uses encryption:
690
+
691
+ **Prompt to AI:**
692
+ ```
693
+ My device uses AES-CTR encryption with a 128-bit key. The encrypted data starts
694
+ at byte 8 of the manufacturer data. The nonce/counter is in bytes 0-7.
695
+
696
+ Please implement:
697
+ 1. A decrypt() method using Node.js crypto module
698
+ 2. Update propertiesChanged() to decrypt before parsing
699
+ 3. Add encryptionKey parameter to configuration
700
+
701
+ Follow the pattern from VictronSensor.js.
702
+ ```
703
+
704
+ ### Multiple Cell Voltages
705
+ For batteries with multiple cells:
706
+
707
+ **Prompt to AI:**
708
+ ```
709
+ The battery has [N] cells. Cell voltages are packed into the data:
710
+ - Starting at byte X
711
+ - Each cell is 2 bytes, little-endian
712
+ - Values are in millivolts (1mV = 0.001V)
713
+
714
+ Please:
715
+ 1. Add a loop in initSchema() to create cell voltage paths dynamically
716
+ 2. Create read functions for each cell
717
+ 3. Use template {batteryID} in paths like:
718
+ electrical.batteries.{batteryID}.cell[N].voltage
719
+ ```
720
+
721
+ ### Checksum Validation
722
+ For protocols with checksums:
723
+
724
+ **Prompt to AI:**
725
+ ```
726
+ The protocol includes a checksum at bytes [X-Y]:
727
+ - Type: [CRC-16/sum/XOR]
728
+ - Calculated over bytes [A-B]
729
+ - [Describe algorithm]
730
+
731
+ Please implement a validateChecksum() function and call it before parsing data.
732
+ Reject invalid packets with this.debug() message.
733
+
734
+ See JBDBMS.js checkSum() function as reference.
735
+ ```
736
+
737
+ ## Troubleshooting Guide
738
+
739
+ | Problem | Likely Cause | Solution Prompt |
740
+ |---------|-------------|-----------------|
741
+ | Device not appearing in scan | UUID filter, not advertising | "Check if identify() method is correct. Show me how to debug device detection." |
742
+ | Values are NaN | Wrong byte offsets, invalid data | "Values showing NaN. Help me verify byte offsets and add validation." |
743
+ | Connection fails | Wrong service UUID, device busy | "GATT connection failing with error: [error]. What should I check?" |
744
+ | Values incorrect magnitude | Wrong scaling factor | "Values are 100x too large. Check my scaling factors." |
745
+ | Negative when should be positive | Unsigned vs signed | "Current showing negative when charging. Fix my Int/UInt usage." |
746
+ | Updates stop after a while | Memory leak, no cleanup | "Device stops updating after 5 minutes. Check my cleanup in stopListening()." |
747
+
748
+ ## Resources
749
+
750
+ - [SignalK Specification](http://signalk.org/specification/latest/doc/vesselsBranch.html)
751
+ - [Bluetooth GATT Specifications](https://www.bluetooth.com/specifications/specs/)
752
+ - [Node.js Buffer Documentation](https://nodejs.org/api/buffer.html)
753
+ - Plugin README: [`README.md`](README.md:1)
754
+ - Development Guide: [`sensor_classes/DEVELOPMENT.md`](sensor_classes/DEVELOPMENT.md:1)
755
+ - Base Sensor Class: [`BTSensor.js`](BTSensor.js:1)
756
+
757
+ ## Summary
758
+
759
+ The AI-assisted development process follows this pattern:
760
+
761
+ 1. **Research** - Analyze similar implementations
762
+ 2. **Identify** - Determine communication method (advertising vs GATT)
763
+ 3. **Create** - Generate skeleton sensor class
764
+ 4. **Implement** - Add device identification logic
765
+ 5. **Parse** - Implement data parsing with correct byte operations
766
+ 6. **Map** - Create SignalK path mappings with defaults
767
+ 7. **Register** - Add to classLoader.js
768
+ 8. **Test** - Verify with real hardware
769
+ 9. **Debug** - Fix scaling, offsets, byte order issues
770
+ 10. **Refine** - Add error handling and documentation
771
+ 11. **Complete** - Test thoroughly and contribute back
772
+
773
+ By providing clear, specific prompts with actual device data (hex dumps, protocol specs, test results), AI can effectively assist in creating robust, maintainable sensor implementations that follow the established patterns in this codebase.
774
+
775
+ ## Key Success Factors
776
+
777
+ 1. **Have real device data** - Actual advertising packets or GATT responses
778
+ 2. **Know the protocol** - Byte layout, scaling factors, data types
779
+ 3. **Test iteratively** - Make small changes, test each one
780
+ 4. **Compare values** - Match against device's own display
781
+ 5. **Use existing patterns** - Don't reinvent, follow proven implementations
782
+ 6. **Document thoroughly** - Help the next developer
783
+
784
+ Good luck adding your Bluetooth battery! 🔋⚡
package/README.md CHANGED
@@ -2,11 +2,17 @@
2
2
 
3
3
  ## WHAT'S NEW
4
4
 
5
+
6
+ # Version 1.3.4
7
+
8
+ - Inactivity timeout configuration option. If > 0 and there's been no contact with any Bluetooth device, the plugin will power cycle the bluetooth adapter.
9
+ - Exclude non-active devices that are Out of Range from Last Error and Status on SignalK Dashboard
10
+
5
11
  # Version 1.3.3
6
12
 
7
13
  - Support for additional Xiaomi environmental sensors
8
14
  - Out Of Range device automatic retry
9
- - Pairing guide
15
+ - [Device pairing guide](./pairing.md)
10
16
 
11
17
  # Version 1.3.2-1
12
18
 
package/index.js CHANGED
@@ -178,6 +178,11 @@ module.exports = function (app) {
178
178
  minimum: 10,
179
179
  maximum: 3600
180
180
  },
181
+ inactivityTimeout: {title: "Inactivity timeout in seconds -- set to 0 to disable. (If no contact with any sensors for this period, the plugin will attempt to power cycle the Bluetooth adapter.)",
182
+ type: "integer", default: 0,
183
+ minimum: 0,
184
+ maximum: 3600
185
+ },
181
186
  discoveryInterval: {title: "Scan for new devices interval (in seconds-- 0 for no new device scanning)",
182
187
  type: "integer",
183
188
  default: 10,
@@ -323,7 +328,8 @@ module.exports = function (app) {
323
328
  transport: options.transport,
324
329
  duplicateData: options.duplicateData,
325
330
  discoveryTimeout: options.discoveryTimeout,
326
- discoveryInterval: options.discoveryInterval
331
+ discoveryInterval: options.discoveryInterval,
332
+ inactivityTimeout: options.inactivityTimeout
327
333
  }
328
334
  }
329
335
  );
@@ -498,6 +504,10 @@ module.exports = function (app) {
498
504
  s.listen()
499
505
  if (config.active)
500
506
  await s.activate(config, plugin)
507
+ else {
508
+ s.unsetError()
509
+ s.setState("DORMANT")
510
+ }
501
511
  removeSensorFromList(s)
502
512
  addSensorToList(s)
503
513
  })
@@ -510,7 +520,8 @@ module.exports = function (app) {
510
520
  {
511
521
  s.setError(errorTxt)
512
522
  } else {
513
- plugin.setError(errorTxt)
523
+ if (config.active)
524
+ plugin.setError(errorTxt)
514
525
  }
515
526
  plugin.debug(e)
516
527
 
@@ -753,15 +764,27 @@ module.exports = function (app) {
753
764
  }
754
765
  const minTimeout=Math.min(...deviceConfigs.map((dc)=>dc?.discoveryTimeout??options.discoveryTimeout))
755
766
  const intervalTimeout = ((minTimeout==Infinity)?(options?.discoveryTimeout??plugin.schema.properties.discoveryTimeout.default):minTimeout)*1000
756
- deviceHealthID = setInterval( ()=> {
767
+
768
+ deviceHealthID = setInterval( async ()=> {
769
+ let lastContactDelta=Infinity
757
770
  sensorMap.forEach((sensor)=>{
758
771
  const config = getDeviceConfig(sensor.getMacAddress())
759
772
  const dt = config?.discoveryTimeout??options.discoveryTimeout
760
773
  const lc=sensor.elapsedTimeSinceLastContact()
774
+ if (lc<lastContactDelta) //get min last contact delta
775
+ lastContactDelta=lc
761
776
  if (lc > dt) {
762
777
  updateSensor(sensor)
763
778
  }
764
779
  })
780
+ if (sensorMap.size && options.inactivityTimeout && lastContactDelta > options.inactivityTimeout)
781
+ {
782
+
783
+ plugin.debug(`No contact with any sensors for ${lastContactDelta} seconds. Recycling Bluetooth adapter.`)
784
+ await adapter.setPowered(false)
785
+ await adapter.setPowered(true)
786
+ }
787
+
765
788
  }, intervalTimeout)
766
789
 
767
790
  if (!options.hasOwnProperty("discoveryInterval" )) //no config -- first run
@@ -789,6 +812,11 @@ module.exports = function (app) {
789
812
  progressTimeoutID=null
790
813
  }
791
814
 
815
+ if (deviceHealthID) {
816
+ clearTimeout(deviceHealthID)
817
+ deviceHealthID=null
818
+ }
819
+
792
820
  if ((sensorMap)){
793
821
  for await (const sensorEntry of sensorMap.entries()) {
794
822
  try{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bt-sensors-plugin-sk",
3
- "version": "1.3.3",
3
+ "version": "1.3.4",
4
4
  "description": "Bluetooth Sensors for Signalk - see https://www.npmjs.com/package/bt-sensors-plugin-sk#supported-sensors for a list of supported sensors",
5
5
  "main": "index.js",
6
6
  "dependencies": {
@@ -13,7 +13,7 @@
13
13
  "@jellybrick/dbus-next": "^0.10.3",
14
14
  "int24": "^0.0.1",
15
15
  "kaitai-struct": "^0.10.0",
16
- "@naugehyde/node-ble":"^1.13.4",
16
+ "@naugehyde/node-ble":"^1.13.5",
17
17
  "semver": "^7.7.1",
18
18
  "lru-cache": "^11.1.0"
19
19
  },
@@ -0,0 +1,385 @@
1
+ const BTSensor = require("../BTSensor");
2
+
3
+ /**
4
+ * HSC14F Battery Management System Sensor Class
5
+ *
6
+ * Manufacturer: BMC (HumsiENK branded)
7
+ * Protocol: Custom BLE protocol using AA prefix commands
8
+ *
9
+ * Discovered protocol details:
10
+ * - TX Handle: 0x000c (write commands to battery)
11
+ * - RX Handle: 0x000e (receive notifications from battery)
12
+ * - Command format: aa [CMD] 00 [CMD] 00
13
+ * - Multi-part responses (some commands send 2-3 notifications)
14
+ *
15
+ * Key Commands:
16
+ * - 0x00: Handshake
17
+ * - 0x21: Real-time battery data (voltage, current, SOC, temps) - PRIMARY
18
+ * - 0x22: Individual cell voltages
19
+ * - 0x23: Battery status/warnings
20
+ * - 0x20: Configuration data
21
+ * - 0x10: Manufacturer name
22
+ * - 0x11: Model number
23
+ */
24
+
25
+ class HumsienkBMS extends BTSensor {
26
+ static Domain = BTSensor.SensorDomains.electrical;
27
+
28
+ // Discovered actual UUIDs from device
29
+ static TX_RX_SERVICE = "00000001-0000-1000-8000-00805f9b34fb";
30
+ static WRITE_CHAR_UUID = "00000002-0000-1000-8000-00805f9b34fb";
31
+ static NOTIFY_CHAR_UUID = "00000003-0000-1000-8000-00805f9b34fb";
32
+
33
+ static identify(device) {
34
+ // HSC14F batteries advertise with this service UUID
35
+ // Further identification would require GATT connection to read manufacturer/model
36
+ return null;
37
+ }
38
+
39
+ static ImageFile = "JBDBMS.webp"; // Using similar BMS image for now
40
+ static Manufacturer = "BMC (HumsiENK)";
41
+ static Description = "HSC14F LiFePO4 Battery Management System";
42
+
43
+ /**
44
+ * Create HSC14F command
45
+ * Format: aa [CMD] 00 [CMD] 00
46
+ */
47
+ hsc14fCommand(command) {
48
+ return [0xaa, command, 0x00, command, 0x00];
49
+ }
50
+
51
+ /**
52
+ * Send command to battery
53
+ * HSC14F requires Write Request (0x12) not Write Command (0x52)
54
+ */
55
+ async sendCommand(command) {
56
+ this.debug(`Sending command 0x${command.toString(16)} to ${this.getName()}`);
57
+ return await this.txChar.writeValue(
58
+ Buffer.from(this.hsc14fCommand(command))
59
+ );
60
+ }
61
+
62
+ async initSchema() {
63
+ super.initSchema();
64
+ this.addDefaultParam("batteryID");
65
+
66
+ // Number of cells parameter - configurable (default 4 for LiFePO4)
67
+ if (this.numberOfCells === undefined) {
68
+ this.numberOfCells = 4;
69
+ }
70
+
71
+ this.addParameter("numberOfCells", {
72
+ title: "Number of cells",
73
+ description: "Number of cells in the battery (typically 4 for 12V LiFePO4)",
74
+ type: "number",
75
+ isRequired: true,
76
+ default: this.numberOfCells,
77
+ minimum: 1,
78
+ maximum: 16,
79
+ multipleOf: 1
80
+ });
81
+
82
+ // Voltage
83
+ this.addDefaultPath("voltage", "electrical.batteries.voltage").read = (
84
+ buffer
85
+ ) => {
86
+ // Bytes 3-4: voltage in mV, little-endian
87
+ return buffer.readUInt16LE(3) / 1000;
88
+ };
89
+
90
+ // Current - CORRECTED based on buffer analysis
91
+ this.addDefaultPath("current", "electrical.batteries.current").read = (
92
+ buffer
93
+ ) => {
94
+ // Bytes 7-8: current in milliamps, signed little-endian
95
+ // Negative = charging, Positive = discharging
96
+ return buffer.readInt16LE(7) / 1000;
97
+ };
98
+
99
+ // State of Charge
100
+ this.addDefaultPath(
101
+ "SOC",
102
+ "electrical.batteries.capacity.stateOfCharge"
103
+ ).read = (buffer) => {
104
+ // Byte 11: SOC percentage (0-100)
105
+ return buffer.readUInt8(11) / 100;
106
+ };
107
+
108
+ // Temperature 1 (ENV temperature) - CORRECTED byte position
109
+ this.addMetadatum("temp1", "K", "Battery Environment Temperature", (buffer) => {
110
+ // Byte 27: Environment temperature in °C
111
+ const tempC = buffer.readUInt8(27);
112
+ return 273.15 + tempC; // Convert to Kelvin
113
+ }).default = "electrical.batteries.{batteryID}.temperature";
114
+
115
+ // Temperature 2 (MOS temperature) - CORRECTED byte position
116
+ this.addMetadatum("temp2", "K", "Battery MOS Temperature", (buffer) => {
117
+ // Byte 28: MOS (MOSFET) temperature in °C
118
+ const tempC = buffer.readUInt8(28);
119
+ return 273.15 + tempC;
120
+ }).default = "electrical.batteries.{batteryID}.mosfetTemperature";
121
+
122
+ // Temperature 3 (Sensor) - CORRECTED byte position
123
+ this.addMetadatum("temp3", "K", "Battery Sensor Temperature", (buffer) => {
124
+ // Byte 29: Additional temperature sensor in °C
125
+ const tempC = buffer.readUInt8(29);
126
+ return 273.15 + tempC;
127
+ }).default = "electrical.batteries.{batteryID}.sensorTemperature";
128
+
129
+ // Manufacturer (from command 0x10)
130
+ this.addMetadatum(
131
+ "manufacturer",
132
+ "",
133
+ "Battery Manufacturer",
134
+ (buffer) => {
135
+ // Response: aa 10 03 42 4d 43 ...
136
+ // ASCII bytes starting at position 3
137
+ const len = buffer.readUInt8(2);
138
+ return buffer.toString("ascii", 3, 3 + len);
139
+ }
140
+ ).default = "electrical.batteries.{batteryID}.manufacturer";
141
+
142
+ // Model (from command 0x11)
143
+ this.addMetadatum("model", "", "Battery Model", (buffer) => {
144
+ // Response: aa 11 0a 42 4d 43 2d 30 34 53 30 30 31 ...
145
+ const len = buffer.readUInt8(2);
146
+ return buffer.toString("ascii", 3, 3 + len);
147
+ }).default = "electrical.batteries.{batteryID}.model";
148
+
149
+ // Cell voltages (from command 0x22)
150
+ // Number of cells is configurable via numberOfCells parameter
151
+ for (let i = 0; i < this.numberOfCells; i++) {
152
+ this.addMetadatum(
153
+ `cell${i}Voltage`,
154
+ "V",
155
+ `Cell ${i + 1} voltage`,
156
+ (buffer) => {
157
+ // Cell voltages: aa 22 30 6a 0d 58 0d 8f 0d 34 0d ...
158
+ // Starting at byte 3, each cell is 2 bytes little-endian in mV
159
+ return buffer.readUInt16LE(3 + i * 2) / 1000;
160
+ }
161
+ ).default = `electrical.batteries.{batteryID}.cell${i}.voltage`;
162
+ }
163
+ }
164
+
165
+ hasGATT() {
166
+ return true;
167
+ }
168
+
169
+ usingGATT() {
170
+ return true;
171
+ }
172
+
173
+ async initGATTNotifications() {
174
+ // Don't use notifications for polling mode
175
+ // The parent class initGATTInterval will handle periodic connections
176
+ }
177
+
178
+ async emitGATT() {
179
+ try {
180
+ await this.getAndEmitBatteryInfo();
181
+ } catch (e) {
182
+ this.debug(`Failed to emit battery info for ${this.getName()}: ${e}`);
183
+ throw e; // Re-throw so parent can handle reconnection
184
+ }
185
+
186
+ // Get cell voltages after a short delay
187
+ await new Promise(resolve => setTimeout(resolve, 2000));
188
+
189
+ try {
190
+ await this.getAndEmitCellVoltages();
191
+ } catch (e) {
192
+ this.debug(`Failed to emit cell voltages for ${this.getName()}: ${e}`);
193
+ throw e; // Re-throw so parent can handle reconnection
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Get buffer response from battery command
199
+ * HSC14F sends multi-part responses for some commands
200
+ */
201
+ async getBuffer(command) {
202
+ let result = Buffer.alloc(256);
203
+ let offset = 0;
204
+ let lastPacketTime = Date.now();
205
+
206
+ // Set up listener first
207
+ const responsePromise = new Promise((resolve, reject) => {
208
+ const timer = setTimeout(() => {
209
+ clearTimeout(timer);
210
+ if (completionTimer) clearTimeout(completionTimer);
211
+ this.rxChar.removeAllListeners("valuechanged");
212
+ reject(
213
+ new Error(
214
+ `Response timed out (+10s) from HSC14F device ${this.getName()}.`
215
+ )
216
+ );
217
+ }, 10000);
218
+
219
+ let completionTimer = null;
220
+
221
+ const valChanged = (buffer) => {
222
+ // HSC14F responses start with 0xaa followed by command byte
223
+ if (offset === 0 && (buffer[0] !== 0xaa || buffer[1] !== command)) {
224
+ this.debug(
225
+ `Invalid buffer from ${this.getName()}, expected command 0x${command.toString(
226
+ 16
227
+ )}, got 0x${buffer[0].toString(16)} 0x${buffer[1].toString(16)}`
228
+ );
229
+ return;
230
+ }
231
+
232
+ buffer.copy(result, offset);
233
+ offset += buffer.length;
234
+ lastPacketTime = Date.now();
235
+
236
+ // Clear any existing completion timer
237
+ if (completionTimer) clearTimeout(completionTimer);
238
+
239
+ // Wait 200ms after last packet to consider response complete
240
+ // This allows multi-packet responses to assemble properly
241
+ completionTimer = setTimeout(() => {
242
+ result = Uint8Array.prototype.slice.call(result, 0, offset);
243
+ this.rxChar.removeAllListeners("valuechanged");
244
+ clearTimeout(timer);
245
+ resolve(result);
246
+ }, 200);
247
+ };
248
+
249
+ // Set up listener BEFORE sending command
250
+ this.rxChar.on("valuechanged", valChanged);
251
+ });
252
+
253
+ // Small delay to ensure listener is attached
254
+ await new Promise(r => setTimeout(r, 100));
255
+
256
+ // Send the command
257
+ try {
258
+ await this.sendCommand(command);
259
+ } catch (err) {
260
+ this.rxChar.removeAllListeners("valuechanged");
261
+ throw err;
262
+ }
263
+
264
+ // Wait for response
265
+ return responsePromise;
266
+ }
267
+
268
+ async initGATTConnection(isReconnecting = false) {
269
+ await super.initGATTConnection(isReconnecting);
270
+
271
+ // Set up GATT characteristics
272
+ const gattServer = await this.device.gatt();
273
+ const txRxService = await gattServer.getPrimaryService(
274
+ this.constructor.TX_RX_SERVICE
275
+ );
276
+ this.rxChar = await txRxService.getCharacteristic(
277
+ this.constructor.NOTIFY_CHAR_UUID
278
+ );
279
+ this.txChar = await txRxService.getCharacteristic(
280
+ this.constructor.WRITE_CHAR_UUID
281
+ );
282
+ await this.rxChar.startNotifications();
283
+
284
+ return this;
285
+ }
286
+
287
+ /**
288
+ * Get and emit main battery data (voltage, current, SOC, temp)
289
+ * Uses command 0x21
290
+ */
291
+ async getAndEmitBatteryInfo() {
292
+ return this.getBuffer(0x21).then((buffer) => {
293
+ // Debug logging to verify buffer received
294
+ this.debug(`Command 0x21 response: ${buffer.length} bytes, hex: ${buffer.slice(0, 30).toString('hex')}`);
295
+
296
+ ["voltage", "current", "SOC", "temp1", "temp2", "temp3"].forEach((tag) => {
297
+ this.emitData(tag, buffer);
298
+ });
299
+ });
300
+ }
301
+
302
+ /**
303
+ * Get and emit individual cell voltages
304
+ * Uses command 0x22
305
+ */
306
+ async getAndEmitCellVoltages() {
307
+ return this.getBuffer(0x22).then((buffer) => {
308
+ // Debug logging to verify buffer received
309
+ this.debug(`Command 0x22 response: ${buffer.length} bytes, hex: ${buffer.slice(0, 30).toString('hex')}`);
310
+
311
+ for (let i = 0; i < this.numberOfCells; i++) {
312
+ this.emitData(`cell${i}Voltage`, buffer);
313
+ }
314
+ });
315
+ }
316
+
317
+ async initGATTInterval() {
318
+ // Get static info once (manufacturer, model) at first connection
319
+ try {
320
+ const mfgBuffer = await this.getBuffer(0x10);
321
+ this.emitData("manufacturer", mfgBuffer);
322
+
323
+ const modelBuffer = await this.getBuffer(0x11);
324
+ this.emitData("model", modelBuffer);
325
+ } catch (e) {
326
+ this.debug(`Failed to get device info: ${e.message}`);
327
+ }
328
+
329
+ // Get first poll data before disconnecting
330
+ try {
331
+ await this.emitGATT();
332
+ } catch (error) {
333
+ this.debug(`Initial poll failed: ${error.message}`);
334
+ }
335
+
336
+ // Disconnect after initial data collection
337
+ await this.deactivateGATT().catch((e) => {
338
+ this.debug(`Error deactivating GATT Connection: ${e.message}`);
339
+ });
340
+ this.setState("WAITING");
341
+
342
+ // Set up polling interval - reconnect, poll, disconnect
343
+ this.intervalID = setInterval(async () => {
344
+ try {
345
+ this.setState("CONNECTING");
346
+ await this.initGATTConnection(true);
347
+ await this.emitGATT();
348
+ } catch (error) {
349
+ // Check if device has been removed from BlueZ cache
350
+ if (error.message && error.message.includes("interface not found in proxy object")) {
351
+ this.debug(`Device removed from BlueZ cache. Clearing stale connection state.`);
352
+ this.setError(`Device out of range or removed from Bluetooth cache. Waiting for rediscovery...`);
353
+
354
+ // Clear the interval to stop futile reconnection attempts
355
+ if (this.intervalID) {
356
+ clearInterval(this.intervalID);
357
+ this.intervalID = null;
358
+ }
359
+
360
+ // Set state to indicate waiting for rediscovery
361
+ this.setState("OUT_OF_RANGE");
362
+ return;
363
+ }
364
+
365
+ this.debug(error);
366
+ this.setError(`Unable to emit values for device: ${error.message}`);
367
+ } finally {
368
+ await this.deactivateGATT().catch((e) => {
369
+ // Suppress errors when device is already removed from BlueZ
370
+ if (!e.message || !e.message.includes("interface not found")) {
371
+ this.debug(`Error deactivating GATT Connection: ${e.message}`);
372
+ }
373
+ });
374
+ this.setState("WAITING");
375
+ }
376
+ }, this.pollFreq * 1000);
377
+ }
378
+
379
+ async deactivateGATT() {
380
+ await this.stopGATTNotifications(this.rxChar);
381
+ await super.deactivateGATT();
382
+ }
383
+ }
384
+
385
+ module.exports = HumsienkBMS;