kahu-signalk 0.0.9 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/plugin/index.js +223 -88
package/README.md
CHANGED
|
@@ -207,7 +207,7 @@ You may see an **ExperimentalWarning** about SQLite from Node; that is expected
|
|
|
207
207
|
|
|
208
208
|
Use `--config-dir` (with an **n**), not `--confic-dir`:
|
|
209
209
|
```bash
|
|
210
|
-
npm start -- --config-dir /home/
|
|
210
|
+
npm start -- --config-dir /home/signalk-server/config
|
|
211
211
|
```
|
|
212
212
|
The `--` is needed so npm passes `--config-dir` to the start script.
|
|
213
213
|
|
package/package.json
CHANGED
package/plugin/index.js
CHANGED
|
@@ -1,164 +1,299 @@
|
|
|
1
|
-
const { v4: uuidv4 } = require(
|
|
1
|
+
const { v4: uuidv4 } = require("uuid");
|
|
2
2
|
const { Routecache } = require("./routecache");
|
|
3
3
|
const { Connector } = require("./connector");
|
|
4
|
-
const path = require(
|
|
5
|
-
const fs = require(
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const fs = require("fs");
|
|
6
6
|
|
|
7
7
|
let packageDir = __dirname;
|
|
8
|
-
while (
|
|
9
|
-
|
|
8
|
+
while (
|
|
9
|
+
packageDir !== path.dirname(packageDir) &&
|
|
10
|
+
!fs.existsSync(path.join(packageDir, "package.json"))
|
|
11
|
+
) {
|
|
10
12
|
packageDir = path.dirname(packageDir);
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
const
|
|
15
|
+
const parseOptionalFloat = (value) => {
|
|
16
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
17
|
+
const parsed = parseFloat(value);
|
|
18
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const parseRattmSentence = (sentence) => {
|
|
22
|
+
const [payload] = sentence.split("*");
|
|
23
|
+
if (!payload) return null;
|
|
24
|
+
|
|
25
|
+
const parts = payload.trim().split(",");
|
|
26
|
+
if (parts[0] !== "$RATTM") return null;
|
|
27
|
+
if (parts.length < 16) return null;
|
|
28
|
+
|
|
29
|
+
const target_id = parseInt(parts[1], 10);
|
|
30
|
+
const target_distance = parseOptionalFloat(parts[2]);
|
|
31
|
+
const target_bearing = parseOptionalFloat(parts[3]);
|
|
32
|
+
const target_bearing_unit = parts[4] || "";
|
|
33
|
+
const target_speed = parseOptionalFloat(parts[5]);
|
|
34
|
+
const target_course = parseOptionalFloat(parts[6]);
|
|
35
|
+
const target_course_unit = parts[7] || "";
|
|
36
|
+
const target_distance_unit = parts[10] || "";
|
|
37
|
+
const target_name = parts[11] || "";
|
|
38
|
+
const target_status = parts[12] || "";
|
|
39
|
+
|
|
40
|
+
if (
|
|
41
|
+
!Number.isFinite(target_id) ||
|
|
42
|
+
!Number.isFinite(target_distance) ||
|
|
43
|
+
!Number.isFinite(target_bearing)
|
|
44
|
+
) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
target_id,
|
|
50
|
+
target_distance,
|
|
51
|
+
target_bearing,
|
|
52
|
+
target_bearing_unit,
|
|
53
|
+
target_speed,
|
|
54
|
+
target_course,
|
|
55
|
+
target_course_unit,
|
|
56
|
+
target_distance_unit,
|
|
57
|
+
target_name,
|
|
58
|
+
target_status,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
14
61
|
|
|
15
62
|
const deg2rad = (degrees) => degrees * (Math.PI / 180);
|
|
16
63
|
|
|
17
64
|
const polar2Pos = (ownPos, bearing, distance) => {
|
|
18
65
|
return {
|
|
19
|
-
latitude:
|
|
20
|
-
|
|
21
|
-
|
|
66
|
+
latitude:
|
|
67
|
+
ownPos.latitude + (distance * Math.cos(deg2rad(bearing))) / 60 / 1852,
|
|
68
|
+
longitude:
|
|
69
|
+
ownPos.longitude +
|
|
70
|
+
(distance * Math.sin(deg2rad(bearing))) /
|
|
71
|
+
Math.cos(deg2rad(ownPos.latitude)) /
|
|
72
|
+
60 /
|
|
73
|
+
1852,
|
|
74
|
+
};
|
|
75
|
+
};
|
|
22
76
|
|
|
23
|
-
|
|
77
|
+
const buildRattmDelta = ({
|
|
78
|
+
route_id,
|
|
79
|
+
target_distance,
|
|
80
|
+
target_bearing,
|
|
81
|
+
target_bearing_unit,
|
|
82
|
+
target_distance_unit,
|
|
83
|
+
target_name,
|
|
84
|
+
target_speed,
|
|
85
|
+
target_course,
|
|
86
|
+
targetPos,
|
|
87
|
+
ownPos,
|
|
88
|
+
}) => {
|
|
89
|
+
const relative = {
|
|
90
|
+
position: ownPos,
|
|
91
|
+
distance: target_distance,
|
|
92
|
+
bearing: target_bearing,
|
|
93
|
+
bearing_unit: target_bearing_unit,
|
|
94
|
+
distance_unit: target_distance_unit,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
context: "vessels.urn:mrn:signalk:uuid:" + route_id,
|
|
99
|
+
updates: [
|
|
100
|
+
{
|
|
101
|
+
values: [
|
|
102
|
+
{ path: "name", value: target_name },
|
|
103
|
+
{ path: "navigation.speedOverGround", value: target_speed },
|
|
104
|
+
{ path: "navigation.courseOverGroundTrue", value: target_course },
|
|
105
|
+
{ path: "navigation.position", value: { ...targetPos, relative } },
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
};
|
|
24
111
|
|
|
112
|
+
module.exports = (app) => {
|
|
25
113
|
const plugin = {
|
|
26
|
-
id:
|
|
27
|
-
name:
|
|
114
|
+
id: "radarhub",
|
|
115
|
+
name: "KAHU Radar Hub",
|
|
28
116
|
start: async (settings, restartPlugin) => {
|
|
29
|
-
console.log("Started KAHU radar Hub")
|
|
117
|
+
console.log("Started KAHU radar Hub");
|
|
118
|
+
|
|
119
|
+
plugin.pending_rattm = [];
|
|
120
|
+
plugin.last_no_ownpos_warning_at = 0;
|
|
30
121
|
|
|
31
122
|
plugin.cache = new Routecache(
|
|
32
123
|
path.join(packageDir, "data", "protocol", "migrations"),
|
|
33
|
-
path.join(app.getDataDirPath(), "routecache.sqlite3")
|
|
124
|
+
path.join(app.getDataDirPath(), "routecache.sqlite3"),
|
|
125
|
+
);
|
|
34
126
|
await plugin.cache.init();
|
|
35
|
-
|
|
127
|
+
|
|
36
128
|
plugin.connector = new Connector({
|
|
37
129
|
routecache: plugin.cache,
|
|
38
130
|
plugin_dir: packageDir,
|
|
39
131
|
config: settings,
|
|
40
|
-
status_function: app.setPluginStatus.bind(app)
|
|
41
|
-
|
|
132
|
+
status_function: app.setPluginStatus.bind(app),
|
|
133
|
+
});
|
|
134
|
+
|
|
42
135
|
const now = new Date(1970, 1, 1);
|
|
43
136
|
plugin.route_updates = Array.from(Array(100)).map(() => now);
|
|
44
137
|
plugin.route_ids = Array.from(Array(100));
|
|
45
|
-
|
|
46
|
-
app.emitPropertyValue(
|
|
47
|
-
sentence:
|
|
138
|
+
|
|
139
|
+
app.emitPropertyValue("nmea0183sentenceParser", {
|
|
140
|
+
sentence: "TTM",
|
|
48
141
|
parser: ({ id, sentence, parts, tags }, session) => {
|
|
49
142
|
if (sentence.startsWith("$RATTL")) {
|
|
50
143
|
} else if (sentence.startsWith("$RATTM")) {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
console.log("Only parsed ", (match.length - 1), " fields of RATTM NMEA sentence: [", sentence, "]");
|
|
144
|
+
const parsed = parseRattmSentence(sentence);
|
|
145
|
+
if (!parsed) {
|
|
146
|
+
console.error(
|
|
147
|
+
"Failed to parse RATTM NMEA sentence: [",
|
|
148
|
+
sentence,
|
|
149
|
+
"]",
|
|
150
|
+
);
|
|
59
151
|
return;
|
|
60
152
|
}
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
153
|
+
|
|
154
|
+
const {
|
|
155
|
+
target_id,
|
|
156
|
+
target_distance,
|
|
157
|
+
target_bearing,
|
|
158
|
+
target_bearing_unit,
|
|
159
|
+
target_speed,
|
|
160
|
+
target_course,
|
|
161
|
+
target_course_unit,
|
|
162
|
+
target_distance_unit,
|
|
163
|
+
target_name,
|
|
164
|
+
target_status,
|
|
165
|
+
} = parsed;
|
|
166
|
+
|
|
167
|
+
if (target_bearing_unit === "R") {
|
|
168
|
+
console.warn(
|
|
169
|
+
"Relative bearings not yet supported, skipping RATTM sentence",
|
|
170
|
+
);
|
|
69
171
|
return;
|
|
70
172
|
}
|
|
71
|
-
|
|
173
|
+
|
|
72
174
|
const ownPos = app.getSelfPath("navigation.position")?.value;
|
|
73
175
|
if (!ownPos) {
|
|
74
|
-
|
|
176
|
+
// Common at startup / in mixed streams: RATTM may arrive before the first GPS fix.
|
|
177
|
+
plugin.pending_rattm.push({ parsed, received_at: Date.now() });
|
|
178
|
+
if (plugin.pending_rattm.length > 500) plugin.pending_rattm.shift();
|
|
179
|
+
|
|
180
|
+
const nowMs = Date.now();
|
|
181
|
+
if (nowMs - plugin.last_no_ownpos_warning_at > 5000) {
|
|
182
|
+
plugin.last_no_ownpos_warning_at = nowMs;
|
|
183
|
+
console.warn(
|
|
184
|
+
"No own-ship position available yet; buffering RATTM sentences until GPS fix is available",
|
|
185
|
+
);
|
|
186
|
+
}
|
|
75
187
|
return;
|
|
76
188
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
189
|
+
|
|
190
|
+
// If GPS is available now, flush any buffered RATTM sentences.
|
|
191
|
+
if (plugin.pending_rattm?.length) {
|
|
192
|
+
if (typeof app.handleMessage === "function") {
|
|
193
|
+
const buffered = plugin.pending_rattm.splice(0);
|
|
194
|
+
for (const item of buffered) {
|
|
195
|
+
const b = item?.parsed;
|
|
196
|
+
if (!b) continue;
|
|
197
|
+
if (b.target_bearing_unit === "R") continue;
|
|
198
|
+
const bTargetPos = polar2Pos(
|
|
199
|
+
ownPos,
|
|
200
|
+
b.target_bearing,
|
|
201
|
+
b.target_distance,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const bNow = new Date();
|
|
205
|
+
if (bNow - plugin.route_updates[b.target_id] > 60000) {
|
|
206
|
+
plugin.route_updates[b.target_id] = bNow;
|
|
207
|
+
plugin.route_ids[b.target_id] = uuidv4();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
app.handleMessage(
|
|
211
|
+
plugin.id,
|
|
212
|
+
buildRattmDelta({
|
|
213
|
+
route_id: plugin.route_ids[b.target_id],
|
|
214
|
+
target_distance: b.target_distance,
|
|
215
|
+
target_bearing: b.target_bearing,
|
|
216
|
+
target_bearing_unit: b.target_bearing_unit,
|
|
217
|
+
target_distance_unit: b.target_distance_unit,
|
|
218
|
+
target_name: b.target_name,
|
|
219
|
+
target_speed: b.target_speed,
|
|
220
|
+
target_course: b.target_course,
|
|
221
|
+
targetPos: bTargetPos,
|
|
222
|
+
ownPos,
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
// Can't publish buffered deltas in this environment; drop to prevent leaks.
|
|
228
|
+
plugin.pending_rattm.length = 0;
|
|
229
|
+
}
|
|
85
230
|
}
|
|
231
|
+
const targetPos = polar2Pos(
|
|
232
|
+
ownPos,
|
|
233
|
+
target_bearing,
|
|
234
|
+
target_distance,
|
|
235
|
+
);
|
|
86
236
|
|
|
87
|
-
const target_speed = parseFloat(match[5]);
|
|
88
|
-
const target_course = parseFloat(match[6]);
|
|
89
|
-
const target_course_unit = match[7];
|
|
90
237
|
// target_distance_closes_point_of_approac: parseInt(match[8]),
|
|
91
238
|
// target_time_closes_point_of_approac: parseInt(match[9]),
|
|
92
|
-
|
|
93
|
-
const target_status = match[12];
|
|
94
|
-
|
|
239
|
+
|
|
95
240
|
const now = new Date();
|
|
96
241
|
if (now - plugin.route_updates[target_id] > 60000) {
|
|
97
242
|
plugin.route_updates[target_id] = now;
|
|
98
243
|
plugin.route_ids[target_id] = uuidv4();
|
|
99
244
|
}
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
value: target_course
|
|
114
|
-
},
|
|
115
|
-
{ path: 'navigation.position',
|
|
116
|
-
value: {...targetPos, relative}
|
|
117
|
-
},
|
|
118
|
-
]
|
|
119
|
-
}
|
|
120
|
-
]
|
|
121
|
-
};
|
|
245
|
+
|
|
246
|
+
return buildRattmDelta({
|
|
247
|
+
route_id: plugin.route_ids[target_id],
|
|
248
|
+
target_distance,
|
|
249
|
+
target_bearing,
|
|
250
|
+
target_bearing_unit,
|
|
251
|
+
target_distance_unit,
|
|
252
|
+
target_name,
|
|
253
|
+
target_speed,
|
|
254
|
+
target_course,
|
|
255
|
+
targetPos,
|
|
256
|
+
ownPos,
|
|
257
|
+
});
|
|
122
258
|
}
|
|
123
|
-
}
|
|
259
|
+
},
|
|
124
260
|
});
|
|
125
261
|
|
|
126
262
|
app.streambundle
|
|
127
|
-
.getBus(
|
|
263
|
+
.getBus("navigation.position")
|
|
128
264
|
.forEach(plugin.updatePosition);
|
|
129
|
-
|
|
130
265
|
},
|
|
131
266
|
stop: async () => {
|
|
132
267
|
await plugin.connector?.destroy?.();
|
|
133
268
|
await plugin.cache?.destroy?.();
|
|
134
|
-
console.log("Stopped KAHU radar Hub")
|
|
269
|
+
console.log("Stopped KAHU radar Hub");
|
|
135
270
|
},
|
|
136
271
|
schema: () => {
|
|
137
272
|
return {
|
|
138
273
|
properties: {
|
|
139
|
-
server: {type: "string", default: "crowdsource.kahu.earth"},
|
|
140
|
-
port: {type: "number", default: 9900},
|
|
141
|
-
api_key: {type: "string"},
|
|
142
|
-
min_reconnect_time: {type: "number", default: 100.0},
|
|
143
|
-
max_reconnect_time: {type: "number", default: 600.0}
|
|
144
|
-
}
|
|
274
|
+
server: { type: "string", default: "crowdsource.kahu.earth" },
|
|
275
|
+
port: { type: "number", default: 9900 },
|
|
276
|
+
api_key: { type: "string" },
|
|
277
|
+
min_reconnect_time: { type: "number", default: 100.0 },
|
|
278
|
+
max_reconnect_time: { type: "number", default: 600.0 },
|
|
279
|
+
},
|
|
145
280
|
};
|
|
146
281
|
},
|
|
147
282
|
updatePosition: (pos) => {
|
|
148
283
|
if (pos.source.sentence != "TTM") return;
|
|
149
284
|
|
|
150
285
|
const rest = app.getPath(pos.context);
|
|
151
|
-
|
|
286
|
+
|
|
152
287
|
const target_id = pos.context.split("vessels.urn:mrn:signalk:uuid:")[1];
|
|
153
|
-
|
|
288
|
+
|
|
154
289
|
plugin.cache.insert({
|
|
155
290
|
target_id: target_id,
|
|
156
291
|
position: pos.value,
|
|
157
292
|
speedOverGround: rest?.navigation?.speedOverGround?.value,
|
|
158
293
|
courseOverGroundTrue: rest?.navigation?.courseOverGroundTrue?.value,
|
|
159
294
|
name: rest?.name?.value,
|
|
160
|
-
|
|
161
|
-
}
|
|
295
|
+
});
|
|
296
|
+
},
|
|
162
297
|
};
|
|
163
298
|
|
|
164
299
|
return plugin;
|