@meri-imperiumi/signalk-aprs 0.1.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/.eslintrc.json +6 -0
- package/.github/workflows/publish.yml +30 -0
- package/.github/workflows/test.yml +21 -0
- package/README.md +35 -0
- package/package.json +33 -0
- package/plugin/aprs.js +34 -0
- package/plugin/index.js +501 -0
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Publish Node.js Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v3.0.2
|
|
13
|
+
- uses: actions/setup-node@v3.1.1
|
|
14
|
+
with:
|
|
15
|
+
node-version: 22
|
|
16
|
+
- run: npm install
|
|
17
|
+
- run: npm test
|
|
18
|
+
|
|
19
|
+
publish-npm:
|
|
20
|
+
needs: build
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v3.0.2
|
|
24
|
+
- uses: actions/setup-node@v3.1.1
|
|
25
|
+
with:
|
|
26
|
+
node-version: 22
|
|
27
|
+
registry-url: https://registry.npmjs.org/
|
|
28
|
+
- run: npm publish
|
|
29
|
+
env:
|
|
30
|
+
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Node CI
|
|
2
|
+
|
|
3
|
+
on: [push, pull_request]
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
test:
|
|
7
|
+
name: Run test suite
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
strategy:
|
|
10
|
+
matrix:
|
|
11
|
+
node-version: [22.x]
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v3.0.2
|
|
14
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
15
|
+
uses: actions/setup-node@v3.1.1
|
|
16
|
+
with:
|
|
17
|
+
node-version: ${{ matrix.node-version }}
|
|
18
|
+
- run: npm install
|
|
19
|
+
- run: npm test
|
|
20
|
+
env:
|
|
21
|
+
CI: true
|
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Signal K APRS Plugin
|
|
2
|
+
====================
|
|
3
|
+
|
|
4
|
+
This plugin integrates [Signal K](https://signalk.org) with the [Automatic Packet Reporting System](https://www.aprs.org) (APRS), a packet radio system for Radio Amateurs. The plugin connects to various APRS-capable radio systems using the KISS TNC protocol.
|
|
5
|
+
|
|
6
|
+
You need to be a licensed radio amateur to use APRS. For everybody else, [Signal K Meshtastic Plugin](https://github.com/meri-imperiumi/signalk-meshtastic) is the way to go.
|
|
7
|
+
|
|
8
|
+
APRS is a registered trademark Bob Bruninga, WB4APR.
|
|
9
|
+
|
|
10
|
+
## Status
|
|
11
|
+
|
|
12
|
+
Very early stages, being tested with the [LoRa APRS iGate](https://github.com/richonguzman/LoRa_APRS_iGate) firmware.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
* Support for connecting to multiple KISS TNCs
|
|
17
|
+
- This allows connecting to both LoRa APRS and VHF APRS for instance
|
|
18
|
+
- TX can be enabled separately for each TNC, allowing listen-only connections
|
|
19
|
+
- The plugin will keep trying to reconnect to TNCs that are offline
|
|
20
|
+
* Periodically sending vessel position as a beacon to TX-enabled TNCs
|
|
21
|
+
* Populating received WX stations into Signal K data structure
|
|
22
|
+
|
|
23
|
+
## Planned features
|
|
24
|
+
|
|
25
|
+
* Transmit WX data from boat sensors (wind, temperature, etc)
|
|
26
|
+
* Send telemetry (battery status, water depth, anchor distance) over APRS
|
|
27
|
+
* Show other APRS beacons as vessels in Freeboard etc
|
|
28
|
+
* Send alerts to crew over APRS
|
|
29
|
+
* Get a [dedicated TOCALL for this plugin](https://github.com/aprsorg/aprs-deviceid/issues/244)
|
|
30
|
+
* Figure out how to handle APRS messaging from/to boat
|
|
31
|
+
|
|
32
|
+
## Changes
|
|
33
|
+
|
|
34
|
+
* 0.1.0 (2025-10-29)
|
|
35
|
+
- Initial release, can beacon vessel position
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@meri-imperiumi/signalk-aprs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Signal K plugin for interacting with the Automatic Packet Reporting System for radio amateurs",
|
|
5
|
+
"main": "plugin/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "eslint ."
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git://github.com/meri-imperiumi/signalk-aprs.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"aprs",
|
|
15
|
+
"signalk-node-server-plugin",
|
|
16
|
+
"signalk-category-hardware"
|
|
17
|
+
],
|
|
18
|
+
"signalk-plugin-enabled-by-default": false,
|
|
19
|
+
"author": "Henri Bergius <henri.bergius@iki.fi>",
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"kiss-tnc": "^2.0.1",
|
|
26
|
+
"utils-for-aprs": "^3.1.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"eslint": "^8.57.1",
|
|
30
|
+
"eslint-config-airbnb-base": "^15.0.0",
|
|
31
|
+
"eslint-plugin-import": "^2.32.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/plugin/aprs.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function formatLatitude(coord) {
|
|
2
|
+
const degFloat = Math.abs(coord);
|
|
3
|
+
const deg = String(Math.floor(degFloat)).padStart(2, '0');
|
|
4
|
+
const minFloat = 60 * (degFloat - Math.floor(degFloat));
|
|
5
|
+
const min = String(Math.floor(minFloat)).padStart(2, '0');
|
|
6
|
+
const secFloat = 60 * (minFloat - Math.floor(minFloat));
|
|
7
|
+
const sec = String(Math.floor(secFloat)).padStart(2, '0');
|
|
8
|
+
const sign = coord > 0 ? 'N' : 'S';
|
|
9
|
+
return `${deg}${min}.${sec}${sign}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatLongitude(coord) {
|
|
13
|
+
const degFloat = Math.abs(coord);
|
|
14
|
+
const deg = String(Math.floor(degFloat)).padStart(3, '0');
|
|
15
|
+
const minFloat = 60 * (degFloat - Math.floor(degFloat));
|
|
16
|
+
const min = String(Math.floor(minFloat)).padStart(2, '0');
|
|
17
|
+
const secFloat = 60 * (minFloat - Math.floor(minFloat));
|
|
18
|
+
const sec = String(Math.floor(secFloat)).padStart(2, '0');
|
|
19
|
+
const sign = coord > 0 ? 'E' : 'W';
|
|
20
|
+
return `${deg}${min}.${sec}${sign}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatAddress(obj) {
|
|
24
|
+
if (!obj.ssid) {
|
|
25
|
+
return obj.callsign;
|
|
26
|
+
}
|
|
27
|
+
return `${obj.callsign}-${obj.ssid}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
formatLatitude,
|
|
32
|
+
formatLongitude,
|
|
33
|
+
formatAddress,
|
|
34
|
+
};
|
package/plugin/index.js
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
const { Socket } = require('node:net');
|
|
2
|
+
const { KISSSender } = require('kiss-tnc');
|
|
3
|
+
const { APRSProcessor, newKISSFrame } = require('utils-for-aprs');
|
|
4
|
+
const { formatLatitude, formatLongitude, formatAddress } = require('./aprs');
|
|
5
|
+
|
|
6
|
+
module.exports = (app) => {
|
|
7
|
+
const plugin = {};
|
|
8
|
+
let unsubscribes = [];
|
|
9
|
+
let beacons = {};
|
|
10
|
+
let connections = [];
|
|
11
|
+
let publishInterval;
|
|
12
|
+
let state = '';
|
|
13
|
+
plugin.id = 'signalk-aprs';
|
|
14
|
+
plugin.name = 'APRS';
|
|
15
|
+
plugin.description = 'Connect Signal K with the Automatic Packet Reporting System for Radio Amateurs';
|
|
16
|
+
|
|
17
|
+
function beaconsOnline() {
|
|
18
|
+
// LoRa APRS iGate uses 30min
|
|
19
|
+
const onlineSecs = 60 * 60 * 0.5;
|
|
20
|
+
const now = new Date();
|
|
21
|
+
return Object
|
|
22
|
+
.keys(beacons)
|
|
23
|
+
.filter((b) => {
|
|
24
|
+
if (beacons[b] > now.getTime() - (onlineSecs * 1000)) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
})
|
|
29
|
+
.length;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function setConnectionStatus() {
|
|
33
|
+
const connected = connections.filter((c) => c.online);
|
|
34
|
+
if (connected.length === 0) {
|
|
35
|
+
app.setPluginStatus('No TNC connection');
|
|
36
|
+
}
|
|
37
|
+
const connectedStr = connected
|
|
38
|
+
.map((c) => c.address).join(', ');
|
|
39
|
+
app.setPluginStatus(`Connected to TNC ${connectedStr}. ${beaconsOnline()} beacons online.`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
plugin.start = (settings) => {
|
|
43
|
+
if (!settings.connections || !settings.connections.length) {
|
|
44
|
+
// Not much to do here
|
|
45
|
+
app.setPluginStatus('No TNC connections configured');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const processor = new APRSProcessor();
|
|
49
|
+
processor.on('aprsData', (data) => {
|
|
50
|
+
app.setPluginStatus(`RX ${data.info}`);
|
|
51
|
+
app.debug('RX', data);
|
|
52
|
+
beacons[formatAddress(data.source)] = new Date();
|
|
53
|
+
if (data.weather) {
|
|
54
|
+
// WX station, populate to Signal K
|
|
55
|
+
const values = [];
|
|
56
|
+
if (data.position
|
|
57
|
+
&& data.position.coords
|
|
58
|
+
&& Number.isFinite(data.position.coords.latitude)) {
|
|
59
|
+
values.push({
|
|
60
|
+
path: 'navigation.position',
|
|
61
|
+
value: {
|
|
62
|
+
latitude: data.position.coords.latitude,
|
|
63
|
+
longitude: data.position.coords.longitude,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (Number.isFinite(data.weather.temperature)) {
|
|
68
|
+
values.push({
|
|
69
|
+
path: 'environment.outside.temperature',
|
|
70
|
+
value: (data.weather.temperature + 459.67) * (5 / 9), // APRS uses Fahrenheit
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (Number.isFinite(data.weather.windSpeed)) {
|
|
74
|
+
values.push({
|
|
75
|
+
path: 'environment.wind.speedTrue',
|
|
76
|
+
value: data.weather.windSpeed * 0.44704, // APRS uses mph
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (Number.isFinite(data.weather.windDirection)) {
|
|
80
|
+
values.push({
|
|
81
|
+
path: 'environment.wind.directionTrue',
|
|
82
|
+
value: data.weather.windDirection * (Math.PI / 180),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (Number.isFinite(data.weather.barometer)) {
|
|
86
|
+
values.push({
|
|
87
|
+
path: 'environment.outside.pressure',
|
|
88
|
+
value: data.weather.barometer * 1000, // APRS uses tenths of a mb
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (Number.isFinite(data.weather.humidity)) {
|
|
92
|
+
let humidity = data.weather.humidity / 100;
|
|
93
|
+
if (data.weather.humidity === 0) {
|
|
94
|
+
// 00 is 100
|
|
95
|
+
humidity = 1;
|
|
96
|
+
}
|
|
97
|
+
values.push({
|
|
98
|
+
path: 'environment.outside.absoluteHumidity',
|
|
99
|
+
value: humidity,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (values.length) {
|
|
103
|
+
values.push({
|
|
104
|
+
path: '',
|
|
105
|
+
value: {
|
|
106
|
+
name: data.source.callsign,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
values.push({
|
|
110
|
+
path: 'communication.aprs.callsign',
|
|
111
|
+
value: data.source.callsign,
|
|
112
|
+
});
|
|
113
|
+
values.push({
|
|
114
|
+
path: 'communication.aprs.ssid',
|
|
115
|
+
value: data.source.ssid,
|
|
116
|
+
});
|
|
117
|
+
values.push({
|
|
118
|
+
path: 'communication.aprs.route',
|
|
119
|
+
value: data.repeaterPath.map((r) => formatAddress(r)),
|
|
120
|
+
});
|
|
121
|
+
values.push({
|
|
122
|
+
path: 'communication.aprs.comment',
|
|
123
|
+
value: data.comment || '',
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
app.handleMessage('signalk-aprs', {
|
|
127
|
+
context: `meteo.${data.source.callsign}`,
|
|
128
|
+
updates: [
|
|
129
|
+
{
|
|
130
|
+
source: {
|
|
131
|
+
label: 'signalk-aprs',
|
|
132
|
+
src: formatAddress(data.source),
|
|
133
|
+
},
|
|
134
|
+
timestamp: new Date(data.position.timestamp).toISOString(),
|
|
135
|
+
values,
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
setConnectionStatus();
|
|
143
|
+
}, 3000);
|
|
144
|
+
});
|
|
145
|
+
settings.connections.forEach((connectionSetting) => {
|
|
146
|
+
if (!connectionSetting.enabled) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
let attempt = 0;
|
|
150
|
+
const connectionStr = `${connectionSetting.host}:${connectionSetting.port}`;
|
|
151
|
+
const socket = new Socket();
|
|
152
|
+
const conn = {
|
|
153
|
+
address: connectionStr,
|
|
154
|
+
socket,
|
|
155
|
+
tx: connectionSetting.tx || false,
|
|
156
|
+
online: false,
|
|
157
|
+
reconnect: undefined,
|
|
158
|
+
};
|
|
159
|
+
connections.push(conn);
|
|
160
|
+
|
|
161
|
+
const connect = () => {
|
|
162
|
+
attempt += 1;
|
|
163
|
+
app.debug(`${connectionStr} connect attempt ${attempt}`);
|
|
164
|
+
app.setPluginStatus(`Connecting to TNC ${connectionStr}, attempt ${attempt}`);
|
|
165
|
+
socket.connect(connectionSetting.port, connectionSetting.host);
|
|
166
|
+
};
|
|
167
|
+
const onConnectionError = (e) => {
|
|
168
|
+
app.error(e);
|
|
169
|
+
app.setPluginError(`Failed to connect to ${connectionStr}: ${e.message}`);
|
|
170
|
+
if (conn.reconnect) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
app.debug(`Setting eventual reconnect for ${connectionStr}`);
|
|
174
|
+
conn.reconnect = setTimeout(() => {
|
|
175
|
+
conn.reconnect = undefined;
|
|
176
|
+
// Retry to connect
|
|
177
|
+
connect();
|
|
178
|
+
}, 10000);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
socket.setTimeout(10000);
|
|
182
|
+
socket.once('error', onConnectionError);
|
|
183
|
+
socket.on('ready', () => {
|
|
184
|
+
app.debug(`${connectionStr} connected`);
|
|
185
|
+
const mySendStream = new KISSSender();
|
|
186
|
+
mySendStream.pipe(socket);
|
|
187
|
+
conn.sender = mySendStream;
|
|
188
|
+
conn.online = true;
|
|
189
|
+
socket.removeListener('error', onConnectionError);
|
|
190
|
+
setConnectionStatus();
|
|
191
|
+
});
|
|
192
|
+
socket.on('data', (data) => {
|
|
193
|
+
app.debug(`${connectionStr} RX`, data);
|
|
194
|
+
if (data.length < 4) {
|
|
195
|
+
// We don't want to parse empty frames
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// Remove FEND and FEND before processing
|
|
199
|
+
processor.data(data.slice(1, -1));
|
|
200
|
+
});
|
|
201
|
+
socket.on('timeout', () => {
|
|
202
|
+
app.debug(`${connectionStr} connection timeout`);
|
|
203
|
+
socket.end();
|
|
204
|
+
});
|
|
205
|
+
socket.on('error', (e) => {
|
|
206
|
+
app.error(e);
|
|
207
|
+
app.setPluginError(`Error with ${connectionStr}: ${e.message}`);
|
|
208
|
+
});
|
|
209
|
+
socket.on('close', () => {
|
|
210
|
+
app.debug(`${connectionStr} connection closed`);
|
|
211
|
+
conn.online = false;
|
|
212
|
+
socket.once('error', onConnectionError);
|
|
213
|
+
if (conn.reconnect) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
app.debug(`Setting eventual reconnect for ${connectionStr}`);
|
|
217
|
+
conn.reconnect = setTimeout(() => {
|
|
218
|
+
conn.reconnect = undefined;
|
|
219
|
+
// Retry to connect
|
|
220
|
+
connect();
|
|
221
|
+
}, 10000);
|
|
222
|
+
});
|
|
223
|
+
connect();
|
|
224
|
+
});
|
|
225
|
+
const minutes = settings.beacon.interval || 15;
|
|
226
|
+
app.subscriptionmanager.subscribe(
|
|
227
|
+
{
|
|
228
|
+
context: 'vessels.self',
|
|
229
|
+
subscribe: [
|
|
230
|
+
{
|
|
231
|
+
path: 'navigation.position',
|
|
232
|
+
period: minutes * 60 * 1000,
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
path: 'navigation.state',
|
|
236
|
+
period: 60 * 1000,
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
},
|
|
240
|
+
unsubscribes,
|
|
241
|
+
(subscriptionError) => {
|
|
242
|
+
app.error(subscriptionError);
|
|
243
|
+
},
|
|
244
|
+
(delta) => {
|
|
245
|
+
if (!delta.updates) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
// Record inputs
|
|
249
|
+
delta.updates.forEach((u) => {
|
|
250
|
+
if (!u.values) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
u.values.forEach((v) => {
|
|
254
|
+
if (v.path === 'navigation.state') {
|
|
255
|
+
state = v.value;
|
|
256
|
+
}
|
|
257
|
+
if (v.path !== 'navigation.position') {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const payload = `=${formatLatitude(v.value.latitude)}${settings.beacon.symbol[0]}${formatLongitude(v.value.longitude)}${settings.beacon.symbol[1]} ${app.getSelfPath('name')} ${state} ${settings.beacon.note}`;
|
|
261
|
+
const frame = newKISSFrame().fromFrame({
|
|
262
|
+
destination: {
|
|
263
|
+
callsign: 'APZ42', // FIXME: https://github.com/aprsorg/aprs-deviceid/issues/244
|
|
264
|
+
},
|
|
265
|
+
source: {
|
|
266
|
+
callsign: settings.beacon.callsign,
|
|
267
|
+
ssid: settings.beacon.ssid,
|
|
268
|
+
},
|
|
269
|
+
repeaters: [
|
|
270
|
+
{
|
|
271
|
+
callsign: 'WIDE1',
|
|
272
|
+
ssid: '1',
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
info: payload,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const frameBuffer = frame.build();
|
|
279
|
+
app.debug('TX', frame);
|
|
280
|
+
connections.forEach((conn) => {
|
|
281
|
+
if (!conn.tx || !conn.online || !conn.sender) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
app.debug(`${conn.address} TX`, frameBuffer);
|
|
285
|
+
conn.sender.write(frameBuffer.slice(1));
|
|
286
|
+
app.setPluginStatus(`TX ${payload}`);
|
|
287
|
+
});
|
|
288
|
+
setTimeout(() => {
|
|
289
|
+
setConnectionStatus();
|
|
290
|
+
}, 3000);
|
|
291
|
+
app.handleMessage('signalk-aprs', {
|
|
292
|
+
context: 'vessels.self',
|
|
293
|
+
updates: [
|
|
294
|
+
{
|
|
295
|
+
source: {
|
|
296
|
+
label: 'signalk-aprs',
|
|
297
|
+
src: formatAddress(settings.beacon),
|
|
298
|
+
},
|
|
299
|
+
timestamp: new Date().toISOString(),
|
|
300
|
+
values: [
|
|
301
|
+
{
|
|
302
|
+
path: 'communication.aprs.callsign',
|
|
303
|
+
value: settings.beacon.callsign,
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
path: 'communication.aprs.ssid',
|
|
307
|
+
value: settings.beacon.ssid,
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
path: 'communication.aprs.symbol',
|
|
311
|
+
value: settings.beacon.symbol,
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
},
|
|
320
|
+
);
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
plugin.stop = () => {
|
|
324
|
+
if (publishInterval) {
|
|
325
|
+
clearInterval(publishInterval);
|
|
326
|
+
}
|
|
327
|
+
unsubscribes.forEach((f) => f());
|
|
328
|
+
unsubscribes = [];
|
|
329
|
+
connections.forEach((c) => {
|
|
330
|
+
c.socket.removeAllListeners();
|
|
331
|
+
c.socket.destroy();
|
|
332
|
+
});
|
|
333
|
+
connections = [];
|
|
334
|
+
beacons = {};
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
plugin.schema = {
|
|
338
|
+
type: 'object',
|
|
339
|
+
properties: {
|
|
340
|
+
beacon: {
|
|
341
|
+
type: 'object',
|
|
342
|
+
title: 'APRS Beacon settings',
|
|
343
|
+
properties: {
|
|
344
|
+
callsign: {
|
|
345
|
+
type: 'string',
|
|
346
|
+
description: 'Callsign',
|
|
347
|
+
default: 'NOCALL',
|
|
348
|
+
},
|
|
349
|
+
ssid: {
|
|
350
|
+
type: 'integer',
|
|
351
|
+
description: 'SSID',
|
|
352
|
+
default: 8,
|
|
353
|
+
oneOf: [
|
|
354
|
+
{
|
|
355
|
+
const: 0,
|
|
356
|
+
title: '0: Primary station',
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
const: 1,
|
|
360
|
+
title: '1: Generic additional station',
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
const: 2,
|
|
364
|
+
title: '1: Generic additional station',
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
const: 3,
|
|
368
|
+
title: '3: Generic additional station',
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
const: 4,
|
|
372
|
+
title: '4: Generic additional station',
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
const: 5,
|
|
376
|
+
title: '5: Other network (D-Star, 3G)',
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
const: 6,
|
|
380
|
+
title: '6: Satellite',
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
const: 7,
|
|
384
|
+
title: '7: Handheld radion',
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
const: 8,
|
|
388
|
+
title: '8: Boat / ship',
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
const: 9,
|
|
392
|
+
title: '9: Mobile station',
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
const: 10,
|
|
396
|
+
title: '10: APRS-IS (no radio)',
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
const: 11,
|
|
400
|
+
title: '11: Balloon, aircraft, spacecraft',
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
const: 12,
|
|
404
|
+
title: '12: APRStt, DTMF, ... (one-way)',
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
const: 13,
|
|
408
|
+
title: '13: Weather station',
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
const: 14,
|
|
412
|
+
title: '14: Freight vehicle',
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
const: 15,
|
|
416
|
+
title: '15: Generic additional station',
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
},
|
|
420
|
+
symbol: {
|
|
421
|
+
type: 'string',
|
|
422
|
+
description: 'APRS symbol',
|
|
423
|
+
default: '/Y',
|
|
424
|
+
oneOf: [
|
|
425
|
+
{
|
|
426
|
+
const: '/Y',
|
|
427
|
+
title: '/Y: Yacht (sailboat)',
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
const: '/s',
|
|
431
|
+
title: '/s: Ship (power boat)',
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
const: '/C',
|
|
435
|
+
title: '/C: Canoe',
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
const: '\\C',
|
|
439
|
+
title: '\\C: Coastguard',
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
const: '\\N',
|
|
443
|
+
title: '\\N: Navigation Buoy',
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
const: '/i',
|
|
447
|
+
title: '/i: Island',
|
|
448
|
+
},
|
|
449
|
+
],
|
|
450
|
+
},
|
|
451
|
+
note: {
|
|
452
|
+
type: 'string',
|
|
453
|
+
description: 'Personal note',
|
|
454
|
+
default: 'https://signalk.org',
|
|
455
|
+
},
|
|
456
|
+
interval: {
|
|
457
|
+
type: 'integer',
|
|
458
|
+
description: 'Beacon transmission interval (in minutes)',
|
|
459
|
+
default: 15,
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
connections: {
|
|
464
|
+
type: 'array',
|
|
465
|
+
title: 'KISS-TNC connections',
|
|
466
|
+
minItems: 0,
|
|
467
|
+
items: {
|
|
468
|
+
type: 'object',
|
|
469
|
+
required: ['host', 'port'],
|
|
470
|
+
properties: {
|
|
471
|
+
description: {
|
|
472
|
+
type: 'string',
|
|
473
|
+
description: 'TNC description',
|
|
474
|
+
},
|
|
475
|
+
host: {
|
|
476
|
+
type: 'string',
|
|
477
|
+
description: 'TNC host address',
|
|
478
|
+
},
|
|
479
|
+
port: {
|
|
480
|
+
type: 'integer',
|
|
481
|
+
description: 'TNC host port',
|
|
482
|
+
default: 8001,
|
|
483
|
+
},
|
|
484
|
+
enabled: {
|
|
485
|
+
type: 'boolean',
|
|
486
|
+
description: 'Enable TNC connection',
|
|
487
|
+
default: true,
|
|
488
|
+
},
|
|
489
|
+
tx: {
|
|
490
|
+
type: 'boolean',
|
|
491
|
+
description: 'Transmit beacon to this TNC',
|
|
492
|
+
default: false,
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
return plugin;
|
|
501
|
+
};
|