diff --git a/README.md b/README.md index 277963f3211fcc86c2f533b4ff27431895c22fe1..c92eea7d10179938eeac31d5ebd5b2026459c3b0 100644 --- a/README.md +++ b/README.md @@ -21,34 +21,39 @@ port = 4403 [module_echo] name = Echo path = echo -devices = 0 +device = 0 channel = 0 port = 1 enabled = true debug = false reply = same +hopLimit = 0 [module_weather] name = Weather path = weather -devices = 0 +device = 0 channel = 0 port = 1 enabled = true debug = false reply = same +hopLimit = 0 ``` Device section names must begin with `device_`, module section names must begin with `module_`, but the rest of the name is arbitrary. +If you want to use the same module with multiple devices, you'll need to create a module configuration for each device. + In module config: - `path` is either a magic word for one of the built-in modules, or the path to an external Synchronet JavaScript module (.js file). -- `devices` can be a single number or a comma-separated list of device IDs (which must match the `id` field for a `[device_*]` above) +- `device` must match the `id` of one of `device_*` - `port` is a Meshtastic [port number](https://buf.build/meshtastic/protobufs/docs/main:meshtastic#meshtastic.PortNum) for this module to listen on - `reply` is one of `same` (default), `direct`, or `broadcast` - `same` means that if the module receives a direct message, it will respond in a direct message, and if it receives the command in a broadcast message, it will respond with a broadcast message - `direct` means that regardless of how the original message was addressed, the response will be sent in a direct message - `broadcast` means that regardless of how the original message was addressed, the response will be sent in a broadcast message +- `hopLimit` specifies the number of hops you want messages from this module to make on the mesh network ([see here for more info](https://meshtastic.org/docs/overview/mesh-algo/)) ## Operation diff --git a/build/synctastic.js b/build/synctastic.js index 599c5ba213a4e93e228f5f17ed0b804325aba705..97a68984971ae2aaa4382b224a5ba18ff30ee462 100644 --- a/build/synctastic.js +++ b/build/synctastic.js @@ -3232,31 +3232,6 @@ } }); - // node_modules/core-js/internals/array-for-each.js - var require_array_for_each = __commonJS({ - "node_modules/core-js/internals/array-for-each.js": function(exports, module) { - "use strict"; - var $forEach = require_array_iteration().forEach; - var arrayMethodIsStrict = require_array_method_is_strict(); - var STRICT_METHOD = arrayMethodIsStrict("forEach"); - module.exports = !STRICT_METHOD ? function forEach(callbackfn) { - return $forEach(this, callbackfn, arguments.length > 1 ? arguments[1] : void 0); - } : [].forEach; - } - }); - - // node_modules/core-js/modules/es.array.for-each.js - var require_es_array_for_each = __commonJS({ - "node_modules/core-js/modules/es.array.for-each.js": function() { - "use strict"; - var $ = require_export(); - var forEach = require_array_for_each(); - $({ target: "Array", proto: true, forced: [].forEach !== forEach }, { - forEach: forEach - }); - } - }); - // node_modules/core-js/modules/es.array.index-of.js var require_es_array_index_of = __commonJS({ "node_modules/core-js/modules/es.array.index-of.js": function() { @@ -5428,7 +5403,7 @@ "use strict"; var $ = require_export(); $({ target: "Number", stat: true }, { - isNaN: function isNaN2(number) { + isNaN: function isNaN(number) { return number !== number; } }); @@ -9281,33 +9256,6 @@ } }); - // node_modules/core-js/modules/web.dom-collections.for-each.js - var require_web_dom_collections_for_each = __commonJS({ - "node_modules/core-js/modules/web.dom-collections.for-each.js": function() { - "use strict"; - var global2 = require_global(); - var DOMIterables = require_dom_iterables(); - var DOMTokenListPrototype = require_dom_token_list_prototype(); - var forEach = require_array_for_each(); - var createNonEnumerableProperty = require_create_non_enumerable_property(); - var handlePrototype = function(CollectionPrototype) { - if (CollectionPrototype && CollectionPrototype.forEach !== forEach) - try { - createNonEnumerableProperty(CollectionPrototype, "forEach", forEach); - } catch (error) { - CollectionPrototype.forEach = forEach; - } - }; - for (COLLECTION_NAME in DOMIterables) { - if (DOMIterables[COLLECTION_NAME]) { - handlePrototype(global2[COLLECTION_NAME] && global2[COLLECTION_NAME].prototype); - } - } - var COLLECTION_NAME; - handlePrototype(DOMTokenListPrototype); - } - }); - // node_modules/core-js/modules/web.dom-collections.iterator.js var require_web_dom_collections_iterator = __commonJS({ "node_modules/core-js/modules/web.dom-collections.iterator.js": function() { @@ -9407,7 +9355,6 @@ require_es_array_filter(); require_es_array_find(); require_es_array_find_index(); - require_es_array_for_each(); require_es_array_index_of(); require_es_array_is_array(); require_es_array_iterator(); @@ -9506,7 +9453,6 @@ require_esnext_typed_array_to_reversed(); require_esnext_typed_array_to_sorted(); require_esnext_typed_array_with(); - require_web_dom_collections_for_each(); require_web_dom_collections_iterator(); require_web_self(); function _callSuper(t, o, e) { @@ -19976,23 +19922,12 @@ } else if (typeof config.reply !== "string" || config.reply !== "same" && config.reply !== "direct" && config.reply !== "broadcast") { throw new Error("Invalid 'reply' setting for module ".concat(config.name)); } - var devices = []; - if (typeof config.devices !== "number") { - if (typeof config.devices !== "string") - throw new Error("Invalid device list ".concat(config.devices, " for module ").concat(config.name)); - config.devices.split(",").forEach(function(e) { - var n = parseInt(e, 10); - if (isNaN(n)) - throw new Error("Invalid device id ".concat(e, " for module ").concat(config.name)); - devices.push(n); - }); - } else { - devices.push(config.devices); - } + if (typeof config.device !== "number") + throw new Error("Invalid device ".concat(config.device, " for module ").concat(config.name)); var ret = __spreadProps(__spreadValues({}, config), { name: config.name, path: config.path, - devices: devices, + device: config.device, channel: config.channel, port: config.port, enabled: config.enabled, @@ -20041,10 +19976,10 @@ }; } var Module = /* @__PURE__ */ function() { - function Module2(config, devices) { + function Module2(config, device) { _classCallCheck(this, Module2); this.config = config; - this.devices = devices; + this.device = device; } return _createClass(Module2, [{ key: "log", @@ -20059,14 +19994,14 @@ }]); }(); var Echo = /* @__PURE__ */ function(_Module) { - function Echo2(config, devices) { + function Echo2(config, device) { _classCallCheck(this, Echo2); - return _callSuper(this, Echo2, [config, devices]); + return _callSuper(this, Echo2, [config, device]); } _inherits(Echo2, _Module); return _createClass(Echo2, [{ key: "handlePacket", - value: function handlePacket(packet, device, replyTo) { + value: function handlePacket(packet, replyTo) { var _a; if (packet.payloadVariant["case"] !== "decoded") return false; @@ -20076,7 +20011,7 @@ return false; var echo = "You sent: ".concat(msg.replace(/^.echo\s/, "")); var hopLimit = (_a = this.config.hopLimit) != null ? _a : void 0; - device.sendText({ + this.device.sendText({ text: echo, to: replyTo, channel: packet.channel, @@ -20099,11 +20034,11 @@ return "\uD83D\uDFE3"; } var Weather = /* @__PURE__ */ function(_Module2) { - function Weather2(config, devices) { + function Weather2(config, device) { var _this4; _classCallCheck(this, Weather2); var _a; - _this4 = _callSuper(this, Weather2, [config, devices]); + _this4 = _callSuper(this, Weather2, [config, device]); var wc = config; _this4.units = "metric"; if (typeof wc.units === "string") { @@ -20115,21 +20050,8 @@ } _this4.alertRebroadcast = (_a = wc.alertRebroadcast) != null ? _a : false; if (typeof wc.alertInterval === "number" && wc.alertInterval > 0) { - var alertBroadcastInterval2 = function alertBroadcastInterval22() { - for (var d2 = 0; d2 < this.devices.length; d2++) { - this.broadcastAlerts(this.devices[d2], this.config.channel, this.config.hopLimit); - } - }; - var alertBroadcastInterval = alertBroadcastInterval2; - js.setInterval(alertBroadcastInterval2, wc.alertInterval, _this4); - var _loop3 = function _loop32(d2) { - _this4.devices[d2].on("ready", function() { - _this4.broadcastAlerts(_this4.devices[d2], _this4.config.channel, _this4.config.hopLimit); - }); - }; - for (var d = 0; d < _this4.devices.length; d++) { - _loop3(d); - } + js.setInterval(_this4.broadcastAlerts, wc.alertInterval, _this4); + _this4.device.on("ready", _this4.broadcastAlerts.bind(_this4)); } return _this4; } @@ -20193,10 +20115,10 @@ } }, { key: "canBroadcastAlert", - value: function canBroadcastAlert(wa, devId) { + value: function canBroadcastAlert(wa) { var ret = true; var wah = js.global.md5_calc(wa); - var wal = "".concat(Date.now(), ",").concat(devId, ",").concat(wah); + var wal = "".concat(Date.now(), ",").concat(this.device.configId, ",").concat(wah); var fn = "".concat(js.global.system.temp_dir, "synctastic-weather-alerts.txt"); var f = new js.global.File(fn); if (!f.exists) { @@ -20209,7 +20131,7 @@ throw new Error("Failed to open ".concat(fn, " for reading")); while (!f.eof) { var l = f.readln().split(","); - if (parseInt(l[1], 10) !== devId) + if (parseInt(l[1], 10) !== this.device.configId) continue; if (l[2] !== wah) continue; @@ -20228,8 +20150,8 @@ } }, { key: "broadcastAlerts", - value: function broadcastAlerts(device, channel, hopLimit) { - var nodeInfo = device.getNodeInfo(device.myNodeInfo.myNodeNum); + value: function broadcastAlerts() { + var nodeInfo = this.device.getNodeInfo(this.device.myNodeInfo.myNodeNum); if ((nodeInfo == null ? void 0 : nodeInfo.position) === void 0) return; var lat = nodeInfo.position.latitudeI * 1e-7; @@ -20237,17 +20159,17 @@ var wa = this.getCurrentAlerts(lat, lon); if (wa === void 0) return; - if (!this.alertRebroadcast && !this.canBroadcastAlert(wa, device.configId)) + if (!this.alertRebroadcast && !this.canBroadcastAlert(wa)) return; - device.sendText({ + this.device.sendText({ text: wa, - channel: channel, - hopLimit: hopLimit + channel: this.config.channel, + hopLimit: this.config.hopLimit }); } }, { key: "handlePacket", - value: function handlePacket(packet, device, replyTo) { + value: function handlePacket(packet, replyTo) { if (packet.payloadVariant["case"] !== "decoded") return false; var dec = new TextDecoder(); @@ -20255,12 +20177,12 @@ if (!msg.startsWith(".wx")) return false; try { - var nodeInfo = device.getNodeInfo(packet.from); + var nodeInfo = this.device.getNodeInfo(packet.from); if ((nodeInfo == null ? void 0 : nodeInfo.position) === void 0) { if (this.config.debug) { this.log(sbbsdefs.LOG_DEBUG, "failed to find position for ".concat(packet.from, " in node DB. Falling back on self position")); } - nodeInfo = device.getNodeInfo(device.myNodeInfo.myNodeNum); + nodeInfo = this.device.getNodeInfo(this.device.myNodeInfo.myNodeNum); } if ((nodeInfo == null ? void 0 : nodeInfo.position) === void 0) { throw new Error("failed to find self position, cannot send lat/lon to weather API"); @@ -20282,7 +20204,7 @@ } if (ws === void 0) return false; - device.sendText({ + this.device.sendText({ text: ws, to: replyTo, channel: packet.channel, @@ -20301,7 +20223,7 @@ var config = getConfig(); var devices = {}; var modules = []; - var _loop4 = function _loop42() { + var _loop3 = function _loop32() { var dev = void 0; if (config.devices[device].type === "serial") { var cfg = config.devices[device]; @@ -20328,7 +20250,7 @@ var _module = _modules[_i17]; if (_module.config.channel !== packet.channel) continue; - if (_module.config.devices.indexOf(dev.configId) < 0) + if (_module.config.device !== dev.configId) continue; if (packet.to !== defs_exports.BROADCAST && packet.to !== dev.myNodeInfo.myNodeNum) continue; @@ -20340,32 +20262,30 @@ if (_module.config.reply === "broadcast" || _module.config.reply === "same" && packet.to === defs_exports.BROADCAST) { to = defs_exports.BROADCAST; } - _module.handlePacket(packet, dev, to); + _module.handlePacket(packet, to); } }); dev.connect(); devices[config.devices[device].id] = dev; }; for (var device in config.devices) { - if (_loop4()) + if (_loop3()) continue; } for (var module in config.modules) { - var devs = []; - for (var devId in config.modules[module].devices) { - if (devices[devId] !== void 0) - devs.push(devices[devId]); + if (devices[config.modules[module].device] === void 0) { + throw new Error("Invalid device specified for ".concat(config.modules[module].name)); } switch (config.modules[module].path) { case "echo": - modules.push(new Echo(config.modules[module], devs)); + modules.push(new Echo(config.modules[module], devices[config.modules[module].device])); break; case "weather": - modules.push(new Weather(config.modules[module], devs)); + modules.push(new Weather(config.modules[module], devices[config.modules[module].device])); break; default: var m = js.global.load({}, config.modules[module].path); - modules.push(new m(config.modules[module])); + modules.push(new m(config.modules[module], devices[config.modules[module].device])); break; } } diff --git a/src/lib/config.ts b/src/lib/config.ts index 8569fad798927846ca00555ff683a38d7676b244..db89fa4eb1a57cdef908c60b235811c60f2f18d0 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -26,7 +26,7 @@ type ReplyType = 'same' | 'direct' | 'broadcast'; export interface IModuleConfig { name: string, path: string, - devices: number[], + device: number, channel: number, port: number, enabled: boolean, @@ -105,24 +105,13 @@ function parseModuleConfig(config: IniSection): IModuleConfig { } else if (typeof config.reply !== 'string' || (config.reply !== 'same' && config.reply !== 'direct' && config.reply !== 'broadcast')) { throw new Error(`Invalid 'reply' setting for module ${config.name}`); } - - const devices = []; - if (typeof config.devices !== 'number') { - if (typeof config.devices !== 'string') throw new Error(`Invalid device list ${config.devices} for module ${config.name}`); - config.devices.split(',').forEach(e => { - const n = parseInt(e, 10); - if (isNaN(n)) throw new Error(`Invalid device id ${e} for module ${config.name}`); - devices.push(n); - }); - } else { - devices.push(config.devices); - } + if (typeof config.device !== 'number') throw new Error(`Invalid device ${config.device} for module ${config.name}`); const ret: IModuleConfig = { ...config, name: config.name, path: config.path, - devices, + device: config.device, channel: config.channel, port: config.port, enabled: config.enabled, diff --git a/src/lib/module.ts b/src/lib/module.ts index 6e338c3bfea163c2343af67e957d83721b2cd193..d66dfeb16d54b4fc0310951a829fdcbb5c27d398 100644 --- a/src/lib/module.ts +++ b/src/lib/module.ts @@ -4,14 +4,14 @@ import { IModuleConfig } from './config'; export default abstract class Module { config: IModuleConfig; - devices: Device[]; + device: Device; - constructor(config: IModuleConfig, devices: Device[]) { + constructor(config: IModuleConfig, device: Device) { this.config = config; - this.devices = devices; + this.device = device; } - abstract handlePacket(packet: protobuf.Mesh.MeshPacket, device: Device, replyTo: number): boolean; // 'true' if handled + abstract handlePacket(packet: protobuf.Mesh.MeshPacket, replyTo: number): boolean; // 'true' if handled log(level: number, msg: string): void { js.global.log(level, `${this.config.name}: ${msg}`); diff --git a/src/modules/echo.ts b/src/modules/echo.ts index a63b4372a7f918fb368f514c095ed1203d49bda9..ca3159142b6476266ddc5d12ba37503af247b672 100644 --- a/src/modules/echo.ts +++ b/src/modules/echo.ts @@ -4,11 +4,11 @@ import { IModuleConfig } from '../lib/config'; export default class Echo extends Module { - constructor(config: IModuleConfig, devices: Device[]) { - super(config, devices); + constructor(config: IModuleConfig, device: Device) { + super(config, device); } - handlePacket(packet: protobuf.Mesh.MeshPacket, device: Device, replyTo: number): boolean { + handlePacket(packet: protobuf.Mesh.MeshPacket, replyTo: number): boolean { if (packet.payloadVariant.case !== 'decoded') return false; // @ts-expect-error It's ambient const dec = new TextDecoder(); @@ -16,7 +16,7 @@ export default class Echo extends Module { if (!msg.startsWith('.echo ')) return false; const echo = `You sent: ${msg.replace(/^.echo\s/, '')}`; const hopLimit = this.config.hopLimit ?? undefined; - device.sendText({ text: echo, to: replyTo, channel: packet.channel, replyId: packet.id, hopLimit }); + this.device.sendText({ text: echo, to: replyTo, channel: packet.channel, replyId: packet.id, hopLimit }); return true; } diff --git a/src/modules/weather.ts b/src/modules/weather.ts index 198ab807d5a435d65d8a8df0153db45e338158c4..ebc5402a0e0fff018e0917f74ef8e2876946be24 100644 --- a/src/modules/weather.ts +++ b/src/modules/weather.ts @@ -24,8 +24,8 @@ export default class Weather extends Module { private units: unit; private alertRebroadcast: boolean; - constructor(config: IModuleConfig, devices: Device[]) { - super(config, devices); + constructor(config: IModuleConfig, device: Device) { + super(config, device); const wc: IWeatherModuleConfig = config as IWeatherModuleConfig; this.units = 'metric'; @@ -40,17 +40,8 @@ export default class Weather extends Module { this.alertRebroadcast = wc.alertRebroadcast ?? false; if (typeof wc.alertInterval === 'number' && wc.alertInterval > 0) { - function alertBroadcastInterval(this: Weather): void { - for (let d = 0; d < this.devices.length; d++) { - this.broadcastAlerts(this.devices[d], this.config.channel, this.config.hopLimit); - } - } - js.setInterval(alertBroadcastInterval, wc.alertInterval, this); - for (let d = 0; d < this.devices.length; d++) { - this.devices[d].on('ready', () => { - this.broadcastAlerts(this.devices[d], this.config.channel, this.config.hopLimit); - }); - } + js.setInterval(this.broadcastAlerts, wc.alertInterval, this); + this.device.on('ready', this.broadcastAlerts.bind(this)); } } @@ -92,10 +83,10 @@ export default class Weather extends Module { return ws === '' ? undefined : ws; } - private canBroadcastAlert(wa: string, devId: number): boolean { + private canBroadcastAlert(wa: string): boolean { let ret = true; const wah = js.global.md5_calc(wa); - const wal = `${Date.now()},${devId},${wah}`; + const wal = `${Date.now()},${this.device.configId},${wah}`; const fn = `${js.global.system.temp_dir}synctastic-weather-alerts.txt`; const f = new js.global.File(fn); if (!f.exists) { @@ -106,7 +97,7 @@ export default class Weather extends Module { if (!f.open('r')) throw new Error(`Failed to open ${fn} for reading`); while (!f.eof) { const l = f.readln().split(','); - if (parseInt(l[1], 10) !== devId) continue; + if (parseInt(l[1], 10) !== this.device.configId) continue; if (l[2] !== wah) continue; ret = false; break; @@ -121,18 +112,18 @@ export default class Weather extends Module { return ret; } - private broadcastAlerts(device: Device, channel: number, hopLimit: number | undefined): void { - const nodeInfo = device.getNodeInfo(device.myNodeInfo.myNodeNum); + private broadcastAlerts(): void { + const nodeInfo = this.device.getNodeInfo(this.device.myNodeInfo.myNodeNum); if (nodeInfo?.position === undefined) return; const lat = nodeInfo.position.latitudeI * .0000001; const lon = nodeInfo.position.longitudeI * .0000001; const wa = this.getCurrentAlerts(lat, lon); if (wa === undefined) return; - if (!this.alertRebroadcast && !this.canBroadcastAlert(wa, device.configId)) return; - device.sendText({ text: wa, channel: channel, hopLimit }); + if (!this.alertRebroadcast && !this.canBroadcastAlert(wa)) return; + this.device.sendText({ text: wa, channel: this.config.channel, hopLimit: this.config.hopLimit }); } - handlePacket(packet: protobuf.Mesh.MeshPacket, device: Device, replyTo: number): boolean { + handlePacket(packet: protobuf.Mesh.MeshPacket, replyTo: number): boolean { if (packet.payloadVariant.case !== 'decoded') return false; // @ts-expect-error It's ambient const dec = new TextDecoder(); @@ -140,12 +131,12 @@ export default class Weather extends Module { if (!msg.startsWith('.wx')) return false; try { - let nodeInfo = device.getNodeInfo(packet.from); + let nodeInfo = this.device.getNodeInfo(packet.from); if (nodeInfo?.position === undefined) { if (this.config.debug) { this.log(sbbsdefs.LOG_DEBUG, `failed to find position for ${packet.from} in node DB. Falling back on self position`); } - nodeInfo = device.getNodeInfo(device.myNodeInfo.myNodeNum); + nodeInfo = this.device.getNodeInfo(this.device.myNodeInfo.myNodeNum); } if (nodeInfo?.position === undefined) { throw new Error('failed to find self position, cannot send lat/lon to weather API'); @@ -167,7 +158,7 @@ export default class Weather extends Module { } if (ws === undefined) return false; - device.sendText({ text: ws, to: replyTo, channel: packet.channel, replyId: packet.id, hopLimit: this.config.hopLimit }); + this.device.sendText({ text: ws, to: replyTo, channel: packet.channel, replyId: packet.id, hopLimit: this.config.hopLimit }); return true; } catch (err: any) { this.error(err as string); diff --git a/src/synctastic.ts b/src/synctastic.ts index 05a9f340cbf9d8e15e907ec646c0655df0e345b7..4e90233dd0352395965656d7c39803023ab1638f 100644 --- a/src/synctastic.ts +++ b/src/synctastic.ts @@ -33,7 +33,7 @@ function init(): void { if (dev === undefined) return; for (const module of modules) { if (module.config.channel !== packet.channel) continue; - if (module.config.devices.indexOf(dev.configId) < 0) continue; + if (module.config.device !== dev.configId) continue; if (packet.to !== defs.BROADCAST && packet.to !== dev.myNodeInfo.myNodeNum) continue; if (packet.payloadVariant.case !== 'decoded') continue; if (module.config.port !== packet.payloadVariant.value.portnum) continue; @@ -41,7 +41,7 @@ function init(): void { if (module.config.reply === 'broadcast' || (module.config.reply === 'same' && packet.to === defs.BROADCAST)) { to = defs.BROADCAST; } - module.handlePacket(packet, dev, to); + module.handlePacket(packet, to); } }); @@ -53,21 +53,20 @@ function init(): void { for (const module in config.modules) { - const devs: Device[] = []; - for (const devId in config.modules[module].devices) { - if (devices[devId] !== undefined) devs.push(devices[devId]); + if (devices[config.modules[module].device] === undefined) { + throw new Error(`Invalid device specified for ${config.modules[module].name}`); } switch (config.modules[module].path) { case 'echo': - modules.push(new Echo(config.modules[module], devs)); + modules.push(new Echo(config.modules[module], devices[config.modules[module].device])); break; case 'weather': - modules.push(new Weather(config.modules[module], devs)); + modules.push(new Weather(config.modules[module], devices[config.modules[module].device])); break; default: const m = js.global.load({}, config.modules[module].path); - modules.push(new m(config.modules[module])); + modules.push(new m(config.modules[module], devices[config.modules[module].device])); break; }