signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
292 lines (291 loc) • 12 kB
JavaScript
;
/*
* Copyright 2017 Scott Bender <scott@scottbender.net>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
const debug_1 = require("./debug");
const mdnsPatch_1 = require("./mdnsPatch");
const debug = (0, debug_1.createDebug)('signalk-server:discovery');
const canboatjs = require('@canboat/canboatjs');
const dgram = require('dgram');
(0, mdnsPatch_1.patchAstronautLabsMdns)();
const { Browser } = require('@astronautlabs/mdns');
const { networkInterfaces } = require('os');
module.exports.runDiscovery = function (app) {
if (canboatjs.discover) {
try {
canboatjs.discover(app);
}
catch (ex) {
console.log(ex);
}
}
discoverWLN10();
discoverGoFree();
discoverSignalkWs('ws');
discoverSignalkWs('wss');
function findUDPProvider(port) {
return app.config.settings.pipedProviders.find((provider) => {
return (provider.pipeElements &&
provider.pipeElements.length === 1 &&
provider.pipeElements[0].type === 'providers/simple' &&
provider.pipeElements[0].options &&
provider.pipeElements[0].options.type === 'NMEA0183' &&
provider.pipeElements[0].options.subOptions.type === 'udp' &&
provider.pipeElements[0].options.subOptions.port === port);
});
}
function findTCPProvider(host, port) {
return app.config.settings.pipedProviders.find((provider) => {
return (provider.pipeElements &&
provider.pipeElements.length === 1 &&
provider.pipeElements[0].type === 'providers/simple' &&
provider.pipeElements[0].options &&
provider.pipeElements[0].options.type === 'NMEA0183' &&
provider.pipeElements[0].options.subOptions.type === 'tcp' &&
provider.pipeElements[0].options.subOptions.host === host &&
provider.pipeElements[0].options.subOptions.port === port);
});
}
function findWSProvider(ip, wsType, host, port) {
return app.config.settings.pipedProviders.find((provider) => {
return (provider.pipeElements &&
provider.pipeElements.length === 1 &&
provider.pipeElements[0].type === 'providers/simple' &&
provider.pipeElements[0].options &&
provider.pipeElements[0].options.type === 'SignalK' &&
provider.pipeElements[0].options.subOptions.type === wsType &&
provider.pipeElements[0].options.subOptions.port.toString() ===
port.toString() &&
(provider.pipeElements[0].options.subOptions.host === host ||
provider.pipeElements[0].options.subOptions.host === ip));
});
}
function discoverGoFree() {
const socket = dgram.createSocket('udp4');
const found = [];
socket.on('message', function (buffer) {
const msg = buffer.toString('utf8');
if (msg[0] === '{') {
try {
const json = JSON.parse(msg);
const serial = json.SerialNumber;
if (json.Services && found.indexOf(serial) === -1) {
json.Services.forEach((service) => {
if (service.Service === 'nmea-0183' &&
!findTCPProvider(json.IP, service.Port)) {
const id = `${json.Name} (${serial})`;
debug('found GoFree %s at %s:%s', id, json.IP, service.Port);
found.push(serial);
app.emit('discovered', {
id: id,
pipeElements: [
{
type: 'providers/simple',
options: {
type: 'NMEA0183',
subOptions: {
type: 'tcp',
host: json.IP,
port: service.Port
}
}
}
]
});
}
});
}
}
catch (err) {
debug('discoverGoFree:', err);
return;
}
}
});
socket.on('error', (error) => {
debug('discoverGoFree:', error);
});
socket.on('close', () => {
debug('discoverGoFree close');
});
debug('looking for GoFree broadcasting on UDP port 2052');
try {
socket.bind(2052, () => {
try {
socket.addMembership('239.2.1.1');
}
catch (ex) {
debug('discoverGoFree:', ex);
}
});
}
catch (ex) {
debug('discoverGoFree:', ex);
}
setTimeout(() => {
if (socket) {
socket.close();
}
}, 10000);
}
function discoverWLN10() {
if (!findUDPProvider('2000')) {
let socket = dgram.createSocket('udp4');
socket.on('message', function (buffer) {
const msg = buffer.toString('utf8');
if (msg[0] === '$') {
socket.close();
socket = undefined;
app.emit('discovered', {
id: 'WLN10',
pipeElements: [
{
type: 'providers/simple',
options: {
type: 'NMEA0183',
subOptions: {
type: 'udp',
port: '2000'
}
}
}
]
});
}
});
socket.on('error', (error) => {
debug('discoverWLN10:', error);
});
socket.on('close', () => {
debug('discoverWLN10 close');
});
debug('looking for a WLN10 broadcasting UDP port 2000');
try {
socket.bind(2000);
}
catch (ex) {
debug('discoverWLN10:', ex);
}
setTimeout(() => {
if (socket) {
socket.close();
}
}, 5000);
}
}
function discoverSignalkWs(wsType) {
try {
const browser = new Browser('_signalk-' + wsType + '._tcp');
let browserStopped = false;
debug('looking for SignalK ' + wsType);
browser.on('serviceUp', function onServiceUp(data) {
try {
// Suppress the local server's own advertisement. data.addresses
// can contain a mix of IPv4 and IPv6 entries (including
// loopback) depending on the underlying mDNS responder, so a
// single address from the list is not enough — any local match
// anywhere in the list means this is our own service.
const addresses = Array.isArray(data.addresses) ? data.addresses : [];
const primaryAddress = addresses[0];
if (primaryAddress &&
!addresses.some(isOwnAddress) &&
data.type &&
data.type.name === 'signalk-' + wsType &&
!findWSProvider(primaryAddress, wsType, data.host, data.port)) {
debug('discoverSignalkWs found data[' + wsType + ']:', data);
const providerId = `${wsType}-${data.host}:${data.port} (${primaryAddress})`;
app.emit('discovered', {
id: providerId,
enabled: false,
pipeElements: [
{
type: 'providers/simple',
options: {
type: 'SignalK',
subOptions: {
type: wsType,
host: data.host,
port: data.port,
address: primaryAddress,
providerId
},
providerId
}
}
]
});
}
}
catch (err) {
debug('discoverSignalkWs:', err);
}
});
browser.on('error', (err) => {
browserStopped = true;
debug('discoverSignalkWs:', err);
});
browser.start();
setTimeout(() => {
try {
if (browserStopped) {
return;
}
browserStopped = true;
browser.stop();
debug('discoverSignalkWs close');
}
catch (err) {
debug('discoverSignalkWs:', err);
}
}, 5000);
}
catch (err) {
debug('discoverSignalkWs:', err);
}
}
// True when `addr` is an address bound to one of our own interfaces,
// covering IPv4 (including 127.0.0.0/8), IPv6 (including ::1 and
// link-local fe80::*), and the zone-ID suffix mDNS responders append
// to link-local IPv6 (e.g. 'fe80::1%eth0'). Used to filter the local
// server's own mDNS advertisement out of discovery results.
function isOwnAddress(addr) {
if (typeof addr !== 'string' || addr.length === 0)
return false;
const normalized = addr.replace(/%.*$/, '').toLowerCase();
const nets = networkInterfaces();
for (const name of Object.keys(nets)) {
for (const net of nets[name] || []) {
if (net.address && net.address.toLowerCase() === normalized) {
return true;
}
}
}
return false;
}
};
if (require.main === module) {
const app = {
config: {
settings: {
pipedProviders: []
}
},
emit: (event, p) => {
console.log(`found ${JSON.stringify(p)}`);
}
};
module.exports.runDiscovery(app);
}
//# sourceMappingURL=discovery.js.map