bt-sensors-plugin-sk 1.3.6-3 → 1.3.6-beta4

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 CHANGED
@@ -975,6 +975,9 @@ class BTSensor extends EventEmitter {
975
975
 
976
976
  emit(tag, value){
977
977
  super.emit(tag, value)
978
+ if (this.usingGATT()) //update last contact time only for GATT devices
979
+ //which do not receive propertyChanged events when connected
980
+ this._lastContact=Date.now()
978
981
  this.setCurrentValue(tag,value)
979
982
  }
980
983
 
@@ -1091,8 +1094,10 @@ class BTSensor extends EventEmitter {
1091
1094
  })
1092
1095
  }
1093
1096
  updatePath(path, val, id, source){
1097
+
1094
1098
  this._app.handleMessage(id, {updates: [ { $source: source, values: [ { path: path, value: val }] } ] })
1095
1099
  }
1100
+
1096
1101
  elapsedTimeSinceLastContact(){
1097
1102
  if (this.device instanceof OutOfRangeDevice)
1098
1103
  return Infinity
package/README.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## WHAT'S NEW
4
4
 
5
+ # Version 1.3.6-beta4
6
+
7
+ - https://github.com/naugehyde/bt-sensors-plugin-sk/issues/107
8
+ - https://github.com/naugehyde/bt-sensors-plugin-sk/issues/106
9
+ - https://github.com/naugehyde/bt-sensors-plugin-sk/issues/103
10
+
5
11
  # Version 1.3.6
6
12
 
7
13
  - New sensor parameter: no contact threshhold.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bt-sensors-plugin-sk",
3
- "version": "1.3.6-3",
3
+ "version": "1.3.6-beta4",
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": {
@@ -180,8 +180,7 @@
180
180
  "message": "Battery very low - change now"
181
181
  }
182
182
  ],
183
- "description":"Sensor battery strength",
184
- "renderer":{ "type": "meter", "params": {}}
183
+ "description":"Sensor battery strength"
185
184
  },
186
185
 
187
186
  "batteryVoltage":
@@ -5,7 +5,7 @@ function getDistances(distanceManager, mac, txPower, rssi ){
5
5
  const accuracy = (Math.min(1,+(txPower/rssi).toFixed(3)))
6
6
  return {
7
7
  avgDistance: +distanceManager.getDistance(mac, txPower, DistanceManager.METHOD_AVG, true).toFixed(2),
8
- weightedAveDistance: +distanceManager.getDistance(mac, txPower, DistanceManager.METHOD_WEIGHTED_AVG, true).toFixed(2),
8
+ weightedAvgDistance: +distanceManager.getDistance(mac, txPower, DistanceManager.METHOD_WEIGHTED_AVG, true).toFixed(2),
9
9
  sampledDistance: +distanceManager.getDistance(mac, txPower, DistanceManager.METHOD_LAST_FEW_SAMPLES, true).toFixed(2),
10
10
  accuracy: accuracy
11
11
  }
@@ -20,7 +20,7 @@ class AbstractBeaconMixin {
20
20
  this.propertiesChanged=this.propertiesChanged.bind(obj)
21
21
  this.activate=this.activate.bind(obj)
22
22
  this.stopListening=this.stopListening.bind(obj)
23
- obj.GPSLog=[]
23
+ obj.GPSLog={log:[],beam:0,loa:0}
24
24
  }
25
25
 
26
26
  initListen() {
@@ -86,16 +86,17 @@ class AbstractBeaconMixin {
86
86
  this.emit("approxDistance", distances)
87
87
  this.emit("onBoard", distances.avgDistance<this?.presenceThreshold??20)
88
88
  }
89
- if (this._position){
90
- this.GPSLog.unshift(
89
+ if (his.GPSLog.currentPosition){
90
+ this.GPSLog.log.unshift(
91
91
  {
92
92
  timestamp: new Date().toISOString(),
93
- latitude: this._position.latitude,
94
- longitude: this._position.longitude,
93
+ heading: this.GPSLog.heading,
95
94
  distances: distances
96
95
  })
97
- if (this.GPSLog.length>AbstractBeaconMixin.logSize){
98
- this.GPSLog = this.GPSLog.slice(0,AbstractBeaconMixin.logSize)
96
+ Object.assign(this.GPSLog.log[0], this.GPSLog.currentPosition)
97
+
98
+ if (this.GPSLog.log.length>AbstractBeaconMixin.logSize){
99
+ this.GPSLog.log = this.GPSLog.log.slice(0,AbstractBeaconMixin.logSize)
99
100
  }
100
101
  this.emit("GPSLog", this.GPSLog)
101
102
  }
@@ -104,13 +105,26 @@ class AbstractBeaconMixin {
104
105
 
105
106
  async activate(config,paths){
106
107
 
108
+ this.GPSLog.beam=this._app.getSelfPath('design.beam')
109
+ this.GPSLog.loa=this._app.getSelfPath('design.length.overall')
110
+
107
111
  this._positionSub = this._app.streambundle.getSelfStream('navigation.position')
108
112
  .onValue(
109
113
  (pos) => {
110
- this._position=pos
114
+ this.GPSLog.currentPosition=pos
115
+
116
+ this.emit("GPSLog",this.GPSLog)
111
117
  }
112
118
  );
113
119
 
120
+ this._headingSub = this._app.streambundle.getSelfStream('navigation.headingTrue')
121
+ .onValue(
122
+ (pos) => {
123
+ this.GPSLog.heading=heading
124
+
125
+ this.emit("GPSLog",this.GPSLog)
126
+ }
127
+ );
114
128
  }
115
129
 
116
130
  async stopListening(){
@@ -118,6 +132,10 @@ class AbstractBeaconMixin {
118
132
  this._positionSub()
119
133
  this._positionSub = null
120
134
  }
135
+ if (this._headingSub){
136
+ this._headingSub()
137
+ this._headingSub = null
138
+ }
121
139
  }
122
140
 
123
141
  }
@@ -301,6 +301,16 @@ class GobiusCTankMeter extends BTSensor{
301
301
  this.addDefaultParam("id")
302
302
  .default="1"
303
303
 
304
+ this.addParameter("capacity",
305
+ {
306
+ title:"capacity of tank in m3 (liters/1000)",
307
+ type:"number",
308
+ unit:"m3",
309
+ isRequired: true,
310
+ default:10000
311
+ }
312
+ )
313
+
304
314
  this.addMetadatum("mst","","Device state",
305
315
  (buffer)=>{return GobiusState.get(buffer.readInt8(0))}
306
316
  )
@@ -322,6 +332,11 @@ class GobiusCTankMeter extends BTSensor{
322
332
  )
323
333
  .default='tanks.{type}.{id}.currentLevel'
324
334
 
335
+ this.addMetadatum("mfr","m3","Remaining volume",
336
+ (buffer)=>{return this.capacity*(buffer.readInt16BE(3)/1000)}
337
+ )
338
+ .default='tanks.{type}.{id}.remaining'
339
+
325
340
  this.addMetadatum("minc","rad","Sensor inclination",
326
341
  (buffer)=>{return buffer.readInt8(5) * Math.PI/180}
327
342
  )
@@ -16,19 +16,25 @@ const VictronIdentifier = require('./VictronIdentifier.js');
16
16
  }
17
17
 
18
18
 
19
- static async getDataPacket(device, md){
19
+ static async getDataPacket(device, md, timeout=60000) {
20
20
  if (md && md[this.ManufacturerID]?.value[0]==0x10)
21
21
  return md[this.ManufacturerID].value
22
22
 
23
23
  device.helper._prepare()
24
24
 
25
25
  return new Promise((resolve, reject) => {
26
+ const timeoutID = setTimeout(() => {
27
+ device.helper.removeListeners()
28
+ reject(new Error("Timeout waiting for Victron Manufacturer Data"))
29
+ }, timeout);
30
+
26
31
  device.helper.on("PropertiesChanged",
27
32
  (props)=> {
28
33
  if (Object.hasOwn(props,'ManufacturerData')){
29
34
  const md = props['ManufacturerData'].value
30
35
  if(md[this.ManufacturerID].value[0]==0x10) {
31
36
  device.helper.removeListeners()
37
+ clearTimeout(timeoutID);
32
38
  resolve(md[this.ManufacturerID].value)
33
39
  }
34
40
  }
@@ -0,0 +1,261 @@
1
+ import React, { useState } from 'react'
2
+ const RadialRing = ({
3
+ size = 400,
4
+ radius = 200,
5
+ centerOffset = {x:0, y:0},
6
+ offsets = { inner: 0.6, middle: 0.8 },
7
+ colors = { primary: "#ff0000", accent: "#ff0000" }, pulse=false
8
+ }) => {
9
+
10
+ const cx = size / 2;
11
+ const cy = size / 2
12
+ const pulsar=`
13
+ @keyframes pulse {
14
+ 0% {
15
+ transform: scale(.5);
16
+ opacity: 0.8;
17
+ }
18
+ 100% {
19
+ transform: scale(1);
20
+ opacity: 0;
21
+ }
22
+ }
23
+ `;
24
+ const ringStyle = {
25
+ transformOrigin: 'center',
26
+ animation: pulse ? `pulse 2s ease-out infinite` : 'none',
27
+ };
28
+ return (
29
+ <div>
30
+ <style>{pulsar}</style>
31
+ <svg
32
+ viewBox={`${0} ${0} ${size} ${size}`}
33
+ >
34
+ <defs>
35
+ <radialGradient id="ringGradient">
36
+ <stop offset={offsets.inner} stopColor={colors.primary} stopOpacity={0}/>
37
+ <stop offset={offsets.middle} stopColor={colors.accent} stopOpacity={1}/>
38
+ <stop offset={1} stopColor={colors.primary} stopOpacity={0}/>
39
+ </radialGradient>
40
+ </defs>
41
+ <g
42
+ transform={`translate(${centerOffset.x}, ${-1*centerOffset.y})`}>
43
+
44
+ <circle cx={cx} cy={cy} r={radius} fill="url(#ringGradient)" style={ringStyle} />
45
+ </g>
46
+ </svg>
47
+ </div>
48
+ );
49
+ };
50
+
51
+ const FuzzyDistance = ({ distance = 10, accuracy = 0.75, size = 200, centerOffset={x:0, y:0}, scale=100, pulse=false }) => {
52
+ const ptom = size / scale;
53
+ const delta = (((1 - (accuracy>=1?.96:accuracy)) * distance))
54
+ const r = (distance + delta)*ptom;
55
+ const innerR = (distance - delta)*ptom;
56
+
57
+ const offset0 = innerR/r
58
+ const offset1 = (1 + (innerR / r)) / 2;
59
+
60
+ return (
61
+ <RadialRing
62
+ pulse={pulse}
63
+ radius={r}
64
+ centerOffset={{x:centerOffset.x*ptom, y: centerOffset.y*ptom}}
65
+ offsets={{ inner: offset0, middle: offset1 }}
66
+ />
67
+ );
68
+ };
69
+
70
+
71
+ const Boat = ({
72
+ lengthMeters = 5,
73
+ widthMeters = 2,
74
+ offset = { x: 0, y: 0 }
75
+ }) => {
76
+ // We use a constant internal coordinate system (e.g., 1 unit = 1 meter)
77
+ // The viewBox will handle the scaling to the actual pixel size
78
+ const halfW = widthMeters / 2;
79
+ const halfL = lengthMeters / 2;
80
+ const padding = 0 ; // 1 meter of padding around the boat
81
+
82
+ // Calculate the viewBox to ensure the boat and the dot are always visible
83
+ const minX = -Math.max(halfW, Math.abs(offset.x)) - padding;
84
+ const minY = -Math.max(halfL, Math.abs(offset.y)) - padding;
85
+ const width = (Math.max(halfW, Math.abs(offset.x)) + padding) * 2;
86
+ const height = (Math.max(halfL, Math.abs(offset.y)) + padding) * 2;
87
+
88
+ // Path data using SVG Command syntax:
89
+ // M = MoveTo, C = Cubic Bezier, L = LineTo, Z = ClosePath
90
+ const hullPath = `
91
+ M 0 ${-halfL}
92
+ C ${halfW * 1.2} ${-halfL * 0.5}, ${halfW} ${halfL * 0.5}, ${halfW * 0.8} ${halfL}
93
+ L ${-halfW * 0.8} ${halfL}
94
+ C ${-halfW} ${halfL * 0.5}, ${-halfW * 1.2} ${-halfL * 0.5}, 0 ${-halfL}
95
+ Z
96
+ `;
97
+
98
+
99
+ return (
100
+ <svg
101
+ viewBox={`${minX} ${minY} ${width} ${height}`}
102
+ style={{ width: '100%', height: '100%' }}
103
+ >
104
+ {/* The Boat Hull */}
105
+ <path
106
+ d={hullPath}
107
+ fill="white"
108
+ stroke="#333"
109
+ strokeWidth={widthMeters * 0.02}
110
+ strokeLinejoin="round"
111
+ />
112
+
113
+ <g
114
+ transform={`translate(${offset.x}, ${-1*offset.y})`}
115
+ stroke="#211d3a77"
116
+ strokeWidth={widthMeters * 0.015}
117
+ >
118
+ {/* Horizontal line */}
119
+ <line x1="-0.3" y1="0" x2="0.3" y2="0" />
120
+ {/* Vertical line */}
121
+ <line x1="0" y1="-0.3" x2="0" y2="0.3" />
122
+ </g>
123
+ </svg>
124
+ );
125
+ };
126
+ function formatMilliseconds(ms) {
127
+ const seconds = Math.floor((ms / 1000) % 60);
128
+ const minutes = Math.floor((ms / (1000 * 60)) % 60);
129
+ const hours = Math.floor((ms / (1000 * 60 * 60)) % 24);
130
+ const days = Math.floor(ms / (1000 * 60 * 60 * 24));
131
+
132
+ // Format with leading zeros
133
+ return `${days}${days>0?'d':''} ${days>0|hours>0?String(hours).padStart(2, '0')+'h':''} ${hours>0|minutes>0?String(minutes).padStart(2, '0')+'m':''} ${String(seconds).padStart(2, '0')}s`;
134
+ }
135
+
136
+ /**
137
+ * Calculates the nautical distance between two points on Earth.
138
+ * @param {number} lat1 - Latitude of first point
139
+ * @param {number} lon1 - Longitude of first point
140
+ * @param {number} lat2 - Latitude of second point
141
+ * @param {number} lon2 - Longitude of second point
142
+ * @returns {number} Distance in Nautical Miles (NM)
143
+ *//**
144
+ * Calculates the nautical distance between two points on Earth.
145
+ * @param {number} lat1 - Latitude of first point
146
+ * @param {number} lon1 - Longitude of first point
147
+ * @param {number} lat2 - Latitude of second point
148
+ * @param {number} lon2 - Longitude of second point
149
+ * @returns {number} Distance in Nautical Miles (NM)
150
+ */
151
+ function calculateNauticalDistance(lat1, lon1, lat2, lon2) {
152
+ const R = 3440.065; // Earth's radius in Nautical Miles
153
+ const dLat = (lat2 - lat1) * (Math.PI / 180);
154
+ const dLon = (lon2 - lon1) * (Math.PI / 180);
155
+
156
+ const a =
157
+ Math.sin(dLat / 2) * Math.sin(dLat / 2) +
158
+ Math.cos(lat1 * (Math.PI / 180)) *
159
+ Math.cos(lat2 * (Math.PI / 180)) *
160
+ Math.sin(dLon / 2) * Math.sin(dLon / 2);
161
+
162
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
163
+ const d = R * c; // Distance in NM
164
+
165
+ return d;
166
+ }
167
+
168
+ function getLatLonOffset(lat1, lon1, lat2, lon2) {
169
+ const R = 6371000; // Earth radius in meters
170
+ const toRad = (deg) => (deg * Math.PI) / 180;
171
+
172
+ // 1. Calculate the difference in Radians
173
+ const dLat = toRad(lat2 - lat1);
174
+ const dLon = toRad(lon2 - lon1);
175
+ const latAvg = toRad((lat1 + lat2) / 2);
176
+
177
+ // 2. Apply the projection
178
+ const dy = toRad(lat2 - lat1) * R;
179
+ const dx = dLon * R * Math.cos(latAvg);
180
+
181
+ return { x: dx, y: dy };
182
+ }
183
+
184
+ function getBearing(lat1, lon1, lat2, lon2) {
185
+ // Convert degrees to radians
186
+ const toRadians = (degrees) => (degrees * Math.PI) / 180;
187
+ const toDegrees = (radians) => (radians * 180) / Math.PI;
188
+
189
+ const startLat = toRadians(lat1);
190
+ const startLng = toRadians(lon1);
191
+ const destLat = toRadians(lat2);
192
+ const destLng = toRadians(lon2);
193
+
194
+ const y = Math.sin(destLng - startLng) * Math.cos(destLat);
195
+ const x = Math.cos(startLat) * Math.sin(destLat) -
196
+ Math.sin(startLat) * Math.cos(destLat) * Math.cos(destLng - startLng);
197
+
198
+ let bearing = Math.atan2(y, x);
199
+ bearing = toDegrees(bearing);
200
+
201
+ // Normalize to 0-360 degrees
202
+ return (bearing + 360) % 360;
203
+ }
204
+
205
+ const BeaconRenderer = ({value, centerOffset={x:0,y:0}, size}) => {
206
+ const [index, setIndex] = useState(0);
207
+ const [distanceTo, setDistanceTo ] = useState(calculateNauticalDistance(value.latitude, value.longitude, value[index].latitude, value[index].longitude ))
208
+
209
+ const latLonOffset = getLatLonOffset( value.log[index].latitude, value.log[index].longitude, value.latitude, value.longitude )
210
+
211
+ const minSize=Math.max(Math.abs(latLonOffset.x*1.5),Math.abs(latLonOffset.y*1.5))
212
+ const spriteScale=minSize<beam?0.9:scale/2
213
+ const ptom = size/(loa/scale)
214
+ console.log(scale, value.loa/(1/scale), ptom, latLonOffset.x*ptom, latLonOffset.y*ptom)
215
+ return (
216
+ <div>
217
+
218
+ <div style={{ width: size, height: size, background: '#42b9f5', border: '1px solid #ccc', position:'absolute' }}>
219
+ <div style={{ transform: [
220
+ `translateX(${latLonOffset.x*ptom/2}px)
221
+ translateY(${latLonOffset.y*ptom/2}px)`]
222
+ }}>
223
+ <div style={{ zIndex:1, width: size, height: size, position: 'absolute',
224
+ transform: [`scale(${spriteScale})
225
+ rotate(${value.heading}rad)`]}}>
226
+ <Boat lengthMeters={value.loa} widthMeters={value.beam} offset={centerOffset} />
227
+ </div>
228
+ </div>
229
+ <div style={{ transform: [`scale(${spriteScale})`], width: size, height: size, position: 'absolute', zIndex:2}}>
230
+ <FuzzyDistance pulse={distanceTo*1852>(value.loa/2)} scale={(value.loa)} accuracy={value.log[index].distances.accuracy} distance={value.log[index].distances.avgDistance} centerOffset={centerOffset} size={size}/>
231
+ </div>
232
+
233
+ <div style={{ color:"black", fontSize: `${size/22}px`}}>
234
+ <div style={{ position: 'absolute', top:10, left: 10}}>
235
+
236
+ {value.log[0].latitude}, {value.log[0].longitude} <p/>
237
+ {formatMilliseconds(timeNow-new Date(value.log[index].timestamp).valueOf())}
238
+ </div>
239
+ <div style={{ position: 'absolute', bottom:10, left:10 }}>
240
+ {distanceTo>.25?distanceTo.toFixed(2)+'nm':(distanceTo*1852).toFixed(2)+'m'} {getBearing(value.latitude, value.longitude, value.log[index].latitude, value.log[index].longitude ).toFixed(2)}°
241
+ </div>
242
+ </div>
243
+ <div style={{ color:"black", position: 'absolute', bottom:10, right:40 }}>
244
+ <button
245
+ onClick={() => {setIndex(index==0?value.log.length-1:index - 1)}}
246
+ >
247
+
248
+ </button>
249
+ </div>
250
+ <div style={{ color:"black", position: 'absolute', bottom:10, right: 10}}>
251
+ <button
252
+ onClick={() => {setIndex(index==value.log.length-1?0:index + 1)}}
253
+ >
254
+
255
+ </button>
256
+ </div>
257
+ </div>
258
+ </div>
259
+ );
260
+ };
261
+ export default BeaconRenderer