@meri-imperiumi/signalk-meshtastic 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/.eslintrc.json +6 -0
- package/.github/workflows/publish.yml +30 -0
- package/.github/workflows/test.yml +21 -0
- package/README.md +92 -0
- package/doc/config-crew-role.png +0 -0
- package/doc/meshtastic-bequia.png +0 -0
- package/doc/telemetry.png +0 -0
- package/package.json +35 -0
- package/plugin/commands/index.js +37 -0
- package/plugin/commands/ping.js +6 -0
- package/plugin/commands/switching.js +33 -0
- package/plugin/commands/waypoint.js +56 -0
- package/plugin/index.js +1037 -0
- package/plugin/telemetry.js +69 -0
- package/plugin/waypoint.js +29 -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,92 @@
|
|
|
1
|
+
Signal K integration with Meshtastic
|
|
2
|
+
====================================
|
|
3
|
+
|
|
4
|
+
This plugin enables vessels running [Signal K](https://signalk.org) to interact with the [Meshtastic](https://meshtastic.org) mesh network. Meshtastic allows radio communications between relatively inexpensive LoRa devices over long distances. The Signal K Meshtastic plugin allows seeing telemetry and receiving alerts from your vessel while ashore. It can also control vessel features like digital switching over text message.
|
|
5
|
+
|
|
6
|
+
If desired, telemetry and position information can also be shared between multiple Meshtastic-using vessels, making it effectively a "pseudo-AIS" system.
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
Being a mesh network, there is no need for external telecommunications infrastructure or monthly payments. This means communication between Meshtastic devices onboard and on shore can work just as well in the Finnish Archipelago Sea as in the Tuamotus. In more densely populated places communications may benefit from other Meshtastic users relaying the messages, making it possible to communicate with the boat across a city.
|
|
11
|
+
|
|
12
|
+
This plugin is designed to work with regular unmodified Meshtastic devices and settings. You can keep your boat and other Meshtastic nodes either in the public channel, or [set up your private mesh](https://meshtastic.org/docs/configuration/tips/#not-sharing-your-location). With a private channel the location of your devices won't be visible to the public, and communications between them will be encrypted.
|
|
13
|
+
|
|
14
|
+
Many different kinds of Meshtastic devices are available, from basic microcontroller boards to ready-to-go consumer devices. Some are entirely standalone with screens and keyboards, and others need a smartphone app.
|
|
15
|
+
|
|
16
|
+
[Here is a good introduction](https://tech.gillyb.net/understanding-meshtastic-off-grid-communication-made-simple/) to Meshtastic:
|
|
17
|
+
> Picture this: you’re a family is anchored in a bay. Parents are hiking inland, kids are visiting friends on another boat, and you’re on your boat (“Boat Node”) in the middle. When the kids send, “What’s for dinner?”, Meshtastic uses “flood routing” to make sure it reaches everyone.
|
|
18
|
+
> Here’s how it works:
|
|
19
|
+
> - The kids’ node sends the message.
|
|
20
|
+
> - Boat Node receives it and decides, “This is new—let’s pass it on!”
|
|
21
|
+
> - Boat Node rebroadcasts the message.
|
|
22
|
+
> - Parents’ node, unable to hear the kids directly, receives it through Boat Node.
|
|
23
|
+
|
|
24
|
+
## Status
|
|
25
|
+
|
|
26
|
+
In production use on several boats.
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
* Connect to a Meshtastic node
|
|
31
|
+
* Keep a persistent database of all seen Meshtastic nodes
|
|
32
|
+
* Update Meshtastic node position from Signal K GNSS position
|
|
33
|
+
* Send Signal K alerts as Meshtastic text messages to crew
|
|
34
|
+
* MOB alerts (for example from [signalk-mob-notifier](https://github.com/meri-imperiumi/signalk-mob-notifier)) also send a waypoint to the MOB beacon
|
|
35
|
+
* Control Signal K with Meshtastic direct messages:
|
|
36
|
+
* Share Meshtastic waypoints for AIS targets (_"Waypoint DH8613"_)
|
|
37
|
+
* Control digital switching (_"Turn decklight on"_). Opt-in.
|
|
38
|
+
* Share weather station data from Signal K (wind, temperature, etc) over Meshtastic. Opt-in.
|
|
39
|
+
* Show position-sharing Meshtastic nodes as vessels in Freeboard etc. Opt-in.
|
|
40
|
+
* Associate Meshtastic nodes with other (AIS) vessels based on the `Some node name DE <callsign>` pattern
|
|
41
|
+
|
|
42
|
+
## Planned features
|
|
43
|
+
|
|
44
|
+
* Guard mode to alert if the tracked dinghy moves
|
|
45
|
+
* Keeping a mileage log for the dinghy
|
|
46
|
+
* More text commands over Meshtastic to query vessel status etc
|
|
47
|
+
|
|
48
|
+
## Requirements
|
|
49
|
+
|
|
50
|
+
* This plugin running inside your Signal K installation
|
|
51
|
+
* One [Meshtastic device](https://meshtastic.org/docs/hardware/devices/) running and connected to the same network (typically boat WiFi) as Signal K. This should be an [ESP32 based](https://meshtastic.org/docs/hardware/devices/heltec-automation/lora32/?heltec=v3) device for WiFi connectivity
|
|
52
|
+
* At least one additional Meshtastic device for the crew ashore. [Seeed T1000-e](https://meshtastic.org/docs/hardware/devices/seeed-studio/sensecap/card-tracker/) is a great option, but any battery-powered Meshtastic device will work. Having a device for each crew member is even better. In busy areas these should be set to [`CLIENT_MUTE` role](https://meshtastic.org/blog/choosing-the-right-device-role/)
|
|
53
|
+
* Optionally, a Meshtastic GPS tracker device installed in the dinghy
|
|
54
|
+
* Optionally, a [Meshtastic mast-top repeater](https://www.printables.com/model/1396221-meshtastic-boat-module-masthead) for greatly increased communications range
|
|
55
|
+
|
|
56
|
+
LoRa is line-of-sight communications quite similarly to VHF. Communications range would greatly benefit from a Meshtastic repeater installed high in the mast. Similarly, repeaters on nearby hills or high buildings can be helpful. But just with the boat node and the node carried by crew it should be possible to reach ranges of over 1km. We've been able to communicate at over 8km distances in our early tests in Curacao.
|
|
57
|
+
|
|
58
|
+
**Please note** that this plugin connects to the "boat node" as a client, meaning that while Signal K is running, the regular Meshtastic client app _won't be able to connect to the same device_. It is a good idea to [enable remote administration](https://meshtastic.org/docs/configuration/radio/security/#admin-key) so that you can modify the settings of the device over LoRa.
|
|
59
|
+
|
|
60
|
+
## Getting started
|
|
61
|
+
|
|
62
|
+
* Configure your "boat Meshtastic node" device so that it is connected to your boat network
|
|
63
|
+
* If you have a valid Ship Station License, add your callsign to the long name of the node. Typical pattern is `<Vessel name> DE <Callsign>`, for example _"Lille Oe DE DH8613"_<br>
|
|
64
|
+
(yes, you need to use `DE` also for non-German vessels. This is radio slang for "this is", not a country code)
|
|
65
|
+
* Install and enable this plugin, and set up the connection details (IP address etc)
|
|
66
|
+
* Wait for some minutes for the plugin to see nearby Meshtastic nodes
|
|
67
|
+
* Configure plugin and set appropriate roles for the crew and dinghy tracker Meshtastic devices
|
|
68
|
+
|
|
69
|
+

|
|
70
|
+
|
|
71
|
+
## Telemetry sent to Meshtastic
|
|
72
|
+
|
|
73
|
+
If enabled, your "boat node" will transmit the following telemetry to Meshtastic. This enables tracking various important metrics about your boat also remotely. They are visible in the device details in your Meshtastic app:
|
|
74
|
+
|
|
75
|
+

|
|
76
|
+
|
|
77
|
+
Metrics used:
|
|
78
|
+
|
|
79
|
+
* Temperature (from `environment.outside.temperature`)
|
|
80
|
+
* Relative humidity (from `environment.outside.relativeHumidity`)
|
|
81
|
+
* Barometric pressure (from `environment.outside.pressure`)
|
|
82
|
+
* Wind direction (from `environment.wind.directionTrue`)
|
|
83
|
+
* Wind speed (median of last ten minutes from `environment.wind.speedOverGround`)
|
|
84
|
+
* Battery voltage (from `electrical.batteries.house.voltage`)
|
|
85
|
+
* Battery current (from `electrical.batteries.house.current`)
|
|
86
|
+
* If anchored, distance to anchor (from `navigation.anchor.distanceFromBow`)
|
|
87
|
+
* If not anchored, distance is water depth (from `environment.depth.belowSurface`)
|
|
88
|
+
|
|
89
|
+
## Changes
|
|
90
|
+
|
|
91
|
+
* 1.0.0 (2025-09-11)
|
|
92
|
+
- Initial release with HTTP and TCP transports
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@meri-imperiumi/signalk-meshtastic",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Signal K plugin for interfacing with the Meshtastic LoRa mesh network",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "eslint ."
|
|
7
|
+
},
|
|
8
|
+
"keywords": [
|
|
9
|
+
"meshtastic",
|
|
10
|
+
"signalk-node-server-plugin",
|
|
11
|
+
"signalk-category-ais",
|
|
12
|
+
"signalk-category-hardware"
|
|
13
|
+
],
|
|
14
|
+
"main": "plugin/index.js",
|
|
15
|
+
"signalk-plugin-enabled-by-default": false,
|
|
16
|
+
"author": "Henri Bergius <henri.bergius@iki.fi>",
|
|
17
|
+
"license": "GPLV3",
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@bufbuild/protobuf": "^2.6.0",
|
|
23
|
+
"@meshtastic/core": "^2.6.7",
|
|
24
|
+
"@meshtastic/transport-http": "^0.2.5",
|
|
25
|
+
"@meshtastic/transport-node": "^0.0.2"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"eslint": "^8.57.1",
|
|
29
|
+
"eslint-config-airbnb-base": "^15.0.0",
|
|
30
|
+
"eslint-plugin-import": "^2.32.0"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
exports.ping = require('./ping');
|
|
2
|
+
exports.switching = require('./switching');
|
|
3
|
+
exports.waypoint = require('./waypoint');
|
|
4
|
+
|
|
5
|
+
exports.isFromCrew = (msg, settings) => {
|
|
6
|
+
const crew = settings.nodes
|
|
7
|
+
.filter((node) => {
|
|
8
|
+
if (node.role === 'crew') {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
return false;
|
|
12
|
+
})
|
|
13
|
+
.map((node) => node.node);
|
|
14
|
+
if (crew.indexOf(msg.from) !== -1) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
exports.help = {
|
|
21
|
+
crewOnly: false,
|
|
22
|
+
example: 'Help',
|
|
23
|
+
accept: (msg) => (msg.data.toLowerCase() === 'help'),
|
|
24
|
+
handle: (msg, settings, device) => {
|
|
25
|
+
const commands = Object.keys(exports).filter((cmd) => {
|
|
26
|
+
if (cmd === 'isFromCrew') {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
if (!exports.isFromCrew(msg, settings) && exports[cmd].crewOnly) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
})
|
|
34
|
+
.map((cmd) => exports[cmd].example);
|
|
35
|
+
return device.sendText(`Commands: ${commands.join(', ')}`, msg.from, true, false);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
crewOnly: true,
|
|
3
|
+
example: 'Turn <switch name> on',
|
|
4
|
+
accept: (msg, settings) => {
|
|
5
|
+
const switching = msg.data.match(/turn ([a-z0-9]+) (on|off)/i);
|
|
6
|
+
if (settings.communications
|
|
7
|
+
&& settings.communications.digital_switching
|
|
8
|
+
&& switching) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
return false;
|
|
12
|
+
},
|
|
13
|
+
handle: (msg, settings, device, app) => {
|
|
14
|
+
const switching = msg.data.match(/turn ([a-z0-9]+) (on|off)/i);
|
|
15
|
+
const light = switching[1];
|
|
16
|
+
const value = switching[2] === 'on';
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
app.putSelfPath(`electrical.switches.${light}.state`, value, (res) => {
|
|
19
|
+
if (res.state !== 'COMPLETED') {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (res.statusCode !== 200) {
|
|
23
|
+
reject(new Error(res.message));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
resolve();
|
|
27
|
+
device.sendText(res.message, msg.from, true, false)
|
|
28
|
+
.catch((e) => app.error(`Failed to send message: ${e.message}`));
|
|
29
|
+
});
|
|
30
|
+
})
|
|
31
|
+
.then(() => device.sendText(`OK, ${light} is ${switching[2]}`, msg.from, true, false));
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const { vesselIcon } = require('../waypoint');
|
|
2
|
+
|
|
3
|
+
const regex = /waypoint ([a-z0-9]+)( ([0-9]+)h)?/i;
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
crewOnly: true,
|
|
7
|
+
example: 'Waypoint <callsign or boat name>',
|
|
8
|
+
accept: (msg) => {
|
|
9
|
+
// FIXME: Add support for vessel names with spaces
|
|
10
|
+
const waypointTgt = msg.data.match(regex);
|
|
11
|
+
if (waypointTgt) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
|
+
},
|
|
16
|
+
handle: (msg, settings, device, app, create, Protobuf) => {
|
|
17
|
+
const waypointTgt = msg.data.match(regex);
|
|
18
|
+
const identifier = waypointTgt[1];
|
|
19
|
+
const length = waypointTgt[3] || 1;
|
|
20
|
+
const waypointVesselCtx = Object.keys(app.signalk.root.vessels)
|
|
21
|
+
.find((vesselCtx) => {
|
|
22
|
+
const vessel = app.signalk.root.vessels[vesselCtx];
|
|
23
|
+
const lIdentifier = identifier.toLowerCase();
|
|
24
|
+
if (vessel.mmsi === identifier) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (vessel.name && vessel.name.toLowerCase() === lIdentifier) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (vessel.communication
|
|
31
|
+
&& vessel.communication.callsignVhf
|
|
32
|
+
&& vessel.communication.callsignVhf.toLowerCase() === lIdentifier) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
});
|
|
37
|
+
if (!waypointVesselCtx) {
|
|
38
|
+
return device.sendText(`Unable to find vessel ${identifier}`, msg.from, true, false);
|
|
39
|
+
}
|
|
40
|
+
const waypointVessel = app.signalk.root.vessels[waypointVesselCtx];
|
|
41
|
+
if (!waypointVessel.navigation.position.value
|
|
42
|
+
|| !waypointVessel.navigation.position.value.latitude) {
|
|
43
|
+
return device.sendText(`Vessel ${identifier} has no known position`, msg.from, true, false);
|
|
44
|
+
}
|
|
45
|
+
const setWaypointMessage = create(Protobuf.Mesh.WaypointSchema, {
|
|
46
|
+
id: waypointVessel.mmsi,
|
|
47
|
+
latitudeI: Math.floor(waypointVessel.navigation.position.value.latitude / 1e-7),
|
|
48
|
+
longitudeI: Math.floor(waypointVessel.navigation.position.value.longitude / 1e-7),
|
|
49
|
+
expire: Math.floor((new Date().getTime() / 1000) + (length * 60 * 60)),
|
|
50
|
+
name: waypointVessel.name,
|
|
51
|
+
description: `AIS vessel ${waypointVessel.mmsi}`,
|
|
52
|
+
icon: vesselIcon(waypointVessel),
|
|
53
|
+
});
|
|
54
|
+
return device.sendWaypoint(setWaypointMessage, 'broadcast', 0);
|
|
55
|
+
},
|
|
56
|
+
};
|