vnode 0.1.6__tar.gz → 0.1.7__tar.gz
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.
- {vnode-0.1.6 → vnode-0.1.7}/PKG-INFO +26 -2
- {vnode-0.1.6 → vnode-0.1.7}/README.md +25 -1
- {vnode-0.1.6 → vnode-0.1.7}/pyproject.toml +1 -1
- {vnode-0.1.6 → vnode-0.1.7}/vnode/vnode/runtime.py +276 -14
- {vnode-0.1.6 → vnode-0.1.7}/LICENSE +0 -0
- {vnode-0.1.6 → vnode-0.1.7}/vnode/vnode/__init__.py +0 -0
- {vnode-0.1.6 → vnode-0.1.7}/vnode/vnode/__main__.py +0 -0
- {vnode-0.1.6 → vnode-0.1.7}/vnode/vnode/cli.py +0 -0
- {vnode-0.1.6 → vnode-0.1.7}/vnode/vnode/config.py +0 -0
- {vnode-0.1.6 → vnode-0.1.7}/vnode/vnode/crypto.py +0 -0
- {vnode-0.1.6 → vnode-0.1.7}/vnode/vnode/example-node.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vnode
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: Virtual Meshtastic node using mudp transport and meshdb storage.
|
|
5
5
|
License-Expression: GPL-3.0-only
|
|
6
6
|
License-File: LICENSE
|
|
@@ -30,6 +30,20 @@ Virtual Meshtastic node runtime built around:
|
|
|
30
30
|
- `meshdb` for packet and node persistence
|
|
31
31
|
- Meshtastic-style PKI DM encryption using X25519 + SHA-256 + AES-CCM
|
|
32
32
|
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
Install from PyPI:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install vnode
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
If you are working from this repository instead:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
.venv/bin/pip install -e .
|
|
45
|
+
```
|
|
46
|
+
|
|
33
47
|
## Config
|
|
34
48
|
|
|
35
49
|
Edit [node.json](node.json).
|
|
@@ -58,7 +72,6 @@ broadcasts.
|
|
|
58
72
|
## Run
|
|
59
73
|
|
|
60
74
|
```bash
|
|
61
|
-
.venv/bin/pip install -e .
|
|
62
75
|
.venv/bin/python -m vnode --vnode-file node.json run
|
|
63
76
|
```
|
|
64
77
|
|
|
@@ -84,6 +97,15 @@ node = VirtualNode("node.json")
|
|
|
84
97
|
packet_id = node.send_text("!1234abcd", "hello")
|
|
85
98
|
```
|
|
86
99
|
|
|
100
|
+
Supported `VirtualNode` calls:
|
|
101
|
+
|
|
102
|
+
- Lifecycle: `start()`, `stop()`, `close()`, `run_forever()`
|
|
103
|
+
- Subscription: `receive(callback)`, `unreceive(callback)`
|
|
104
|
+
- Node identity: `getLongName()`, `getShortName()`, `getPublicKey()`, `getMyUser()`, `getMyNodeInfo()`
|
|
105
|
+
- Native vnode sends: `send_text()`, `send_reply()`, `send_nodeinfo()`, `send_position()`
|
|
106
|
+
- MeshInterface-compatible sends: `sendText()`, `sendAlert()`, `sendData()`, `sendPosition()`
|
|
107
|
+
- Packet helpers: `is_direct_message_for_me()`, `is_text_message()`, `get_text_message()`, `reply_to_packet()`
|
|
108
|
+
|
|
87
109
|
## Examples
|
|
88
110
|
|
|
89
111
|
Use these as small runnable references for common tasks:
|
|
@@ -91,6 +113,7 @@ Use these as small runnable references for common tasks:
|
|
|
91
113
|
- `examples/autoresponder.py`: DM-only reply bot for inbound direct text messages
|
|
92
114
|
- `examples/basic_subscriptions.py`: minimal `node.receive()` subscription example
|
|
93
115
|
- `examples/listen_packets.py`: packet logger for multicast traffic and decoded text
|
|
116
|
+
- `examples/mesh_interface_compat.py`: examples of MeshInterface-style compatibility calls on `VirtualNode`
|
|
94
117
|
- `examples/serial_or_vnode.py`: try a serial Meshtastic node first, then fall back to `VirtualNode`
|
|
95
118
|
- `examples/send_dm.py`: minimal one-shot direct-message sender
|
|
96
119
|
- `examples/library_embed.py`: minimal application-style embedding example using `VirtualNode` directly
|
|
@@ -100,6 +123,7 @@ Use these as small runnable references for common tasks:
|
|
|
100
123
|
.venv/bin/python examples/autoresponder.py
|
|
101
124
|
.venv/bin/python examples/basic_subscriptions.py
|
|
102
125
|
.venv/bin/python examples/listen_packets.py
|
|
126
|
+
.venv/bin/python examples/mesh_interface_compat.py
|
|
103
127
|
.venv/bin/python examples/serial_or_vnode.py
|
|
104
128
|
.venv/bin/python examples/send_dm.py --to '!1234abcd' --message 'hello'
|
|
105
129
|
.venv/bin/python examples/library_embed.py
|
|
@@ -6,6 +6,20 @@ Virtual Meshtastic node runtime built around:
|
|
|
6
6
|
- `meshdb` for packet and node persistence
|
|
7
7
|
- Meshtastic-style PKI DM encryption using X25519 + SHA-256 + AES-CCM
|
|
8
8
|
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
Install from PyPI:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install vnode
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
If you are working from this repository instead:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
.venv/bin/pip install -e .
|
|
21
|
+
```
|
|
22
|
+
|
|
9
23
|
## Config
|
|
10
24
|
|
|
11
25
|
Edit [node.json](node.json).
|
|
@@ -34,7 +48,6 @@ broadcasts.
|
|
|
34
48
|
## Run
|
|
35
49
|
|
|
36
50
|
```bash
|
|
37
|
-
.venv/bin/pip install -e .
|
|
38
51
|
.venv/bin/python -m vnode --vnode-file node.json run
|
|
39
52
|
```
|
|
40
53
|
|
|
@@ -60,6 +73,15 @@ node = VirtualNode("node.json")
|
|
|
60
73
|
packet_id = node.send_text("!1234abcd", "hello")
|
|
61
74
|
```
|
|
62
75
|
|
|
76
|
+
Supported `VirtualNode` calls:
|
|
77
|
+
|
|
78
|
+
- Lifecycle: `start()`, `stop()`, `close()`, `run_forever()`
|
|
79
|
+
- Subscription: `receive(callback)`, `unreceive(callback)`
|
|
80
|
+
- Node identity: `getLongName()`, `getShortName()`, `getPublicKey()`, `getMyUser()`, `getMyNodeInfo()`
|
|
81
|
+
- Native vnode sends: `send_text()`, `send_reply()`, `send_nodeinfo()`, `send_position()`
|
|
82
|
+
- MeshInterface-compatible sends: `sendText()`, `sendAlert()`, `sendData()`, `sendPosition()`
|
|
83
|
+
- Packet helpers: `is_direct_message_for_me()`, `is_text_message()`, `get_text_message()`, `reply_to_packet()`
|
|
84
|
+
|
|
63
85
|
## Examples
|
|
64
86
|
|
|
65
87
|
Use these as small runnable references for common tasks:
|
|
@@ -67,6 +89,7 @@ Use these as small runnable references for common tasks:
|
|
|
67
89
|
- `examples/autoresponder.py`: DM-only reply bot for inbound direct text messages
|
|
68
90
|
- `examples/basic_subscriptions.py`: minimal `node.receive()` subscription example
|
|
69
91
|
- `examples/listen_packets.py`: packet logger for multicast traffic and decoded text
|
|
92
|
+
- `examples/mesh_interface_compat.py`: examples of MeshInterface-style compatibility calls on `VirtualNode`
|
|
70
93
|
- `examples/serial_or_vnode.py`: try a serial Meshtastic node first, then fall back to `VirtualNode`
|
|
71
94
|
- `examples/send_dm.py`: minimal one-shot direct-message sender
|
|
72
95
|
- `examples/library_embed.py`: minimal application-style embedding example using `VirtualNode` directly
|
|
@@ -76,6 +99,7 @@ Use these as small runnable references for common tasks:
|
|
|
76
99
|
.venv/bin/python examples/autoresponder.py
|
|
77
100
|
.venv/bin/python examples/basic_subscriptions.py
|
|
78
101
|
.venv/bin/python examples/listen_packets.py
|
|
102
|
+
.venv/bin/python examples/mesh_interface_compat.py
|
|
79
103
|
.venv/bin/python examples/serial_or_vnode.py
|
|
80
104
|
.venv/bin/python examples/send_dm.py --to '!1234abcd' --message 'hello'
|
|
81
105
|
.venv/bin/python examples/library_embed.py
|
|
@@ -7,9 +7,10 @@ import time
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Any, Dict, Optional, Union
|
|
9
9
|
|
|
10
|
+
from google.protobuf.json_format import MessageToDict
|
|
10
11
|
import meshdb
|
|
11
12
|
from google.protobuf.message import DecodeError
|
|
12
|
-
from meshtastic import BROADCAST_NUM, config_pb2, mesh_pb2, portnums_pb2
|
|
13
|
+
from meshtastic import BROADCAST_ADDR, BROADCAST_NUM, LOCAL_ADDR, config_pb2, mesh_pb2, portnums_pb2, protocols
|
|
13
14
|
from mudp import UDPPacketStream
|
|
14
15
|
from mudp.encryption import encrypt_packet as mudp_encrypt_packet
|
|
15
16
|
from mudp.reliability import is_ack, is_nak, publish_ack, register_pending_ack, send_ack
|
|
@@ -82,6 +83,7 @@ class VirtualNode:
|
|
|
82
83
|
self._last_nodeinfo_sent_monotonic = 0.0
|
|
83
84
|
self._last_position_reply_monotonic = 0.0
|
|
84
85
|
self._last_nodeinfo_seen: Dict[int, int] = {}
|
|
86
|
+
self._response_handlers: Dict[int, Dict[str, Any]] = {}
|
|
85
87
|
|
|
86
88
|
self._ensure_security_keys()
|
|
87
89
|
self._write_public_key_file()
|
|
@@ -142,6 +144,9 @@ class VirtualNode:
|
|
|
142
144
|
return
|
|
143
145
|
pub.subscribe(self._handle_raw_packet, "mesh.rx.packet")
|
|
144
146
|
pub.subscribe(self._handle_unique_packet, self.PACKET_TOPIC)
|
|
147
|
+
pub.subscribe(self._handle_compat_response_packet, self.RECEIVE_TOPIC)
|
|
148
|
+
pub.subscribe(self._handle_compat_ack, "mesh.rx.ack")
|
|
149
|
+
pub.subscribe(self._handle_compat_nak, "mesh.rx.nak")
|
|
145
150
|
self.stream = UDPPacketStream(
|
|
146
151
|
self.config.udp.mcast_group,
|
|
147
152
|
int(self.config.udp.mcast_port),
|
|
@@ -171,6 +176,15 @@ class VirtualNode:
|
|
|
171
176
|
pub.unsubscribe(self._handle_unique_packet, self.PACKET_TOPIC)
|
|
172
177
|
except KeyError:
|
|
173
178
|
pass
|
|
179
|
+
for topic, listener in (
|
|
180
|
+
(self.RECEIVE_TOPIC, self._handle_compat_response_packet),
|
|
181
|
+
("mesh.rx.ack", self._handle_compat_ack),
|
|
182
|
+
("mesh.rx.nak", self._handle_compat_nak),
|
|
183
|
+
):
|
|
184
|
+
try:
|
|
185
|
+
pub.unsubscribe(listener, topic)
|
|
186
|
+
except KeyError:
|
|
187
|
+
pass
|
|
174
188
|
if self._broadcast_thread and self._broadcast_thread.is_alive():
|
|
175
189
|
self._broadcast_thread.join(timeout=2.0)
|
|
176
190
|
|
|
@@ -191,6 +205,161 @@ class VirtualNode:
|
|
|
191
205
|
except KeyError:
|
|
192
206
|
pass
|
|
193
207
|
|
|
208
|
+
def close(self) -> None:
|
|
209
|
+
self.stop()
|
|
210
|
+
|
|
211
|
+
def getLongName(self):
|
|
212
|
+
return self.config.long_name
|
|
213
|
+
|
|
214
|
+
def getShortName(self):
|
|
215
|
+
return self.config.short_name
|
|
216
|
+
|
|
217
|
+
def getPublicKey(self):
|
|
218
|
+
return b64_decode(self._public_key_b64) if self._public_key_b64 else None
|
|
219
|
+
|
|
220
|
+
def getMyUser(self):
|
|
221
|
+
user = mesh_pb2.User(
|
|
222
|
+
id=self.config.node_id,
|
|
223
|
+
long_name=self.config.long_name,
|
|
224
|
+
short_name=self.config.short_name,
|
|
225
|
+
hw_model=resolve_hw_model(self.config.hw_model),
|
|
226
|
+
)
|
|
227
|
+
user.role = resolve_role(self.config.role)
|
|
228
|
+
if self._public_key_b64:
|
|
229
|
+
user.public_key = b64_decode(self._public_key_b64)
|
|
230
|
+
return MessageToDict(user, preserving_proto_field_name=False, use_integers_for_enums=False)
|
|
231
|
+
|
|
232
|
+
def getMyNodeInfo(self):
|
|
233
|
+
return {
|
|
234
|
+
"num": self.node_num,
|
|
235
|
+
"user": self.getMyUser(),
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
def sendText(
|
|
239
|
+
self,
|
|
240
|
+
text: str,
|
|
241
|
+
destinationId: Union[int, str] = BROADCAST_ADDR,
|
|
242
|
+
wantAck: bool = False,
|
|
243
|
+
wantResponse: bool = False,
|
|
244
|
+
onResponse=None,
|
|
245
|
+
channelIndex: int = 0,
|
|
246
|
+
portNum: int = portnums_pb2.PortNum.TEXT_MESSAGE_APP,
|
|
247
|
+
replyId: Optional[int] = None,
|
|
248
|
+
hopLimit: Optional[int] = None,
|
|
249
|
+
):
|
|
250
|
+
return self.sendData(
|
|
251
|
+
text.encode("utf-8"),
|
|
252
|
+
destinationId=destinationId,
|
|
253
|
+
portNum=portNum,
|
|
254
|
+
wantAck=wantAck,
|
|
255
|
+
wantResponse=wantResponse,
|
|
256
|
+
onResponse=onResponse,
|
|
257
|
+
channelIndex=channelIndex,
|
|
258
|
+
hopLimit=hopLimit,
|
|
259
|
+
replyId=replyId,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
def sendAlert(
|
|
263
|
+
self,
|
|
264
|
+
text: str,
|
|
265
|
+
destinationId: Union[int, str] = BROADCAST_ADDR,
|
|
266
|
+
onResponse=None,
|
|
267
|
+
channelIndex: int = 0,
|
|
268
|
+
hopLimit: Optional[int] = None,
|
|
269
|
+
):
|
|
270
|
+
return self.sendData(
|
|
271
|
+
text.encode("utf-8"),
|
|
272
|
+
destinationId=destinationId,
|
|
273
|
+
portNum=portnums_pb2.PortNum.ALERT_APP,
|
|
274
|
+
wantAck=False,
|
|
275
|
+
wantResponse=False,
|
|
276
|
+
onResponse=onResponse,
|
|
277
|
+
channelIndex=channelIndex,
|
|
278
|
+
hopLimit=hopLimit,
|
|
279
|
+
priority=mesh_pb2.MeshPacket.Priority.ALERT,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def sendData(
|
|
283
|
+
self,
|
|
284
|
+
data,
|
|
285
|
+
destinationId: Union[int, str] = BROADCAST_ADDR,
|
|
286
|
+
portNum: int = portnums_pb2.PortNum.PRIVATE_APP,
|
|
287
|
+
wantAck: bool = False,
|
|
288
|
+
wantResponse: bool = False,
|
|
289
|
+
onResponse=None,
|
|
290
|
+
onResponseAckPermitted: bool = False,
|
|
291
|
+
channelIndex: int = 0,
|
|
292
|
+
hopLimit: Optional[int] = None,
|
|
293
|
+
pkiEncrypted: Optional[bool] = False,
|
|
294
|
+
publicKey: Optional[bytes] = None,
|
|
295
|
+
priority: int = mesh_pb2.MeshPacket.Priority.RELIABLE,
|
|
296
|
+
replyId: Optional[int] = None,
|
|
297
|
+
):
|
|
298
|
+
if getattr(data, "SerializeToString", None):
|
|
299
|
+
data = data.SerializeToString()
|
|
300
|
+
if not isinstance(data, (bytes, bytearray)):
|
|
301
|
+
raise TypeError("data must be bytes or a protobuf with SerializeToString()")
|
|
302
|
+
if int(channelIndex) != 0:
|
|
303
|
+
raise NotImplementedError("Only channelIndex=0 is currently supported")
|
|
304
|
+
if publicKey is not None:
|
|
305
|
+
raise NotImplementedError("publicKey override is not currently supported")
|
|
306
|
+
if int(portNum) == portnums_pb2.PortNum.UNKNOWN_APP:
|
|
307
|
+
raise ValueError("A non-zero port number must be specified")
|
|
308
|
+
|
|
309
|
+
destination = self._resolve_destination_compat(destinationId)
|
|
310
|
+
force_pki = bool(pkiEncrypted)
|
|
311
|
+
data_message = mesh_pb2.Data()
|
|
312
|
+
data_message.portnum = int(portNum)
|
|
313
|
+
data_message.payload = bytes(data)
|
|
314
|
+
data_message.want_response = bool(wantResponse)
|
|
315
|
+
data_message.source = self.node_num
|
|
316
|
+
data_message.dest = int(destination)
|
|
317
|
+
if replyId is not None:
|
|
318
|
+
data_message.reply_id = int(replyId)
|
|
319
|
+
|
|
320
|
+
packet = self._send_data_packet(
|
|
321
|
+
data_message,
|
|
322
|
+
destination=int(destination),
|
|
323
|
+
force_pki=force_pki,
|
|
324
|
+
hop_limit=hopLimit,
|
|
325
|
+
want_ack=wantAck,
|
|
326
|
+
priority=int(priority),
|
|
327
|
+
)
|
|
328
|
+
if onResponse is not None:
|
|
329
|
+
self._response_handlers[int(packet.id)] = {
|
|
330
|
+
"callback": onResponse,
|
|
331
|
+
"ack_permitted": bool(onResponseAckPermitted) or getattr(onResponse, "__name__", "") == "onAckNak",
|
|
332
|
+
}
|
|
333
|
+
return packet
|
|
334
|
+
|
|
335
|
+
def sendPosition(
|
|
336
|
+
self,
|
|
337
|
+
latitude: float = 0.0,
|
|
338
|
+
longitude: float = 0.0,
|
|
339
|
+
altitude: int = 0,
|
|
340
|
+
destinationId: Union[int, str] = BROADCAST_ADDR,
|
|
341
|
+
wantAck: bool = False,
|
|
342
|
+
wantResponse: bool = False,
|
|
343
|
+
channelIndex: int = 0,
|
|
344
|
+
hopLimit: Optional[int] = None,
|
|
345
|
+
):
|
|
346
|
+
position = mesh_pb2.Position()
|
|
347
|
+
if latitude != 0.0:
|
|
348
|
+
position.latitude_i = int(latitude * 1e7)
|
|
349
|
+
if longitude != 0.0:
|
|
350
|
+
position.longitude_i = int(longitude * 1e7)
|
|
351
|
+
if altitude != 0:
|
|
352
|
+
position.altitude = int(altitude)
|
|
353
|
+
return self.sendData(
|
|
354
|
+
position,
|
|
355
|
+
destinationId=destinationId,
|
|
356
|
+
portNum=portnums_pb2.PortNum.POSITION_APP,
|
|
357
|
+
wantAck=wantAck,
|
|
358
|
+
wantResponse=wantResponse,
|
|
359
|
+
channelIndex=channelIndex,
|
|
360
|
+
hopLimit=hopLimit,
|
|
361
|
+
)
|
|
362
|
+
|
|
194
363
|
def connect_send_socket(self) -> None:
|
|
195
364
|
if getattr(conn, "socket", None) is None:
|
|
196
365
|
conn.setup_multicast(self.config.udp.mcast_group, int(self.config.udp.mcast_port))
|
|
@@ -390,13 +559,38 @@ class VirtualNode:
|
|
|
390
559
|
hop_limit: Optional[int] = None,
|
|
391
560
|
hop_start: Optional[int] = None,
|
|
392
561
|
want_ack: bool = False,
|
|
562
|
+
priority: Optional[int] = None,
|
|
393
563
|
) -> int:
|
|
564
|
+
packet = self._send_data_packet(
|
|
565
|
+
data,
|
|
566
|
+
destination=destination,
|
|
567
|
+
force_pki=force_pki,
|
|
568
|
+
hop_limit=hop_limit,
|
|
569
|
+
hop_start=hop_start,
|
|
570
|
+
want_ack=want_ack,
|
|
571
|
+
priority=priority,
|
|
572
|
+
)
|
|
573
|
+
return packet.id
|
|
574
|
+
|
|
575
|
+
def _send_data_packet(
|
|
576
|
+
self,
|
|
577
|
+
data: mesh_pb2.Data,
|
|
578
|
+
*,
|
|
579
|
+
destination: int,
|
|
580
|
+
force_pki: bool,
|
|
581
|
+
hop_limit: Optional[int] = None,
|
|
582
|
+
hop_start: Optional[int] = None,
|
|
583
|
+
want_ack: bool = False,
|
|
584
|
+
priority: Optional[int] = None,
|
|
585
|
+
) -> mesh_pb2.MeshPacket:
|
|
394
586
|
self.connect_send_socket()
|
|
395
587
|
packet = mesh_pb2.MeshPacket()
|
|
396
588
|
packet.id = self._next_packet_id()
|
|
397
589
|
setattr(packet, "from", self.node_num)
|
|
398
590
|
packet.to = int(destination)
|
|
399
591
|
packet.want_ack = bool(want_ack)
|
|
592
|
+
if priority is not None:
|
|
593
|
+
packet.priority = int(priority)
|
|
400
594
|
resolved_hop_limit = int(self.config.hop_limit if hop_limit is None else hop_limit)
|
|
401
595
|
resolved_hop_start = int(resolved_hop_limit if hop_start is None else hop_start)
|
|
402
596
|
if resolved_hop_start < resolved_hop_limit:
|
|
@@ -429,7 +623,7 @@ class VirtualNode:
|
|
|
429
623
|
register_pending_ack(packet, raw_packet)
|
|
430
624
|
conn.sendto(raw_packet, (conn.host, conn.port))
|
|
431
625
|
self._persist_outbound_packet(packet, data)
|
|
432
|
-
return packet
|
|
626
|
+
return packet
|
|
433
627
|
|
|
434
628
|
def _handle_raw_packet(self, packet: mesh_pb2.MeshPacket, addr: Any = None) -> None:
|
|
435
629
|
del addr
|
|
@@ -594,10 +788,7 @@ class VirtualNode:
|
|
|
594
788
|
)
|
|
595
789
|
|
|
596
790
|
def _publish_receive(self, packet: mesh_pb2.MeshPacket) -> None:
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
packet_dict = meshdb.normalize_packet(packet, "udp")
|
|
600
|
-
packet_dict["raw"] = packet
|
|
791
|
+
packet_dict = self._mesh_interface_packet_dict(packet)
|
|
601
792
|
|
|
602
793
|
topic = self.RECEIVE_TOPIC
|
|
603
794
|
decoded = packet_dict.get("decoded")
|
|
@@ -605,23 +796,94 @@ class VirtualNode:
|
|
|
605
796
|
if isinstance(decoded, dict):
|
|
606
797
|
raw_portnum = decoded.get("portnum")
|
|
607
798
|
if raw_portnum is not None:
|
|
608
|
-
|
|
609
|
-
portnum = int(raw_portnum)
|
|
610
|
-
except (TypeError, ValueError):
|
|
611
|
-
portnum = None
|
|
799
|
+
portnum = raw_portnum
|
|
612
800
|
|
|
613
801
|
if portnum is not None:
|
|
614
|
-
|
|
802
|
+
try:
|
|
803
|
+
portnum_value = portnums_pb2.PortNum.Value(str(portnum))
|
|
804
|
+
except ValueError:
|
|
805
|
+
portnum_value = None
|
|
806
|
+
handler = protocols.get(portnum_value) if portnum_value is not None else None
|
|
615
807
|
if handler is not None:
|
|
616
808
|
topic = f"{self.RECEIVE_TOPIC}.{handler.name}"
|
|
617
809
|
else:
|
|
618
|
-
|
|
619
|
-
topic = f"{self.RECEIVE_TOPIC}.data.{
|
|
620
|
-
|
|
810
|
+
if isinstance(portnum, str):
|
|
811
|
+
topic = f"{self.RECEIVE_TOPIC}.data.{portnum}"
|
|
812
|
+
else:
|
|
621
813
|
topic = f"{self.RECEIVE_TOPIC}.data.{portnum}"
|
|
622
814
|
|
|
623
815
|
pub.sendMessage(topic, packet=packet_dict, interface=self)
|
|
624
816
|
|
|
817
|
+
def _mesh_interface_packet_dict(self, packet: mesh_pb2.MeshPacket) -> Dict[str, Any]:
|
|
818
|
+
packet_dict = meshdb.normalize_packet(packet, "udp")
|
|
819
|
+
packet_dict["raw"] = packet
|
|
820
|
+
|
|
821
|
+
if packet.HasField("decoded"):
|
|
822
|
+
decoded = packet_dict.setdefault("decoded", {})
|
|
823
|
+
decoded["payload"] = bytes(packet.decoded.payload)
|
|
824
|
+
try:
|
|
825
|
+
decoded["portnum"] = portnums_pb2.PortNum.Name(int(packet.decoded.portnum))
|
|
826
|
+
except ValueError:
|
|
827
|
+
decoded["portnum"] = int(packet.decoded.portnum)
|
|
828
|
+
if getattr(packet.decoded, "want_response", False):
|
|
829
|
+
decoded["wantResponse"] = True
|
|
830
|
+
if getattr(packet.decoded, "request_id", 0):
|
|
831
|
+
decoded["requestId"] = int(packet.decoded.request_id)
|
|
832
|
+
if getattr(packet.decoded, "reply_id", 0):
|
|
833
|
+
decoded["replyId"] = int(packet.decoded.reply_id)
|
|
834
|
+
if getattr(packet.decoded, "emoji", 0):
|
|
835
|
+
decoded["emoji"] = int(packet.decoded.emoji)
|
|
836
|
+
|
|
837
|
+
handler = protocols.get(int(packet.decoded.portnum))
|
|
838
|
+
if handler is not None and getattr(handler, "protobufFactory", None) is not None:
|
|
839
|
+
try:
|
|
840
|
+
pb = handler.protobufFactory()
|
|
841
|
+
pb.ParseFromString(bytes(packet.decoded.payload))
|
|
842
|
+
decoded[handler.name] = MessageToDict(
|
|
843
|
+
pb,
|
|
844
|
+
preserving_proto_field_name=False,
|
|
845
|
+
use_integers_for_enums=False,
|
|
846
|
+
)
|
|
847
|
+
except Exception:
|
|
848
|
+
pass
|
|
849
|
+
return packet_dict
|
|
850
|
+
|
|
851
|
+
def _resolve_destination_compat(self, destination: Union[int, str]) -> int:
|
|
852
|
+
if destination == BROADCAST_ADDR:
|
|
853
|
+
return BROADCAST_NUM
|
|
854
|
+
if destination == LOCAL_ADDR:
|
|
855
|
+
return self.node_num
|
|
856
|
+
return self._resolve_destination(destination)
|
|
857
|
+
|
|
858
|
+
def _handle_compat_response_packet(self, packet, interface=None) -> None:
|
|
859
|
+
if interface is not self:
|
|
860
|
+
return
|
|
861
|
+
raw_packet = packet.get("raw") if isinstance(packet, dict) else None
|
|
862
|
+
if not isinstance(raw_packet, mesh_pb2.MeshPacket) or not raw_packet.HasField("decoded"):
|
|
863
|
+
return
|
|
864
|
+
request_id = int(getattr(raw_packet.decoded, "request_id", 0))
|
|
865
|
+
if request_id <= 0 or is_ack(raw_packet) or is_nak(raw_packet):
|
|
866
|
+
return
|
|
867
|
+
handler = self._response_handlers.pop(request_id, None)
|
|
868
|
+
if handler is not None:
|
|
869
|
+
handler["callback"](packet)
|
|
870
|
+
|
|
871
|
+
def _handle_compat_ack(self, packet, routing=None, addr=None, pending=None) -> None:
|
|
872
|
+
del routing, addr, pending
|
|
873
|
+
request_id = int(getattr(packet.decoded, "request_id", 0)) if packet.HasField("decoded") else 0
|
|
874
|
+
handler = self._response_handlers.get(request_id)
|
|
875
|
+
if handler is None or not handler["ack_permitted"]:
|
|
876
|
+
return
|
|
877
|
+
self._response_handlers.pop(request_id, None)
|
|
878
|
+
handler["callback"](self._mesh_interface_packet_dict(packet))
|
|
879
|
+
|
|
880
|
+
def _handle_compat_nak(self, packet, routing=None, addr=None, pending=None) -> None:
|
|
881
|
+
del routing, addr, pending
|
|
882
|
+
request_id = int(getattr(packet.decoded, "request_id", 0)) if packet.HasField("decoded") else 0
|
|
883
|
+
handler = self._response_handlers.pop(request_id, None)
|
|
884
|
+
if handler is not None:
|
|
885
|
+
handler["callback"](self._mesh_interface_packet_dict(packet))
|
|
886
|
+
|
|
625
887
|
def _broadcast_loop(self) -> None:
|
|
626
888
|
nodeinfo_interval = int(self.config.broadcasts.nodeinfo_interval_seconds)
|
|
627
889
|
position_interval = int(self.config.position.position_interval_seconds)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|