kahu-signalk 0.0.10 → 0.0.12
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/data/protocol/migrations/0001-create-targets.sql +1 -1
- package/package.json +1 -1
- package/plugin/index.js +176 -82
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
|
|
|
@@ -6,7 +6,7 @@ create table if not exists target (
|
|
|
6
6
|
create table if not exists target_position (
|
|
7
7
|
id integer primary key autoincrement,
|
|
8
8
|
timestamp datetime default current_timestamp,
|
|
9
|
-
target_id integer references target(
|
|
9
|
+
target_id integer references target(target_id),
|
|
10
10
|
target_distance float,
|
|
11
11
|
target_bearing float,
|
|
12
12
|
target_bearing_unit text,
|
package/package.json
CHANGED
package/plugin/index.js
CHANGED
|
@@ -1,43 +1,47 @@
|
|
|
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
15
|
const parseOptionalFloat = (value) => {
|
|
14
|
-
if (value === undefined || value === null || value ===
|
|
16
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
15
17
|
const parsed = parseFloat(value);
|
|
16
18
|
return Number.isNaN(parsed) ? undefined : parsed;
|
|
17
19
|
};
|
|
18
20
|
|
|
19
21
|
const parseRattmSentence = (sentence) => {
|
|
20
|
-
const [payload] = sentence.split(
|
|
22
|
+
const [payload] = sentence.split("*");
|
|
21
23
|
if (!payload) return null;
|
|
22
24
|
|
|
23
|
-
const parts = payload.trim().split(
|
|
24
|
-
if (parts[0] !==
|
|
25
|
+
const parts = payload.trim().split(",");
|
|
26
|
+
if (parts[0] !== "$RATTM") return null;
|
|
25
27
|
if (parts.length < 16) return null;
|
|
26
28
|
|
|
27
29
|
const target_id = parseInt(parts[1], 10);
|
|
28
30
|
const target_distance = parseOptionalFloat(parts[2]);
|
|
29
31
|
const target_bearing = parseOptionalFloat(parts[3]);
|
|
30
|
-
const target_bearing_unit = parts[4] ||
|
|
32
|
+
const target_bearing_unit = parts[4] || "";
|
|
31
33
|
const target_speed = parseOptionalFloat(parts[5]);
|
|
32
34
|
const target_course = parseOptionalFloat(parts[6]);
|
|
33
|
-
const target_course_unit = parts[7] ||
|
|
34
|
-
const target_distance_unit = parts[10] ||
|
|
35
|
-
const target_name = parts[
|
|
36
|
-
const target_status = parts[
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
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
|
+
) {
|
|
41
45
|
return null;
|
|
42
46
|
}
|
|
43
47
|
|
|
@@ -59,41 +63,91 @@ const deg2rad = (degrees) => degrees * (Math.PI / 180);
|
|
|
59
63
|
|
|
60
64
|
const polar2Pos = (ownPos, bearing, distance) => {
|
|
61
65
|
return {
|
|
62
|
-
latitude:
|
|
63
|
-
|
|
64
|
-
|
|
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
|
+
};
|
|
65
76
|
|
|
66
|
-
|
|
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
|
+
};
|
|
67
111
|
|
|
112
|
+
module.exports = (app) => {
|
|
68
113
|
const plugin = {
|
|
69
|
-
id:
|
|
70
|
-
name:
|
|
114
|
+
id: "radarhub",
|
|
115
|
+
name: "KAHU Radar Hub",
|
|
71
116
|
start: async (settings, restartPlugin) => {
|
|
72
|
-
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;
|
|
73
121
|
|
|
74
122
|
plugin.cache = new Routecache(
|
|
75
123
|
path.join(packageDir, "data", "protocol", "migrations"),
|
|
76
|
-
path.join(app.getDataDirPath(), "routecache.sqlite3")
|
|
124
|
+
path.join(app.getDataDirPath(), "routecache.sqlite3"),
|
|
125
|
+
);
|
|
77
126
|
await plugin.cache.init();
|
|
78
|
-
|
|
127
|
+
|
|
79
128
|
plugin.connector = new Connector({
|
|
80
129
|
routecache: plugin.cache,
|
|
81
130
|
plugin_dir: packageDir,
|
|
82
131
|
config: settings,
|
|
83
|
-
status_function: app.setPluginStatus.bind(app)
|
|
84
|
-
|
|
132
|
+
status_function: app.setPluginStatus.bind(app),
|
|
133
|
+
});
|
|
134
|
+
|
|
85
135
|
const now = new Date(1970, 1, 1);
|
|
86
136
|
plugin.route_updates = Array.from(Array(100)).map(() => now);
|
|
87
137
|
plugin.route_ids = Array.from(Array(100));
|
|
88
|
-
|
|
89
|
-
app.emitPropertyValue(
|
|
90
|
-
sentence:
|
|
138
|
+
|
|
139
|
+
app.emitPropertyValue("nmea0183sentenceParser", {
|
|
140
|
+
sentence: "TTM",
|
|
91
141
|
parser: ({ id, sentence, parts, tags }, session) => {
|
|
92
142
|
if (sentence.startsWith("$RATTL")) {
|
|
93
143
|
} else if (sentence.startsWith("$RATTM")) {
|
|
94
144
|
const parsed = parseRattmSentence(sentence);
|
|
95
145
|
if (!parsed) {
|
|
96
|
-
console.error(
|
|
146
|
+
console.error(
|
|
147
|
+
"Failed to parse RATTM NMEA sentence: [",
|
|
148
|
+
sentence,
|
|
149
|
+
"]",
|
|
150
|
+
);
|
|
97
151
|
return;
|
|
98
152
|
}
|
|
99
153
|
|
|
@@ -110,96 +164,136 @@ module.exports = (app) => {
|
|
|
110
164
|
target_status,
|
|
111
165
|
} = parsed;
|
|
112
166
|
|
|
113
|
-
if (target_bearing_unit ===
|
|
114
|
-
console.warn(
|
|
167
|
+
if (target_bearing_unit === "R") {
|
|
168
|
+
console.warn(
|
|
169
|
+
"Relative bearings not yet supported, skipping RATTM sentence",
|
|
170
|
+
);
|
|
115
171
|
return;
|
|
116
172
|
}
|
|
117
|
-
|
|
173
|
+
|
|
118
174
|
const ownPos = app.getSelfPath("navigation.position")?.value;
|
|
119
175
|
if (!ownPos) {
|
|
120
|
-
|
|
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
|
+
}
|
|
121
187
|
return;
|
|
122
188
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
}
|
|
131
230
|
}
|
|
231
|
+
const targetPos = polar2Pos(
|
|
232
|
+
ownPos,
|
|
233
|
+
target_bearing,
|
|
234
|
+
target_distance,
|
|
235
|
+
);
|
|
132
236
|
|
|
133
237
|
// target_distance_closes_point_of_approac: parseInt(match[8]),
|
|
134
238
|
// target_time_closes_point_of_approac: parseInt(match[9]),
|
|
135
|
-
|
|
239
|
+
|
|
136
240
|
const now = new Date();
|
|
137
241
|
if (now - plugin.route_updates[target_id] > 60000) {
|
|
138
242
|
plugin.route_updates[target_id] = now;
|
|
139
243
|
plugin.route_ids[target_id] = uuidv4();
|
|
140
244
|
}
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
value: target_course
|
|
155
|
-
},
|
|
156
|
-
{ path: 'navigation.position',
|
|
157
|
-
value: {...targetPos, relative}
|
|
158
|
-
},
|
|
159
|
-
]
|
|
160
|
-
}
|
|
161
|
-
]
|
|
162
|
-
};
|
|
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
|
+
});
|
|
163
258
|
}
|
|
164
|
-
}
|
|
259
|
+
},
|
|
165
260
|
});
|
|
166
261
|
|
|
167
262
|
app.streambundle
|
|
168
|
-
.getBus(
|
|
263
|
+
.getBus("navigation.position")
|
|
169
264
|
.forEach(plugin.updatePosition);
|
|
170
|
-
|
|
171
265
|
},
|
|
172
266
|
stop: async () => {
|
|
173
267
|
await plugin.connector?.destroy?.();
|
|
174
268
|
await plugin.cache?.destroy?.();
|
|
175
|
-
console.log("Stopped KAHU radar Hub")
|
|
269
|
+
console.log("Stopped KAHU radar Hub");
|
|
176
270
|
},
|
|
177
271
|
schema: () => {
|
|
178
272
|
return {
|
|
179
273
|
properties: {
|
|
180
|
-
server: {type: "string", default: "crowdsource.kahu.earth"},
|
|
181
|
-
port: {type: "number", default: 9900},
|
|
182
|
-
api_key: {type: "string"},
|
|
183
|
-
min_reconnect_time: {type: "number", default: 100.0},
|
|
184
|
-
max_reconnect_time: {type: "number", default: 600.0}
|
|
185
|
-
}
|
|
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
|
+
},
|
|
186
280
|
};
|
|
187
281
|
},
|
|
188
282
|
updatePosition: (pos) => {
|
|
189
283
|
if (pos.source.sentence != "TTM") return;
|
|
190
284
|
|
|
191
285
|
const rest = app.getPath(pos.context);
|
|
192
|
-
|
|
286
|
+
|
|
193
287
|
const target_id = pos.context.split("vessels.urn:mrn:signalk:uuid:")[1];
|
|
194
|
-
|
|
288
|
+
|
|
195
289
|
plugin.cache.insert({
|
|
196
290
|
target_id: target_id,
|
|
197
291
|
position: pos.value,
|
|
198
292
|
speedOverGround: rest?.navigation?.speedOverGround?.value,
|
|
199
293
|
courseOverGroundTrue: rest?.navigation?.courseOverGroundTrue?.value,
|
|
200
294
|
name: rest?.name?.value,
|
|
201
|
-
|
|
202
|
-
}
|
|
295
|
+
});
|
|
296
|
+
},
|
|
203
297
|
};
|
|
204
298
|
|
|
205
299
|
return plugin;
|