@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 ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "airbnb-base",
3
+ "parserOptions": {
4
+ "ecmaVersion": 11
5
+ }
6
+ }
@@ -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
+ ![Basic Meshtastic communications](./doc/meshtastic-bequia.png)
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
+ ![](./doc/config-crew-role.png)
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
+ ![](./doc/telemetry.png)
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,6 @@
1
+ module.exports = {
2
+ crewOnly: false,
3
+ example: 'Ping',
4
+ accept: (msg) => (msg.data.toLowerCase() === 'ping'),
5
+ handle: (msg, settings, device) => device.sendText('Pong', msg.from, true, false),
6
+ };
@@ -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
+ };