neoagent 2.3.1-beta.31 → 2.3.1-beta.33
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neoagent",
|
|
3
|
-
"version": "2.3.1-beta.
|
|
3
|
+
"version": "2.3.1-beta.33",
|
|
4
4
|
"description": "Proactive personal AI agent with no limits",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "server/index.js",
|
|
@@ -54,8 +54,6 @@
|
|
|
54
54
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
55
55
|
"@google/generative-ai": "^0.24.0",
|
|
56
56
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
57
|
-
"@meshtastic/core": "^2.6.7",
|
|
58
|
-
"@meshtastic/transport-node": "^0.0.2",
|
|
59
57
|
"baileys": "^6.7.21",
|
|
60
58
|
"bcrypt": "^6.0.0",
|
|
61
59
|
"better-sqlite3": "^11.8.1",
|
|
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"42d3d75a56efe1a2e9902f52dc8006099c45d9
|
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
40
|
+
serviceWorkerVersion: "2675743838" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
|
41
41
|
}
|
|
42
42
|
});
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const { BasePlatform } = require('./base');
|
|
4
4
|
const { readMeshtasticEnabled } = require('./meshtastic_env');
|
|
5
|
+
const { MeshtasticTcpTransport } = require('./meshtastic_tcp_transport');
|
|
6
|
+
const { BROADCAST_NUM } = require('./meshtastic_protocol');
|
|
5
7
|
|
|
6
8
|
const DEFAULT_TCP_PORT = 4403;
|
|
7
9
|
const DEFAULT_CHANNEL = 0;
|
|
8
10
|
|
|
9
|
-
let meshtasticModulesPromise = null;
|
|
10
|
-
|
|
11
11
|
function requireText(value, label) {
|
|
12
12
|
const text = String(value || '').trim();
|
|
13
13
|
if (!text) throw new Error(`${label} is required`);
|
|
@@ -46,20 +46,6 @@ function parseChannel(value) {
|
|
|
46
46
|
return channel;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
async function loadMeshtasticModules() {
|
|
50
|
-
if (!meshtasticModulesPromise) {
|
|
51
|
-
meshtasticModulesPromise = Promise.all([
|
|
52
|
-
import('@meshtastic/core'),
|
|
53
|
-
import('@meshtastic/transport-node'),
|
|
54
|
-
]).then(([core, transport]) => ({
|
|
55
|
-
MeshDevice: core.MeshDevice,
|
|
56
|
-
Types: core.Types,
|
|
57
|
-
TransportNode: transport.TransportNode,
|
|
58
|
-
}));
|
|
59
|
-
}
|
|
60
|
-
return meshtasticModulesPromise;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
49
|
function normalizeNodeId(value) {
|
|
64
50
|
const text = String(value || '').trim();
|
|
65
51
|
if (!text) return '';
|
|
@@ -87,13 +73,8 @@ class MeshtasticPlatform extends BasePlatform {
|
|
|
87
73
|
this.channel = DEFAULT_CHANNEL;
|
|
88
74
|
this.authInfo = null;
|
|
89
75
|
this._transport = null;
|
|
90
|
-
this._device = null;
|
|
91
|
-
this._modules = null;
|
|
92
76
|
this._connectPromise = null;
|
|
93
77
|
this._disconnecting = false;
|
|
94
|
-
this._configured = false;
|
|
95
|
-
this._lastMyNodeInfo = null;
|
|
96
|
-
this._lastNodeUsers = new Map();
|
|
97
78
|
}
|
|
98
79
|
|
|
99
80
|
async connect() {
|
|
@@ -119,99 +100,31 @@ class MeshtasticPlatform extends BasePlatform {
|
|
|
119
100
|
this.port = endpoint.port;
|
|
120
101
|
this.channel = parseChannel(this.config.channel);
|
|
121
102
|
this._disconnecting = false;
|
|
122
|
-
this._configured = false;
|
|
123
|
-
this._lastMyNodeInfo = null;
|
|
124
|
-
this._lastNodeUsers.clear();
|
|
125
103
|
this.status = 'connecting';
|
|
126
104
|
|
|
127
|
-
const
|
|
128
|
-
this._modules = modules;
|
|
129
|
-
|
|
130
|
-
const transport = await modules.TransportNode.create(this.host, this.port, 60000);
|
|
105
|
+
const transport = await MeshtasticTcpTransport.create(this.host, this.port, 60000);
|
|
131
106
|
this._transport = transport;
|
|
132
107
|
|
|
133
|
-
const
|
|
134
|
-
this._device = device;
|
|
135
|
-
this._wireDeviceEvents(device, modules.Types);
|
|
136
|
-
|
|
137
|
-
const ready = new Promise((resolve, reject) => {
|
|
138
|
-
let settled = false;
|
|
139
|
-
const resolveOnce = () => {
|
|
140
|
-
if (settled) return;
|
|
141
|
-
settled = true;
|
|
142
|
-
resolve({ status: this.status });
|
|
143
|
-
};
|
|
144
|
-
const rejectOnce = (error) => {
|
|
145
|
-
if (settled) return;
|
|
146
|
-
settled = true;
|
|
147
|
-
reject(error);
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
device.events.onDeviceStatus.subscribe((status) => {
|
|
151
|
-
if (status === modules.Types.DeviceStatusEnum.DeviceConnected) {
|
|
152
|
-
device.configure().catch((error) => {
|
|
153
|
-
if (!this._disconnecting) {
|
|
154
|
-
rejectOnce(error);
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (status === modules.Types.DeviceStatusEnum.DeviceConfigured) {
|
|
161
|
-
this._configured = true;
|
|
162
|
-
this.status = 'connected';
|
|
163
|
-
this.authInfo = this._buildAuthInfo();
|
|
164
|
-
this.emit('connected');
|
|
165
|
-
resolveOnce();
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (status === modules.Types.DeviceStatusEnum.DeviceDisconnected) {
|
|
170
|
-
this.status = 'disconnected';
|
|
171
|
-
if (!this._disconnecting) {
|
|
172
|
-
const error = new Error('Meshtastic device disconnected');
|
|
173
|
-
rejectOnce(error);
|
|
174
|
-
this.emit('disconnected', { reason: 'device_disconnected' });
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
await ready;
|
|
181
|
-
return { status: this.status };
|
|
182
|
-
}
|
|
108
|
+
const conn = transport.connection;
|
|
183
109
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
this._lastMyNodeInfo = info;
|
|
187
|
-
this.authInfo = this._buildAuthInfo();
|
|
110
|
+
conn.on('myNodeInfo', () => {
|
|
111
|
+
this.authInfo = this._buildAuthInfo(conn);
|
|
188
112
|
});
|
|
189
113
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
});
|
|
114
|
+
conn.on('textMessage', (msg) => {
|
|
115
|
+
if (msg.channel !== this.channel) return;
|
|
116
|
+
|
|
117
|
+
const localNodeNum = conn.myNodeNum;
|
|
118
|
+
if (msg.from > 0 && localNodeNum > 0 && msg.from === localNodeNum) return;
|
|
196
119
|
|
|
197
|
-
|
|
198
|
-
if (!packet) return;
|
|
199
|
-
const channel = Number(packet.channel);
|
|
200
|
-
if (channel !== this.channel) return;
|
|
201
|
-
|
|
202
|
-
const senderNum = Number(packet.from || 0);
|
|
203
|
-
const localNodeNum = Number(this._lastMyNodeInfo?.myNodeNum || 0);
|
|
204
|
-
if (senderNum > 0 && localNodeNum > 0 && senderNum === localNodeNum) {
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
const senderUser = this._lastNodeUsers.get(senderNum) || null;
|
|
120
|
+
const senderUser = conn.nodeUsers.get(msg.from) || null;
|
|
208
121
|
const senderName = senderUser?.longName || senderUser?.shortName || null;
|
|
209
122
|
const senderUsername = normalizeNodeId(senderUser?.id || '');
|
|
210
|
-
const chatId = `channel:${channel}`;
|
|
123
|
+
const chatId = `channel:${msg.channel}`;
|
|
211
124
|
|
|
212
125
|
const access = this._checkInboundAccess({
|
|
213
126
|
platform: this.name,
|
|
214
|
-
senderId: String(
|
|
127
|
+
senderId: String(msg.from || ''),
|
|
215
128
|
chatId,
|
|
216
129
|
isDirect: false,
|
|
217
130
|
isShared: true,
|
|
@@ -233,35 +146,46 @@ class MeshtasticPlatform extends BasePlatform {
|
|
|
233
146
|
|
|
234
147
|
this.emit('message', {
|
|
235
148
|
chatId,
|
|
236
|
-
sender: String(
|
|
149
|
+
sender: String(msg.from || ''),
|
|
237
150
|
senderName,
|
|
238
151
|
senderUsername: senderUsername || null,
|
|
239
152
|
senderTag: senderUsername || null,
|
|
240
|
-
content:
|
|
153
|
+
content: msg.data,
|
|
241
154
|
mediaType: null,
|
|
242
155
|
isGroup: true,
|
|
243
|
-
messageId: String(
|
|
244
|
-
timestamp: toIsoTimestamp(
|
|
156
|
+
messageId: String(msg.id || `${Date.now()}`),
|
|
157
|
+
timestamp: toIsoTimestamp(msg.rxTime),
|
|
245
158
|
metadata: {
|
|
246
|
-
channel,
|
|
159
|
+
channel: msg.channel,
|
|
247
160
|
host: this.host,
|
|
248
161
|
meshNodeId: senderUsername || null,
|
|
249
|
-
meshDestination:
|
|
162
|
+
meshDestination: msg.type || 'broadcast',
|
|
250
163
|
},
|
|
251
164
|
rawMessage: {
|
|
252
|
-
id:
|
|
253
|
-
from:
|
|
254
|
-
to:
|
|
255
|
-
type:
|
|
256
|
-
channel:
|
|
165
|
+
id: msg.id,
|
|
166
|
+
from: msg.from,
|
|
167
|
+
to: msg.to,
|
|
168
|
+
type: msg.type,
|
|
169
|
+
channel: msg.channel,
|
|
257
170
|
},
|
|
258
171
|
});
|
|
259
172
|
});
|
|
173
|
+
|
|
174
|
+
conn.on('disconnected', (info) => {
|
|
175
|
+
if (this._disconnecting) return;
|
|
176
|
+
this.status = 'disconnected';
|
|
177
|
+
this.emit('disconnected', { reason: info?.reason || 'device_disconnected' });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
this.status = 'connected';
|
|
181
|
+
this.authInfo = this._buildAuthInfo(conn);
|
|
182
|
+
this.emit('connected');
|
|
183
|
+
return { status: this.status };
|
|
260
184
|
}
|
|
261
185
|
|
|
262
|
-
_buildAuthInfo() {
|
|
263
|
-
const
|
|
264
|
-
const user =
|
|
186
|
+
_buildAuthInfo(conn) {
|
|
187
|
+
const nodeNum = conn?.myNodeNum || 0;
|
|
188
|
+
const user = conn?.nodeUsers.get(nodeNum) || {};
|
|
265
189
|
return {
|
|
266
190
|
label: user.longName || user.shortName || normalizeNodeId(user.id) || this.host || 'Meshtastic',
|
|
267
191
|
nodeId: normalizeNodeId(user.id),
|
|
@@ -271,7 +195,7 @@ class MeshtasticPlatform extends BasePlatform {
|
|
|
271
195
|
}
|
|
272
196
|
|
|
273
197
|
async sendMessage(to, content) {
|
|
274
|
-
if (this.status !== 'connected' || !this.
|
|
198
|
+
if (this.status !== 'connected' || !this._transport) {
|
|
275
199
|
throw new Error('Meshtastic is not connected');
|
|
276
200
|
}
|
|
277
201
|
|
|
@@ -280,11 +204,11 @@ class MeshtasticPlatform extends BasePlatform {
|
|
|
280
204
|
throw new Error(`Meshtastic is configured for channel ${this.channel}`);
|
|
281
205
|
}
|
|
282
206
|
|
|
283
|
-
await this.
|
|
207
|
+
await this._transport.connection.sendText(
|
|
284
208
|
String(content || ''),
|
|
285
|
-
'broadcast',
|
|
286
|
-
true,
|
|
287
209
|
this.channel,
|
|
210
|
+
BROADCAST_NUM,
|
|
211
|
+
true,
|
|
288
212
|
);
|
|
289
213
|
return { success: true };
|
|
290
214
|
}
|
|
@@ -293,15 +217,10 @@ class MeshtasticPlatform extends BasePlatform {
|
|
|
293
217
|
this._disconnecting = true;
|
|
294
218
|
this.status = 'disconnected';
|
|
295
219
|
|
|
296
|
-
const device = this._device;
|
|
297
220
|
const transport = this._transport;
|
|
298
|
-
this._device = null;
|
|
299
221
|
this._transport = null;
|
|
300
|
-
this._configured = false;
|
|
301
222
|
|
|
302
|
-
if (
|
|
303
|
-
await device.disconnect().catch(() => {});
|
|
304
|
-
} else if (transport && typeof transport.disconnect === 'function') {
|
|
223
|
+
if (transport) {
|
|
305
224
|
await transport.disconnect().catch(() => {});
|
|
306
225
|
}
|
|
307
226
|
}
|
|
@@ -311,7 +230,7 @@ class MeshtasticPlatform extends BasePlatform {
|
|
|
311
230
|
}
|
|
312
231
|
|
|
313
232
|
getAuthInfo() {
|
|
314
|
-
return this.authInfo || this._buildAuthInfo();
|
|
233
|
+
return this.authInfo || this._buildAuthInfo(this._transport?.connection);
|
|
315
234
|
}
|
|
316
235
|
}
|
|
317
236
|
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Socket } = require('node:net');
|
|
4
|
+
const { EventEmitter } = require('node:events');
|
|
5
|
+
|
|
6
|
+
// Meshtastic TCP wire framing constants (from public protocol docs)
|
|
7
|
+
const FRAME_START_1 = 0x94;
|
|
8
|
+
const FRAME_START_2 = 0xC3;
|
|
9
|
+
|
|
10
|
+
const BROADCAST_NUM = 0xFFFFFFFF;
|
|
11
|
+
|
|
12
|
+
// PortNum values from public protocol specification
|
|
13
|
+
const PortNum = Object.freeze({
|
|
14
|
+
UNKNOWN_APP: 0,
|
|
15
|
+
TEXT_MESSAGE_APP: 1,
|
|
16
|
+
POSITION_APP: 3,
|
|
17
|
+
NODEINFO_APP: 4,
|
|
18
|
+
ROUTING_APP: 5,
|
|
19
|
+
ADMIN_APP: 6,
|
|
20
|
+
TELEMETRY_APP: 67,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// --------------------------------------------------------------------------
|
|
24
|
+
// Minimal protobuf wire-format encoder/decoder (Google public standard)
|
|
25
|
+
// Implements only the subset needed: varint, length-delimited, fixed32
|
|
26
|
+
// --------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
const WIRE_VARINT = 0;
|
|
29
|
+
const WIRE_FIXED64 = 1;
|
|
30
|
+
const WIRE_LENGTH_DELIMITED = 2;
|
|
31
|
+
const WIRE_FIXED32 = 5;
|
|
32
|
+
|
|
33
|
+
function encodeVarint(value) {
|
|
34
|
+
const bytes = [];
|
|
35
|
+
let v = value >>> 0;
|
|
36
|
+
while (v > 0x7F) {
|
|
37
|
+
bytes.push((v & 0x7F) | 0x80);
|
|
38
|
+
v >>>= 7;
|
|
39
|
+
}
|
|
40
|
+
bytes.push(v & 0x7F);
|
|
41
|
+
return bytes;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function encodeTag(fieldNumber, wireType) {
|
|
45
|
+
return encodeVarint((fieldNumber << 3) | wireType);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function encodeVarintField(fieldNumber, value) {
|
|
49
|
+
if (value === 0 || value == null) return [];
|
|
50
|
+
return [...encodeTag(fieldNumber, WIRE_VARINT), ...encodeVarint(value)];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function encodeBoolField(fieldNumber, value) {
|
|
54
|
+
if (!value) return [];
|
|
55
|
+
return [...encodeTag(fieldNumber, WIRE_VARINT), 1];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function encodeFixed32Field(fieldNumber, value) {
|
|
59
|
+
if (value === 0 || value == null) return [];
|
|
60
|
+
const buf = Buffer.alloc(4);
|
|
61
|
+
buf.writeUInt32LE(value >>> 0);
|
|
62
|
+
return [...encodeTag(fieldNumber, WIRE_FIXED32), ...buf];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function encodeBytesField(fieldNumber, bytes) {
|
|
66
|
+
if (!bytes || bytes.length === 0) return [];
|
|
67
|
+
return [
|
|
68
|
+
...encodeTag(fieldNumber, WIRE_LENGTH_DELIMITED),
|
|
69
|
+
...encodeVarint(bytes.length),
|
|
70
|
+
...bytes,
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function encodeStringField(fieldNumber, str) {
|
|
75
|
+
if (!str) return [];
|
|
76
|
+
return encodeBytesField(fieldNumber, Buffer.from(str, 'utf8'));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function encodeMessageField(fieldNumber, messageBytes) {
|
|
80
|
+
return encodeBytesField(fieldNumber, messageBytes);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function decodeVarint(buf, offset) {
|
|
84
|
+
let result = 0;
|
|
85
|
+
let shift = 0;
|
|
86
|
+
let pos = offset;
|
|
87
|
+
while (pos < buf.length) {
|
|
88
|
+
const b = buf[pos++];
|
|
89
|
+
result |= (b & 0x7F) << shift;
|
|
90
|
+
if ((b & 0x80) === 0) return { value: result >>> 0, offset: pos };
|
|
91
|
+
shift += 7;
|
|
92
|
+
if (shift > 35) throw new Error('Varint too long');
|
|
93
|
+
}
|
|
94
|
+
throw new Error('Unexpected end of varint');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function decodeFields(buf) {
|
|
98
|
+
const fields = [];
|
|
99
|
+
let offset = 0;
|
|
100
|
+
while (offset < buf.length) {
|
|
101
|
+
const tag = decodeVarint(buf, offset);
|
|
102
|
+
offset = tag.offset;
|
|
103
|
+
const fieldNumber = tag.value >>> 3;
|
|
104
|
+
const wireType = tag.value & 0x07;
|
|
105
|
+
|
|
106
|
+
switch (wireType) {
|
|
107
|
+
case WIRE_VARINT: {
|
|
108
|
+
const val = decodeVarint(buf, offset);
|
|
109
|
+
offset = val.offset;
|
|
110
|
+
fields.push({ field: fieldNumber, wire: wireType, value: val.value });
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case WIRE_FIXED64: {
|
|
114
|
+
offset += 8;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case WIRE_LENGTH_DELIMITED: {
|
|
118
|
+
const len = decodeVarint(buf, offset);
|
|
119
|
+
offset = len.offset;
|
|
120
|
+
const data = buf.subarray(offset, offset + len.value);
|
|
121
|
+
offset += len.value;
|
|
122
|
+
fields.push({ field: fieldNumber, wire: wireType, value: data });
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case WIRE_FIXED32: {
|
|
126
|
+
const val32 = buf.readUInt32LE(offset);
|
|
127
|
+
offset += 4;
|
|
128
|
+
fields.push({ field: fieldNumber, wire: wireType, value: val32 });
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
default:
|
|
132
|
+
throw new Error(`Unsupported wire type ${wireType}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return fields;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getField(fields, fieldNumber) {
|
|
139
|
+
return fields.find((f) => f.field === fieldNumber) || null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --------------------------------------------------------------------------
|
|
143
|
+
// Protocol message builders and parsers
|
|
144
|
+
// Field numbers from the public Meshtastic protobuf specification
|
|
145
|
+
// --------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
function encodeData(payload, portnum, opts = {}) {
|
|
148
|
+
return new Uint8Array([
|
|
149
|
+
...encodeVarintField(1, portnum),
|
|
150
|
+
...encodeBytesField(2, payload),
|
|
151
|
+
...encodeBoolField(3, opts.wantResponse),
|
|
152
|
+
...encodeFixed32Field(6, opts.requestId),
|
|
153
|
+
...encodeFixed32Field(7, opts.replyId),
|
|
154
|
+
...encodeFixed32Field(8, opts.emoji),
|
|
155
|
+
]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function encodeMeshPacket(from, to, channel, id, decoded, opts = {}) {
|
|
159
|
+
return new Uint8Array([
|
|
160
|
+
...encodeFixed32Field(1, from),
|
|
161
|
+
...encodeFixed32Field(2, to),
|
|
162
|
+
...encodeVarintField(3, channel),
|
|
163
|
+
...encodeMessageField(4, decoded),
|
|
164
|
+
...encodeFixed32Field(6, id),
|
|
165
|
+
...encodeBoolField(10, opts.wantAck),
|
|
166
|
+
]);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function encodeToRadioPacket(meshPacketBytes) {
|
|
170
|
+
return new Uint8Array(encodeMessageField(1, meshPacketBytes));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function encodeToRadioWantConfig(configId) {
|
|
174
|
+
return new Uint8Array(encodeVarintField(3, configId));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function decodeUser(buf) {
|
|
178
|
+
const fields = decodeFields(buf);
|
|
179
|
+
return {
|
|
180
|
+
id: getField(fields, 1)?.value?.toString('utf8') || '',
|
|
181
|
+
longName: getField(fields, 2)?.value?.toString('utf8') || '',
|
|
182
|
+
shortName: getField(fields, 3)?.value?.toString('utf8') || '',
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function decodeData(buf) {
|
|
187
|
+
const fields = decodeFields(buf);
|
|
188
|
+
return {
|
|
189
|
+
portnum: getField(fields, 1)?.value || 0,
|
|
190
|
+
payload: getField(fields, 2)?.value || Buffer.alloc(0),
|
|
191
|
+
wantResponse: !!(getField(fields, 3)?.value),
|
|
192
|
+
source: getField(fields, 5)?.value || 0,
|
|
193
|
+
dest: getField(fields, 4)?.value || 0,
|
|
194
|
+
requestId: getField(fields, 6)?.value || 0,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function decodeMeshPacket(buf) {
|
|
199
|
+
const fields = decodeFields(buf);
|
|
200
|
+
const decodedField = getField(fields, 4);
|
|
201
|
+
const encryptedField = getField(fields, 5);
|
|
202
|
+
return {
|
|
203
|
+
from: getField(fields, 1)?.value || 0,
|
|
204
|
+
to: getField(fields, 2)?.value || 0,
|
|
205
|
+
channel: getField(fields, 3)?.value || 0,
|
|
206
|
+
decoded: decodedField ? decodeData(decodedField.value) : null,
|
|
207
|
+
encrypted: encryptedField ? encryptedField.value : null,
|
|
208
|
+
id: getField(fields, 6)?.value || 0,
|
|
209
|
+
rxTime: getField(fields, 7)?.value || 0,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function decodeNodeInfo(buf) {
|
|
214
|
+
const fields = decodeFields(buf);
|
|
215
|
+
const userField = getField(fields, 2);
|
|
216
|
+
return {
|
|
217
|
+
num: getField(fields, 1)?.value || 0,
|
|
218
|
+
user: userField ? decodeUser(userField.value) : null,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function decodeMyNodeInfo(buf) {
|
|
223
|
+
const fields = decodeFields(buf);
|
|
224
|
+
return {
|
|
225
|
+
myNodeNum: getField(fields, 1)?.value || 0,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function decodeFromRadio(buf) {
|
|
230
|
+
const fields = decodeFields(buf);
|
|
231
|
+
const id = getField(fields, 1)?.value || 0;
|
|
232
|
+
|
|
233
|
+
const packetField = getField(fields, 2);
|
|
234
|
+
if (packetField) return { id, type: 'packet', packet: decodeMeshPacket(packetField.value) };
|
|
235
|
+
|
|
236
|
+
const myInfoField = getField(fields, 3);
|
|
237
|
+
if (myInfoField) return { id, type: 'myInfo', myInfo: decodeMyNodeInfo(myInfoField.value) };
|
|
238
|
+
|
|
239
|
+
const nodeInfoField = getField(fields, 4);
|
|
240
|
+
if (nodeInfoField) return { id, type: 'nodeInfo', nodeInfo: decodeNodeInfo(nodeInfoField.value) };
|
|
241
|
+
|
|
242
|
+
const configCompleteField = getField(fields, 7);
|
|
243
|
+
if (configCompleteField) return { id, type: 'configComplete', configId: configCompleteField.value };
|
|
244
|
+
|
|
245
|
+
const rebootedField = getField(fields, 8);
|
|
246
|
+
if (rebootedField) return { id, type: 'rebooted' };
|
|
247
|
+
|
|
248
|
+
return { id, type: 'unknown' };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// --------------------------------------------------------------------------
|
|
252
|
+
// TCP wire framing: [0x94, 0xC3, len_msb, len_lsb, ...protobuf_payload]
|
|
253
|
+
// --------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
function framePacket(protobufBytes) {
|
|
256
|
+
const len = protobufBytes.length;
|
|
257
|
+
const frame = Buffer.alloc(4 + len);
|
|
258
|
+
frame[0] = FRAME_START_1;
|
|
259
|
+
frame[1] = FRAME_START_2;
|
|
260
|
+
frame[2] = (len >> 8) & 0xFF;
|
|
261
|
+
frame[3] = len & 0xFF;
|
|
262
|
+
frame.set(protobufBytes, 4);
|
|
263
|
+
return frame;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function createFrameParser(onPacket) {
|
|
267
|
+
let buffer = Buffer.alloc(0);
|
|
268
|
+
return (chunk) => {
|
|
269
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
270
|
+
while (buffer.length >= 4) {
|
|
271
|
+
const idx = buffer.indexOf(FRAME_START_1);
|
|
272
|
+
if (idx === -1) { buffer = Buffer.alloc(0); return; }
|
|
273
|
+
if (idx > 0) { buffer = buffer.subarray(idx); }
|
|
274
|
+
if (buffer.length < 2) return;
|
|
275
|
+
if (buffer[1] !== FRAME_START_2) { buffer = buffer.subarray(1); continue; }
|
|
276
|
+
if (buffer.length < 4) return;
|
|
277
|
+
const payloadLen = (buffer[2] << 8) | buffer[3];
|
|
278
|
+
if (buffer.length < 4 + payloadLen) return;
|
|
279
|
+
const payload = buffer.subarray(4, 4 + payloadLen);
|
|
280
|
+
buffer = buffer.subarray(4 + payloadLen);
|
|
281
|
+
onPacket(payload);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// --------------------------------------------------------------------------
|
|
287
|
+
// MeshtasticConnection — TCP connection + protocol state machine
|
|
288
|
+
// --------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
class MeshtasticConnection extends EventEmitter {
|
|
291
|
+
constructor() {
|
|
292
|
+
super();
|
|
293
|
+
this._socket = null;
|
|
294
|
+
this._configId = (Math.random() * 0x7FFFFFFF) >>> 0;
|
|
295
|
+
this._myNodeNum = 0;
|
|
296
|
+
this._configured = false;
|
|
297
|
+
this._closing = false;
|
|
298
|
+
this._nodeUsers = new Map();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
get myNodeNum() { return this._myNodeNum; }
|
|
302
|
+
get nodeUsers() { return this._nodeUsers; }
|
|
303
|
+
get configured() { return this._configured; }
|
|
304
|
+
|
|
305
|
+
async connect(host, port, timeout = 60000) {
|
|
306
|
+
if (this._socket) throw new Error('Already connected');
|
|
307
|
+
this._closing = false;
|
|
308
|
+
this._configured = false;
|
|
309
|
+
this._myNodeNum = 0;
|
|
310
|
+
this._nodeUsers.clear();
|
|
311
|
+
|
|
312
|
+
return new Promise((resolve, reject) => {
|
|
313
|
+
const socket = new Socket();
|
|
314
|
+
let settled = false;
|
|
315
|
+
|
|
316
|
+
const fail = (err) => {
|
|
317
|
+
if (settled) return;
|
|
318
|
+
settled = true;
|
|
319
|
+
socket.destroy();
|
|
320
|
+
reject(err);
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const timer = setTimeout(() => fail(new Error('Connection timeout')), timeout);
|
|
324
|
+
|
|
325
|
+
socket.once('error', fail);
|
|
326
|
+
socket.once('ready', () => {
|
|
327
|
+
socket.removeListener('error', fail);
|
|
328
|
+
this._socket = socket;
|
|
329
|
+
this._wireSocket(socket);
|
|
330
|
+
this.emit('status', 'connected');
|
|
331
|
+
|
|
332
|
+
const onConfigured = () => {
|
|
333
|
+
if (settled) return;
|
|
334
|
+
settled = true;
|
|
335
|
+
clearTimeout(timer);
|
|
336
|
+
resolve();
|
|
337
|
+
};
|
|
338
|
+
this.once('configured', onConfigured);
|
|
339
|
+
|
|
340
|
+
const toRadio = encodeToRadioWantConfig(this._configId);
|
|
341
|
+
socket.write(framePacket(toRadio));
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
socket.setTimeout(timeout);
|
|
345
|
+
socket.connect(port, host);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
_wireSocket(socket) {
|
|
350
|
+
const parser = createFrameParser((payload) => {
|
|
351
|
+
if (this._closing) return;
|
|
352
|
+
try {
|
|
353
|
+
const msg = decodeFromRadio(payload);
|
|
354
|
+
this._handleFromRadio(msg);
|
|
355
|
+
} catch (err) {
|
|
356
|
+
this.emit('error', err);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
socket.on('data', parser);
|
|
361
|
+
socket.on('error', () => this._onDisconnected('socket-error'));
|
|
362
|
+
socket.on('end', () => this._onDisconnected('socket-end'));
|
|
363
|
+
socket.on('close', () => this._onDisconnected('socket-closed'));
|
|
364
|
+
socket.on('timeout', () => {
|
|
365
|
+
this._onDisconnected('socket-timeout');
|
|
366
|
+
socket.destroy();
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
_onDisconnected(reason) {
|
|
371
|
+
if (this._closing) return;
|
|
372
|
+
this._configured = false;
|
|
373
|
+
this.emit('status', 'disconnected');
|
|
374
|
+
this.emit('disconnected', { reason });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
_handleFromRadio(msg) {
|
|
378
|
+
switch (msg.type) {
|
|
379
|
+
case 'myInfo':
|
|
380
|
+
this._myNodeNum = msg.myInfo.myNodeNum;
|
|
381
|
+
this.emit('myNodeInfo', msg.myInfo);
|
|
382
|
+
break;
|
|
383
|
+
|
|
384
|
+
case 'nodeInfo':
|
|
385
|
+
if (msg.nodeInfo.user && msg.nodeInfo.num) {
|
|
386
|
+
this._nodeUsers.set(msg.nodeInfo.num, msg.nodeInfo.user);
|
|
387
|
+
this.emit('nodeInfo', msg.nodeInfo);
|
|
388
|
+
}
|
|
389
|
+
break;
|
|
390
|
+
|
|
391
|
+
case 'configComplete':
|
|
392
|
+
if (msg.configId === this._configId) {
|
|
393
|
+
this._configured = true;
|
|
394
|
+
this.emit('configured');
|
|
395
|
+
}
|
|
396
|
+
break;
|
|
397
|
+
|
|
398
|
+
case 'rebooted':
|
|
399
|
+
this._configured = false;
|
|
400
|
+
this._socket?.write(framePacket(encodeToRadioWantConfig(this._configId)));
|
|
401
|
+
break;
|
|
402
|
+
|
|
403
|
+
case 'packet': {
|
|
404
|
+
const pkt = msg.packet;
|
|
405
|
+
if (!pkt.decoded) break;
|
|
406
|
+
this._handleDecodedPacket(pkt);
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
_handleDecodedPacket(pkt) {
|
|
413
|
+
const { decoded } = pkt;
|
|
414
|
+
|
|
415
|
+
switch (decoded.portnum) {
|
|
416
|
+
case PortNum.TEXT_MESSAGE_APP:
|
|
417
|
+
this.emit('textMessage', {
|
|
418
|
+
id: pkt.id,
|
|
419
|
+
from: pkt.from,
|
|
420
|
+
to: pkt.to,
|
|
421
|
+
channel: pkt.channel,
|
|
422
|
+
rxTime: pkt.rxTime,
|
|
423
|
+
type: pkt.to === BROADCAST_NUM ? 'broadcast' : 'direct',
|
|
424
|
+
data: decoded.payload.toString('utf8'),
|
|
425
|
+
});
|
|
426
|
+
break;
|
|
427
|
+
|
|
428
|
+
case PortNum.NODEINFO_APP:
|
|
429
|
+
try {
|
|
430
|
+
const user = decodeUser(decoded.payload);
|
|
431
|
+
if (pkt.from) this._nodeUsers.set(pkt.from, user);
|
|
432
|
+
this.emit('nodeInfo', { num: pkt.from, user });
|
|
433
|
+
} catch {}
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async sendText(text, channel = 0, destination = BROADCAST_NUM, wantAck = true) {
|
|
439
|
+
if (!this._socket || !this._configured) throw new Error('Not connected');
|
|
440
|
+
const payload = Buffer.from(text, 'utf8');
|
|
441
|
+
const data = encodeData(payload, PortNum.TEXT_MESSAGE_APP, { wantResponse: false });
|
|
442
|
+
const id = (Math.random() * 0x7FFFFFFF) >>> 0;
|
|
443
|
+
const packet = encodeMeshPacket(this._myNodeNum, destination, channel, id, data, { wantAck });
|
|
444
|
+
const toRadio = encodeToRadioPacket(packet);
|
|
445
|
+
this._socket.write(framePacket(toRadio));
|
|
446
|
+
return id;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async disconnect() {
|
|
450
|
+
this._closing = true;
|
|
451
|
+
this._configured = false;
|
|
452
|
+
const socket = this._socket;
|
|
453
|
+
this._socket = null;
|
|
454
|
+
if (socket) {
|
|
455
|
+
socket.removeAllListeners();
|
|
456
|
+
socket.destroy();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
module.exports = {
|
|
462
|
+
MeshtasticConnection,
|
|
463
|
+
PortNum,
|
|
464
|
+
BROADCAST_NUM,
|
|
465
|
+
encodeVarint,
|
|
466
|
+
decodeVarint,
|
|
467
|
+
decodeFields,
|
|
468
|
+
decodeFromRadio,
|
|
469
|
+
decodeMeshPacket,
|
|
470
|
+
decodeUser,
|
|
471
|
+
framePacket,
|
|
472
|
+
createFrameParser,
|
|
473
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { MeshtasticConnection } = require('./meshtastic_protocol');
|
|
4
|
+
|
|
5
|
+
class MeshtasticTcpTransport {
|
|
6
|
+
constructor(connection) {
|
|
7
|
+
this._connection = connection;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
static async create(hostname, port = 4403, timeout = 60000) {
|
|
11
|
+
const connection = new MeshtasticConnection();
|
|
12
|
+
await connection.connect(hostname, port, timeout);
|
|
13
|
+
return new MeshtasticTcpTransport(connection);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get connection() { return this._connection; }
|
|
17
|
+
|
|
18
|
+
async disconnect() {
|
|
19
|
+
await this._connection.disconnect();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
MeshtasticTcpTransport,
|
|
25
|
+
};
|