mesh-tools 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.
Files changed (3) hide show
  1. package/README.md +50 -0
  2. package/package.json +36 -0
  3. package/src/index.js +420 -0
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # Mesh-Tools
2
+
3
+ This is a packer made for the Meshtastic Project. I am not affiliated with Meshtastic or sponsored. This project is just for fun. [Meshtastic](https://meshtastic.org)
4
+
5
+ ## Documentation
6
+
7
+ Here is the table of contents to the documentation;
8
+
9
+ [Table Of Contents](/docs/table-of-contents.md)
10
+
11
+ ## What this package does #
12
+ This package is meant to abstract the functions of the meshtastic node.js cli package. It is meant to make the developement of programs that interact with the mesh network easier and more efficient.
13
+
14
+ This is a side project so don't expect this to always be up to date but I'll do my best.
15
+
16
+ It can easily handle connecting to nodes, detecting when someone has dmed you, sending dms, sending on channels, receiving on channels and more. All of the stuff you would ever want.
17
+
18
+ ## How it works
19
+
20
+ I tried my best to abstract all of the features that are unnecessary to the average user. The package has been designed around making bots for Meshtastic.
21
+
22
+ Here is all of the code needed to connect to your node. It is much shorter than what would be needed if you were not using my package.
23
+ ```js
24
+ // Import the package (not on npm yet)
25
+ const MeshTools = require('mesh-tools')
26
+ // Define the port your node is on over serial
27
+ const node = new MeshTools.SerialNode('/dev/ttyACM1');
28
+ // Connect the node to your computer
29
+ node.connect()
30
+ ```
31
+
32
+ If for instance you wanted to listen for direct messages to your node, it is as easy as this chunk of code.
33
+
34
+ ```js
35
+ // Connect to the receiveDm event
36
+ node.events.on('receiveDm',(data) => {
37
+ // Send a DM back to the sender
38
+ node.sendDirectMessage(`Hello!`,data.from)
39
+ })
40
+ ```
41
+
42
+ It's that easy! I'll have more examples later.
43
+
44
+ ## Why I made this
45
+
46
+ I made this because I was annoyed at how difficult it was to work with the nodejs cli for Meshtastic. There was little documentation and I frequently ran into issues. To solve these issues, I made this package that bundles all the most common features of the cli into one easy to use package.
47
+
48
+ Thanks for checking this out! Please give it a star :)
49
+
50
+ -JStudios6118
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "mesh-tools",
3
+ "version": "1.0.0",
4
+ "description": "A helpful package for automating the Meshtastic node api",
5
+ "license": "ISC",
6
+ "author": "Josh Carlson",
7
+ "type": "commonjs",
8
+ "main": "src/index.js",
9
+ "scripts": {
10
+ "test": "node test.js"
11
+ },
12
+ "engines": { "node": ">=18.0.0" },
13
+ "dependencies": {
14
+ "@meshtastic/core": "^2.6.7",
15
+ "@meshtastic/transport-http": "^0.2.5",
16
+ "@meshtastic/transport-node": "^0.0.2",
17
+ "@meshtastic/transport-node-serial": "^0.0.2",
18
+ "lowdb": "^7.0.1"
19
+ },
20
+ "keywords": [
21
+ "meshtastic",
22
+ "mesh",
23
+ "iot",
24
+ "lora",
25
+ "radio",
26
+ "bot"
27
+ ],
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/JStudios6118/Mesh-Tools"
31
+ },
32
+ "homepage": "https://github.com/JStudios6118/Mesh-Tools#readme",
33
+ "bugs": "https://github.com/JStudios6118/Mesh-Tools/issues",
34
+ "files": ["src"]
35
+
36
+ }
package/src/index.js ADDED
@@ -0,0 +1,420 @@
1
+ const { MeshDevice, Protobuf } = require("@meshtastic/core");
2
+ const { TransportNodeSerial } = require("@meshtastic/transport-node-serial");
3
+ const { TransportHTTP } = require("@meshtastic/transport-http");
4
+ const { TransportNode } = require("@meshtastic/transport-node");
5
+
6
+
7
+ const { EventEmitter } = require('events');
8
+ const { JSONFilePreset } = require("lowdb/node");
9
+ const path = require('path');
10
+
11
+ //Logger.setLogLevel(LogLevel.NONE)
12
+
13
+ // Base class for all nodes (connected to host device)
14
+
15
+ /*
16
+
17
+ TODO:
18
+ -ADD MESSAGE ACKNOWLEDGEMENT HANDLING
19
+ -ADD TCP AND HTTP CONNECTION TYPES
20
+ -MAKE EVERYTHING ASYNCHRONOUS
21
+ -ADD LOG LEVELS
22
+ -ADD SUPPORT FOR DIFFERENT PACKET TYPES LIKE TELEMETRY AND ETC
23
+ -ADD PROPER JSDOC DOCUMENTATION FOR AUTOFILL AND SUCH
24
+
25
+ DONE:
26
+ -ADD BROADCAST MESSAGING
27
+ -ADD A NODE DATABASE BUILDER
28
+
29
+ */
30
+
31
+ // Some settings. Will add more later.
32
+ settings = {
33
+ print_logs: true
34
+ }
35
+
36
+ // A configure function to change global settings.
37
+ function Configure(property, value){
38
+ settings[property] = value
39
+ }
40
+
41
+ // Simple Logger class to provide nice looking, customizable logs
42
+ class Logger {
43
+ #name;
44
+
45
+ // Sets the name that will show up before a message to help identify where the message is coming from.
46
+ constructor(name){
47
+ this.#name = name;
48
+ }
49
+
50
+ log(message){
51
+ if (!settings.print_logs){
52
+ return;
53
+ }
54
+ console.log(`[MT] ${this.#name} | ${message}`)
55
+ }
56
+ }
57
+
58
+ class NodeDB {
59
+
60
+ // In the following functions, please note that the identifier fields can take in either the nodes number or id.
61
+
62
+ #logger = new Logger('[Node DB]')
63
+
64
+ #db = null;
65
+ #dbData;
66
+
67
+ // Initialize the database file
68
+ async init(databasePath){
69
+ if (this.#db!=null){ this.#logger.log("Database Already Initialized. Skipping..."); return false }
70
+
71
+ this.#db = await JSONFilePreset(path.join(databasePath,'nodeDb.json'), { nodes: [] });
72
+ this.#dbData = this.#db.data;
73
+ await this.#db.write();
74
+ return true
75
+ }
76
+
77
+ // Internal method to check if an node of an id exists and return its index.
78
+ #checkId(input){
79
+ //this.#logger.log(`IN: ${input}`)
80
+ if (typeof input === "number"){
81
+ return this.#dbData.nodes.findIndex(p => p.number === input)
82
+ } else if (typeof input === "string"){
83
+ return this.#dbData.nodes.findIndex(p => p.id === input)
84
+ } else {
85
+ throw new Error('.nodeExists: Invalid type! Must be either a string or number.');
86
+ }
87
+ }
88
+
89
+ // Internal command that returns a node from the database based on the id. Not compatible with node number.
90
+ #getNodeIndexById(identifier){
91
+ if (this.#db===null){ this.#logger.log("Database has not been initialized yet!"); return false }
92
+ return this.#dbData.nodes.findIndex(p => p.id === identifier);
93
+ }
94
+
95
+ // Returns a bool on whether or not a node of the id exists.
96
+ nodeExists(identifier){
97
+ if (this.#db===null){ this.#logger.log("Database has not been initialized yet!"); return false }
98
+ //console.log(typeof identifier)
99
+ const index = this.#checkId(identifier)
100
+ if (index===-1){
101
+ return false;
102
+ }
103
+ return true;
104
+
105
+ }
106
+
107
+ // Gets a desired node using the identifier
108
+ getNode(identifier){
109
+ if (this.#db===null){ this.#logger.log("Database has not been initialized yet!"); return false }
110
+ const index = this.#checkId(identifier)
111
+ if (index===-1){
112
+ return null;
113
+ }
114
+
115
+ //this.#logger.log(`Index of Node: ${index}`)
116
+
117
+ const { longName, shortName, id, number, storedData } = this.#dbData.nodes[index];
118
+
119
+ return new Node(longName,shortName,id,number,storedData);
120
+ }
121
+
122
+ // Add a node to the database. If the node is already present, update its lastheard value and validate other data. Does not affect stored data.
123
+ async push(node){
124
+ //console.log('pushing node:', node.id, 'number:', node.number)
125
+ if (this.#db===null){ this.#logger.log("Database has not been initialized yet!"); return false }
126
+ if (this.nodeExists(node.id)){
127
+ let nodeData = this.getNode(node.id)
128
+ this.#dbData.nodes[this.#getNodeIndexById(node.id)] = {
129
+ ...nodeData.info,
130
+ id:node.id,
131
+ number:node.number,
132
+ longName:node.longName,
133
+ shortName:node.shortName,
134
+ lastHeard: Math.floor(Date.now() / 1000)
135
+ }
136
+ } else {
137
+ //console.log(node.info())
138
+ this.#dbData.nodes.push(node.info)
139
+ }
140
+
141
+ //console.log("Succeeded!")
142
+
143
+ await this.#db.write();
144
+
145
+ return true;
146
+ }
147
+
148
+ // Gets the data on a desired node as defined by the identifier
149
+ async getNodeStoredData(identifier){
150
+ if (this.#db===null){ this.#logger.log("Database has not been initialized yet!"); return false }
151
+
152
+ const index = this.#checkId(identifier)
153
+ if (index===-1){
154
+ return null;
155
+ }
156
+
157
+ //this.#logger.log(`All Node Data: ${JSON.stringify(this.getNode(identifier),null,2)}`)
158
+
159
+ return this.getNode(identifier).storedData;
160
+
161
+
162
+ }
163
+
164
+ // Update the custom stored data in the node as defined by the identifier
165
+ async updateStoredData(identifier, data) {
166
+ if (this.#db === null) { this.#logger.log("Database has not been initialized yet!"); return false }
167
+
168
+ const index = this.#checkId(identifier);
169
+ if (index === -1) { this.#logger.log("Node not found!"); return false }
170
+
171
+ this.#dbData.nodes[index].storedData = {
172
+ ...this.#dbData.nodes[index].storedData,
173
+ ...data
174
+ };
175
+
176
+ await this.#db.write();
177
+ return true;
178
+ }
179
+
180
+ }
181
+
182
+ // Node class for NodeDB
183
+ class Node {
184
+ constructor(longName,shortName,id,number,storedData){
185
+ this.longName = longName;
186
+ this.shortName = shortName;
187
+ this.id = id;
188
+ this.number = number;
189
+ this.lastHeard = Math.floor(Date.now() / 1000); // Set lastHeard time to current epoch time
190
+ this.storedData = storedData || {};
191
+ }
192
+
193
+ // Returns all data on Node as a JSON object.
194
+ get info(){
195
+ return {
196
+ longName:this.longName,
197
+ shortName:this.shortName,
198
+ id:this.id,
199
+ number:this.number,
200
+ lastHeard:this.lastHeard,
201
+ storedData:this.storedData
202
+ }
203
+ }
204
+
205
+ }
206
+
207
+ // The base device class for all Meshtastic node connections. Houses all main logic and code for handling packets events and more.
208
+ class Device {
209
+
210
+ #device = null; // Mesh device. When set to null, many command will not run
211
+ #ownId = null; // Id of connected node
212
+ #longName;
213
+ #shortName;
214
+
215
+ // Creates an empty db class. Does nothing by itself, needs to be activated. I didn't leave it empty for autofill reaons and my sanity.
216
+ db = new NodeDB();
217
+
218
+ //#pendingMessages = new Map();
219
+
220
+ // Creates a variable that developers can listen for events on.
221
+ events = new EventEmitter();
222
+
223
+ get ownId(){ return this.#ownId } // Getter for grabbing the devices own id number.
224
+
225
+ // Constructor called by extended class
226
+ constructor(){
227
+ this.logger = new Logger("Mesh Device");
228
+ }
229
+
230
+ // Connect super script. Uses transport to init a MeshDevice.
231
+ async connect(transport){
232
+
233
+ this.logger.log('Connecting Node...')
234
+
235
+ if (transport.port?.flush) {
236
+ this.logger.log('Flushing Transport')
237
+ await transport.port.flush();
238
+ }
239
+
240
+ this.#device = new MeshDevice(transport);
241
+ this.#device.log.settings.minLevel = 5;
242
+
243
+ // wait for the configured event instead of awaiting configure()
244
+ await new Promise((resolve, reject) => {
245
+ this.#device.events.onDeviceStatus.subscribe((status) => {
246
+ if (status === 7) resolve() // 7 = DeviceConfigured
247
+ if (status === 9) reject(new Error("Device disconnected during config"))
248
+ })
249
+
250
+ this.#device.configure().catch(reject)
251
+ })
252
+
253
+ this.logger.log('Device Successfully Configured!')
254
+
255
+ //console.log(this.#device)
256
+
257
+ // Save nodes id. Useful for handling dms
258
+ this.#ownId = this.#device.myNodeInfo.myNodeNum;
259
+
260
+ // Add an event listerner for ownNodeInfo to get the nodes own information
261
+ this.#setupListeners();
262
+
263
+ this.events.emit('connected', {ownId:this.#ownId});
264
+
265
+ return {ownId:this.#ownId}
266
+ }
267
+
268
+ // Starts and configures the Node Database.
269
+ async startNodeDB(databasePath){
270
+ this.db = new NodeDB();
271
+ await this.db.init(databasePath);
272
+
273
+ this.logger.log('Started Database!')
274
+
275
+ return true;
276
+ }
277
+
278
+ // Setup listeners for different Meshtastic events
279
+ #setupListeners() {
280
+ // Not sure if this does anything. Needs more testing
281
+ this.#device.events.onNodeInfoPacket.subscribe((nodeInfo) => {
282
+ if (nodeInfo.num === this.#ownId) {
283
+ this.#longName = nodeInfo.user.longName;
284
+ this.#shortName = nodeInfo.user.shortName;
285
+ this.events.emit('ownNameReceived', { longName: this.#longName, shortName:this.#shortName });
286
+ }
287
+ });
288
+
289
+ // Unused for now
290
+ this.#device.events.onMeshPacket.subscribe((packet) => {
291
+ if (packet.from === 2996808676){
292
+ }
293
+ });
294
+
295
+ // Triggers when the node receives info about another node (name,location,etc)
296
+ this.#device.events.onUserPacket.subscribe((packet) => {
297
+ const dat = packet.data
298
+ const data = {longName:dat.longName, shortName:dat.shortName, id:dat.id, number:packet.from}
299
+ //console.log(`NODEINFO: ${JSON.stringify(packet,null,2)}`)
300
+ const nodeInfo = new Node(data.longName,data.shortName,data.id,data.number);
301
+ this.events.emit("nodeInfoReceived", nodeInfo);
302
+ })
303
+
304
+ // Triggers when a message is received. Handles packet type.
305
+ this.#device.events.onMessagePacket.subscribe((packet) => {
306
+ if (packet.to === this.#ownId && packet.type === 'direct'){
307
+ this.events.emit('receiveDm',packet)
308
+ } else if (packet.type === 'broadcast') {
309
+ this.events.emit('receiveMessage',packet)
310
+ }
311
+ });
312
+
313
+
314
+ }
315
+
316
+ // Send a message in a channel
317
+ async sendMessage(message,channel){
318
+ const id = await this.#device.sendText(message,0xFFFFFFFF,true,channel);
319
+ return id
320
+ }
321
+
322
+ // Send a message in a channel as a reply.
323
+ async sendReplyMessage(message,channel,replyId){
324
+ const id = await this.#device.sendText(message,0xFFFFFFFF,true,channel,replyId);
325
+ return id
326
+ }
327
+
328
+ // Send a direct message to another node.
329
+ async sendDirectMessage(message,to){
330
+ const id = await this.#device.sendText(message,to);
331
+ return id
332
+ //this.logger.log(`PACKER ID: ${id}`)
333
+ }
334
+
335
+ // Send a direct message to another node as a reply.
336
+ async sendReplyDirectMessage(message,to,replyId){
337
+ const id = await this.#device.sendText(message,to,true,null,replyId);
338
+ return id
339
+ }
340
+
341
+ }
342
+
343
+ // Constructor for Mesh Nodes connected to host over serial
344
+ class SerialNode extends Device {
345
+
346
+ #serial_address;
347
+
348
+ constructor(serial_address){
349
+ super();
350
+ this.#serial_address = serial_address;
351
+ }
352
+
353
+ async connect() {
354
+ if (this.device != null) {
355
+ this.logger.log("Device is already connected! Skipping...");
356
+ return;
357
+ }
358
+
359
+ this.logger.log("Creating Transport...")
360
+
361
+ const transport = await TransportNodeSerial.create(this.#serial_address);
362
+ return await super.connect(transport);
363
+
364
+ }
365
+
366
+ }
367
+
368
+ // Constructor for Mesh Node connected to host over TCP
369
+ // Untested
370
+ class TCPNode extends Device {
371
+
372
+ #tcp_ip_address;
373
+
374
+ constructor(tcp_ip_address){
375
+ super();
376
+ this.#tcp_ip_address = tcp_ip_address;
377
+ }
378
+
379
+ async connect() {
380
+ if (this.device != null) {
381
+ this.logger.log("Device is already connected! Skipping...");
382
+ return;
383
+ }
384
+
385
+ this.logger.log("Creating Transport...")
386
+
387
+ const transport = await TransportNode.create(this.#tcp_ip_address);
388
+ return await super.connect(transport);
389
+
390
+ }
391
+
392
+ }
393
+
394
+ // Constructor for Mesh Node connected to host over HTTP
395
+ // Untested
396
+ class HTTPNode extends Device {
397
+
398
+ #http_ip_address;
399
+
400
+ constructor(http_ip_address){
401
+ super();
402
+ this.#http_ip_address = http_ip_address;
403
+ }
404
+
405
+ async connect() {
406
+ if (this.device != null) {
407
+ this.logger.log("Device is already connected! Skipping...");
408
+ return;
409
+ }
410
+
411
+ this.logger.log("Creating Transport...")
412
+
413
+ const transport = await TransportHTTP.create(this.#http_ip_address);
414
+ return await super.connect(transport);
415
+
416
+ }
417
+
418
+ }
419
+
420
+ module.exports = { SerialNode, TCPNode, HTTPNode, Configure }