bt-sensors-plugin-sk 1.0.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/BTSensor.js ADDED
@@ -0,0 +1,77 @@
1
+ const EventEmitter = require('node:events');
2
+
3
+ /**
4
+ * @classdesc Abstract class that all sensor classes should inherit from. Sensor subclasses monitor a
5
+ * BT peripheral and emit changes in the sensors's value like "temp" or "humidity"
6
+ * @class BTSensor
7
+ * @see EventEmitter, node-ble/Device
8
+ */
9
+ class BTSensor {
10
+
11
+ constructor(device) {
12
+ this.device=device
13
+ this.eventEmitter = new EventEmitter();
14
+ }
15
+ /**
16
+ * tells plugin if the class needs to keep the scanner running.
17
+ * defaults to [true].
18
+ * If any loaded instance of a sensor needs the scanner, it stays on for all.
19
+ * @static
20
+ * @returns boolean
21
+ */
22
+ static needsScannerOn(){
23
+ return true
24
+ }
25
+ /**
26
+ *
27
+ * @returns empty array
28
+ */
29
+ static events() {
30
+ throw new Error("events() static function must be implemented by subclass")
31
+ }
32
+
33
+ static metadataTags() {
34
+ return this.metadata.keys()
35
+ }
36
+
37
+ static hasMetaData(id) {
38
+ return this.metadata.has(id)
39
+ }
40
+
41
+ static unitFor(id){
42
+ return this.metadata.get(id)?.unit
43
+ }
44
+ /**
45
+ * Connect to sensor.
46
+ * This is where the logic for connecting to sensor, listening for changes in values and emitting those values go
47
+ * @throws Error if unimplemented by subclass
48
+ */
49
+
50
+ connect(){
51
+ throw new Error("connect() member function must be implemented by subclass")
52
+ }
53
+ /**
54
+ * Discconnect from sensor.
55
+ * Implemented by subclass if additional behavior necessary (like disconnect from device's GattServer etc.)
56
+ */
57
+
58
+ disconnect(){
59
+ this.eventEmitter.removeAllListeners()
60
+ }
61
+
62
+ /**
63
+ * Convenience method for emitting value changes.
64
+ * Just passes on(eventName, ...args) through to EventEmitter instance
65
+ */
66
+
67
+
68
+ on(eventName, ...args){
69
+ this.eventEmitter.on(eventName, ...args)
70
+ }
71
+ emit(eventName, value){
72
+ this.eventEmitter.emit(eventName,value);
73
+ }
74
+
75
+ }
76
+
77
+ module.exports = BTSensor
package/README.md ADDED
@@ -0,0 +1,261 @@
1
+ # Bluetooth Sensors for [Signal K](http://www.signalk.org)
2
+
3
+
4
+ ## WHAT IT IS
5
+
6
+ BT Sensors Plugin for Signalk is a lightweight BLE (Bluetooth Low Energy) framework for connecting to Bluetooth sensors on your boat and sending deltas to Signalk paths with the values the sensors reports. <br>
7
+
8
+ A typical use case is a Bluetooth thermometer like the Xiaomi LYWSD03MMC, an inexpensive Bluetooth thermometer that runs on a 3V watch battery that can report the current temperature and humidity in your refrigerator or cabin or wherever you want to stick it (no judgement.) <br>
9
+
10
+ The reported temperature can then be displayed on a Signalk app like Kip or, with appropiate mapping to NMEA-2000, a NMEA 2000 Multi-function display.
11
+
12
+ The Plugin currently supports the Xiaomi LYWSD03MMC, [ATC](https://github.com/atc1441/ATC_MiThermometer) flashed LYWSD03MMCs, Victron SmartShunt and the Inkbird IBS-TH2 thermometer.
13
+
14
+ Sounds like meager offerings but it's pretty easy to write and deploy your own sensor class for any currently unsupported sensor. More on that in [the development section](#development).
15
+
16
+ ## WHO IS IT FOR
17
+
18
+ Signalk users with a Linux boat-puter (Windows and MacOS are NOT supported) and Bluetooth sensors they'd like to integrate into their Signalk datastream.
19
+
20
+ ## ALTERNATIVES
21
+
22
+ An [MQTT](https://mqtt.org/) server with an appropriate SK client plugin. There are several MQTT plugin clients in the Signalk appstore.
23
+
24
+ Advantages of this plugin over an MQTT server and client plugin are:
25
+ * simplicity of setup
26
+ * reduced overhead on server
27
+ * one less piece of software to maintain on your boat-puter
28
+ * ease of rolling your own sensor classes
29
+
30
+ The key advantages of an MQTT setup is comprehensive support for BT devices and non-Linux platforms.
31
+
32
+ ## REQUIREMENTS
33
+
34
+ * A Linux Signalk boat-puter with System-D (NOTE: Most Linux installations support System-D)
35
+ * A Bluetooth adapter
36
+ * [Bluez](https://www.bluez.org) installed
37
+ (Go here for [Snap installation instructions](https://snapcraft.io/bluez))
38
+ * [Node-ble](https://www.npmjs.com/package/node-ble) (installs with the plugin)
39
+ * [utilities-sk](https://github.com/naugehyde/utilities-sk)
40
+
41
+ ## INSTALLATION
42
+ ### Signalk Appstore
43
+ This will be the recommended installation when the code is ready for wider sharing. In the meantime, use the platform-specific developer install instructions below.
44
+
45
+ ### Linux
46
+ From a command prompt:<br>
47
+
48
+ <pre> cd ~/[some_dir]
49
+ git clone https://github.com/naugehyde/ping-ac-outlet-plugin-sk
50
+ cd ping_ac_outlet_plugin_sk
51
+ npm i
52
+ [sudo] npm link
53
+ cd [signalk_home]
54
+ npm link ping-ac-outlet-plugin-sk</pre>
55
+
56
+ Finally, restart SK. Plugin should appear in your server plugins list.<br>
57
+
58
+ > NOTE: "~/.signalk" is the default signalk home on Linux. If you're
59
+ > getting permissions errors executing npm link, try executing "npm link" under sudo.
60
+
61
+ ## CONFIGURATION
62
+
63
+ After installing and restarting Signalk you should see a "BT Sensors Plugin" option in the Signalk->Server->Plugin Config page.<br><br>
64
+
65
+ <img width="1135" alt="Screenshot 2024-09-01 at 8 35 34 PM" src="https://github.com/user-attachments/assets/7e5b7d87-92e3-4fcd-8971-95ef6799c62f"><br><br>
66
+
67
+ On initial configuration, wait 45 seconds (by default) for the Bluetooth device to complete its scan of nearby devices. Until the scan is complete, the Sensors section will be disabled. When the scan is complete your screen should look something like this:<br><br>
68
+
69
+ <img width="378" alt="Screenshot 2024-08-30 at 11 21 39 AM" src="https://github.com/user-attachments/assets/6e24b880-7ee7-4deb-9608-fb42d9db74f7"><br><br>
70
+
71
+ > TIP: If after 45 seconds (or whatever your Initial Scan Timeout is set to) you don't see "Scan completed. Found x Bluetooth devices." atop the config screen and the Sensors section is still disabled, close and re-open the config screen to refresh the screen. The config screen isn't as <i>reactive</i> as it oughtta be.<br><br>
72
+
73
+ Then press the + button to add a sensor. Your screen should look like this:<br><br>
74
+
75
+ <img width="1116" alt="Screenshot 2024-09-01 at 8 47 53 PM" src="https://github.com/user-attachments/assets/6fdab5cc-ab01-4441-a88f-2753a49aadbd">
76
+ <br><br>
77
+
78
+ Then select the sensor you want to connect to from the drop down.<br>
79
+
80
+ >TIP: If you need to rescan, disable and re-enable the plugin or restart Signalk.<br><br>
81
+
82
+ <img width="1109" alt="Screenshot 2024-09-01 at 8 48 04 PM" src="https://github.com/user-attachments/assets/264a8737-8c0e-4b34-a737-e0d834b54b99">
83
+ <br><br>
84
+
85
+ Then select the class of bluetooth device. The class should have a similar name to the device. If you don't see the class for your device, you can develop your own (check out [the development section](#development).). <br><br>
86
+
87
+ <img width="1104" alt="Screenshot 2024-09-01 at 8 48 19 PM" src="https://github.com/user-attachments/assets/b55ac065-6c57-48eb-8321-e40138d1ca99"><br><br>
88
+
89
+ Then it's a simple matter of associating the data emitted by the sensor with the Signalk path you want it to update. In the example pictured here there are three data points (temperature, humidity and sensor voltage). Other classes will expose different data.
90
+
91
+ <img width="1107" alt="Screenshot 2024-09-01 at 8 48 38 PM" src="https://github.com/user-attachments/assets/cb5aeb01-2e9a-44b3-ac3a-401cd7d6c3e7"><br><br>
92
+
93
+ Remember to hit the submit button.
94
+
95
+ The plugin doesn't need for Signalk to restart but restart if that makes you more comfortable.
96
+
97
+ ## NOW WHAT?
98
+
99
+ You should see data appear in your data browser. Here's a screenshot of Signalk on my boat displaying battery data from a Victron SmartShunt. <br><br>
100
+
101
+ <img width="1142" alt="Screenshot 2024-09-01 at 9 14 27 PM" src="https://github.com/user-attachments/assets/80abbc1c-e01e-4908-aa1a-eec83679cd7c"><br><br>
102
+
103
+ You can now take the data and display it using Kip, or route it to NMEA-2K and display it on a N2K MFD, or use it to create and respond to alerts in Node-Red. Life is good. So good.
104
+
105
+ # <a name="development"></a>BLUETOOTH SENSOR CLASS DEVELOPMENT
106
+
107
+ The goal of this project is to support as many mariner-useful sensors as possible. If there's anything we can do to make sensor class development easier, please let us know.<br><br>
108
+
109
+ ## REQUIREMENTS
110
+
111
+ * programming knowledge, preferably class programming in Nodejs
112
+ * familiarity with [Node-ble](https://www.npmjs.com/package/node-ble)
113
+ * ideally, the device manufacturer's specification for their bluetooth API
114
+ * failing the above, a willingness to hack or at the very least google aggressively
115
+
116
+ ## PROGRAMMING PROCESS
117
+
118
+ ### Discovery
119
+
120
+ You'll first need to know what data your sensor produces and the means by which it provides data (GATT Server or advertisement) and the details thereof.<br><br>
121
+
122
+ The first approach is to see if the device manufacturer has documented this. Not all do. Don't worry if you can't find OEM docs, it's likely someone on the internet has figured out your device's whys and wherefores for you already. Google that thang. <br><br>
123
+
124
+ If you're still coming up empty, there are any number of tools you can use to examine the Bluetooth data stream of your device. <br><br>
125
+
126
+ * bluetoothctl (installed with Bluez on Linux)
127
+ * [NRF Connect](https://play.google.com/store/apps/details?id=no.nordicsemi.android.mcp&hl=en_US)
128
+
129
+ With these tools you can see what data your device advertises, and what data it provides via a connection to its GATT Server. <br><br>
130
+
131
+ ### Coding
132
+
133
+ #### Get the code
134
+
135
+ To get the code you'll first need to clone this repository then create a branch. That's git talk. Google it if you don't know what that means.<br>
136
+
137
+ Once you've done that you're ready for...
138
+
139
+ #### Actual coding
140
+
141
+ Below is a simple Device class for the Xiaomi thermometer with stock firmware. The code demonstrates the core responsibilities of a Bluetooth sensor device class in the BT-Sensor-plugin's framework:
142
+
143
+ * resides in the sensor_classes subdirectory
144
+ * extends the BTSensor class
145
+ * provides metadata for the device's various data points (the static metadata class member)
146
+ * overrides the BTSensor::connect() and disconnect() methods
147
+ * emits values for each data point
148
+ * exports the class
149
+
150
+ <pre>const BTSensor = require("../BTSensor");
151
+
152
+ class LYWSD03MMC extends BTSensor{
153
+
154
+ static needsScannerOn(){
155
+ return false
156
+ }
157
+ static metadata = new Map()
158
+ .set('temp',{unit:'K', description: 'temperature'})
159
+ .set('humidity',{unit:'ratio', description: 'humidity'})
160
+ .set('voltage',{unit:'V', description: 'sensor battery voltage'})
161
+
162
+ constructor(device){
163
+ super(device)
164
+ }
165
+
166
+ emitValues(buffer){
167
+ this.emit("temp",((buffer.readInt16LE(0))/100) + 273.1);
168
+ this.emit("humidity",buffer.readUInt8(2)/100);
169
+ this.emit("voltage",buffer.readUInt16LE(3)/1000);
170
+ }
171
+
172
+ async connect() {
173
+ await this.device.connect()
174
+ var gattServer = await this.device.gatt()
175
+ var gattService = await gattServer.getPrimaryService("ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6")
176
+ var gattCharacteristic = await gattService.getCharacteristic("ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6")
177
+ this.emitValues(await gattCharacteristic.readValue())
178
+ await gattCharacteristic.startNotifications();
179
+ gattCharacteristic.on('valuechanged', buffer => {
180
+ this.emitValues(buffer)
181
+ })
182
+ }
183
+ async disconnect(){
184
+ super.disconnect()
185
+ await this.device.disconnect()
186
+ }
187
+ }
188
+ module.exports=LYWSD03MMC</pre>
189
+
190
+ Most of the work is done in the connect() method. In this case, the connect() method creates a connection to the device:
191
+ <pre>await this.device.connect()</pre>
192
+ Then it gets the device's gattServer and primary service:
193
+ <pre>
194
+ var gattServer = await this.device.gatt()
195
+ var gattService = await gattServer.getPrimaryService("ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6")
196
+ </pre>
197
+ Then, it requests the device's "characteristic" that will send us data.
198
+ <pre>var gattCharacteristic = await gattService.getCharacteristic("ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6")</pre>
199
+ Then it asks the characteristic for notifications when its value changes.
200
+ <pre>await gattCharacteristic.startNotifications();</pre>
201
+ Then most importantly it emits the values when the data has changed:
202
+ <pre>gattCharacteristic.on('valuechanged', buffer => {
203
+ this.emitValues(buffer)
204
+ })</pre>
205
+ In this implementation, the emitValues member function does the work of parsing the buffer and emitting the values.
206
+ <pre>
207
+ emitValues(buffer){
208
+ this.emit("temp",((buffer.readInt16LE(0))/100) + 273.1);
209
+ this.emit("humidity",buffer.readUInt8(2)/100);
210
+ this.emit("voltage",buffer.readUInt16LE(3)/1000);
211
+ }
212
+ </pre>
213
+ NOTE: If you guessed that the plugin listens to changes to device objects and then publishes the deltas, you guessed right.
214
+
215
+ ### All that said
216
+ The problem with Gatt Server devices is they stay connected and eat up a lot of energy, draining your device's batteries. You can deactivate the device from the config screen when it's not in use or in this case you can flash the device with custom firmware that changes the device to a broadcast device that advertises its data obviating the need for a battery-draining connection. In the case of Xiaomi LYWSD03MMC you can flash it with some very useful software called [ATC](https://github.com/atc1441/ATC_MiThermometer?tab=readme-ov-file) </pre>
217
+
218
+ Below is an example of a BTSensor subclass that uses the advertising protocol to get the data from a flashed Xiaomi thermometer.
219
+
220
+ <pre>const BTSensor = require("../BTSensor");
221
+ const LYWSD03MMC = require('./LYWSD03MMC.js')
222
+ class ATC extends BTSensor{
223
+
224
+ constructor(device){
225
+ super(device)
226
+ }
227
+
228
+ static metadata = LYWSD03MMC.metadata
229
+
230
+ connect() {
231
+ const cb = async (propertiesChanged) => {
232
+ try{
233
+ this.device.getServiceData().then((data)=>{
234
+ //TBD Check if the service ID is universal across ATC variants
235
+ const buff=data['0000181a-0000-1000-8000-00805f9b34fb'];
236
+ this.emit("temp",((buff.readInt16LE(6))/100) + 273.1);
237
+ this.emit("humidity",buff.readUInt16LE(8)/10000);
238
+ this.emit("voltage",buff.readUInt16LE(10)/1000);
239
+ })
240
+ }
241
+ catch (error) {
242
+ throw new Error(`Unable to read data from ${util.inspect(device)}: ${error}` )
243
+ }
244
+ }
245
+ cb();
246
+ this.device.helper.on('PropertiesChanged', cb)
247
+ }
248
+ }
249
+ module.exports=ATC</pre>
250
+
251
+ The big difference here is in the connect() method. All it does is wait on propertiesChanged and when that event occurs, the device object parses the buffer and emits the data. NOTE: Both classes have the same metadata, so the ATC class "borrows" the metadata from the LYWSD03MMC class.<br>
252
+
253
+ ## LET US KNOW
254
+
255
+ When you're done working on your class and satisified that it's functioning properly, commit and request a merge (more git talk).<br>
256
+
257
+ We love to see new sensor classes!
258
+
259
+
260
+
261
+
package/index.js ADDED
@@ -0,0 +1,254 @@
1
+ const fs = require('fs')
2
+ const util = require('util')
3
+ const path = require('path')
4
+ const {createBluetooth} = require('node-ble')
5
+ const {bluetooth, destroy} = createBluetooth()
6
+
7
+ const BTSensor = require('./BTSensor.js')
8
+ module.exports = function (app) {
9
+ const discoveryTimeout = 30
10
+ const adapterID = 'hci0'
11
+
12
+ var peripherals=[]
13
+ var starts=0
14
+ var classMap
15
+ var utilities_sk
16
+
17
+ var plugin = {};
18
+ plugin.id = 'bt-sensors-plugin-sk';
19
+ plugin.name = 'BT Sensors plugin';
20
+ plugin.description = 'Plugin to communicate with and update paths to BLE Sensors in Signalk';
21
+
22
+ //Try and load utilities-sk NOTE: should be installed from App Store--
23
+ //But there's a fail safe because I'm a reasonable man.
24
+
25
+ try{
26
+ utilities_sk = require('../_utilities-sk/utilities.js')
27
+ }
28
+ catch (error){
29
+ try {
30
+ utilities_sk = require('utilities-sk/utilities.js')
31
+ } catch(error){
32
+ console.log(`${plugin.id} Plugin utilities-sk not found. Please install.`)
33
+ utilities_sk = {
34
+ loadClasses: function(dir, ext='.js')
35
+ {
36
+ const classMap = new Map()
37
+ const classFiles = fs.readdirSync(dir)
38
+ .filter(file => file.endsWith(ext));
39
+
40
+ classFiles.forEach(file => {
41
+ const filePath = path.join(dir, file);
42
+ const cls = require(filePath);
43
+ classMap.set(cls.name, cls);
44
+ })
45
+ return classMap
46
+ }
47
+ }
48
+ }
49
+ }
50
+ function createPaths(sensorClass, peripheral){
51
+
52
+ for (const tag of sensorClass.metadataTags()) {
53
+ const path = peripheral[tag]
54
+ if (!(path===undefined))
55
+ app.handleMessage(plugin.id,
56
+ {
57
+ updates:
58
+ [{ meta: [{path: path, value: { units: sensorClass.unitFor(tag) }}]}]
59
+ }
60
+ )
61
+ }
62
+ }
63
+
64
+ function updatePath(path, val){
65
+ app.handleMessage(plugin.id, {updates: [ { values: [ {path: path, value: val }] } ] })
66
+ }
67
+
68
+ function loadClassMap() {
69
+ classMap = utilities_sk.loadClasses(path.join(__dirname, 'sensor_classes'))
70
+ }
71
+
72
+ app.debug('Loading plugin')
73
+
74
+ plugin.uiSchema = {
75
+ peripherals: {
76
+ 'ui:disabled': true
77
+ }
78
+ }
79
+
80
+ plugin.schema = {
81
+ type: "object",
82
+ description: "",
83
+ properties: {
84
+ adapter: {title: "Bluetooth Adapter", type: "string", enum:[], default:'hci0' },
85
+ discoveryTimeout: {title: "Initial scan timeout (in seconds)", type: "number",default: 45 },
86
+ peripherals:
87
+ { type: "array", title: "Sensors", items:{
88
+ title: "", type: "object",
89
+ properties:{
90
+ active: {title: "Active", type: "boolean", default: true },
91
+ mac_address: {title: "Bluetooth Device", enum: [], enumNames:[], type: "string" },
92
+ BT_class: {title: "Bluetooth sensor class", type: "string", enum: []},
93
+ discoveryTimeout: {title: "Discovery timeout (in seconds)", type: "number", default:30},
94
+ }, dependencies:{BT_class:{oneOf:[]}}
95
+
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ function setDeviceNameInList(device,name){
102
+ const deviceNamesList = plugin.schema.properties.peripherals.items.properties.
103
+ mac_address.enumNames
104
+ const deviceList = plugin.schema.properties.peripherals.items.properties.
105
+ mac_address.enum
106
+
107
+ deviceNamesList[deviceList.indexOf(device)]=`${name} (${device})`
108
+ }
109
+
110
+ function addDeviceToList( device, name ){
111
+ const devices = plugin.schema.properties.peripherals.items.properties.
112
+ mac_address.enum
113
+
114
+ if (!devices.includes(device)) {
115
+ devices.push(device)
116
+ if (!(name===undefined))
117
+ setDeviceNameInList(device,name)
118
+ }
119
+ }
120
+ async function getDeviceName(device){
121
+ var dn = "UNKNOWN"
122
+ try{
123
+ dn = await device.getName()
124
+ }
125
+ catch (error) {}
126
+ return dn
127
+ }
128
+
129
+ function updateAdapters(){
130
+ bluetooth.adapters().then((adapters)=>
131
+ {plugin.schema.properties.adapter.enum = adapters}
132
+ )
133
+ }
134
+ function updateClassProperties(){
135
+ plugin.schema.properties.peripherals.items.properties.BT_class.enum=[...classMap.keys()]
136
+ classMap.forEach(( cls, className )=>{
137
+ var oneOf = {properties:{BT_class:{enum:[className]}}}
138
+ cls.metadata.forEach((metadatum,tag)=>{
139
+ oneOf.properties[tag]={type:'string', title: "Path for "+metadatum.description}
140
+ })
141
+
142
+ plugin.schema.properties.peripherals.items.dependencies.BT_class.oneOf.push(oneOf)
143
+ })
144
+ }
145
+ function startScanner(){
146
+ bluetooth.getAdapter(app.settings?.btAdapter??adapterID).then(async (adapter) => {
147
+ app.debug("Starting scan...");
148
+ try {
149
+ await adapter.startDiscovery()
150
+ }
151
+ catch (error) {
152
+ }
153
+ plugin.schema.description='Scanning for Bluetooth devices...'
154
+ setTimeout( () => {
155
+ adapter.devices().then((devices)=>{
156
+ app.debug(`Found: ${util.inspect(devices)}`)
157
+ devices.forEach( (device) => {
158
+ adapter.waitDevice(device,discoveryTimeout*1000).then((d)=>{
159
+ getDeviceName(d).then((dn)=>{
160
+ addDeviceToList(device, dn )
161
+ })
162
+ })
163
+ .catch ((e)=> {
164
+ app.debug(e)
165
+ })
166
+ })
167
+ plugin.schema.description=`Scan complete. Found ${devices.length} Bluetooth devices.`
168
+ })
169
+ plugin.uiSchema.peripherals['ui:disabled']=false
170
+ }, app.settings?.btDiscoveryTimeout ?? discoveryTimeout * 1000)
171
+ })
172
+ }
173
+ loadClassMap()
174
+ updateAdapters()
175
+ updateClassProperties()
176
+ startScanner()
177
+ plugin.start = async function (options, restartPlugin) {
178
+ if (starts>0){
179
+ app.debug('Plugin restarted');
180
+ plugin.uiSchema.peripherals['ui:disabled']=true
181
+ loadClassMap()
182
+ updateClassProperties()
183
+ startScanner()
184
+ } else {
185
+ app.debug('Plugin started');
186
+ }
187
+ starts++
188
+ const adapter = await bluetooth.getAdapter(options?.adapter??app.settings?.btAdapter??adapterID)
189
+ peripherals=options.peripherals
190
+ if (!(peripherals===undefined)){
191
+ var found = 0
192
+ for (const peripheral of peripherals) {
193
+
194
+ addDeviceToList(peripheral.mac_address)
195
+ app.setPluginStatus(`Waiting on ${peripheral.mac_address}`);
196
+ adapter.waitDevice(peripheral.mac_address,1000*peripheral.discoveryTimeout).then(async (device)=>
197
+ {
198
+
199
+ setDeviceNameInList(peripheral.mac_address, await getDeviceName(device))
200
+
201
+ if (peripheral.active) {
202
+
203
+ var sensorClass = classMap.get(peripheral.BT_class)
204
+ if (!sensorClass)
205
+ throw new Error(`File for Class ${peripheral.BT_class} not found.`)
206
+ createPaths(sensorClass, peripheral)
207
+
208
+
209
+ peripheral.sensor = new sensorClass(device);
210
+ await peripheral.sensor.connect();
211
+ for (const tag of sensorClass.metadataTags()){
212
+ const path = peripheral[tag];
213
+ if (!(path === undefined))
214
+ peripheral.sensor.on(tag, (val)=>{
215
+ updatePath(path,val)
216
+ })
217
+ }
218
+ app.debug('Device: '+peripheral.mac_address+' connected.')
219
+ app.setPluginStatus(`Connected to ${found++} sensors.`);
220
+ }
221
+ })
222
+ .catch ((e)=> {
223
+ if (peripheral.sensor)
224
+ peripheral.sensor.disconnect()
225
+ app.debug("Unable to connect to device " + peripheral.mac_address +". Reason: "+ e.message )
226
+ })
227
+ .finally( ()=>{
228
+ app.setPluginStatus(`Connected to ${found} sensors.`);
229
+ }
230
+ )
231
+ }
232
+ }
233
+
234
+ }
235
+ plugin.stop = async function () {
236
+ var adapter = await bluetooth.getAdapter(app.settings?.btAdapter??adapterID)
237
+
238
+ if (adapter && await adapter.isDiscovering()){
239
+ try{await adapter.stopDiscovery()} catch (e){
240
+ app.debug(e.message)
241
+ }
242
+ }
243
+ if (!(peripherals === undefined)){
244
+ for (p of peripherals) {
245
+ if (p.sensor)
246
+ p.sensor.disconnect()
247
+ }
248
+ }
249
+ //destroy();
250
+ app.debug('BT Sensors plugin stopped');
251
+ }
252
+
253
+ return plugin;
254
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "bt-sensors-plugin-sk",
3
+ "version": "1.0.0",
4
+ "description": "Bluetooth Sensors for Signalk",
5
+ "main": "index.js",
6
+ "dependencies": {
7
+ "dbus-next": "^0.10.2",
8
+ "node-ble": "^1.9.0"
9
+ },
10
+ "scripts": {
11
+ "test": "echo \"Error: no test specified\" && exit 1"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/naugehyde/bt-sensors-plugin-sk.git"
16
+ },
17
+ "keywords": [
18
+ "signalk-node-server-plugin", "signalk-category-hardware"
19
+ ],
20
+ "author": "Andrew Gerngross",
21
+ "license": "ISC",
22
+ "bugs": {
23
+ "url": "https://github.com/naugehyde/bt-sensors-plugin-sk/issues"
24
+ },
25
+ "homepage": "https://github.com/naugehyde/bt-sensors-plugin-sk#readme"
26
+ }
@@ -0,0 +1,30 @@
1
+ const BTSensor = require("../BTSensor");
2
+ const LYWSD03MMC = require('./LYWSD03MMC.js')
3
+ class ATC extends BTSensor{
4
+
5
+ constructor(device){
6
+ super(device)
7
+ }
8
+
9
+ static metadata = LYWSD03MMC.metadata
10
+
11
+ connect() {
12
+ const cb = async (propertiesChanged) => {
13
+ try{
14
+ this.device.getServiceData().then((data)=>{
15
+ //TBD Check if the service ID is universal across ATC variants
16
+ const buff=data['0000181a-0000-1000-8000-00805f9b34fb'];
17
+ this.emit("temp",((buff.readInt16LE(6))/100) + 273.1);
18
+ this.emit("humidity",buff.readUInt16LE(8)/10000);
19
+ this.emit("voltage",buff.readUInt16LE(10)/1000);
20
+ })
21
+ }
22
+ catch (error) {
23
+ throw new Error(`Unable to read data from ${util.inspect(device)}: ${error}` )
24
+ }
25
+ }
26
+ cb();
27
+ this.device.helper.on('PropertiesChanged', cb)
28
+ }
29
+ }
30
+ module.exports=ATC
@@ -0,0 +1,39 @@
1
+ const BTSensor = require("../BTSensor");
2
+
3
+ class LYWSD03MMC extends BTSensor{
4
+
5
+ static needsScannerOn(){
6
+ return false
7
+ }
8
+ static metadata = new Map()
9
+ .set('temp',{unit:'K', description: 'temperature'})
10
+ .set('humidity',{unit:'ratio', description: 'humidity'})
11
+ .set('voltage',{unit:'V', description: 'sensor battery voltage'})
12
+
13
+ constructor(device){
14
+ super(device)
15
+ }
16
+
17
+ emitValues(buffer){
18
+ this.emit("temp",((buffer.readInt16LE(0))/100) + 273.1);
19
+ this.emit("humidity",buffer.readUInt8(2)/100);
20
+ this.emit("voltage",buffer.readUInt16LE(3)/1000);
21
+ }
22
+
23
+ async connect() {
24
+ await this.device.connect()
25
+ var gattServer = await this.device.gatt()
26
+ var gattService = await gattServer.getPrimaryService("ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6")
27
+ var gattCharacteristic = await gattService.getCharacteristic("ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6")
28
+ this.emitValues(await gattCharacteristic.readValue())
29
+ await gattCharacteristic.startNotifications();
30
+ gattCharacteristic.on('valuechanged', buffer => {
31
+ this.emitValues(buffer)
32
+ })
33
+ }
34
+ async disconnect(){
35
+ super.disconnect()
36
+ await this.device.disconnect()
37
+ }
38
+ }
39
+ module.exports=LYWSD03MMC
@@ -0,0 +1,31 @@
1
+ const BTSensor = require("../BTSensor");
2
+
3
+ class SmartShunt extends BTSensor{
4
+ constructor(device){
5
+ super(device)
6
+ }
7
+ static metadata = new Map()
8
+ .set('current',{unit:'A', description: 'house battery amperage'})
9
+ .set('power',{unit:'W', description: 'house battery wattage'})
10
+ .set('voltage',{unit:'V', description: 'house battery voltage'})
11
+ .set('starterVoltage',{unit:'V', description: 'starter battery voltage'})
12
+ .set('consumed',{unit:'', description: 'amp-hours consumed'})
13
+ .set('soc',{unit:'', description: 'state of charge'})
14
+ .set('ttg',{unit:'s', description: 'time to go'})
15
+ connect() {
16
+ //TBD: Implement AES-Ctr decryption of ManufacturerData per https://github.com/keshavdv/victron-ble
17
+ const cb = async (propertiesChanged) => {
18
+
19
+ this.device.getManufacturerData().then((data)=>{
20
+ //TBD get shunt data and emit
21
+ this.emit("voltage", 14.0);
22
+
23
+ }
24
+ )
25
+ }
26
+ cb();
27
+ device.helper.on('PropertiesChanged', cb)
28
+ }
29
+
30
+ }
31
+ module.exports=SmartShunt
@@ -0,0 +1,61 @@
1
+ const BTSensor = require("../BTSensor");
2
+
3
+ class SmartShunt_GATT extends BTSensor{
4
+
5
+ static needsScannerOn(){
6
+ return false
7
+ }
8
+ static metadata = new Map()
9
+ .set('current',{unit:'A', description: 'house battery amperage',
10
+ gatt: '6597ed8c-4bda-4c1e-af4b-551c4cf74769',
11
+ read: (buff)=>{return buff.readInt32LE()/1000}})
12
+ .set('power',{unit:'W', description: 'house battery wattage',
13
+ gatt: '6597ed8e-4bda-4c1e-af4b-551c4cf74769',
14
+ read: (buff)=>{return buff.readInt16LE()}})
15
+ .set('voltage',{unit:'V', description: 'house battery voltage',
16
+ gatt: '6597ed8d-4bda-4c1e-af4b-551c4cf74769',
17
+ read: (buff)=>{return buff.readInt16LE()/100}})
18
+ .set('starterVoltage',{unit:'V', description: 'starter battery voltage',
19
+ gatt: '6597ed7d-4bda-4c1e-af4b-551c4cf74769',
20
+ read: (buff)=>{return buff.readInt16LE()/100}})
21
+ .set('consumed',{unit:'', description: 'amp-hours consumed',
22
+ gatt: '6597eeff-4bda-4c1e-af4b-551c4cf74769',
23
+ read: (buff)=>{return buff.readInt32LE()/10}})
24
+ .set('soc',{unit:'', description: 'state of charge',
25
+ gatt: '65970fff-4bda-4c1e-af4b-551c4cf74769',
26
+ read: (buff)=>{return buff.readUInt16LE()/10000}})
27
+ .set('ttg',{unit:'s', description: 'time to go',
28
+ gatt: '65970ffe-4bda-4c1e-af4b-551c4cf74769',
29
+ read: (buff)=>{return buff.readUInt16LE()*60}})
30
+
31
+
32
+ constructor(device){
33
+ super(device)
34
+ }
35
+
36
+
37
+ async connect() {
38
+ //TBD implement async version with error-checking
39
+ await this.device.connect()
40
+ const gattServer = await this.device.gatt()
41
+ const gattService = await gattServer.getPrimaryService("65970000-4bda-4c1e-af4b-551c4cf74769")
42
+ const keepAlive =await gattService.getCharacteristic('6597ffff-4bda-4c1e-af4b-551c4cf74769')
43
+ await keepAlive.writeValue(Buffer.from([0xFF,0xFF]), { offset: 0, type: 'request' })
44
+ this.constructor.metadata.forEach(async (datum, id)=> {
45
+ const c = await gattService.getCharacteristic(datum.gatt)
46
+ c.readValue().then( buffer =>
47
+ this.emit(id, datum.read(buffer))
48
+ )
49
+ c.startNotifications();
50
+ c.on('valuechanged', buffer => {
51
+ this.emit(id, datum.read(buffer))
52
+ })
53
+ });
54
+
55
+ }
56
+ async disconnect(){
57
+ super.disconnect()
58
+ await this.device.disconnect()
59
+ }
60
+ }
61
+ module.exports=SmartShunt_GATT
@@ -0,0 +1,27 @@
1
+ const BTSensor = require("../BTSensor");
2
+
3
+ class TPS extends BTSensor{
4
+
5
+ constructor(device){
6
+ super(device)
7
+ }
8
+ static metadata = new Map()
9
+ .set('temp',{unit:'K', description: 'temperature'})
10
+ connect() {
11
+ //TBD figure out what the heck this device is actually broadcasting
12
+ //For now it appears the current temp is in the key of the data but there are multiple keys
13
+ const cb = (propertiesChanged) => {
14
+ try {
15
+ this.device.getManufacturerData().then((data)=>{
16
+ this.emit("temp", (parseInt(Object.keys(data)[0])/100) + 273.1);
17
+ })
18
+ }
19
+ catch (error) {
20
+ throw new Error(`Unable to read data from ${util.inspect(device)}: ${error}` )
21
+ }
22
+ }
23
+ cb();
24
+ this.device.helper.on('PropertiesChanged', cb)
25
+ }
26
+ }
27
+ module.exports=TPS