vnode 0.1.6__tar.gz → 0.1.9__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.9}/PKG-INFO +32 -4
- {vnode-0.1.6 → vnode-0.1.9}/README.md +30 -2
- {vnode-0.1.6 → vnode-0.1.9}/pyproject.toml +2 -2
- {vnode-0.1.6 → vnode-0.1.9}/vnode/vnode/runtime.py +281 -16
- {vnode-0.1.6 → vnode-0.1.9}/LICENSE +0 -0
- {vnode-0.1.6 → vnode-0.1.9}/vnode/vnode/__init__.py +0 -0
- {vnode-0.1.6 → vnode-0.1.9}/vnode/vnode/__main__.py +0 -0
- {vnode-0.1.6 → vnode-0.1.9}/vnode/vnode/cli.py +0 -0
- {vnode-0.1.6 → vnode-0.1.9}/vnode/vnode/config.py +0 -0
- {vnode-0.1.6 → vnode-0.1.9}/vnode/vnode/crypto.py +0 -0
- {vnode-0.1.6 → vnode-0.1.9}/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.9
|
|
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
|
|
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
17
17
|
Requires-Dist: cryptography (>=43,<45)
|
|
18
18
|
Requires-Dist: meshdb (>=0.1.9)
|
|
19
19
|
Requires-Dist: meshtastic (>=2.7,<3.0)
|
|
20
|
-
Requires-Dist: mudp (>=1.1.
|
|
20
|
+
Requires-Dist: mudp (>=1.1.5)
|
|
21
21
|
Project-URL: Homepage, https://github.com/pdxlocations/vnode
|
|
22
22
|
Project-URL: Issues, https://github.com/pdxlocations/vnode/issues
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
@@ -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,19 @@ 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
|
+
|
|
109
|
+
`receive(callback)` is deduplicated. Repeated multicast copies stay on `mudp`'s wire-level
|
|
110
|
+
topics, while `vnode` publishes `meshtastic.receive` from `mesh.rx.unique_packet` so
|
|
111
|
+
application callbacks run once per logical packet.
|
|
112
|
+
|
|
87
113
|
## Examples
|
|
88
114
|
|
|
89
115
|
Use these as small runnable references for common tasks:
|
|
@@ -91,15 +117,17 @@ Use these as small runnable references for common tasks:
|
|
|
91
117
|
- `examples/autoresponder.py`: DM-only reply bot for inbound direct text messages
|
|
92
118
|
- `examples/basic_subscriptions.py`: minimal `node.receive()` subscription example
|
|
93
119
|
- `examples/listen_packets.py`: packet logger for multicast traffic and decoded text
|
|
120
|
+
- `examples/mesh_interface_compat.py`: examples of MeshInterface-style compatibility calls on `VirtualNode`
|
|
94
121
|
- `examples/serial_or_vnode.py`: try a serial Meshtastic node first, then fall back to `VirtualNode`
|
|
95
122
|
- `examples/send_dm.py`: minimal one-shot direct-message sender
|
|
96
123
|
- `examples/library_embed.py`: minimal application-style embedding example using `VirtualNode` directly
|
|
97
|
-
- `examples/watch_reliability.py`: watcher for ACK, NAK, retry, and retransmit-failure events
|
|
124
|
+
- `examples/watch_reliability.py`: watcher for unique ACK, NAK, retry, and retransmit-failure events
|
|
98
125
|
|
|
99
126
|
```bash
|
|
100
127
|
.venv/bin/python examples/autoresponder.py
|
|
101
128
|
.venv/bin/python examples/basic_subscriptions.py
|
|
102
129
|
.venv/bin/python examples/listen_packets.py
|
|
130
|
+
.venv/bin/python examples/mesh_interface_compat.py
|
|
103
131
|
.venv/bin/python examples/serial_or_vnode.py
|
|
104
132
|
.venv/bin/python examples/send_dm.py --to '!1234abcd' --message 'hello'
|
|
105
133
|
.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,19 @@ 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
|
+
|
|
85
|
+
`receive(callback)` is deduplicated. Repeated multicast copies stay on `mudp`'s wire-level
|
|
86
|
+
topics, while `vnode` publishes `meshtastic.receive` from `mesh.rx.unique_packet` so
|
|
87
|
+
application callbacks run once per logical packet.
|
|
88
|
+
|
|
63
89
|
## Examples
|
|
64
90
|
|
|
65
91
|
Use these as small runnable references for common tasks:
|
|
@@ -67,15 +93,17 @@ Use these as small runnable references for common tasks:
|
|
|
67
93
|
- `examples/autoresponder.py`: DM-only reply bot for inbound direct text messages
|
|
68
94
|
- `examples/basic_subscriptions.py`: minimal `node.receive()` subscription example
|
|
69
95
|
- `examples/listen_packets.py`: packet logger for multicast traffic and decoded text
|
|
96
|
+
- `examples/mesh_interface_compat.py`: examples of MeshInterface-style compatibility calls on `VirtualNode`
|
|
70
97
|
- `examples/serial_or_vnode.py`: try a serial Meshtastic node first, then fall back to `VirtualNode`
|
|
71
98
|
- `examples/send_dm.py`: minimal one-shot direct-message sender
|
|
72
99
|
- `examples/library_embed.py`: minimal application-style embedding example using `VirtualNode` directly
|
|
73
|
-
- `examples/watch_reliability.py`: watcher for ACK, NAK, retry, and retransmit-failure events
|
|
100
|
+
- `examples/watch_reliability.py`: watcher for unique ACK, NAK, retry, and retransmit-failure events
|
|
74
101
|
|
|
75
102
|
```bash
|
|
76
103
|
.venv/bin/python examples/autoresponder.py
|
|
77
104
|
.venv/bin/python examples/basic_subscriptions.py
|
|
78
105
|
.venv/bin/python examples/listen_packets.py
|
|
106
|
+
.venv/bin/python examples/mesh_interface_compat.py
|
|
79
107
|
.venv/bin/python examples/serial_or_vnode.py
|
|
80
108
|
.venv/bin/python examples/send_dm.py --to '!1234abcd' --message 'hello'
|
|
81
109
|
.venv/bin/python examples/library_embed.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "vnode"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.9"
|
|
4
4
|
description = "Virtual Meshtastic node using mudp transport and meshdb storage."
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Ben Lipsey",email = "ben@pdxlocations.com"}
|
|
@@ -12,7 +12,7 @@ dependencies = [
|
|
|
12
12
|
"cryptography>=43,<45",
|
|
13
13
|
"meshdb>=0.1.9",
|
|
14
14
|
"meshtastic>=2.7,<3.0",
|
|
15
|
-
"mudp>=1.1.
|
|
15
|
+
"mudp>=1.1.5",
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
[project.urls]
|
|
@@ -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
|
|
@@ -62,7 +63,10 @@ def resolve_role(value: Union[str, int]) -> int:
|
|
|
62
63
|
|
|
63
64
|
|
|
64
65
|
class VirtualNode:
|
|
66
|
+
RAW_PACKET_TOPIC = "mesh.rx.packet"
|
|
65
67
|
PACKET_TOPIC = "mesh.rx.unique_packet"
|
|
68
|
+
ACK_TOPIC = "mesh.rx.ack"
|
|
69
|
+
NAK_TOPIC = "mesh.rx.nak"
|
|
66
70
|
DUPLICATE_TOPIC = "mesh.rx.duplicate"
|
|
67
71
|
RECEIVE_TOPIC = "meshtastic.receive"
|
|
68
72
|
|
|
@@ -82,6 +86,7 @@ class VirtualNode:
|
|
|
82
86
|
self._last_nodeinfo_sent_monotonic = 0.0
|
|
83
87
|
self._last_position_reply_monotonic = 0.0
|
|
84
88
|
self._last_nodeinfo_seen: Dict[int, int] = {}
|
|
89
|
+
self._response_handlers: Dict[int, Dict[str, Any]] = {}
|
|
85
90
|
|
|
86
91
|
self._ensure_security_keys()
|
|
87
92
|
self._write_public_key_file()
|
|
@@ -140,8 +145,11 @@ class VirtualNode:
|
|
|
140
145
|
def start(self) -> None:
|
|
141
146
|
if self.stream is not None:
|
|
142
147
|
return
|
|
143
|
-
pub.subscribe(self._handle_raw_packet,
|
|
148
|
+
pub.subscribe(self._handle_raw_packet, self.RAW_PACKET_TOPIC)
|
|
144
149
|
pub.subscribe(self._handle_unique_packet, self.PACKET_TOPIC)
|
|
150
|
+
pub.subscribe(self._handle_compat_response_packet, self.RECEIVE_TOPIC)
|
|
151
|
+
pub.subscribe(self._handle_compat_ack, self.ACK_TOPIC)
|
|
152
|
+
pub.subscribe(self._handle_compat_nak, self.NAK_TOPIC)
|
|
145
153
|
self.stream = UDPPacketStream(
|
|
146
154
|
self.config.udp.mcast_group,
|
|
147
155
|
int(self.config.udp.mcast_port),
|
|
@@ -164,13 +172,22 @@ class VirtualNode:
|
|
|
164
172
|
self.stream.stop()
|
|
165
173
|
self.stream = None
|
|
166
174
|
try:
|
|
167
|
-
pub.unsubscribe(self._handle_raw_packet,
|
|
175
|
+
pub.unsubscribe(self._handle_raw_packet, self.RAW_PACKET_TOPIC)
|
|
168
176
|
except KeyError:
|
|
169
177
|
pass
|
|
170
178
|
try:
|
|
171
179
|
pub.unsubscribe(self._handle_unique_packet, self.PACKET_TOPIC)
|
|
172
180
|
except KeyError:
|
|
173
181
|
pass
|
|
182
|
+
for topic, listener in (
|
|
183
|
+
(self.RECEIVE_TOPIC, self._handle_compat_response_packet),
|
|
184
|
+
(self.ACK_TOPIC, self._handle_compat_ack),
|
|
185
|
+
(self.NAK_TOPIC, self._handle_compat_nak),
|
|
186
|
+
):
|
|
187
|
+
try:
|
|
188
|
+
pub.unsubscribe(listener, topic)
|
|
189
|
+
except KeyError:
|
|
190
|
+
pass
|
|
174
191
|
if self._broadcast_thread and self._broadcast_thread.is_alive():
|
|
175
192
|
self._broadcast_thread.join(timeout=2.0)
|
|
176
193
|
|
|
@@ -191,6 +208,161 @@ class VirtualNode:
|
|
|
191
208
|
except KeyError:
|
|
192
209
|
pass
|
|
193
210
|
|
|
211
|
+
def close(self) -> None:
|
|
212
|
+
self.stop()
|
|
213
|
+
|
|
214
|
+
def getLongName(self):
|
|
215
|
+
return self.config.long_name
|
|
216
|
+
|
|
217
|
+
def getShortName(self):
|
|
218
|
+
return self.config.short_name
|
|
219
|
+
|
|
220
|
+
def getPublicKey(self):
|
|
221
|
+
return b64_decode(self._public_key_b64) if self._public_key_b64 else None
|
|
222
|
+
|
|
223
|
+
def getMyUser(self):
|
|
224
|
+
user = mesh_pb2.User(
|
|
225
|
+
id=self.config.node_id,
|
|
226
|
+
long_name=self.config.long_name,
|
|
227
|
+
short_name=self.config.short_name,
|
|
228
|
+
hw_model=resolve_hw_model(self.config.hw_model),
|
|
229
|
+
)
|
|
230
|
+
user.role = resolve_role(self.config.role)
|
|
231
|
+
if self._public_key_b64:
|
|
232
|
+
user.public_key = b64_decode(self._public_key_b64)
|
|
233
|
+
return MessageToDict(user, preserving_proto_field_name=False, use_integers_for_enums=False)
|
|
234
|
+
|
|
235
|
+
def getMyNodeInfo(self):
|
|
236
|
+
return {
|
|
237
|
+
"num": self.node_num,
|
|
238
|
+
"user": self.getMyUser(),
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
def sendText(
|
|
242
|
+
self,
|
|
243
|
+
text: str,
|
|
244
|
+
destinationId: Union[int, str] = BROADCAST_ADDR,
|
|
245
|
+
wantAck: bool = False,
|
|
246
|
+
wantResponse: bool = False,
|
|
247
|
+
onResponse=None,
|
|
248
|
+
channelIndex: int = 0,
|
|
249
|
+
portNum: int = portnums_pb2.PortNum.TEXT_MESSAGE_APP,
|
|
250
|
+
replyId: Optional[int] = None,
|
|
251
|
+
hopLimit: Optional[int] = None,
|
|
252
|
+
):
|
|
253
|
+
return self.sendData(
|
|
254
|
+
text.encode("utf-8"),
|
|
255
|
+
destinationId=destinationId,
|
|
256
|
+
portNum=portNum,
|
|
257
|
+
wantAck=wantAck,
|
|
258
|
+
wantResponse=wantResponse,
|
|
259
|
+
onResponse=onResponse,
|
|
260
|
+
channelIndex=channelIndex,
|
|
261
|
+
hopLimit=hopLimit,
|
|
262
|
+
replyId=replyId,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def sendAlert(
|
|
266
|
+
self,
|
|
267
|
+
text: str,
|
|
268
|
+
destinationId: Union[int, str] = BROADCAST_ADDR,
|
|
269
|
+
onResponse=None,
|
|
270
|
+
channelIndex: int = 0,
|
|
271
|
+
hopLimit: Optional[int] = None,
|
|
272
|
+
):
|
|
273
|
+
return self.sendData(
|
|
274
|
+
text.encode("utf-8"),
|
|
275
|
+
destinationId=destinationId,
|
|
276
|
+
portNum=portnums_pb2.PortNum.ALERT_APP,
|
|
277
|
+
wantAck=False,
|
|
278
|
+
wantResponse=False,
|
|
279
|
+
onResponse=onResponse,
|
|
280
|
+
channelIndex=channelIndex,
|
|
281
|
+
hopLimit=hopLimit,
|
|
282
|
+
priority=mesh_pb2.MeshPacket.Priority.ALERT,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def sendData(
|
|
286
|
+
self,
|
|
287
|
+
data,
|
|
288
|
+
destinationId: Union[int, str] = BROADCAST_ADDR,
|
|
289
|
+
portNum: int = portnums_pb2.PortNum.PRIVATE_APP,
|
|
290
|
+
wantAck: bool = False,
|
|
291
|
+
wantResponse: bool = False,
|
|
292
|
+
onResponse=None,
|
|
293
|
+
onResponseAckPermitted: bool = False,
|
|
294
|
+
channelIndex: int = 0,
|
|
295
|
+
hopLimit: Optional[int] = None,
|
|
296
|
+
pkiEncrypted: Optional[bool] = False,
|
|
297
|
+
publicKey: Optional[bytes] = None,
|
|
298
|
+
priority: int = mesh_pb2.MeshPacket.Priority.RELIABLE,
|
|
299
|
+
replyId: Optional[int] = None,
|
|
300
|
+
):
|
|
301
|
+
if getattr(data, "SerializeToString", None):
|
|
302
|
+
data = data.SerializeToString()
|
|
303
|
+
if not isinstance(data, (bytes, bytearray)):
|
|
304
|
+
raise TypeError("data must be bytes or a protobuf with SerializeToString()")
|
|
305
|
+
if int(channelIndex) != 0:
|
|
306
|
+
raise NotImplementedError("Only channelIndex=0 is currently supported")
|
|
307
|
+
if publicKey is not None:
|
|
308
|
+
raise NotImplementedError("publicKey override is not currently supported")
|
|
309
|
+
if int(portNum) == portnums_pb2.PortNum.UNKNOWN_APP:
|
|
310
|
+
raise ValueError("A non-zero port number must be specified")
|
|
311
|
+
|
|
312
|
+
destination = self._resolve_destination_compat(destinationId)
|
|
313
|
+
force_pki = bool(pkiEncrypted)
|
|
314
|
+
data_message = mesh_pb2.Data()
|
|
315
|
+
data_message.portnum = int(portNum)
|
|
316
|
+
data_message.payload = bytes(data)
|
|
317
|
+
data_message.want_response = bool(wantResponse)
|
|
318
|
+
data_message.source = self.node_num
|
|
319
|
+
data_message.dest = int(destination)
|
|
320
|
+
if replyId is not None:
|
|
321
|
+
data_message.reply_id = int(replyId)
|
|
322
|
+
|
|
323
|
+
packet = self._send_data_packet(
|
|
324
|
+
data_message,
|
|
325
|
+
destination=int(destination),
|
|
326
|
+
force_pki=force_pki,
|
|
327
|
+
hop_limit=hopLimit,
|
|
328
|
+
want_ack=wantAck,
|
|
329
|
+
priority=int(priority),
|
|
330
|
+
)
|
|
331
|
+
if onResponse is not None:
|
|
332
|
+
self._response_handlers[int(packet.id)] = {
|
|
333
|
+
"callback": onResponse,
|
|
334
|
+
"ack_permitted": bool(onResponseAckPermitted) or getattr(onResponse, "__name__", "") == "onAckNak",
|
|
335
|
+
}
|
|
336
|
+
return packet
|
|
337
|
+
|
|
338
|
+
def sendPosition(
|
|
339
|
+
self,
|
|
340
|
+
latitude: float = 0.0,
|
|
341
|
+
longitude: float = 0.0,
|
|
342
|
+
altitude: int = 0,
|
|
343
|
+
destinationId: Union[int, str] = BROADCAST_ADDR,
|
|
344
|
+
wantAck: bool = False,
|
|
345
|
+
wantResponse: bool = False,
|
|
346
|
+
channelIndex: int = 0,
|
|
347
|
+
hopLimit: Optional[int] = None,
|
|
348
|
+
):
|
|
349
|
+
position = mesh_pb2.Position()
|
|
350
|
+
if latitude != 0.0:
|
|
351
|
+
position.latitude_i = int(latitude * 1e7)
|
|
352
|
+
if longitude != 0.0:
|
|
353
|
+
position.longitude_i = int(longitude * 1e7)
|
|
354
|
+
if altitude != 0:
|
|
355
|
+
position.altitude = int(altitude)
|
|
356
|
+
return self.sendData(
|
|
357
|
+
position,
|
|
358
|
+
destinationId=destinationId,
|
|
359
|
+
portNum=portnums_pb2.PortNum.POSITION_APP,
|
|
360
|
+
wantAck=wantAck,
|
|
361
|
+
wantResponse=wantResponse,
|
|
362
|
+
channelIndex=channelIndex,
|
|
363
|
+
hopLimit=hopLimit,
|
|
364
|
+
)
|
|
365
|
+
|
|
194
366
|
def connect_send_socket(self) -> None:
|
|
195
367
|
if getattr(conn, "socket", None) is None:
|
|
196
368
|
conn.setup_multicast(self.config.udp.mcast_group, int(self.config.udp.mcast_port))
|
|
@@ -390,13 +562,38 @@ class VirtualNode:
|
|
|
390
562
|
hop_limit: Optional[int] = None,
|
|
391
563
|
hop_start: Optional[int] = None,
|
|
392
564
|
want_ack: bool = False,
|
|
565
|
+
priority: Optional[int] = None,
|
|
393
566
|
) -> int:
|
|
567
|
+
packet = self._send_data_packet(
|
|
568
|
+
data,
|
|
569
|
+
destination=destination,
|
|
570
|
+
force_pki=force_pki,
|
|
571
|
+
hop_limit=hop_limit,
|
|
572
|
+
hop_start=hop_start,
|
|
573
|
+
want_ack=want_ack,
|
|
574
|
+
priority=priority,
|
|
575
|
+
)
|
|
576
|
+
return packet.id
|
|
577
|
+
|
|
578
|
+
def _send_data_packet(
|
|
579
|
+
self,
|
|
580
|
+
data: mesh_pb2.Data,
|
|
581
|
+
*,
|
|
582
|
+
destination: int,
|
|
583
|
+
force_pki: bool,
|
|
584
|
+
hop_limit: Optional[int] = None,
|
|
585
|
+
hop_start: Optional[int] = None,
|
|
586
|
+
want_ack: bool = False,
|
|
587
|
+
priority: Optional[int] = None,
|
|
588
|
+
) -> mesh_pb2.MeshPacket:
|
|
394
589
|
self.connect_send_socket()
|
|
395
590
|
packet = mesh_pb2.MeshPacket()
|
|
396
591
|
packet.id = self._next_packet_id()
|
|
397
592
|
setattr(packet, "from", self.node_num)
|
|
398
593
|
packet.to = int(destination)
|
|
399
594
|
packet.want_ack = bool(want_ack)
|
|
595
|
+
if priority is not None:
|
|
596
|
+
packet.priority = int(priority)
|
|
400
597
|
resolved_hop_limit = int(self.config.hop_limit if hop_limit is None else hop_limit)
|
|
401
598
|
resolved_hop_start = int(resolved_hop_limit if hop_start is None else hop_start)
|
|
402
599
|
if resolved_hop_start < resolved_hop_limit:
|
|
@@ -429,7 +626,7 @@ class VirtualNode:
|
|
|
429
626
|
register_pending_ack(packet, raw_packet)
|
|
430
627
|
conn.sendto(raw_packet, (conn.host, conn.port))
|
|
431
628
|
self._persist_outbound_packet(packet, data)
|
|
432
|
-
return packet
|
|
629
|
+
return packet
|
|
433
630
|
|
|
434
631
|
def _handle_raw_packet(self, packet: mesh_pb2.MeshPacket, addr: Any = None) -> None:
|
|
435
632
|
del addr
|
|
@@ -594,10 +791,7 @@ class VirtualNode:
|
|
|
594
791
|
)
|
|
595
792
|
|
|
596
793
|
def _publish_receive(self, packet: mesh_pb2.MeshPacket) -> None:
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
packet_dict = meshdb.normalize_packet(packet, "udp")
|
|
600
|
-
packet_dict["raw"] = packet
|
|
794
|
+
packet_dict = self._mesh_interface_packet_dict(packet)
|
|
601
795
|
|
|
602
796
|
topic = self.RECEIVE_TOPIC
|
|
603
797
|
decoded = packet_dict.get("decoded")
|
|
@@ -605,23 +799,94 @@ class VirtualNode:
|
|
|
605
799
|
if isinstance(decoded, dict):
|
|
606
800
|
raw_portnum = decoded.get("portnum")
|
|
607
801
|
if raw_portnum is not None:
|
|
608
|
-
|
|
609
|
-
portnum = int(raw_portnum)
|
|
610
|
-
except (TypeError, ValueError):
|
|
611
|
-
portnum = None
|
|
802
|
+
portnum = raw_portnum
|
|
612
803
|
|
|
613
804
|
if portnum is not None:
|
|
614
|
-
|
|
805
|
+
try:
|
|
806
|
+
portnum_value = portnums_pb2.PortNum.Value(str(portnum))
|
|
807
|
+
except ValueError:
|
|
808
|
+
portnum_value = None
|
|
809
|
+
handler = protocols.get(portnum_value) if portnum_value is not None else None
|
|
615
810
|
if handler is not None:
|
|
616
811
|
topic = f"{self.RECEIVE_TOPIC}.{handler.name}"
|
|
617
812
|
else:
|
|
618
|
-
|
|
619
|
-
topic = f"{self.RECEIVE_TOPIC}.data.{
|
|
620
|
-
|
|
813
|
+
if isinstance(portnum, str):
|
|
814
|
+
topic = f"{self.RECEIVE_TOPIC}.data.{portnum}"
|
|
815
|
+
else:
|
|
621
816
|
topic = f"{self.RECEIVE_TOPIC}.data.{portnum}"
|
|
622
817
|
|
|
623
818
|
pub.sendMessage(topic, packet=packet_dict, interface=self)
|
|
624
819
|
|
|
820
|
+
def _mesh_interface_packet_dict(self, packet: mesh_pb2.MeshPacket) -> Dict[str, Any]:
|
|
821
|
+
packet_dict = meshdb.normalize_packet(packet, "udp")
|
|
822
|
+
packet_dict["raw"] = packet
|
|
823
|
+
|
|
824
|
+
if packet.HasField("decoded"):
|
|
825
|
+
decoded = packet_dict.setdefault("decoded", {})
|
|
826
|
+
decoded["payload"] = bytes(packet.decoded.payload)
|
|
827
|
+
try:
|
|
828
|
+
decoded["portnum"] = portnums_pb2.PortNum.Name(int(packet.decoded.portnum))
|
|
829
|
+
except ValueError:
|
|
830
|
+
decoded["portnum"] = int(packet.decoded.portnum)
|
|
831
|
+
if getattr(packet.decoded, "want_response", False):
|
|
832
|
+
decoded["wantResponse"] = True
|
|
833
|
+
if getattr(packet.decoded, "request_id", 0):
|
|
834
|
+
decoded["requestId"] = int(packet.decoded.request_id)
|
|
835
|
+
if getattr(packet.decoded, "reply_id", 0):
|
|
836
|
+
decoded["replyId"] = int(packet.decoded.reply_id)
|
|
837
|
+
if getattr(packet.decoded, "emoji", 0):
|
|
838
|
+
decoded["emoji"] = int(packet.decoded.emoji)
|
|
839
|
+
|
|
840
|
+
handler = protocols.get(int(packet.decoded.portnum))
|
|
841
|
+
if handler is not None and getattr(handler, "protobufFactory", None) is not None:
|
|
842
|
+
try:
|
|
843
|
+
pb = handler.protobufFactory()
|
|
844
|
+
pb.ParseFromString(bytes(packet.decoded.payload))
|
|
845
|
+
decoded[handler.name] = MessageToDict(
|
|
846
|
+
pb,
|
|
847
|
+
preserving_proto_field_name=False,
|
|
848
|
+
use_integers_for_enums=False,
|
|
849
|
+
)
|
|
850
|
+
except Exception:
|
|
851
|
+
pass
|
|
852
|
+
return packet_dict
|
|
853
|
+
|
|
854
|
+
def _resolve_destination_compat(self, destination: Union[int, str]) -> int:
|
|
855
|
+
if destination == BROADCAST_ADDR:
|
|
856
|
+
return BROADCAST_NUM
|
|
857
|
+
if destination == LOCAL_ADDR:
|
|
858
|
+
return self.node_num
|
|
859
|
+
return self._resolve_destination(destination)
|
|
860
|
+
|
|
861
|
+
def _handle_compat_response_packet(self, packet, interface=None) -> None:
|
|
862
|
+
if interface is not self:
|
|
863
|
+
return
|
|
864
|
+
raw_packet = packet.get("raw") if isinstance(packet, dict) else None
|
|
865
|
+
if not isinstance(raw_packet, mesh_pb2.MeshPacket) or not raw_packet.HasField("decoded"):
|
|
866
|
+
return
|
|
867
|
+
request_id = int(getattr(raw_packet.decoded, "request_id", 0))
|
|
868
|
+
if request_id <= 0 or is_ack(raw_packet) or is_nak(raw_packet):
|
|
869
|
+
return
|
|
870
|
+
handler = self._response_handlers.pop(request_id, None)
|
|
871
|
+
if handler is not None:
|
|
872
|
+
handler["callback"](packet)
|
|
873
|
+
|
|
874
|
+
def _handle_compat_ack(self, packet, routing=None, addr=None, pending=None) -> None:
|
|
875
|
+
del routing, addr, pending
|
|
876
|
+
request_id = int(getattr(packet.decoded, "request_id", 0)) if packet.HasField("decoded") else 0
|
|
877
|
+
handler = self._response_handlers.get(request_id)
|
|
878
|
+
if handler is None or not handler["ack_permitted"]:
|
|
879
|
+
return
|
|
880
|
+
self._response_handlers.pop(request_id, None)
|
|
881
|
+
handler["callback"](self._mesh_interface_packet_dict(packet))
|
|
882
|
+
|
|
883
|
+
def _handle_compat_nak(self, packet, routing=None, addr=None, pending=None) -> None:
|
|
884
|
+
del routing, addr, pending
|
|
885
|
+
request_id = int(getattr(packet.decoded, "request_id", 0)) if packet.HasField("decoded") else 0
|
|
886
|
+
handler = self._response_handlers.pop(request_id, None)
|
|
887
|
+
if handler is not None:
|
|
888
|
+
handler["callback"](self._mesh_interface_packet_dict(packet))
|
|
889
|
+
|
|
625
890
|
def _broadcast_loop(self) -> None:
|
|
626
891
|
nodeinfo_interval = int(self.config.broadcasts.nodeinfo_interval_seconds)
|
|
627
892
|
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
|