vnode 0.1.5__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.5 → vnode-0.1.7}/PKG-INFO +32 -13
- {vnode-0.1.5 → vnode-0.1.7}/README.md +31 -12
- {vnode-0.1.5 → vnode-0.1.7}/pyproject.toml +1 -1
- {vnode-0.1.5 → vnode-0.1.7}/vnode/vnode/runtime.py +215 -266
- {vnode-0.1.5 → vnode-0.1.7}/LICENSE +0 -0
- {vnode-0.1.5 → vnode-0.1.7}/vnode/vnode/__init__.py +0 -0
- {vnode-0.1.5 → vnode-0.1.7}/vnode/vnode/__main__.py +0 -0
- {vnode-0.1.5 → vnode-0.1.7}/vnode/vnode/cli.py +0 -0
- {vnode-0.1.5 → vnode-0.1.7}/vnode/vnode/config.py +0 -0
- {vnode-0.1.5 → vnode-0.1.7}/vnode/vnode/crypto.py +0 -0
- {vnode-0.1.5 → 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).
|
|
@@ -39,6 +53,8 @@ If `node.json` does not exist yet, the runtime will create it automatically from
|
|
|
39
53
|
|
|
40
54
|
If the template leaves `node_id` blank, the runtime will generate and persist a random
|
|
41
55
|
Meshtastic-style node ID when it creates or first loads `node.json`.
|
|
56
|
+
If `long_name` or `short_name` are blank, the runtime will also generate them from the
|
|
57
|
+
last 4 hex characters of the node ID, for example `Meshtastic 691c` and `691c`.
|
|
42
58
|
|
|
43
59
|
The runtime will generate and persist a PKI private key into `node.json` on first run if
|
|
44
60
|
the private key is blank.
|
|
@@ -56,7 +72,6 @@ broadcasts.
|
|
|
56
72
|
## Run
|
|
57
73
|
|
|
58
74
|
```bash
|
|
59
|
-
.venv/bin/pip install -e .
|
|
60
75
|
.venv/bin/python -m vnode --vnode-file node.json run
|
|
61
76
|
```
|
|
62
77
|
|
|
@@ -82,32 +97,36 @@ node = VirtualNode("node.json")
|
|
|
82
97
|
packet_id = node.send_text("!1234abcd", "hello")
|
|
83
98
|
```
|
|
84
99
|
|
|
85
|
-
|
|
100
|
+
Supported `VirtualNode` calls:
|
|
86
101
|
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
|
|
90
|
-
|
|
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()`
|
|
91
108
|
|
|
92
109
|
## Examples
|
|
93
110
|
|
|
94
111
|
Use these as small runnable references for common tasks:
|
|
95
112
|
|
|
96
113
|
- `examples/autoresponder.py`: DM-only reply bot for inbound direct text messages
|
|
97
|
-
- `examples/
|
|
114
|
+
- `examples/basic_subscriptions.py`: minimal `node.receive()` subscription example
|
|
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`
|
|
117
|
+
- `examples/serial_or_vnode.py`: try a serial Meshtastic node first, then fall back to `VirtualNode`
|
|
98
118
|
- `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
|
|
119
|
+
- `examples/library_embed.py`: minimal application-style embedding example using `VirtualNode` directly
|
|
102
120
|
- `examples/watch_reliability.py`: watcher for ACK, NAK, retry, and retransmit-failure events
|
|
103
121
|
|
|
104
122
|
```bash
|
|
105
123
|
.venv/bin/python examples/autoresponder.py
|
|
124
|
+
.venv/bin/python examples/basic_subscriptions.py
|
|
106
125
|
.venv/bin/python examples/listen_packets.py
|
|
126
|
+
.venv/bin/python examples/mesh_interface_compat.py
|
|
127
|
+
.venv/bin/python examples/serial_or_vnode.py
|
|
107
128
|
.venv/bin/python examples/send_dm.py --to '!1234abcd' --message 'hello'
|
|
108
129
|
.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
130
|
.venv/bin/python examples/watch_reliability.py --to '!1234abcd' --message 'hello'
|
|
112
131
|
```
|
|
113
132
|
|
|
@@ -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).
|
|
@@ -15,6 +29,8 @@ If `node.json` does not exist yet, the runtime will create it automatically from
|
|
|
15
29
|
|
|
16
30
|
If the template leaves `node_id` blank, the runtime will generate and persist a random
|
|
17
31
|
Meshtastic-style node ID when it creates or first loads `node.json`.
|
|
32
|
+
If `long_name` or `short_name` are blank, the runtime will also generate them from the
|
|
33
|
+
last 4 hex characters of the node ID, for example `Meshtastic 691c` and `691c`.
|
|
18
34
|
|
|
19
35
|
The runtime will generate and persist a PKI private key into `node.json` on first run if
|
|
20
36
|
the private key is blank.
|
|
@@ -32,7 +48,6 @@ broadcasts.
|
|
|
32
48
|
## Run
|
|
33
49
|
|
|
34
50
|
```bash
|
|
35
|
-
.venv/bin/pip install -e .
|
|
36
51
|
.venv/bin/python -m vnode --vnode-file node.json run
|
|
37
52
|
```
|
|
38
53
|
|
|
@@ -58,32 +73,36 @@ node = VirtualNode("node.json")
|
|
|
58
73
|
packet_id = node.send_text("!1234abcd", "hello")
|
|
59
74
|
```
|
|
60
75
|
|
|
61
|
-
|
|
76
|
+
Supported `VirtualNode` calls:
|
|
62
77
|
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
|
|
66
|
-
|
|
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()`
|
|
67
84
|
|
|
68
85
|
## Examples
|
|
69
86
|
|
|
70
87
|
Use these as small runnable references for common tasks:
|
|
71
88
|
|
|
72
89
|
- `examples/autoresponder.py`: DM-only reply bot for inbound direct text messages
|
|
73
|
-
- `examples/
|
|
90
|
+
- `examples/basic_subscriptions.py`: minimal `node.receive()` subscription example
|
|
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`
|
|
93
|
+
- `examples/serial_or_vnode.py`: try a serial Meshtastic node first, then fall back to `VirtualNode`
|
|
74
94
|
- `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
|
|
95
|
+
- `examples/library_embed.py`: minimal application-style embedding example using `VirtualNode` directly
|
|
78
96
|
- `examples/watch_reliability.py`: watcher for ACK, NAK, retry, and retransmit-failure events
|
|
79
97
|
|
|
80
98
|
```bash
|
|
81
99
|
.venv/bin/python examples/autoresponder.py
|
|
100
|
+
.venv/bin/python examples/basic_subscriptions.py
|
|
82
101
|
.venv/bin/python examples/listen_packets.py
|
|
102
|
+
.venv/bin/python examples/mesh_interface_compat.py
|
|
103
|
+
.venv/bin/python examples/serial_or_vnode.py
|
|
83
104
|
.venv/bin/python examples/send_dm.py --to '!1234abcd' --message 'hello'
|
|
84
105
|
.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
106
|
.venv/bin/python examples/watch_reliability.py --to '!1234abcd' --message 'hello'
|
|
88
107
|
```
|
|
89
108
|
|
|
@@ -5,13 +5,12 @@ 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
|
-
import meshdb
|
|
12
10
|
from google.protobuf.json_format import MessageToDict
|
|
11
|
+
import meshdb
|
|
13
12
|
from google.protobuf.message import DecodeError
|
|
14
|
-
from meshtastic import BROADCAST_ADDR, 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
|
|
15
14
|
from mudp import UDPPacketStream
|
|
16
15
|
from mudp.encryption import encrypt_packet as mudp_encrypt_packet
|
|
17
16
|
from mudp.reliability import is_ack, is_nak, publish_ack, register_pending_ack, send_ack
|
|
@@ -64,9 +63,9 @@ def resolve_role(value: Union[str, int]) -> int:
|
|
|
64
63
|
|
|
65
64
|
|
|
66
65
|
class VirtualNode:
|
|
67
|
-
RECEIVE_TOPIC = "mesh.rx.packet"
|
|
68
66
|
PACKET_TOPIC = "mesh.rx.unique_packet"
|
|
69
67
|
DUPLICATE_TOPIC = "mesh.rx.duplicate"
|
|
68
|
+
RECEIVE_TOPIC = "meshtastic.receive"
|
|
70
69
|
|
|
71
70
|
def __init__(self, config_path: Union[str, Path] = "node.json") -> None:
|
|
72
71
|
self.config_path = Path(config_path).resolve()
|
|
@@ -84,11 +83,7 @@ class VirtualNode:
|
|
|
84
83
|
self._last_nodeinfo_sent_monotonic = 0.0
|
|
85
84
|
self._last_position_reply_monotonic = 0.0
|
|
86
85
|
self._last_nodeinfo_seen: Dict[int, int] = {}
|
|
87
|
-
self._response_handlers: Dict[int,
|
|
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
|
|
86
|
+
self._response_handlers: Dict[int, Dict[str, Any]] = {}
|
|
92
87
|
|
|
93
88
|
self._ensure_security_keys()
|
|
94
89
|
self._write_public_key_file()
|
|
@@ -129,7 +124,6 @@ class VirtualNode:
|
|
|
129
124
|
is_licensed=int(self.config.is_licensed),
|
|
130
125
|
public_key=self._public_key_b64,
|
|
131
126
|
)
|
|
132
|
-
self._upsert_local_node_cache()
|
|
133
127
|
|
|
134
128
|
def _configure_mudp_globals(self) -> None:
|
|
135
129
|
mudp_node.node_id = self.config.node_id
|
|
@@ -150,6 +144,9 @@ class VirtualNode:
|
|
|
150
144
|
return
|
|
151
145
|
pub.subscribe(self._handle_raw_packet, "mesh.rx.packet")
|
|
152
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")
|
|
153
150
|
self.stream = UDPPacketStream(
|
|
154
151
|
self.config.udp.mcast_group,
|
|
155
152
|
int(self.config.udp.mcast_port),
|
|
@@ -157,10 +154,6 @@ class VirtualNode:
|
|
|
157
154
|
parse_payload=False,
|
|
158
155
|
)
|
|
159
156
|
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
157
|
if self.config.broadcasts.send_startup_nodeinfo:
|
|
165
158
|
self.send_nodeinfo()
|
|
166
159
|
self._broadcast_thread = threading.Thread(
|
|
@@ -172,8 +165,6 @@ class VirtualNode:
|
|
|
172
165
|
|
|
173
166
|
def stop(self) -> None:
|
|
174
167
|
self._stop.set()
|
|
175
|
-
was_connected = self._connected
|
|
176
|
-
self._connected = False
|
|
177
168
|
if self.stream is not None:
|
|
178
169
|
self.stream.stop()
|
|
179
170
|
self.stream = None
|
|
@@ -185,11 +176,17 @@ class VirtualNode:
|
|
|
185
176
|
pub.unsubscribe(self._handle_unique_packet, self.PACKET_TOPIC)
|
|
186
177
|
except KeyError:
|
|
187
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
|
|
188
188
|
if self._broadcast_thread and self._broadcast_thread.is_alive():
|
|
189
189
|
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
190
|
|
|
194
191
|
def run_forever(self) -> None:
|
|
195
192
|
self.start()
|
|
@@ -199,30 +196,57 @@ class VirtualNode:
|
|
|
199
196
|
finally:
|
|
200
197
|
self.stop()
|
|
201
198
|
|
|
202
|
-
def receive(self, callback
|
|
203
|
-
pub.subscribe(callback,
|
|
199
|
+
def receive(self, callback) -> None:
|
|
200
|
+
pub.subscribe(callback, self.RECEIVE_TOPIC)
|
|
204
201
|
|
|
205
|
-
def unreceive(self, callback
|
|
202
|
+
def unreceive(self, callback) -> None:
|
|
206
203
|
try:
|
|
207
|
-
pub.unsubscribe(callback,
|
|
204
|
+
pub.unsubscribe(callback, self.RECEIVE_TOPIC)
|
|
208
205
|
except KeyError:
|
|
209
206
|
pass
|
|
210
207
|
|
|
211
208
|
def close(self) -> None:
|
|
212
209
|
self.stop()
|
|
213
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
|
+
|
|
214
238
|
def sendText(
|
|
215
239
|
self,
|
|
216
240
|
text: str,
|
|
217
241
|
destinationId: Union[int, str] = BROADCAST_ADDR,
|
|
218
242
|
wantAck: bool = False,
|
|
219
243
|
wantResponse: bool = False,
|
|
220
|
-
onResponse
|
|
244
|
+
onResponse=None,
|
|
221
245
|
channelIndex: int = 0,
|
|
222
246
|
portNum: int = portnums_pb2.PortNum.TEXT_MESSAGE_APP,
|
|
223
247
|
replyId: Optional[int] = None,
|
|
224
248
|
hopLimit: Optional[int] = None,
|
|
225
|
-
)
|
|
249
|
+
):
|
|
226
250
|
return self.sendData(
|
|
227
251
|
text.encode("utf-8"),
|
|
228
252
|
destinationId=destinationId,
|
|
@@ -235,51 +259,78 @@ class VirtualNode:
|
|
|
235
259
|
replyId=replyId,
|
|
236
260
|
)
|
|
237
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
|
+
|
|
238
282
|
def sendData(
|
|
239
283
|
self,
|
|
240
|
-
data
|
|
284
|
+
data,
|
|
241
285
|
destinationId: Union[int, str] = BROADCAST_ADDR,
|
|
242
286
|
portNum: int = portnums_pb2.PortNum.PRIVATE_APP,
|
|
243
287
|
wantAck: bool = False,
|
|
244
288
|
wantResponse: bool = False,
|
|
245
|
-
onResponse
|
|
289
|
+
onResponse=None,
|
|
246
290
|
onResponseAckPermitted: bool = False,
|
|
247
291
|
channelIndex: int = 0,
|
|
248
292
|
hopLimit: Optional[int] = None,
|
|
249
|
-
pkiEncrypted: bool = False,
|
|
293
|
+
pkiEncrypted: Optional[bool] = False,
|
|
250
294
|
publicKey: Optional[bytes] = None,
|
|
251
295
|
priority: int = mesh_pb2.MeshPacket.Priority.RELIABLE,
|
|
252
296
|
replyId: Optional[int] = None,
|
|
253
|
-
)
|
|
254
|
-
if channelIndex != 0:
|
|
255
|
-
raise ValueError("vnode currently supports only channelIndex=0")
|
|
297
|
+
):
|
|
256
298
|
if getattr(data, "SerializeToString", None):
|
|
257
299
|
data = data.SerializeToString()
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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)
|
|
267
317
|
if replyId is not None:
|
|
268
|
-
|
|
318
|
+
data_message.reply_id = int(replyId)
|
|
269
319
|
|
|
270
|
-
packet = self.
|
|
271
|
-
|
|
272
|
-
destination=
|
|
273
|
-
force_pki=
|
|
320
|
+
packet = self._send_data_packet(
|
|
321
|
+
data_message,
|
|
322
|
+
destination=int(destination),
|
|
323
|
+
force_pki=force_pki,
|
|
274
324
|
hop_limit=hopLimit,
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
priority=priority,
|
|
278
|
-
remote_public_key=publicKey,
|
|
325
|
+
want_ack=wantAck,
|
|
326
|
+
priority=int(priority),
|
|
279
327
|
)
|
|
280
328
|
if onResponse is not None:
|
|
281
|
-
self._response_handlers[int(packet.id)] =
|
|
282
|
-
|
|
329
|
+
self._response_handlers[int(packet.id)] = {
|
|
330
|
+
"callback": onResponse,
|
|
331
|
+
"ack_permitted": bool(onResponseAckPermitted) or getattr(onResponse, "__name__", "") == "onAckNak",
|
|
332
|
+
}
|
|
333
|
+
return packet
|
|
283
334
|
|
|
284
335
|
def sendPosition(
|
|
285
336
|
self,
|
|
@@ -291,12 +342,12 @@ class VirtualNode:
|
|
|
291
342
|
wantResponse: bool = False,
|
|
292
343
|
channelIndex: int = 0,
|
|
293
344
|
hopLimit: Optional[int] = None,
|
|
294
|
-
)
|
|
345
|
+
):
|
|
295
346
|
position = mesh_pb2.Position()
|
|
296
347
|
if latitude != 0.0:
|
|
297
|
-
position.latitude_i = int(latitude
|
|
348
|
+
position.latitude_i = int(latitude * 1e7)
|
|
298
349
|
if longitude != 0.0:
|
|
299
|
-
position.longitude_i = int(longitude
|
|
350
|
+
position.longitude_i = int(longitude * 1e7)
|
|
300
351
|
if altitude != 0:
|
|
301
352
|
position.altitude = int(altitude)
|
|
302
353
|
return self.sendData(
|
|
@@ -309,23 +360,6 @@ class VirtualNode:
|
|
|
309
360
|
hopLimit=hopLimit,
|
|
310
361
|
)
|
|
311
362
|
|
|
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
363
|
def connect_send_socket(self) -> None:
|
|
330
364
|
if getattr(conn, "socket", None) is None:
|
|
331
365
|
conn.setup_multicast(self.config.udp.mcast_group, int(self.config.udp.mcast_port))
|
|
@@ -525,18 +559,20 @@ class VirtualNode:
|
|
|
525
559
|
hop_limit: Optional[int] = None,
|
|
526
560
|
hop_start: Optional[int] = None,
|
|
527
561
|
want_ack: bool = False,
|
|
562
|
+
priority: Optional[int] = None,
|
|
528
563
|
) -> int:
|
|
529
|
-
packet = self.
|
|
564
|
+
packet = self._send_data_packet(
|
|
530
565
|
data,
|
|
531
566
|
destination=destination,
|
|
532
567
|
force_pki=force_pki,
|
|
533
568
|
hop_limit=hop_limit,
|
|
534
569
|
hop_start=hop_start,
|
|
535
570
|
want_ack=want_ack,
|
|
571
|
+
priority=priority,
|
|
536
572
|
)
|
|
537
573
|
return packet.id
|
|
538
574
|
|
|
539
|
-
def
|
|
575
|
+
def _send_data_packet(
|
|
540
576
|
self,
|
|
541
577
|
data: mesh_pb2.Data,
|
|
542
578
|
*,
|
|
@@ -546,7 +582,6 @@ class VirtualNode:
|
|
|
546
582
|
hop_start: Optional[int] = None,
|
|
547
583
|
want_ack: bool = False,
|
|
548
584
|
priority: Optional[int] = None,
|
|
549
|
-
remote_public_key: Optional[bytes] = None,
|
|
550
585
|
) -> mesh_pb2.MeshPacket:
|
|
551
586
|
self.connect_send_socket()
|
|
552
587
|
packet = mesh_pb2.MeshPacket()
|
|
@@ -564,15 +599,14 @@ class VirtualNode:
|
|
|
564
599
|
packet.hop_start = resolved_hop_start
|
|
565
600
|
|
|
566
601
|
if force_pki:
|
|
567
|
-
|
|
568
|
-
if
|
|
602
|
+
remote_public_key = self._lookup_public_key(destination)
|
|
603
|
+
if remote_public_key is None:
|
|
569
604
|
raise ValueError(f"Destination {destination} does not have a stored public key in meshdb")
|
|
570
605
|
packet.channel = 0
|
|
571
606
|
packet.pki_encrypted = True
|
|
572
|
-
packet.public_key = key_bytes
|
|
573
607
|
packet.encrypted = encrypt_dm(
|
|
574
608
|
sender_private_key=b64_decode(self.config.security.private_key),
|
|
575
|
-
receiver_public_key=
|
|
609
|
+
receiver_public_key=remote_public_key,
|
|
576
610
|
packet_id=packet.id,
|
|
577
611
|
from_node=self.node_num,
|
|
578
612
|
plaintext=data.SerializeToString(),
|
|
@@ -591,19 +625,18 @@ class VirtualNode:
|
|
|
591
625
|
self._persist_outbound_packet(packet, data)
|
|
592
626
|
return packet
|
|
593
627
|
|
|
594
|
-
def _handle_raw_packet(self, packet: mesh_pb2.MeshPacket,
|
|
595
|
-
del
|
|
628
|
+
def _handle_raw_packet(self, packet: mesh_pb2.MeshPacket, addr: Any = None) -> None:
|
|
629
|
+
del addr
|
|
596
630
|
if not getattr(packet, "rx_time", 0):
|
|
597
631
|
packet.rx_time = int(time.time())
|
|
598
632
|
|
|
599
633
|
if not packet.HasField("decoded"):
|
|
600
634
|
self._try_decode_pki(packet)
|
|
601
|
-
self._publish_meshtastic_receive(packet)
|
|
602
635
|
self._maybe_send_ack(packet)
|
|
603
636
|
self._maybe_send_response(packet)
|
|
604
637
|
|
|
605
|
-
def _handle_unique_packet(self, packet: mesh_pb2.MeshPacket,
|
|
606
|
-
del
|
|
638
|
+
def _handle_unique_packet(self, packet: mesh_pb2.MeshPacket, addr: Any = None) -> None:
|
|
639
|
+
del addr
|
|
607
640
|
if not getattr(packet, "rx_time", 0):
|
|
608
641
|
packet.rx_time = int(time.time())
|
|
609
642
|
if not packet.HasField("decoded"):
|
|
@@ -612,186 +645,7 @@ class VirtualNode:
|
|
|
612
645
|
return
|
|
613
646
|
|
|
614
647
|
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)
|
|
648
|
+
self._publish_receive(packet)
|
|
795
649
|
|
|
796
650
|
def _try_decode_pki(self, packet: mesh_pb2.MeshPacket) -> bool:
|
|
797
651
|
if packet.channel != 0 or packet.to != self.node_num or not packet.encrypted:
|
|
@@ -933,6 +787,103 @@ class VirtualNode:
|
|
|
933
787
|
db_path=self.meshdb_path,
|
|
934
788
|
)
|
|
935
789
|
|
|
790
|
+
def _publish_receive(self, packet: mesh_pb2.MeshPacket) -> None:
|
|
791
|
+
packet_dict = self._mesh_interface_packet_dict(packet)
|
|
792
|
+
|
|
793
|
+
topic = self.RECEIVE_TOPIC
|
|
794
|
+
decoded = packet_dict.get("decoded")
|
|
795
|
+
portnum = None
|
|
796
|
+
if isinstance(decoded, dict):
|
|
797
|
+
raw_portnum = decoded.get("portnum")
|
|
798
|
+
if raw_portnum is not None:
|
|
799
|
+
portnum = raw_portnum
|
|
800
|
+
|
|
801
|
+
if portnum is not None:
|
|
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
|
|
807
|
+
if handler is not None:
|
|
808
|
+
topic = f"{self.RECEIVE_TOPIC}.{handler.name}"
|
|
809
|
+
else:
|
|
810
|
+
if isinstance(portnum, str):
|
|
811
|
+
topic = f"{self.RECEIVE_TOPIC}.data.{portnum}"
|
|
812
|
+
else:
|
|
813
|
+
topic = f"{self.RECEIVE_TOPIC}.data.{portnum}"
|
|
814
|
+
|
|
815
|
+
pub.sendMessage(topic, packet=packet_dict, interface=self)
|
|
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
|
+
|
|
936
887
|
def _broadcast_loop(self) -> None:
|
|
937
888
|
nodeinfo_interval = int(self.config.broadcasts.nodeinfo_interval_seconds)
|
|
938
889
|
position_interval = int(self.config.position.position_interval_seconds)
|
|
@@ -966,8 +917,6 @@ class VirtualNode:
|
|
|
966
917
|
if isinstance(destination, int):
|
|
967
918
|
return destination
|
|
968
919
|
text = str(destination).strip()
|
|
969
|
-
if text == BROADCAST_ADDR:
|
|
970
|
-
return BROADCAST_NUM
|
|
971
920
|
if text.startswith("!"):
|
|
972
921
|
return parse_node_id(text)
|
|
973
922
|
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
|