vnode 0.1.4__tar.gz → 0.1.6__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.4 → vnode-0.1.6}/PKG-INFO +9 -14
- {vnode-0.1.4 → vnode-0.1.6}/README.md +8 -13
- {vnode-0.1.4 → vnode-0.1.6}/pyproject.toml +1 -1
- {vnode-0.1.4 → vnode-0.1.6}/vnode/vnode/runtime.py +45 -358
- {vnode-0.1.4 → vnode-0.1.6}/LICENSE +0 -0
- {vnode-0.1.4 → vnode-0.1.6}/vnode/vnode/__init__.py +0 -0
- {vnode-0.1.4 → vnode-0.1.6}/vnode/vnode/__main__.py +0 -0
- {vnode-0.1.4 → vnode-0.1.6}/vnode/vnode/cli.py +0 -0
- {vnode-0.1.4 → vnode-0.1.6}/vnode/vnode/config.py +0 -0
- {vnode-0.1.4 → vnode-0.1.6}/vnode/vnode/crypto.py +0 -0
- {vnode-0.1.4 → vnode-0.1.6}/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.6
|
|
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
|
|
@@ -39,6 +39,8 @@ If `node.json` does not exist yet, the runtime will create it automatically from
|
|
|
39
39
|
|
|
40
40
|
If the template leaves `node_id` blank, the runtime will generate and persist a random
|
|
41
41
|
Meshtastic-style node ID when it creates or first loads `node.json`.
|
|
42
|
+
If `long_name` or `short_name` are blank, the runtime will also generate them from the
|
|
43
|
+
last 4 hex characters of the node ID, for example `Meshtastic 691c` and `691c`.
|
|
42
44
|
|
|
43
45
|
The runtime will generate and persist a PKI private key into `node.json` on first run if
|
|
44
46
|
the private key is blank.
|
|
@@ -82,32 +84,25 @@ node = VirtualNode("node.json")
|
|
|
82
84
|
packet_id = node.send_text("!1234abcd", "hello")
|
|
83
85
|
```
|
|
84
86
|
|
|
85
|
-
The runtime also mirrors the common Meshtastic Python pubsub and method surface:
|
|
86
|
-
|
|
87
|
-
- topics: `meshtastic.connection.established`, `meshtastic.connection.lost`, `meshtastic.receive`, `meshtastic.receive.text`, `meshtastic.receive.position`, `meshtastic.receive.user`, `meshtastic.receive.data.<PORTNUM>`, `meshtastic.node.updated`, `meshtastic.log.line`
|
|
88
|
-
- methods: `receive()`, `unreceive()`, `close()`, `sendText()`, `sendData()`, `sendPosition()`, `getMyNodeInfo()`, `getMyUser()`, `getLongName()`
|
|
89
|
-
|
|
90
|
-
That means code shaped like the Meshtastic Python library can often run on `VirtualNode` with minimal changes.
|
|
91
|
-
|
|
92
87
|
## Examples
|
|
93
88
|
|
|
94
89
|
Use these as small runnable references for common tasks:
|
|
95
90
|
|
|
96
91
|
- `examples/autoresponder.py`: DM-only reply bot for inbound direct text messages
|
|
97
|
-
- `examples/
|
|
92
|
+
- `examples/basic_subscriptions.py`: minimal `node.receive()` subscription example
|
|
93
|
+
- `examples/listen_packets.py`: packet logger for multicast traffic and decoded text
|
|
94
|
+
- `examples/serial_or_vnode.py`: try a serial Meshtastic node first, then fall back to `VirtualNode`
|
|
98
95
|
- `examples/send_dm.py`: minimal one-shot direct-message sender
|
|
99
|
-
- `examples/library_embed.py`: minimal application-style embedding example using
|
|
100
|
-
- `examples/meshtastic_compat.py`: minimal Meshtastic-style example using `meshtastic.*` topics and `sendText()`
|
|
101
|
-
- `examples/serial_or_vnode.py`: try a real serial node first, then fall back to `vnode` when no device is attached
|
|
96
|
+
- `examples/library_embed.py`: minimal application-style embedding example using `VirtualNode` directly
|
|
102
97
|
- `examples/watch_reliability.py`: watcher for ACK, NAK, retry, and retransmit-failure events
|
|
103
98
|
|
|
104
99
|
```bash
|
|
105
100
|
.venv/bin/python examples/autoresponder.py
|
|
101
|
+
.venv/bin/python examples/basic_subscriptions.py
|
|
106
102
|
.venv/bin/python examples/listen_packets.py
|
|
103
|
+
.venv/bin/python examples/serial_or_vnode.py
|
|
107
104
|
.venv/bin/python examples/send_dm.py --to '!1234abcd' --message 'hello'
|
|
108
105
|
.venv/bin/python examples/library_embed.py
|
|
109
|
-
.venv/bin/python examples/meshtastic_compat.py
|
|
110
|
-
.venv/bin/python examples/serial_or_vnode.py
|
|
111
106
|
.venv/bin/python examples/watch_reliability.py --to '!1234abcd' --message 'hello'
|
|
112
107
|
```
|
|
113
108
|
|
|
@@ -15,6 +15,8 @@ If `node.json` does not exist yet, the runtime will create it automatically from
|
|
|
15
15
|
|
|
16
16
|
If the template leaves `node_id` blank, the runtime will generate and persist a random
|
|
17
17
|
Meshtastic-style node ID when it creates or first loads `node.json`.
|
|
18
|
+
If `long_name` or `short_name` are blank, the runtime will also generate them from the
|
|
19
|
+
last 4 hex characters of the node ID, for example `Meshtastic 691c` and `691c`.
|
|
18
20
|
|
|
19
21
|
The runtime will generate and persist a PKI private key into `node.json` on first run if
|
|
20
22
|
the private key is blank.
|
|
@@ -58,32 +60,25 @@ node = VirtualNode("node.json")
|
|
|
58
60
|
packet_id = node.send_text("!1234abcd", "hello")
|
|
59
61
|
```
|
|
60
62
|
|
|
61
|
-
The runtime also mirrors the common Meshtastic Python pubsub and method surface:
|
|
62
|
-
|
|
63
|
-
- topics: `meshtastic.connection.established`, `meshtastic.connection.lost`, `meshtastic.receive`, `meshtastic.receive.text`, `meshtastic.receive.position`, `meshtastic.receive.user`, `meshtastic.receive.data.<PORTNUM>`, `meshtastic.node.updated`, `meshtastic.log.line`
|
|
64
|
-
- methods: `receive()`, `unreceive()`, `close()`, `sendText()`, `sendData()`, `sendPosition()`, `getMyNodeInfo()`, `getMyUser()`, `getLongName()`
|
|
65
|
-
|
|
66
|
-
That means code shaped like the Meshtastic Python library can often run on `VirtualNode` with minimal changes.
|
|
67
|
-
|
|
68
63
|
## Examples
|
|
69
64
|
|
|
70
65
|
Use these as small runnable references for common tasks:
|
|
71
66
|
|
|
72
67
|
- `examples/autoresponder.py`: DM-only reply bot for inbound direct text messages
|
|
73
|
-
- `examples/
|
|
68
|
+
- `examples/basic_subscriptions.py`: minimal `node.receive()` subscription example
|
|
69
|
+
- `examples/listen_packets.py`: packet logger for multicast traffic and decoded text
|
|
70
|
+
- `examples/serial_or_vnode.py`: try a serial Meshtastic node first, then fall back to `VirtualNode`
|
|
74
71
|
- `examples/send_dm.py`: minimal one-shot direct-message sender
|
|
75
|
-
- `examples/library_embed.py`: minimal application-style embedding example using
|
|
76
|
-
- `examples/meshtastic_compat.py`: minimal Meshtastic-style example using `meshtastic.*` topics and `sendText()`
|
|
77
|
-
- `examples/serial_or_vnode.py`: try a real serial node first, then fall back to `vnode` when no device is attached
|
|
72
|
+
- `examples/library_embed.py`: minimal application-style embedding example using `VirtualNode` directly
|
|
78
73
|
- `examples/watch_reliability.py`: watcher for ACK, NAK, retry, and retransmit-failure events
|
|
79
74
|
|
|
80
75
|
```bash
|
|
81
76
|
.venv/bin/python examples/autoresponder.py
|
|
77
|
+
.venv/bin/python examples/basic_subscriptions.py
|
|
82
78
|
.venv/bin/python examples/listen_packets.py
|
|
79
|
+
.venv/bin/python examples/serial_or_vnode.py
|
|
83
80
|
.venv/bin/python examples/send_dm.py --to '!1234abcd' --message 'hello'
|
|
84
81
|
.venv/bin/python examples/library_embed.py
|
|
85
|
-
.venv/bin/python examples/meshtastic_compat.py
|
|
86
|
-
.venv/bin/python examples/serial_or_vnode.py
|
|
87
82
|
.venv/bin/python examples/watch_reliability.py --to '!1234abcd' --message 'hello'
|
|
88
83
|
```
|
|
89
84
|
|
|
@@ -5,13 +5,11 @@ import sqlite3
|
|
|
5
5
|
import threading
|
|
6
6
|
import time
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from
|
|
9
|
-
from typing import Any, Callable, Dict, Optional, Union
|
|
8
|
+
from typing import Any, Dict, Optional, Union
|
|
10
9
|
|
|
11
10
|
import meshdb
|
|
12
|
-
from google.protobuf.json_format import MessageToDict
|
|
13
11
|
from google.protobuf.message import DecodeError
|
|
14
|
-
from meshtastic import
|
|
12
|
+
from meshtastic import BROADCAST_NUM, config_pb2, mesh_pb2, portnums_pb2
|
|
15
13
|
from mudp import UDPPacketStream
|
|
16
14
|
from mudp.encryption import encrypt_packet as mudp_encrypt_packet
|
|
17
15
|
from mudp.reliability import is_ack, is_nak, publish_ack, register_pending_ack, send_ack
|
|
@@ -64,9 +62,9 @@ def resolve_role(value: Union[str, int]) -> int:
|
|
|
64
62
|
|
|
65
63
|
|
|
66
64
|
class VirtualNode:
|
|
67
|
-
RECEIVE_TOPIC = "mesh.rx.packet"
|
|
68
65
|
PACKET_TOPIC = "mesh.rx.unique_packet"
|
|
69
66
|
DUPLICATE_TOPIC = "mesh.rx.duplicate"
|
|
67
|
+
RECEIVE_TOPIC = "meshtastic.receive"
|
|
70
68
|
|
|
71
69
|
def __init__(self, config_path: Union[str, Path] = "node.json") -> None:
|
|
72
70
|
self.config_path = Path(config_path).resolve()
|
|
@@ -84,11 +82,6 @@ class VirtualNode:
|
|
|
84
82
|
self._last_nodeinfo_sent_monotonic = 0.0
|
|
85
83
|
self._last_position_reply_monotonic = 0.0
|
|
86
84
|
self._last_nodeinfo_seen: Dict[int, int] = {}
|
|
87
|
-
self._response_handlers: Dict[int, tuple[Callable[[Dict[str, Any]], Any], bool]] = {}
|
|
88
|
-
self.nodesByNum: Dict[int, Dict[str, Any]] = {}
|
|
89
|
-
self.nodes: Dict[str, Dict[str, Any]] = {}
|
|
90
|
-
self.myInfo = SimpleNamespace(my_node_num=self.node_num)
|
|
91
|
-
self._connected = False
|
|
92
85
|
|
|
93
86
|
self._ensure_security_keys()
|
|
94
87
|
self._write_public_key_file()
|
|
@@ -129,7 +122,6 @@ class VirtualNode:
|
|
|
129
122
|
is_licensed=int(self.config.is_licensed),
|
|
130
123
|
public_key=self._public_key_b64,
|
|
131
124
|
)
|
|
132
|
-
self._upsert_local_node_cache()
|
|
133
125
|
|
|
134
126
|
def _configure_mudp_globals(self) -> None:
|
|
135
127
|
mudp_node.node_id = self.config.node_id
|
|
@@ -157,10 +149,6 @@ class VirtualNode:
|
|
|
157
149
|
parse_payload=False,
|
|
158
150
|
)
|
|
159
151
|
self.stream.start()
|
|
160
|
-
self._connected = True
|
|
161
|
-
self._publish_log_line(f"virtual node connected: {self.config.node_id}")
|
|
162
|
-
pub.sendMessage("meshtastic.connection.established", interface=self)
|
|
163
|
-
self._publish_node_updated(self.nodesByNum[self.node_num])
|
|
164
152
|
if self.config.broadcasts.send_startup_nodeinfo:
|
|
165
153
|
self.send_nodeinfo()
|
|
166
154
|
self._broadcast_thread = threading.Thread(
|
|
@@ -172,8 +160,6 @@ class VirtualNode:
|
|
|
172
160
|
|
|
173
161
|
def stop(self) -> None:
|
|
174
162
|
self._stop.set()
|
|
175
|
-
was_connected = self._connected
|
|
176
|
-
self._connected = False
|
|
177
163
|
if self.stream is not None:
|
|
178
164
|
self.stream.stop()
|
|
179
165
|
self.stream = None
|
|
@@ -187,9 +173,6 @@ class VirtualNode:
|
|
|
187
173
|
pass
|
|
188
174
|
if self._broadcast_thread and self._broadcast_thread.is_alive():
|
|
189
175
|
self._broadcast_thread.join(timeout=2.0)
|
|
190
|
-
if was_connected:
|
|
191
|
-
self._publish_log_line(f"virtual node disconnected: {self.config.node_id}")
|
|
192
|
-
pub.sendMessage("meshtastic.connection.lost", interface=self)
|
|
193
176
|
|
|
194
177
|
def run_forever(self) -> None:
|
|
195
178
|
self.start()
|
|
@@ -199,133 +182,15 @@ class VirtualNode:
|
|
|
199
182
|
finally:
|
|
200
183
|
self.stop()
|
|
201
184
|
|
|
202
|
-
def receive(self, callback
|
|
203
|
-
pub.subscribe(callback,
|
|
185
|
+
def receive(self, callback) -> None:
|
|
186
|
+
pub.subscribe(callback, self.RECEIVE_TOPIC)
|
|
204
187
|
|
|
205
|
-
def unreceive(self, callback
|
|
188
|
+
def unreceive(self, callback) -> None:
|
|
206
189
|
try:
|
|
207
|
-
pub.unsubscribe(callback,
|
|
190
|
+
pub.unsubscribe(callback, self.RECEIVE_TOPIC)
|
|
208
191
|
except KeyError:
|
|
209
192
|
pass
|
|
210
193
|
|
|
211
|
-
def close(self) -> None:
|
|
212
|
-
self.stop()
|
|
213
|
-
|
|
214
|
-
def sendText(
|
|
215
|
-
self,
|
|
216
|
-
text: str,
|
|
217
|
-
destinationId: Union[int, str] = BROADCAST_ADDR,
|
|
218
|
-
wantAck: bool = False,
|
|
219
|
-
wantResponse: bool = False,
|
|
220
|
-
onResponse: Optional[Callable[[dict], Any]] = None,
|
|
221
|
-
channelIndex: int = 0,
|
|
222
|
-
portNum: int = portnums_pb2.PortNum.TEXT_MESSAGE_APP,
|
|
223
|
-
replyId: Optional[int] = None,
|
|
224
|
-
hopLimit: Optional[int] = None,
|
|
225
|
-
) -> mesh_pb2.MeshPacket:
|
|
226
|
-
return self.sendData(
|
|
227
|
-
text.encode("utf-8"),
|
|
228
|
-
destinationId=destinationId,
|
|
229
|
-
portNum=portNum,
|
|
230
|
-
wantAck=wantAck,
|
|
231
|
-
wantResponse=wantResponse,
|
|
232
|
-
onResponse=onResponse,
|
|
233
|
-
channelIndex=channelIndex,
|
|
234
|
-
hopLimit=hopLimit,
|
|
235
|
-
replyId=replyId,
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
def sendData(
|
|
239
|
-
self,
|
|
240
|
-
data: Any,
|
|
241
|
-
destinationId: Union[int, str] = BROADCAST_ADDR,
|
|
242
|
-
portNum: int = portnums_pb2.PortNum.PRIVATE_APP,
|
|
243
|
-
wantAck: bool = False,
|
|
244
|
-
wantResponse: bool = False,
|
|
245
|
-
onResponse: Optional[Callable[[dict], Any]] = None,
|
|
246
|
-
onResponseAckPermitted: bool = False,
|
|
247
|
-
channelIndex: int = 0,
|
|
248
|
-
hopLimit: Optional[int] = None,
|
|
249
|
-
pkiEncrypted: bool = False,
|
|
250
|
-
publicKey: Optional[bytes] = None,
|
|
251
|
-
priority: int = mesh_pb2.MeshPacket.Priority.RELIABLE,
|
|
252
|
-
replyId: Optional[int] = None,
|
|
253
|
-
) -> mesh_pb2.MeshPacket:
|
|
254
|
-
if channelIndex != 0:
|
|
255
|
-
raise ValueError("vnode currently supports only channelIndex=0")
|
|
256
|
-
if getattr(data, "SerializeToString", None):
|
|
257
|
-
data = data.SerializeToString()
|
|
258
|
-
payload = bytes(data)
|
|
259
|
-
destination_num = self._resolve_destination(destinationId)
|
|
260
|
-
|
|
261
|
-
decoded = mesh_pb2.Data()
|
|
262
|
-
decoded.portnum = int(portNum)
|
|
263
|
-
decoded.payload = payload
|
|
264
|
-
decoded.source = self.node_num
|
|
265
|
-
decoded.dest = destination_num
|
|
266
|
-
decoded.want_response = bool(wantResponse)
|
|
267
|
-
if replyId is not None:
|
|
268
|
-
decoded.reply_id = int(replyId)
|
|
269
|
-
|
|
270
|
-
packet = self._send_packet(
|
|
271
|
-
decoded,
|
|
272
|
-
destination=destination_num,
|
|
273
|
-
force_pki=bool(pkiEncrypted),
|
|
274
|
-
hop_limit=hopLimit,
|
|
275
|
-
hop_start=None,
|
|
276
|
-
want_ack=bool(wantAck),
|
|
277
|
-
priority=priority,
|
|
278
|
-
remote_public_key=publicKey,
|
|
279
|
-
)
|
|
280
|
-
if onResponse is not None:
|
|
281
|
-
self._response_handlers[int(packet.id)] = (onResponse, bool(onResponseAckPermitted))
|
|
282
|
-
return self._packet_for_return(packet, decoded)
|
|
283
|
-
|
|
284
|
-
def sendPosition(
|
|
285
|
-
self,
|
|
286
|
-
latitude: float = 0.0,
|
|
287
|
-
longitude: float = 0.0,
|
|
288
|
-
altitude: int = 0,
|
|
289
|
-
destinationId: Union[int, str] = BROADCAST_ADDR,
|
|
290
|
-
wantAck: bool = False,
|
|
291
|
-
wantResponse: bool = False,
|
|
292
|
-
channelIndex: int = 0,
|
|
293
|
-
hopLimit: Optional[int] = None,
|
|
294
|
-
) -> mesh_pb2.MeshPacket:
|
|
295
|
-
position = mesh_pb2.Position()
|
|
296
|
-
if latitude != 0.0:
|
|
297
|
-
position.latitude_i = int(latitude / 1e-7)
|
|
298
|
-
if longitude != 0.0:
|
|
299
|
-
position.longitude_i = int(longitude / 1e-7)
|
|
300
|
-
if altitude != 0:
|
|
301
|
-
position.altitude = int(altitude)
|
|
302
|
-
return self.sendData(
|
|
303
|
-
position,
|
|
304
|
-
destinationId=destinationId,
|
|
305
|
-
portNum=portnums_pb2.PortNum.POSITION_APP,
|
|
306
|
-
wantAck=wantAck,
|
|
307
|
-
wantResponse=wantResponse,
|
|
308
|
-
channelIndex=channelIndex,
|
|
309
|
-
hopLimit=hopLimit,
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
def getMyNodeInfo(self) -> Optional[Dict[str, Any]]:
|
|
313
|
-
return self.nodesByNum.get(self.node_num)
|
|
314
|
-
|
|
315
|
-
def getMyUser(self) -> Optional[Dict[str, Any]]:
|
|
316
|
-
node_info = self.getMyNodeInfo()
|
|
317
|
-
if node_info is None:
|
|
318
|
-
return None
|
|
319
|
-
user = node_info.get("user")
|
|
320
|
-
return user if isinstance(user, dict) else None
|
|
321
|
-
|
|
322
|
-
def getLongName(self) -> Optional[str]:
|
|
323
|
-
user = self.getMyUser()
|
|
324
|
-
if user is None:
|
|
325
|
-
return None
|
|
326
|
-
value = user.get("longName")
|
|
327
|
-
return str(value) if value is not None else None
|
|
328
|
-
|
|
329
194
|
def connect_send_socket(self) -> None:
|
|
330
195
|
if getattr(conn, "socket", None) is None:
|
|
331
196
|
conn.setup_multicast(self.config.udp.mcast_group, int(self.config.udp.mcast_port))
|
|
@@ -526,36 +391,12 @@ class VirtualNode:
|
|
|
526
391
|
hop_start: Optional[int] = None,
|
|
527
392
|
want_ack: bool = False,
|
|
528
393
|
) -> int:
|
|
529
|
-
packet = self._send_packet(
|
|
530
|
-
data,
|
|
531
|
-
destination=destination,
|
|
532
|
-
force_pki=force_pki,
|
|
533
|
-
hop_limit=hop_limit,
|
|
534
|
-
hop_start=hop_start,
|
|
535
|
-
want_ack=want_ack,
|
|
536
|
-
)
|
|
537
|
-
return packet.id
|
|
538
|
-
|
|
539
|
-
def _send_packet(
|
|
540
|
-
self,
|
|
541
|
-
data: mesh_pb2.Data,
|
|
542
|
-
*,
|
|
543
|
-
destination: int,
|
|
544
|
-
force_pki: bool,
|
|
545
|
-
hop_limit: Optional[int] = None,
|
|
546
|
-
hop_start: Optional[int] = None,
|
|
547
|
-
want_ack: bool = False,
|
|
548
|
-
priority: Optional[int] = None,
|
|
549
|
-
remote_public_key: Optional[bytes] = None,
|
|
550
|
-
) -> mesh_pb2.MeshPacket:
|
|
551
394
|
self.connect_send_socket()
|
|
552
395
|
packet = mesh_pb2.MeshPacket()
|
|
553
396
|
packet.id = self._next_packet_id()
|
|
554
397
|
setattr(packet, "from", self.node_num)
|
|
555
398
|
packet.to = int(destination)
|
|
556
399
|
packet.want_ack = bool(want_ack)
|
|
557
|
-
if priority is not None:
|
|
558
|
-
packet.priority = int(priority)
|
|
559
400
|
resolved_hop_limit = int(self.config.hop_limit if hop_limit is None else hop_limit)
|
|
560
401
|
resolved_hop_start = int(resolved_hop_limit if hop_start is None else hop_start)
|
|
561
402
|
if resolved_hop_start < resolved_hop_limit:
|
|
@@ -564,15 +405,14 @@ class VirtualNode:
|
|
|
564
405
|
packet.hop_start = resolved_hop_start
|
|
565
406
|
|
|
566
407
|
if force_pki:
|
|
567
|
-
|
|
568
|
-
if
|
|
408
|
+
remote_public_key = self._lookup_public_key(destination)
|
|
409
|
+
if remote_public_key is None:
|
|
569
410
|
raise ValueError(f"Destination {destination} does not have a stored public key in meshdb")
|
|
570
411
|
packet.channel = 0
|
|
571
412
|
packet.pki_encrypted = True
|
|
572
|
-
packet.public_key = key_bytes
|
|
573
413
|
packet.encrypted = encrypt_dm(
|
|
574
414
|
sender_private_key=b64_decode(self.config.security.private_key),
|
|
575
|
-
receiver_public_key=
|
|
415
|
+
receiver_public_key=remote_public_key,
|
|
576
416
|
packet_id=packet.id,
|
|
577
417
|
from_node=self.node_num,
|
|
578
418
|
plaintext=data.SerializeToString(),
|
|
@@ -589,21 +429,20 @@ class VirtualNode:
|
|
|
589
429
|
register_pending_ack(packet, raw_packet)
|
|
590
430
|
conn.sendto(raw_packet, (conn.host, conn.port))
|
|
591
431
|
self._persist_outbound_packet(packet, data)
|
|
592
|
-
return packet
|
|
432
|
+
return packet.id
|
|
593
433
|
|
|
594
|
-
def _handle_raw_packet(self, packet: mesh_pb2.MeshPacket,
|
|
595
|
-
del
|
|
434
|
+
def _handle_raw_packet(self, packet: mesh_pb2.MeshPacket, addr: Any = None) -> None:
|
|
435
|
+
del addr
|
|
596
436
|
if not getattr(packet, "rx_time", 0):
|
|
597
437
|
packet.rx_time = int(time.time())
|
|
598
438
|
|
|
599
439
|
if not packet.HasField("decoded"):
|
|
600
440
|
self._try_decode_pki(packet)
|
|
601
|
-
self._publish_meshtastic_receive(packet)
|
|
602
441
|
self._maybe_send_ack(packet)
|
|
603
442
|
self._maybe_send_response(packet)
|
|
604
443
|
|
|
605
|
-
def _handle_unique_packet(self, packet: mesh_pb2.MeshPacket,
|
|
606
|
-
del
|
|
444
|
+
def _handle_unique_packet(self, packet: mesh_pb2.MeshPacket, addr: Any = None) -> None:
|
|
445
|
+
del addr
|
|
607
446
|
if not getattr(packet, "rx_time", 0):
|
|
608
447
|
packet.rx_time = int(time.time())
|
|
609
448
|
if not packet.HasField("decoded"):
|
|
@@ -612,186 +451,7 @@ class VirtualNode:
|
|
|
612
451
|
return
|
|
613
452
|
|
|
614
453
|
self._persist_packet(packet)
|
|
615
|
-
self.
|
|
616
|
-
|
|
617
|
-
def _packet_to_receive_dict(self, packet: mesh_pb2.MeshPacket) -> Dict[str, Any]:
|
|
618
|
-
as_dict = MessageToDict(packet)
|
|
619
|
-
as_dict["raw"] = packet
|
|
620
|
-
|
|
621
|
-
if "from" not in as_dict:
|
|
622
|
-
as_dict["from"] = 0
|
|
623
|
-
if "to" not in as_dict:
|
|
624
|
-
as_dict["to"] = 0
|
|
625
|
-
|
|
626
|
-
as_dict["fromId"] = self._node_num_to_id(int(as_dict["from"]), is_dest=False)
|
|
627
|
-
as_dict["toId"] = self._node_num_to_id(int(as_dict["to"]), is_dest=True)
|
|
628
|
-
|
|
629
|
-
if "decoded" in as_dict and packet.HasField("decoded"):
|
|
630
|
-
as_dict["decoded"]["payload"] = packet.decoded.payload
|
|
631
|
-
if packet.decoded.portnum in TEXT_PORTNUMS:
|
|
632
|
-
as_dict["decoded"]["text"] = packet.decoded.payload.decode("utf-8", "ignore")
|
|
633
|
-
elif packet.decoded.portnum == portnums_pb2.PortNum.POSITION_APP:
|
|
634
|
-
position = mesh_pb2.Position()
|
|
635
|
-
try:
|
|
636
|
-
position.ParseFromString(packet.decoded.payload)
|
|
637
|
-
position_dict = MessageToDict(position)
|
|
638
|
-
position_dict["raw"] = position
|
|
639
|
-
as_dict["decoded"]["position"] = position_dict
|
|
640
|
-
except DecodeError:
|
|
641
|
-
pass
|
|
642
|
-
elif packet.decoded.portnum == portnums_pb2.PortNum.NODEINFO_APP:
|
|
643
|
-
user = mesh_pb2.User()
|
|
644
|
-
try:
|
|
645
|
-
user.ParseFromString(packet.decoded.payload)
|
|
646
|
-
user_dict = MessageToDict(user)
|
|
647
|
-
user_dict["raw"] = user
|
|
648
|
-
as_dict["decoded"]["user"] = user_dict
|
|
649
|
-
except DecodeError:
|
|
650
|
-
pass
|
|
651
|
-
|
|
652
|
-
return as_dict
|
|
653
|
-
|
|
654
|
-
def _node_num_to_id(self, node_num: int, *, is_dest: bool) -> Optional[str]:
|
|
655
|
-
if node_num == BROADCAST_NUM:
|
|
656
|
-
return "^all" if is_dest else "Unknown"
|
|
657
|
-
if node_num == self.node_num:
|
|
658
|
-
return self.config.node_id
|
|
659
|
-
|
|
660
|
-
return f"!{int(node_num):08x}"
|
|
661
|
-
|
|
662
|
-
def _packet_for_return(self, packet: mesh_pb2.MeshPacket, data: mesh_pb2.Data) -> mesh_pb2.MeshPacket:
|
|
663
|
-
returned = mesh_pb2.MeshPacket()
|
|
664
|
-
returned.CopyFrom(packet)
|
|
665
|
-
returned.decoded.CopyFrom(data)
|
|
666
|
-
return returned
|
|
667
|
-
|
|
668
|
-
def _publish_meshtastic_receive(self, packet: mesh_pb2.MeshPacket) -> None:
|
|
669
|
-
if int(getattr(packet, "from", 0) or 0) == self.node_num:
|
|
670
|
-
return
|
|
671
|
-
packet_dict = self._packet_to_receive_dict(packet)
|
|
672
|
-
self._dispatch_response_handler(packet, packet_dict)
|
|
673
|
-
topic = "meshtastic.receive"
|
|
674
|
-
if packet.HasField("decoded"):
|
|
675
|
-
portnum_name = str(packet_dict.get("decoded", {}).get("portnum", "UNKNOWN_APP"))
|
|
676
|
-
topic = f"meshtastic.receive.data.{portnum_name}"
|
|
677
|
-
if packet.decoded.portnum in TEXT_PORTNUMS:
|
|
678
|
-
topic = "meshtastic.receive.text"
|
|
679
|
-
elif packet.decoded.portnum == portnums_pb2.PortNum.POSITION_APP:
|
|
680
|
-
topic = "meshtastic.receive.position"
|
|
681
|
-
elif packet.decoded.portnum == portnums_pb2.PortNum.NODEINFO_APP:
|
|
682
|
-
topic = "meshtastic.receive.user"
|
|
683
|
-
pub.sendMessage(topic, packet=packet_dict, interface=self)
|
|
684
|
-
|
|
685
|
-
def _dispatch_response_handler(self, packet: mesh_pb2.MeshPacket, packet_dict: Dict[str, Any]) -> None:
|
|
686
|
-
decoded = packet_dict.get("decoded")
|
|
687
|
-
if not isinstance(decoded, dict):
|
|
688
|
-
return
|
|
689
|
-
request_id = decoded.get("requestId")
|
|
690
|
-
if request_id is None:
|
|
691
|
-
return
|
|
692
|
-
try:
|
|
693
|
-
request_id_num = int(request_id)
|
|
694
|
-
except (TypeError, ValueError):
|
|
695
|
-
return
|
|
696
|
-
handler = self._response_handlers.get(request_id_num)
|
|
697
|
-
if handler is None:
|
|
698
|
-
return
|
|
699
|
-
callback, ack_permitted = handler
|
|
700
|
-
if is_ack(packet) and not ack_permitted and getattr(callback, "__name__", "") != "onAckNak":
|
|
701
|
-
return
|
|
702
|
-
self._response_handlers.pop(request_id_num, None)
|
|
703
|
-
callback(packet_dict)
|
|
704
|
-
|
|
705
|
-
def _upsert_local_node_cache(self) -> None:
|
|
706
|
-
node = self._default_node_dict(self.node_num)
|
|
707
|
-
node["user"].update(
|
|
708
|
-
{
|
|
709
|
-
"id": self.config.node_id,
|
|
710
|
-
"longName": self.config.long_name,
|
|
711
|
-
"shortName": self.config.short_name,
|
|
712
|
-
"hwModel": str(self.config.hw_model),
|
|
713
|
-
"role": str(self.config.role),
|
|
714
|
-
"publicKey": self._public_key_b64,
|
|
715
|
-
"isLicensed": bool(self.config.is_licensed),
|
|
716
|
-
}
|
|
717
|
-
)
|
|
718
|
-
if (
|
|
719
|
-
self.config.position.enabled
|
|
720
|
-
and self.config.position.latitude is not None
|
|
721
|
-
and self.config.position.longitude is not None
|
|
722
|
-
):
|
|
723
|
-
node["position"] = {
|
|
724
|
-
"latitudeI": int(float(self.config.position.latitude) * 1e7),
|
|
725
|
-
"longitudeI": int(float(self.config.position.longitude) * 1e7),
|
|
726
|
-
}
|
|
727
|
-
if self.config.position.altitude is not None:
|
|
728
|
-
node["position"]["altitude"] = int(self.config.position.altitude)
|
|
729
|
-
self.nodesByNum[self.node_num] = node
|
|
730
|
-
self.nodes[self.config.node_id] = node
|
|
731
|
-
|
|
732
|
-
def _default_node_dict(self, node_num: int) -> Dict[str, Any]:
|
|
733
|
-
node_id = f"!{int(node_num):08x}"
|
|
734
|
-
return {
|
|
735
|
-
"num": int(node_num),
|
|
736
|
-
"user": {
|
|
737
|
-
"id": node_id,
|
|
738
|
-
"longName": f"Meshtastic {node_id[-4:]}",
|
|
739
|
-
"shortName": node_id[-4:],
|
|
740
|
-
"hwModel": "UNSET",
|
|
741
|
-
},
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
def _update_node_cache_from_packet(self, packet: mesh_pb2.MeshPacket) -> None:
|
|
745
|
-
sender = int(getattr(packet, "from", 0) or 0)
|
|
746
|
-
if sender == 0:
|
|
747
|
-
return
|
|
748
|
-
packet_dict = self._packet_to_receive_dict(packet)
|
|
749
|
-
node = dict(self.nodesByNum.get(sender, self._default_node_dict(sender)))
|
|
750
|
-
changed = sender not in self.nodesByNum
|
|
751
|
-
|
|
752
|
-
rx_time = packet_dict.get("rxTime")
|
|
753
|
-
if rx_time is not None and node.get("lastHeard") != rx_time:
|
|
754
|
-
node["lastHeard"] = rx_time
|
|
755
|
-
changed = True
|
|
756
|
-
snr = packet_dict.get("rxSnr")
|
|
757
|
-
if snr is not None and node.get("snr") != snr:
|
|
758
|
-
node["snr"] = snr
|
|
759
|
-
changed = True
|
|
760
|
-
|
|
761
|
-
decoded = packet_dict.get("decoded")
|
|
762
|
-
if isinstance(decoded, dict):
|
|
763
|
-
user = decoded.get("user")
|
|
764
|
-
if isinstance(user, dict):
|
|
765
|
-
clean_user = dict(user)
|
|
766
|
-
clean_user.pop("raw", None)
|
|
767
|
-
existing_user = node.get("user", {})
|
|
768
|
-
merged_user = dict(existing_user) if isinstance(existing_user, dict) else {}
|
|
769
|
-
merged_user.update(clean_user)
|
|
770
|
-
if merged_user != existing_user:
|
|
771
|
-
node["user"] = merged_user
|
|
772
|
-
changed = True
|
|
773
|
-
position = decoded.get("position")
|
|
774
|
-
if isinstance(position, dict):
|
|
775
|
-
clean_position = dict(position)
|
|
776
|
-
clean_position.pop("raw", None)
|
|
777
|
-
if clean_position != node.get("position"):
|
|
778
|
-
node["position"] = clean_position
|
|
779
|
-
changed = True
|
|
780
|
-
|
|
781
|
-
self.nodesByNum[sender] = node
|
|
782
|
-
user = node.get("user")
|
|
783
|
-
if isinstance(user, dict):
|
|
784
|
-
node_id = user.get("id")
|
|
785
|
-
if node_id:
|
|
786
|
-
self.nodes[str(node_id)] = node
|
|
787
|
-
if changed:
|
|
788
|
-
self._publish_node_updated(node)
|
|
789
|
-
|
|
790
|
-
def _publish_node_updated(self, node: Dict[str, Any]) -> None:
|
|
791
|
-
pub.sendMessage("meshtastic.node.updated", node=node, interface=self)
|
|
792
|
-
|
|
793
|
-
def _publish_log_line(self, line: str) -> None:
|
|
794
|
-
pub.sendMessage("meshtastic.log.line", line=str(line), interface=self)
|
|
454
|
+
self._publish_receive(packet)
|
|
795
455
|
|
|
796
456
|
def _try_decode_pki(self, packet: mesh_pb2.MeshPacket) -> bool:
|
|
797
457
|
if packet.channel != 0 or packet.to != self.node_num or not packet.encrypted:
|
|
@@ -933,6 +593,35 @@ class VirtualNode:
|
|
|
933
593
|
db_path=self.meshdb_path,
|
|
934
594
|
)
|
|
935
595
|
|
|
596
|
+
def _publish_receive(self, packet: mesh_pb2.MeshPacket) -> None:
|
|
597
|
+
from meshtastic import protocols
|
|
598
|
+
|
|
599
|
+
packet_dict = meshdb.normalize_packet(packet, "udp")
|
|
600
|
+
packet_dict["raw"] = packet
|
|
601
|
+
|
|
602
|
+
topic = self.RECEIVE_TOPIC
|
|
603
|
+
decoded = packet_dict.get("decoded")
|
|
604
|
+
portnum = None
|
|
605
|
+
if isinstance(decoded, dict):
|
|
606
|
+
raw_portnum = decoded.get("portnum")
|
|
607
|
+
if raw_portnum is not None:
|
|
608
|
+
try:
|
|
609
|
+
portnum = int(raw_portnum)
|
|
610
|
+
except (TypeError, ValueError):
|
|
611
|
+
portnum = None
|
|
612
|
+
|
|
613
|
+
if portnum is not None:
|
|
614
|
+
handler = protocols.get(portnum)
|
|
615
|
+
if handler is not None:
|
|
616
|
+
topic = f"{self.RECEIVE_TOPIC}.{handler.name}"
|
|
617
|
+
else:
|
|
618
|
+
try:
|
|
619
|
+
topic = f"{self.RECEIVE_TOPIC}.data.{portnums_pb2.PortNum.Name(portnum)}"
|
|
620
|
+
except ValueError:
|
|
621
|
+
topic = f"{self.RECEIVE_TOPIC}.data.{portnum}"
|
|
622
|
+
|
|
623
|
+
pub.sendMessage(topic, packet=packet_dict, interface=self)
|
|
624
|
+
|
|
936
625
|
def _broadcast_loop(self) -> None:
|
|
937
626
|
nodeinfo_interval = int(self.config.broadcasts.nodeinfo_interval_seconds)
|
|
938
627
|
position_interval = int(self.config.position.position_interval_seconds)
|
|
@@ -966,8 +655,6 @@ class VirtualNode:
|
|
|
966
655
|
if isinstance(destination, int):
|
|
967
656
|
return destination
|
|
968
657
|
text = str(destination).strip()
|
|
969
|
-
if text == BROADCAST_ADDR:
|
|
970
|
-
return BROADCAST_NUM
|
|
971
658
|
if text.startswith("!"):
|
|
972
659
|
return parse_node_id(text)
|
|
973
660
|
resolved = meshdb.get_node_num(text, owner_node_num=self.node_num, db_path=self.meshdb_path)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|