meshcore 2.1.17__py3-none-any.whl → 2.2.2__py3-none-any.whl
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.
- meshcore/commands/__init__.py +6 -1
- meshcore/commands/base.py +6 -2
- meshcore/commands/binary.py +111 -0
- meshcore/commands/contact.py +2 -2
- meshcore/commands/control_data.py +50 -0
- meshcore/commands/device.py +59 -45
- meshcore/commands/messaging.py +35 -4
- meshcore/events.py +6 -0
- meshcore/packets.py +10 -0
- meshcore/reader.py +298 -123
- {meshcore-2.1.17.dist-info → meshcore-2.2.2.dist-info}/METADATA +1 -1
- meshcore-2.2.2.dist-info/RECORD +22 -0
- meshcore-2.1.17.dist-info/RECORD +0 -21
- {meshcore-2.1.17.dist-info → meshcore-2.2.2.dist-info}/WHEEL +0 -0
- {meshcore-2.1.17.dist-info → meshcore-2.2.2.dist-info}/licenses/LICENSE +0 -0
meshcore/commands/__init__.py
CHANGED
|
@@ -7,10 +7,15 @@ from .binary import BinaryCommandHandler
|
|
|
7
7
|
from .contact import ContactCommands
|
|
8
8
|
from .device import DeviceCommands
|
|
9
9
|
from .messaging import MessagingCommands
|
|
10
|
+
from .control_data import ControlDataCommandHandler
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class CommandHandler(
|
|
13
|
-
DeviceCommands,
|
|
14
|
+
DeviceCommands,
|
|
15
|
+
ContactCommands,
|
|
16
|
+
MessagingCommands,
|
|
17
|
+
BinaryCommandHandler,
|
|
18
|
+
ControlDataCommandHandler
|
|
14
19
|
):
|
|
15
20
|
pass
|
|
16
21
|
|
meshcore/commands/base.py
CHANGED
|
@@ -32,10 +32,14 @@ def _validate_destination(dst: DestinationType, prefix_length: int = 6) -> bytes
|
|
|
32
32
|
"""
|
|
33
33
|
if isinstance(dst, bytes):
|
|
34
34
|
# Already bytes, use directly
|
|
35
|
+
if len(dst)<prefix_length:
|
|
36
|
+
raise ValueError(f"Invalid prefix len, expecting {prefix_length}, got {len(dst)}")
|
|
35
37
|
return dst[:prefix_length]
|
|
36
38
|
elif isinstance(dst, str):
|
|
37
39
|
# Hex string, convert to bytes
|
|
38
40
|
try:
|
|
41
|
+
if len(dst)<2*prefix_length:
|
|
42
|
+
raise ValueError(f"Invalid prefix len, expecting {prefix_length}, got {len(dst)/2}")
|
|
39
43
|
return bytes.fromhex(dst)[:prefix_length]
|
|
40
44
|
except ValueError:
|
|
41
45
|
raise ValueError(f"Invalid public key hex string: {dst}")
|
|
@@ -163,7 +167,7 @@ class CommandHandlerBase:
|
|
|
163
167
|
return Event(EventType.OK, {})
|
|
164
168
|
|
|
165
169
|
# attached at base because its a common method
|
|
166
|
-
async def send_binary_req(self, dst: DestinationType, request_type: BinaryReqType, data: Optional[bytes] = None, timeout=None, min_timeout=0) -> Event:
|
|
170
|
+
async def send_binary_req(self, dst: DestinationType, request_type: BinaryReqType, data: Optional[bytes] = None, context={}, timeout=None, min_timeout=0) -> Event:
|
|
167
171
|
dst_bytes = _validate_destination(dst, prefix_length=32)
|
|
168
172
|
pubkey_prefix = _validate_destination(dst, prefix_length=6)
|
|
169
173
|
logger.debug(f"Binary request to {dst_bytes.hex()}")
|
|
@@ -180,6 +184,6 @@ class CommandHandlerBase:
|
|
|
180
184
|
# Use provided timeout or fallback to suggested timeout (with 5s default)
|
|
181
185
|
actual_timeout = timeout if timeout is not None and timeout > 0 else result.payload.get("suggested_timeout", 4000) / 800.0
|
|
182
186
|
actual_timeout = min_timeout if actual_timeout < min_timeout else actual_timeout
|
|
183
|
-
self._reader.register_binary_request(pubkey_prefix.hex(), exp_tag, request_type, actual_timeout)
|
|
187
|
+
self._reader.register_binary_request(pubkey_prefix.hex(), exp_tag, request_type, actual_timeout, context=context)
|
|
184
188
|
|
|
185
189
|
return result
|
meshcore/commands/binary.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import logging
|
|
3
|
+
import random
|
|
2
4
|
|
|
3
5
|
from .base import CommandHandlerBase
|
|
4
6
|
from ..events import EventType
|
|
@@ -131,3 +133,112 @@ class BinaryCommandHandler(CommandHandlerBase):
|
|
|
131
133
|
)
|
|
132
134
|
|
|
133
135
|
return acl_event.payload["acl_data"] if acl_event else None
|
|
136
|
+
|
|
137
|
+
async def req_neighbours_async(self,
|
|
138
|
+
contact,
|
|
139
|
+
count=255,
|
|
140
|
+
offset=0,
|
|
141
|
+
order_by=0,
|
|
142
|
+
pubkey_prefix_length=4,
|
|
143
|
+
timeout=0,
|
|
144
|
+
min_timeout=0
|
|
145
|
+
):
|
|
146
|
+
req = (b"\x00" # version : 0
|
|
147
|
+
+ count.to_bytes(1, "little", signed=False)
|
|
148
|
+
+ offset.to_bytes(2, "little", signed=False)
|
|
149
|
+
+ order_by.to_bytes(1, "little", signed=False)
|
|
150
|
+
+ pubkey_prefix_length.to_bytes(1, "little", signed=False)
|
|
151
|
+
+ random.randint(1, 0xFFFFFFFF).to_bytes(4, "little", signed=False)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
logger.debug(f"Sending binary neighbours req, count: {count}, offset: {offset} {req.hex()}")
|
|
155
|
+
|
|
156
|
+
return await self.send_binary_req (
|
|
157
|
+
contact,
|
|
158
|
+
BinaryReqType.NEIGHBOURS,
|
|
159
|
+
data=req,
|
|
160
|
+
timeout=timeout,
|
|
161
|
+
context={"pubkey_prefix_length": pubkey_prefix_length}
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
async def req_neighbours_sync(self,
|
|
165
|
+
contact,
|
|
166
|
+
count=255,
|
|
167
|
+
offset=0,
|
|
168
|
+
order_by=0,
|
|
169
|
+
pubkey_prefix_length=4,
|
|
170
|
+
timeout=0,
|
|
171
|
+
min_timeout=0
|
|
172
|
+
):
|
|
173
|
+
|
|
174
|
+
res = await self.req_neighbours_async(contact,
|
|
175
|
+
count=count,
|
|
176
|
+
offset=offset,
|
|
177
|
+
order_by=order_by,
|
|
178
|
+
pubkey_prefix_length=pubkey_prefix_length,
|
|
179
|
+
timeout=timeout,
|
|
180
|
+
min_timeout=min_timeout)
|
|
181
|
+
|
|
182
|
+
if res is None or res.type == EventType.ERROR:
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
timeout = res.payload["suggested_timeout"] / 800 if timeout == 0 else timeout
|
|
186
|
+
timeout = timeout if min_timeout < timeout else min_timeout
|
|
187
|
+
|
|
188
|
+
if self.dispatcher is None:
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
# Listen for NEIGHBOUR_RESPONSE
|
|
192
|
+
neighbours_event = await self.dispatcher.wait_for_event(
|
|
193
|
+
EventType.NEIGHBOURS_RESPONSE,
|
|
194
|
+
attribute_filters={"tag": res.payload["expected_ack"].hex()},
|
|
195
|
+
timeout=timeout,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
return neighbours_event.payload if neighbours_event else None
|
|
199
|
+
|
|
200
|
+
# do several queries if not all neighbours have been obtained
|
|
201
|
+
async def fetch_all_neighbours(self,
|
|
202
|
+
contact,
|
|
203
|
+
order_by=0,
|
|
204
|
+
pubkey_prefix_length=4,
|
|
205
|
+
timeout=0,
|
|
206
|
+
min_timeout=0
|
|
207
|
+
):
|
|
208
|
+
|
|
209
|
+
# Initial request
|
|
210
|
+
res = await self.req_neighbours_sync(contact,
|
|
211
|
+
count=255,
|
|
212
|
+
offset=0,
|
|
213
|
+
order_by=order_by,
|
|
214
|
+
pubkey_prefix_length=pubkey_prefix_length,
|
|
215
|
+
timeout=timeout,
|
|
216
|
+
min_timeout=min_timeout)
|
|
217
|
+
|
|
218
|
+
if res is None:
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
neighbours_count = res["neighbours_count"] # total neighbours
|
|
222
|
+
results_count = res["results_count"] # obtained neighbours
|
|
223
|
+
|
|
224
|
+
del res["tag"]
|
|
225
|
+
|
|
226
|
+
while results_count < neighbours_count:
|
|
227
|
+
#await asyncio.sleep(2) # wait 2s before next fetch
|
|
228
|
+
next_res = await self.req_neighbours_sync(contact,
|
|
229
|
+
count=255,
|
|
230
|
+
offset=results_count,
|
|
231
|
+
order_by=order_by,
|
|
232
|
+
pubkey_prefix_length=pubkey_prefix_length,
|
|
233
|
+
timeout=timeout,
|
|
234
|
+
min_timeout=min_timeout+5) # requests are close, so let's have some more timeout
|
|
235
|
+
|
|
236
|
+
if next_res is None :
|
|
237
|
+
return res # caller should check it has everything
|
|
238
|
+
|
|
239
|
+
results_count = results_count + next_res["results_count"]
|
|
240
|
+
|
|
241
|
+
res["results_count"] = results_count
|
|
242
|
+
res["neighbours"] += next_res["neighbours"]
|
|
243
|
+
|
|
244
|
+
return res
|
meshcore/commands/contact.py
CHANGED
|
@@ -124,8 +124,8 @@ class ContactCommands(CommandHandlerBase):
|
|
|
124
124
|
data = (
|
|
125
125
|
b"\x09"
|
|
126
126
|
+ bytes.fromhex(contact["public_key"])
|
|
127
|
-
+ contact["type"].to_bytes(1)
|
|
128
|
-
+ flags.to_bytes(1)
|
|
127
|
+
+ contact["type"].to_bytes(1, "little")
|
|
128
|
+
+ flags.to_bytes(1, "little")
|
|
129
129
|
+ out_path_len.to_bytes(1, "little", signed=True)
|
|
130
130
|
+ bytes.fromhex(out_path_hex)
|
|
131
131
|
+ bytes.fromhex(adv_name_hex)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import random
|
|
3
|
+
|
|
4
|
+
from .base import CommandHandlerBase
|
|
5
|
+
from ..events import EventType, Event
|
|
6
|
+
from ..packets import ControlType, PacketType
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("meshcore")
|
|
9
|
+
|
|
10
|
+
class ControlDataCommandHandler(CommandHandlerBase):
|
|
11
|
+
"""Helper functions to handle binary requests through binary commands"""
|
|
12
|
+
|
|
13
|
+
async def send_control_data (self, control_type: int, payload: bytes) -> Event:
|
|
14
|
+
data = bytearray([PacketType.SEND_CONTROL_DATA.value])
|
|
15
|
+
data.extend(control_type.to_bytes(1, "little", signed = False))
|
|
16
|
+
data.extend(payload)
|
|
17
|
+
|
|
18
|
+
result = await self.send(data, [EventType.OK, EventType.ERROR])
|
|
19
|
+
return result
|
|
20
|
+
|
|
21
|
+
async def send_node_discover_req (
|
|
22
|
+
self,
|
|
23
|
+
filter: int,
|
|
24
|
+
prefix_only: bool=True,
|
|
25
|
+
tag: int=None,
|
|
26
|
+
since: int=None
|
|
27
|
+
) -> Event:
|
|
28
|
+
|
|
29
|
+
if tag is None:
|
|
30
|
+
tag = random.randint(1, 0xFFFFFFFF)
|
|
31
|
+
|
|
32
|
+
data = bytearray()
|
|
33
|
+
data.extend(filter.to_bytes(1, "little", signed=False))
|
|
34
|
+
data.extend(tag.to_bytes(4, "little"))
|
|
35
|
+
if not since is None:
|
|
36
|
+
data.extend(since.to_bytes(4, "little", signed=False))
|
|
37
|
+
|
|
38
|
+
logger.debug(f"sending node discover req {data.hex()}")
|
|
39
|
+
|
|
40
|
+
flags = 0
|
|
41
|
+
flags = flags | 1 if prefix_only else flags
|
|
42
|
+
|
|
43
|
+
res = await self.send_control_data(
|
|
44
|
+
ControlType.NODE_DISCOVER_REQ.value|flags, data)
|
|
45
|
+
|
|
46
|
+
if res is None:
|
|
47
|
+
return None
|
|
48
|
+
else:
|
|
49
|
+
res.payload["tag"] = tag
|
|
50
|
+
return res
|
meshcore/commands/device.py
CHANGED
|
@@ -87,14 +87,18 @@ class DeviceCommands(CommandHandlerBase):
|
|
|
87
87
|
[EventType.OK, EventType.ERROR],
|
|
88
88
|
)
|
|
89
89
|
|
|
90
|
+
# the old set_other_params function has been replaced in
|
|
91
|
+
# favour of set_other_params_from_infos to be more generic
|
|
92
|
+
# stays here for backward compatibility but does not support
|
|
93
|
+
# multi_acks for instance
|
|
90
94
|
async def set_other_params(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
self,
|
|
96
|
+
manual_add_contacts: bool,
|
|
97
|
+
telemetry_mode_base: int,
|
|
98
|
+
telemetry_mode_loc: int,
|
|
99
|
+
telemetry_mode_env: int,
|
|
100
|
+
advert_loc_policy: int,
|
|
101
|
+
) -> Event:
|
|
98
102
|
telemetry_mode = (
|
|
99
103
|
(telemetry_mode_base & 0b11)
|
|
100
104
|
| ((telemetry_mode_loc & 0b11) << 2)
|
|
@@ -102,61 +106,56 @@ class DeviceCommands(CommandHandlerBase):
|
|
|
102
106
|
)
|
|
103
107
|
data = (
|
|
104
108
|
b"\x26"
|
|
105
|
-
+ manual_add_contacts.to_bytes(1)
|
|
106
|
-
+ telemetry_mode.to_bytes(1)
|
|
107
|
-
+ advert_loc_policy.to_bytes(1)
|
|
109
|
+
+ manual_add_contacts.to_bytes(1, "little")
|
|
110
|
+
+ telemetry_mode.to_bytes(1, "little")
|
|
111
|
+
+ advert_loc_policy.to_bytes(1, "little")
|
|
112
|
+
)
|
|
113
|
+
return await self.send(data, [EventType.OK, EventType.ERROR])
|
|
114
|
+
|
|
115
|
+
async def set_other_params_from_infos(self, infos) -> Event:
|
|
116
|
+
telemetry_mode = (
|
|
117
|
+
(infos["telemetry_mode_base"] & 0b11)
|
|
118
|
+
| ((infos["telemetry_mode_loc"] & 0b11) << 2)
|
|
119
|
+
| ((infos["telemetry_mode_env"] & 0b11) << 4)
|
|
120
|
+
)
|
|
121
|
+
data = (
|
|
122
|
+
b"\x26"
|
|
123
|
+
+ infos["manual_add_contacts"].to_bytes(1, "little")
|
|
124
|
+
+ telemetry_mode.to_bytes(1, "little")
|
|
125
|
+
+ infos["adv_loc_policy"].to_bytes(1, "little")
|
|
126
|
+
+ infos["multi_acks"].to_bytes(1, "little")
|
|
108
127
|
)
|
|
109
128
|
return await self.send(data, [EventType.OK, EventType.ERROR])
|
|
110
129
|
|
|
111
130
|
async def set_telemetry_mode_base(self, telemetry_mode_base: int) -> Event:
|
|
112
131
|
infos = (await self.send_appstart()).payload
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
telemetry_mode_base,
|
|
116
|
-
infos["telemetry_mode_loc"],
|
|
117
|
-
infos["telemetry_mode_env"],
|
|
118
|
-
infos["adv_loc_policy"],
|
|
119
|
-
)
|
|
132
|
+
infos["telemetry_mode_base"] = telemetry_mode_base
|
|
133
|
+
return await self.set_other_params_from_infos(infos)
|
|
120
134
|
|
|
121
135
|
async def set_telemetry_mode_loc(self, telemetry_mode_loc: int) -> Event:
|
|
122
136
|
infos = (await self.send_appstart()).payload
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
infos["telemetry_mode_base"],
|
|
126
|
-
telemetry_mode_loc,
|
|
127
|
-
infos["telemetry_mode_env"],
|
|
128
|
-
infos["adv_loc_policy"],
|
|
129
|
-
)
|
|
137
|
+
infos["telemetry_mode_loc"] = telemetry_mode_loc
|
|
138
|
+
return await self.set_other_params_from_infos(infos)
|
|
130
139
|
|
|
131
140
|
async def set_telemetry_mode_env(self, telemetry_mode_env: int) -> Event:
|
|
132
141
|
infos = (await self.send_appstart()).payload
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
infos["telemetry_mode_base"],
|
|
136
|
-
infos["telemetry_mode_loc"],
|
|
137
|
-
telemetry_mode_env,
|
|
138
|
-
infos["adv_loc_policy"],
|
|
139
|
-
)
|
|
142
|
+
infos["telemetry_mode_env"] = telemetry_mode_env
|
|
143
|
+
return await self.set_other_params_from_infos(infos)
|
|
140
144
|
|
|
141
145
|
async def set_manual_add_contacts(self, manual_add_contacts: bool) -> Event:
|
|
142
146
|
infos = (await self.send_appstart()).payload
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
infos["telemetry_mode_base"],
|
|
146
|
-
infos["telemetry_mode_loc"],
|
|
147
|
-
infos["telemetry_mode_env"],
|
|
148
|
-
infos["adv_loc_policy"],
|
|
149
|
-
)
|
|
147
|
+
infos["manual_add_contacts"] = manual_add_contacts
|
|
148
|
+
return await self.set_other_params_from_infos(infos)
|
|
150
149
|
|
|
151
150
|
async def set_advert_loc_policy(self, advert_loc_policy: int) -> Event:
|
|
152
151
|
infos = (await self.send_appstart()).payload
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
)
|
|
152
|
+
infos["adv_loc_policy"] = advert_loc_policy
|
|
153
|
+
return await self.set_other_params_from_infos(infos)
|
|
154
|
+
|
|
155
|
+
async def set_multi_acks(self, multi_acks: int) -> Event:
|
|
156
|
+
infos = (await self.send_appstart()).payload
|
|
157
|
+
infos["multi_acks"] = multi_acks
|
|
158
|
+
return await self.set_other_params_from_infos(infos)
|
|
160
159
|
|
|
161
160
|
async def set_devicepin(self, pin: int) -> Event:
|
|
162
161
|
logger.debug(f"Setting device PIN to: {pin}")
|
|
@@ -206,3 +205,18 @@ class DeviceCommands(CommandHandlerBase):
|
|
|
206
205
|
async def export_private_key(self) -> Event:
|
|
207
206
|
logger.debug("Requesting private key export")
|
|
208
207
|
return await self.send(b"\x17", [EventType.PRIVATE_KEY, EventType.DISABLED, EventType.ERROR])
|
|
208
|
+
|
|
209
|
+
async def get_stats_core(self) -> Event:
|
|
210
|
+
logger.debug("Getting core statistics")
|
|
211
|
+
# CMD_GET_STATS (56) + STATS_TYPE_CORE (0)
|
|
212
|
+
return await self.send(b"\x38\x00", [EventType.STATS_CORE, EventType.ERROR])
|
|
213
|
+
|
|
214
|
+
async def get_stats_radio(self) -> Event:
|
|
215
|
+
logger.debug("Getting radio statistics")
|
|
216
|
+
# CMD_GET_STATS (56) + STATS_TYPE_RADIO (1)
|
|
217
|
+
return await self.send(b"\x38\x01", [EventType.STATS_RADIO, EventType.ERROR])
|
|
218
|
+
|
|
219
|
+
async def get_stats_packets(self) -> Event:
|
|
220
|
+
logger.debug("Getting packet statistics")
|
|
221
|
+
# CMD_GET_STATS (56) + STATS_TYPE_PACKETS (2)
|
|
222
|
+
return await self.send(b"\x38\x02", [EventType.STATS_PACKETS, EventType.ERROR])
|
meshcore/commands/messaging.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import random
|
|
3
3
|
from typing import Optional, Union
|
|
4
|
+
from hashlib import sha256
|
|
4
5
|
|
|
5
6
|
from ..events import Event, EventType
|
|
7
|
+
from ..packets import PacketType
|
|
6
8
|
from .base import CommandHandlerBase, DestinationType, _validate_destination
|
|
7
9
|
|
|
8
10
|
logger = logging.getLogger("meshcore")
|
|
@@ -84,21 +86,33 @@ class MessagingCommands(CommandHandlerBase):
|
|
|
84
86
|
max_attempts=3, max_flood_attempts=2, flood_after=2, timeout=0, min_timeout=0
|
|
85
87
|
) -> Event:
|
|
86
88
|
|
|
87
|
-
|
|
89
|
+
try:
|
|
90
|
+
dst_bytes = _validate_destination(dst, prefix_length=32)
|
|
91
|
+
# with 32 bytes we can reset to flood
|
|
92
|
+
except ValueError:
|
|
93
|
+
# but if we can't, we'll assume we're flood
|
|
94
|
+
dst_bytes = _validate_destination(dst, prefix_length=6)
|
|
88
95
|
contact = self._get_contact_by_prefix(dst_bytes.hex())
|
|
89
96
|
|
|
90
97
|
attempts = 0
|
|
91
98
|
flood_attempts = 0
|
|
92
99
|
if not contact is None :
|
|
93
100
|
flood = contact["out_path_len"] == -1
|
|
101
|
+
if len(dst_bytes) < 32:
|
|
102
|
+
# if we have a contact, then we can get a 32 bytes key !
|
|
103
|
+
dst_bytes = _validate_destination(contact, prefix_length=32)
|
|
94
104
|
else:
|
|
95
|
-
flood
|
|
105
|
+
# we can't know if we're flood without fetching all contacts
|
|
106
|
+
# if we have a full key (meaning we can reset path) consider direct
|
|
107
|
+
# else consider flood
|
|
108
|
+
flood = len(dst_bytes) < 32
|
|
109
|
+
logger.info(f"send_msg_with_retry: can't determine if flood, assume {flood}")
|
|
96
110
|
res = None
|
|
97
111
|
while attempts < max_attempts and res is None \
|
|
98
112
|
and (not flood or flood_attempts < max_flood_attempts):
|
|
99
|
-
if attempts == flood_after : # change path to flood
|
|
113
|
+
if attempts == flood_after and not flood: # change path to flood
|
|
100
114
|
logger.info("Resetting path")
|
|
101
|
-
rp_res = await self.reset_path(
|
|
115
|
+
rp_res = await self.reset_path(dst_bytes)
|
|
102
116
|
if rp_res.type == EventType.ERROR:
|
|
103
117
|
logger.error(f"Couldn't reset path {rp_res} continuing ...")
|
|
104
118
|
else:
|
|
@@ -209,3 +223,20 @@ class MessagingCommands(CommandHandlerBase):
|
|
|
209
223
|
return Event(EventType.ERROR, {"reason": "unsupported_path_type"})
|
|
210
224
|
|
|
211
225
|
return await self.send(cmd_data, [EventType.MSG_SENT, EventType.ERROR])
|
|
226
|
+
|
|
227
|
+
async def set_flood_scope(self, scope):
|
|
228
|
+
if scope.startswith("#"): # an hash
|
|
229
|
+
logger.debug(f"Setting scope from hash {scope}")
|
|
230
|
+
scope_key = sha256(scope.encode("utf-8")).digest()[0:16]
|
|
231
|
+
elif scope == "0" or scope == "None" or scope == "*" or scope == "": # disable
|
|
232
|
+
scope_key = b"\0"*16
|
|
233
|
+
else: # assume the key has been sent directly
|
|
234
|
+
scope_key = scope.encode("utf-8")
|
|
235
|
+
|
|
236
|
+
logger.debug(f"Setting scope to {scope_key.hex()}")
|
|
237
|
+
|
|
238
|
+
cmd_data = bytearray([PacketType.SET_FLOOD_SCOPE.value])
|
|
239
|
+
cmd_data.extend(b"\0")
|
|
240
|
+
cmd_data.extend(scope_key)
|
|
241
|
+
|
|
242
|
+
return await self.send(cmd_data, [EventType.OK, EventType.ERROR])
|
meshcore/events.py
CHANGED
|
@@ -40,10 +40,16 @@ class EventType(Enum):
|
|
|
40
40
|
MMA_RESPONSE = "mma_response"
|
|
41
41
|
ACL_RESPONSE = "acl_response"
|
|
42
42
|
CUSTOM_VARS = "custom_vars"
|
|
43
|
+
STATS_CORE = "stats_core"
|
|
44
|
+
STATS_RADIO = "stats_radio"
|
|
45
|
+
STATS_PACKETS = "stats_packets"
|
|
43
46
|
CHANNEL_INFO = "channel_info"
|
|
44
47
|
PATH_RESPONSE = "path_response"
|
|
45
48
|
PRIVATE_KEY = "private_key"
|
|
46
49
|
DISABLED = "disabled"
|
|
50
|
+
CONTROL_DATA = "control_data"
|
|
51
|
+
DISCOVER_RESPONSE = "discover_response"
|
|
52
|
+
NEIGHBOURS_RESPONSE = "neighbours_response"
|
|
47
53
|
|
|
48
54
|
# Command response types
|
|
49
55
|
OK = "command_ok"
|
meshcore/packets.py
CHANGED
|
@@ -6,6 +6,11 @@ class BinaryReqType(Enum):
|
|
|
6
6
|
TELEMETRY = 0x03
|
|
7
7
|
MMA = 0x04
|
|
8
8
|
ACL = 0x05
|
|
9
|
+
NEIGHBOURS = 0x06
|
|
10
|
+
|
|
11
|
+
class ControlType(Enum):
|
|
12
|
+
NODE_DISCOVER_REQ = 0x80
|
|
13
|
+
NODE_DISCOVER_RESP = 0x90
|
|
9
14
|
|
|
10
15
|
# Packet prefixes for the protocol
|
|
11
16
|
class PacketType(Enum):
|
|
@@ -31,8 +36,12 @@ class PacketType(Enum):
|
|
|
31
36
|
SIGN_START = 19
|
|
32
37
|
SIGNATURE = 20
|
|
33
38
|
CUSTOM_VARS = 21
|
|
39
|
+
STATS = 24
|
|
34
40
|
BINARY_REQ = 50
|
|
35
41
|
FACTORY_RESET = 51
|
|
42
|
+
PATH_DISCOVERY = 52
|
|
43
|
+
SET_FLOOD_SCOPE = 54
|
|
44
|
+
SEND_CONTROL_DATA = 55
|
|
36
45
|
|
|
37
46
|
# Push notifications
|
|
38
47
|
ADVERTISEMENT = 0x80
|
|
@@ -49,3 +58,4 @@ class PacketType(Enum):
|
|
|
49
58
|
TELEMETRY_RESPONSE = 0x8B
|
|
50
59
|
BINARY_RESPONSE = 0x8C
|
|
51
60
|
PATH_DISCOVERY_RESPONSE = 0x8D
|
|
61
|
+
CONTROL_DATA = 0x8E
|
meshcore/reader.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import json
|
|
3
|
+
import struct
|
|
3
4
|
import time
|
|
5
|
+
import io
|
|
4
6
|
from typing import Any, Dict
|
|
5
7
|
from .events import Event, EventType, EventDispatcher
|
|
6
|
-
from .packets import BinaryReqType, PacketType
|
|
8
|
+
from .packets import BinaryReqType, PacketType, ControlType
|
|
7
9
|
from .parsing import lpp_parse, lpp_parse_mma, parse_acl, parse_status
|
|
8
10
|
from cayennelpp import LppFrame, LppData
|
|
9
11
|
from meshcore.lpp_json_encoder import lpp_json_encoder
|
|
@@ -18,20 +20,21 @@ class MessageReader:
|
|
|
18
20
|
# before events are dispatched
|
|
19
21
|
self.contacts = {} # Temporary storage during contact list building
|
|
20
22
|
self.contact_nb = 0 # Used for contact processing
|
|
21
|
-
|
|
23
|
+
|
|
22
24
|
# Track pending binary requests by tag for proper response parsing
|
|
23
25
|
self.pending_binary_requests: Dict[str, Dict[str, Any]] = {} # tag -> {request_type, expires_at}
|
|
24
26
|
|
|
25
|
-
def register_binary_request(self, prefix: str, tag: str, request_type: BinaryReqType, timeout_seconds: float):
|
|
27
|
+
def register_binary_request(self, prefix: str, tag: str, request_type: BinaryReqType, timeout_seconds: float, context={}):
|
|
26
28
|
"""Register a pending binary request for proper response parsing"""
|
|
27
29
|
# Clean up expired requests before adding new one
|
|
28
30
|
self.cleanup_expired_requests()
|
|
29
|
-
|
|
31
|
+
|
|
30
32
|
expires_at = time.time() + timeout_seconds
|
|
31
33
|
self.pending_binary_requests[tag] = {
|
|
32
34
|
"request_type": request_type,
|
|
33
35
|
"pubkey_prefix": prefix,
|
|
34
|
-
"expires_at": expires_at
|
|
36
|
+
"expires_at": expires_at,
|
|
37
|
+
"context": context # optional info we want to keep from req to resp
|
|
35
38
|
}
|
|
36
39
|
logger.debug(f"Registered binary request: tag={tag}, type={request_type}, expires in {timeout_seconds}s")
|
|
37
40
|
|
|
@@ -42,13 +45,18 @@ class MessageReader:
|
|
|
42
45
|
tag for tag, info in self.pending_binary_requests.items()
|
|
43
46
|
if current_time > info["expires_at"]
|
|
44
47
|
]
|
|
45
|
-
|
|
48
|
+
|
|
46
49
|
for tag in expired_tags:
|
|
47
50
|
logger.debug(f"Cleaning up expired binary request: tag={tag}")
|
|
48
51
|
del self.pending_binary_requests[tag]
|
|
49
|
-
|
|
52
|
+
|
|
50
53
|
async def handle_rx(self, data: bytearray):
|
|
51
|
-
|
|
54
|
+
dbuf = io.BytesIO(data)
|
|
55
|
+
try:
|
|
56
|
+
packet_type_value = dbuf.read(1)[0]
|
|
57
|
+
except IndexError as e:
|
|
58
|
+
logger.warning(f"Received empty packet: {e}")
|
|
59
|
+
return
|
|
52
60
|
logger.debug(f"Received data: {data.hex()}")
|
|
53
61
|
|
|
54
62
|
# Handle command responses
|
|
@@ -78,23 +86,24 @@ class MessageReader:
|
|
|
78
86
|
or packet_type_value == PacketType.PUSH_CODE_NEW_ADVERT.value
|
|
79
87
|
):
|
|
80
88
|
c = {}
|
|
81
|
-
c["public_key"] =
|
|
82
|
-
c["type"] =
|
|
83
|
-
c["flags"] =
|
|
84
|
-
|
|
85
|
-
|
|
89
|
+
c["public_key"] = dbuf.read(32).hex()
|
|
90
|
+
c["type"] = dbuf.read(1)[0]
|
|
91
|
+
c["flags"] = dbuf.read(1)[0]
|
|
92
|
+
plen = int.from_bytes(dbuf.read(1), signed=True, byteorder="little")
|
|
93
|
+
c["out_path_len"] = plen
|
|
86
94
|
if plen == -1:
|
|
87
95
|
plen = 0
|
|
88
|
-
|
|
89
|
-
c["
|
|
90
|
-
c["
|
|
96
|
+
path = dbuf.read(64)
|
|
97
|
+
c["out_path"] = path[0:plen].hex()
|
|
98
|
+
c["adv_name"] = dbuf.read(32).decode("utf-8", "ignore").replace("\0", "")
|
|
99
|
+
c["last_advert"] = int.from_bytes(dbuf.read(4), byteorder="little")
|
|
91
100
|
c["adv_lat"] = (
|
|
92
|
-
int.from_bytes(
|
|
101
|
+
int.from_bytes(dbuf.read(4), byteorder="little", signed=True) / 1e6
|
|
93
102
|
)
|
|
94
103
|
c["adv_lon"] = (
|
|
95
|
-
int.from_bytes(
|
|
104
|
+
int.from_bytes(dbuf.read(4), byteorder="little", signed=True) / 1e6
|
|
96
105
|
)
|
|
97
|
-
c["lastmod"] = int.from_bytes(
|
|
106
|
+
c["lastmod"] = int.from_bytes(dbuf.read(4), byteorder="little")
|
|
98
107
|
|
|
99
108
|
if packet_type_value == PacketType.PUSH_CODE_NEW_ADVERT.value:
|
|
100
109
|
await self.dispatcher.dispatch(Event(EventType.NEW_CONTACT, c))
|
|
@@ -103,7 +112,7 @@ class MessageReader:
|
|
|
103
112
|
self.contacts[c["public_key"]] = c
|
|
104
113
|
|
|
105
114
|
elif packet_type_value == PacketType.CONTACT_END.value:
|
|
106
|
-
lastmod = int.from_bytes(
|
|
115
|
+
lastmod = int.from_bytes(dbuf.read(4), byteorder="little")
|
|
107
116
|
attributes = {
|
|
108
117
|
"lastmod": lastmod,
|
|
109
118
|
}
|
|
@@ -113,37 +122,39 @@ class MessageReader:
|
|
|
113
122
|
|
|
114
123
|
elif packet_type_value == PacketType.SELF_INFO.value:
|
|
115
124
|
self_info = {}
|
|
116
|
-
self_info["adv_type"] =
|
|
117
|
-
self_info["tx_power"] =
|
|
118
|
-
self_info["max_tx_power"] =
|
|
119
|
-
self_info["public_key"] =
|
|
125
|
+
self_info["adv_type"] = dbuf.read(1)[0]
|
|
126
|
+
self_info["tx_power"] = dbuf.read(1)[0]
|
|
127
|
+
self_info["max_tx_power"] = dbuf.read(1)[0]
|
|
128
|
+
self_info["public_key"] = dbuf.read(32).hex()
|
|
120
129
|
self_info["adv_lat"] = (
|
|
121
|
-
int.from_bytes(
|
|
130
|
+
int.from_bytes(dbuf.read(4), byteorder="little", signed=True) / 1e6
|
|
122
131
|
)
|
|
123
132
|
self_info["adv_lon"] = (
|
|
124
|
-
int.from_bytes(
|
|
133
|
+
int.from_bytes(dbuf.read(4), byteorder="little", signed=True) / 1e6
|
|
125
134
|
)
|
|
126
|
-
self_info["
|
|
127
|
-
self_info["
|
|
128
|
-
|
|
129
|
-
self_info["
|
|
130
|
-
self_info["
|
|
135
|
+
self_info["multi_acks"] = dbuf.read(1)[0]
|
|
136
|
+
self_info["adv_loc_policy"] = dbuf.read(1)[0]
|
|
137
|
+
telemetry_mode = dbuf.read(1)[0]
|
|
138
|
+
self_info["telemetry_mode_env"] = (telemetry_mode >> 4) & 0b11
|
|
139
|
+
self_info["telemetry_mode_loc"] = (telemetry_mode >> 2) & 0b11
|
|
140
|
+
self_info["telemetry_mode_base"] = (telemetry_mode) & 0b11
|
|
141
|
+
self_info["manual_add_contacts"] = dbuf.read(1)[0] > 0
|
|
131
142
|
self_info["radio_freq"] = (
|
|
132
|
-
int.from_bytes(
|
|
143
|
+
int.from_bytes(dbuf.read(4), byteorder="little") / 1000
|
|
133
144
|
)
|
|
134
145
|
self_info["radio_bw"] = (
|
|
135
|
-
int.from_bytes(
|
|
146
|
+
int.from_bytes(dbuf.read(4), byteorder="little") / 1000
|
|
136
147
|
)
|
|
137
|
-
self_info["radio_sf"] =
|
|
138
|
-
self_info["radio_cr"] =
|
|
139
|
-
self_info["name"] =
|
|
148
|
+
self_info["radio_sf"] = dbuf.read(1)[0]
|
|
149
|
+
self_info["radio_cr"] = dbuf.read(1)[0]
|
|
150
|
+
self_info["name"] = dbuf.read().decode("utf-8", "ignore")
|
|
140
151
|
await self.dispatcher.dispatch(Event(EventType.SELF_INFO, self_info))
|
|
141
152
|
|
|
142
153
|
elif packet_type_value == PacketType.MSG_SENT.value:
|
|
143
154
|
res = {}
|
|
144
|
-
res["type"] =
|
|
145
|
-
res["expected_ack"] =
|
|
146
|
-
res["suggested_timeout"] = int.from_bytes(
|
|
155
|
+
res["type"] = dbuf.read(1)[0]
|
|
156
|
+
res["expected_ack"] = dbuf.read(4)
|
|
157
|
+
res["suggested_timeout"] = int.from_bytes(dbuf.read(4), byteorder="little")
|
|
147
158
|
|
|
148
159
|
attributes = {
|
|
149
160
|
"type": res["type"],
|
|
@@ -155,15 +166,14 @@ class MessageReader:
|
|
|
155
166
|
elif packet_type_value == PacketType.CONTACT_MSG_RECV.value:
|
|
156
167
|
res = {}
|
|
157
168
|
res["type"] = "PRIV"
|
|
158
|
-
res["pubkey_prefix"] =
|
|
159
|
-
res["path_len"] =
|
|
160
|
-
|
|
161
|
-
res["
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
res["
|
|
165
|
-
|
|
166
|
-
res["text"] = data[13:].decode("utf-8", "ignore")
|
|
169
|
+
res["pubkey_prefix"] = dbuf.read(6).hex()
|
|
170
|
+
res["path_len"] = dbuf.read(1)[0]
|
|
171
|
+
txt_type = dbuf.read(1)[0]
|
|
172
|
+
res["txt_type"] = txt_type
|
|
173
|
+
res["sender_timestamp"] = int.from_bytes(dbuf.read(4), byteorder="little")
|
|
174
|
+
if txt_type == 2:
|
|
175
|
+
res["signature"] = dbuf.read(4).hex()
|
|
176
|
+
res["text"] = dbuf.read().decode("utf-8", "ignore")
|
|
167
177
|
|
|
168
178
|
attributes = {
|
|
169
179
|
"pubkey_prefix": res["pubkey_prefix"],
|
|
@@ -177,16 +187,16 @@ class MessageReader:
|
|
|
177
187
|
elif packet_type_value == 16: # A reply to CMD_SYNC_NEXT_MESSAGE (ver >= 3)
|
|
178
188
|
res = {}
|
|
179
189
|
res["type"] = "PRIV"
|
|
180
|
-
res["SNR"] = int.from_bytes(
|
|
181
|
-
|
|
182
|
-
res["
|
|
183
|
-
res["
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
+
res["SNR"] = int.from_bytes(dbuf.read(1), byteorder="little", signed=True) / 4
|
|
191
|
+
dbuf.read(2) # reserved
|
|
192
|
+
res["pubkey_prefix"] = dbuf.read(6).hex()
|
|
193
|
+
res["path_len"] = dbuf.read(1)[0]
|
|
194
|
+
txt_type = dbuf.read(1)[0]
|
|
195
|
+
res["txt_type"] = txt_type
|
|
196
|
+
res["sender_timestamp"] = int.from_bytes(dbuf.read(4), byteorder="little")
|
|
197
|
+
if txt_type == 2:
|
|
198
|
+
res["signature"] = dbuf.read(4).hex()
|
|
199
|
+
res["text"] = dbuf.read().decode("utf-8", "ignore")
|
|
190
200
|
|
|
191
201
|
attributes = {
|
|
192
202
|
"pubkey_prefix": res["pubkey_prefix"],
|
|
@@ -200,11 +210,11 @@ class MessageReader:
|
|
|
200
210
|
elif packet_type_value == PacketType.CHANNEL_MSG_RECV.value:
|
|
201
211
|
res = {}
|
|
202
212
|
res["type"] = "CHAN"
|
|
203
|
-
res["channel_idx"] =
|
|
204
|
-
res["path_len"] =
|
|
205
|
-
res["txt_type"] =
|
|
206
|
-
res["sender_timestamp"] = int.from_bytes(
|
|
207
|
-
res["text"] =
|
|
213
|
+
res["channel_idx"] = dbuf.read(1)[0]
|
|
214
|
+
res["path_len"] = dbuf.read(1)[0]
|
|
215
|
+
res["txt_type"] = dbuf.read(1)[0]
|
|
216
|
+
res["sender_timestamp"] = int.from_bytes(dbuf.read(4), byteorder="little")
|
|
217
|
+
res["text"] = dbuf.read().decode("utf-8", "ignore")
|
|
208
218
|
|
|
209
219
|
attributes = {
|
|
210
220
|
"channel_idx": res["channel_idx"],
|
|
@@ -218,12 +228,13 @@ class MessageReader:
|
|
|
218
228
|
elif packet_type_value == 17: # A reply to CMD_SYNC_NEXT_MESSAGE (ver >= 3)
|
|
219
229
|
res = {}
|
|
220
230
|
res["type"] = "CHAN"
|
|
221
|
-
res["SNR"] = int.from_bytes(
|
|
222
|
-
|
|
223
|
-
res["
|
|
224
|
-
res["
|
|
225
|
-
res["
|
|
226
|
-
res["
|
|
231
|
+
res["SNR"] = int.from_bytes(dbuf.read(1), byteorder="little", signed=True) / 4
|
|
232
|
+
dbuf.read(2) # reserved
|
|
233
|
+
res["channel_idx"] = dbuf.read(1)[0]
|
|
234
|
+
res["path_len"] = dbuf.read(1)[0]
|
|
235
|
+
res["txt_type"] = dbuf.read(1)[0]
|
|
236
|
+
res["sender_timestamp"] = int.from_bytes(dbuf.read(4), byteorder="little")
|
|
237
|
+
res["text"] = dbuf.read().decode("utf-8", "ignore")
|
|
227
238
|
|
|
228
239
|
attributes = {
|
|
229
240
|
"channel_idx": res["channel_idx"],
|
|
@@ -235,7 +246,7 @@ class MessageReader:
|
|
|
235
246
|
)
|
|
236
247
|
|
|
237
248
|
elif packet_type_value == PacketType.CURRENT_TIME.value:
|
|
238
|
-
time_value = int.from_bytes(
|
|
249
|
+
time_value = int.from_bytes(dbuf.read(4), byteorder="little")
|
|
239
250
|
result = {"time": time_value}
|
|
240
251
|
await self.dispatcher.dispatch(Event(EventType.CURRENT_TIME, result))
|
|
241
252
|
|
|
@@ -244,34 +255,34 @@ class MessageReader:
|
|
|
244
255
|
await self.dispatcher.dispatch(Event(EventType.NO_MORE_MSGS, result))
|
|
245
256
|
|
|
246
257
|
elif packet_type_value == PacketType.CONTACT_URI.value:
|
|
247
|
-
contact_uri = "meshcore://" +
|
|
258
|
+
contact_uri = "meshcore://" + dbuf.read().hex()
|
|
248
259
|
result = {"uri": contact_uri}
|
|
249
260
|
await self.dispatcher.dispatch(Event(EventType.CONTACT_URI, result))
|
|
250
261
|
|
|
251
262
|
elif packet_type_value == PacketType.BATTERY.value:
|
|
252
|
-
battery_level = int.from_bytes(
|
|
263
|
+
battery_level = int.from_bytes(dbuf.read(2), byteorder="little")
|
|
253
264
|
result = {"level": battery_level}
|
|
254
265
|
if len(data) > 3: # has storage info as well
|
|
255
|
-
result["used_kb"] = int.from_bytes(
|
|
256
|
-
result["total_kb"] = int.from_bytes(
|
|
266
|
+
result["used_kb"] = int.from_bytes(dbuf.read(4), byteorder="little")
|
|
267
|
+
result["total_kb"] = int.from_bytes(dbuf.read(4), byteorder="little")
|
|
257
268
|
await self.dispatcher.dispatch(Event(EventType.BATTERY, result))
|
|
258
269
|
|
|
259
270
|
elif packet_type_value == PacketType.DEVICE_INFO.value:
|
|
260
271
|
res = {}
|
|
261
|
-
res["fw ver"] =
|
|
272
|
+
res["fw ver"] = dbuf.read(1)[0]
|
|
262
273
|
if data[1] >= 3:
|
|
263
|
-
res["max_contacts"] =
|
|
264
|
-
res["max_channels"] =
|
|
265
|
-
res["ble_pin"] = int.from_bytes(
|
|
266
|
-
res["fw_build"] =
|
|
267
|
-
res["model"] =
|
|
268
|
-
res["ver"] =
|
|
274
|
+
res["max_contacts"] = dbuf.read(1)[0] * 2
|
|
275
|
+
res["max_channels"] = dbuf.read(1)[0]
|
|
276
|
+
res["ble_pin"] = int.from_bytes(dbuf.read(4), byteorder="little")
|
|
277
|
+
res["fw_build"] = dbuf.read(12).decode("utf-8", "ignore").replace("\0", "")
|
|
278
|
+
res["model"] = dbuf.read(40).decode("utf-8", "ignore").replace("\0", "")
|
|
279
|
+
res["ver"] = dbuf.read(20).decode("utf-8", "ignore").replace("\0", "")
|
|
269
280
|
await self.dispatcher.dispatch(Event(EventType.DEVICE_INFO, res))
|
|
270
281
|
|
|
271
282
|
elif packet_type_value == PacketType.CUSTOM_VARS.value:
|
|
272
283
|
logger.debug(f"received custom vars response: {data.hex()}")
|
|
273
284
|
res = {}
|
|
274
|
-
rawdata =
|
|
285
|
+
rawdata = dbuf.read().decode("utf-8", "ignore")
|
|
275
286
|
if not rawdata == "":
|
|
276
287
|
pairs = rawdata.split(",")
|
|
277
288
|
for p in pairs:
|
|
@@ -280,33 +291,114 @@ class MessageReader:
|
|
|
280
291
|
logger.debug(f"got custom vars : {res}")
|
|
281
292
|
await self.dispatcher.dispatch(Event(EventType.CUSTOM_VARS, res))
|
|
282
293
|
|
|
294
|
+
elif packet_type_value == PacketType.STATS.value: # RESP_CODE_STATS (24)
|
|
295
|
+
logger.debug(f"received stats response: {data.hex()}")
|
|
296
|
+
# RESP_CODE_STATS: All stats responses use code 24 with sub-type byte
|
|
297
|
+
# Byte 0: response_code (24), Byte 1: stats_type (0=core, 1=radio, 2=packets)
|
|
298
|
+
if len(data) < 2:
|
|
299
|
+
logger.error(f"Stats response too short: {len(data)} bytes, need at least 2 for header")
|
|
300
|
+
await self.dispatcher.dispatch(Event(EventType.ERROR, {"reason": "invalid_frame_length"}))
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
stats_type = data[1]
|
|
304
|
+
|
|
305
|
+
if stats_type == 0: # STATS_TYPE_CORE
|
|
306
|
+
# RESP_CODE_STATS + STATS_TYPE_CORE: 11 bytes total
|
|
307
|
+
# Format: <B B H I H B (response_code, stats_type, battery_mv, uptime_secs, errors, queue_len)
|
|
308
|
+
if len(data) < 11:
|
|
309
|
+
logger.error(f"Stats core response too short: {len(data)} bytes, expected 11")
|
|
310
|
+
await self.dispatcher.dispatch(Event(EventType.ERROR, {"reason": "invalid_frame_length"}))
|
|
311
|
+
else:
|
|
312
|
+
try:
|
|
313
|
+
battery_mv, uptime_secs, errors, queue_len = struct.unpack('<H I H B', data[2:11])
|
|
314
|
+
res = {
|
|
315
|
+
'battery_mv': battery_mv,
|
|
316
|
+
'uptime_secs': uptime_secs,
|
|
317
|
+
'errors': errors,
|
|
318
|
+
'queue_len': queue_len
|
|
319
|
+
}
|
|
320
|
+
logger.debug(f"parsed stats core: {res}")
|
|
321
|
+
await self.dispatcher.dispatch(Event(EventType.STATS_CORE, res))
|
|
322
|
+
except struct.error as e:
|
|
323
|
+
logger.error(f"Error parsing stats core binary frame: {e}, data: {data.hex()}")
|
|
324
|
+
await self.dispatcher.dispatch(Event(EventType.ERROR, {"reason": f"binary_parse_error: {e}"}))
|
|
325
|
+
|
|
326
|
+
elif stats_type == 1: # STATS_TYPE_RADIO
|
|
327
|
+
# RESP_CODE_STATS + STATS_TYPE_RADIO: 14 bytes total
|
|
328
|
+
# Format: <B B h b b I I (response_code, stats_type, noise_floor, last_rssi, last_snr, tx_air_secs, rx_air_secs)
|
|
329
|
+
if len(data) < 14:
|
|
330
|
+
logger.error(f"Stats radio response too short: {len(data)} bytes, expected 14")
|
|
331
|
+
await self.dispatcher.dispatch(Event(EventType.ERROR, {"reason": "invalid_frame_length"}))
|
|
332
|
+
else:
|
|
333
|
+
try:
|
|
334
|
+
noise_floor, last_rssi, last_snr_scaled, tx_air_secs, rx_air_secs = struct.unpack('<h b b I I', data[2:14])
|
|
335
|
+
res = {
|
|
336
|
+
'noise_floor': noise_floor,
|
|
337
|
+
'last_rssi': last_rssi,
|
|
338
|
+
'last_snr': last_snr_scaled / 4.0, # Unscale SNR (was multiplied by 4)
|
|
339
|
+
'tx_air_secs': tx_air_secs,
|
|
340
|
+
'rx_air_secs': rx_air_secs
|
|
341
|
+
}
|
|
342
|
+
logger.debug(f"parsed stats radio: {res}")
|
|
343
|
+
await self.dispatcher.dispatch(Event(EventType.STATS_RADIO, res))
|
|
344
|
+
except struct.error as e:
|
|
345
|
+
logger.error(f"Error parsing stats radio binary frame: {e}, data: {data.hex()}")
|
|
346
|
+
await self.dispatcher.dispatch(Event(EventType.ERROR, {"reason": f"binary_parse_error: {e}"}))
|
|
347
|
+
|
|
348
|
+
elif stats_type == 2: # STATS_TYPE_PACKETS
|
|
349
|
+
# RESP_CODE_STATS + STATS_TYPE_PACKETS: 26 bytes total
|
|
350
|
+
# Format: <B B I I I I I I (response_code, stats_type, recv, sent, flood_tx, direct_tx, flood_rx, direct_rx)
|
|
351
|
+
if len(data) < 26:
|
|
352
|
+
logger.error(f"Stats packets response too short: {len(data)} bytes, expected 26")
|
|
353
|
+
await self.dispatcher.dispatch(Event(EventType.ERROR, {"reason": "invalid_frame_length"}))
|
|
354
|
+
else:
|
|
355
|
+
try:
|
|
356
|
+
recv, sent, flood_tx, direct_tx, flood_rx, direct_rx = struct.unpack('<I I I I I I', data[2:26])
|
|
357
|
+
res = {
|
|
358
|
+
'recv': recv,
|
|
359
|
+
'sent': sent,
|
|
360
|
+
'flood_tx': flood_tx,
|
|
361
|
+
'direct_tx': direct_tx,
|
|
362
|
+
'flood_rx': flood_rx,
|
|
363
|
+
'direct_rx': direct_rx
|
|
364
|
+
}
|
|
365
|
+
logger.debug(f"parsed stats packets: {res}")
|
|
366
|
+
await self.dispatcher.dispatch(Event(EventType.STATS_PACKETS, res))
|
|
367
|
+
except struct.error as e:
|
|
368
|
+
logger.error(f"Error parsing stats packets binary frame: {e}, data: {data.hex()}")
|
|
369
|
+
await self.dispatcher.dispatch(Event(EventType.ERROR, {"reason": f"binary_parse_error: {e}"}))
|
|
370
|
+
|
|
371
|
+
else:
|
|
372
|
+
logger.error(f"Unknown stats type: {stats_type}, data: {data.hex()}")
|
|
373
|
+
await self.dispatcher.dispatch(Event(EventType.ERROR, {"reason": f"unknown_stats_type: {stats_type}"}))
|
|
374
|
+
|
|
283
375
|
elif packet_type_value == PacketType.CHANNEL_INFO.value:
|
|
284
376
|
logger.debug(f"received channel info response: {data.hex()}")
|
|
285
377
|
res = {}
|
|
286
|
-
res["channel_idx"] =
|
|
378
|
+
res["channel_idx"] = dbuf.read(1)[0]
|
|
287
379
|
|
|
288
380
|
# Channel name is null-terminated, so find the first null byte
|
|
289
|
-
name_bytes =
|
|
381
|
+
name_bytes = dbuf.read(32)
|
|
290
382
|
null_pos = name_bytes.find(0)
|
|
291
383
|
if null_pos >= 0:
|
|
292
384
|
res["channel_name"] = name_bytes[:null_pos].decode("utf-8", "ignore")
|
|
293
385
|
else:
|
|
294
386
|
res["channel_name"] = name_bytes.decode("utf-8", "ignore")
|
|
295
387
|
|
|
296
|
-
res["channel_secret"] =
|
|
388
|
+
res["channel_secret"] = dbuf.read(16)
|
|
297
389
|
await self.dispatcher.dispatch(Event(EventType.CHANNEL_INFO, res, res))
|
|
298
390
|
|
|
299
391
|
# Push notifications
|
|
300
392
|
elif packet_type_value == PacketType.ADVERTISEMENT.value:
|
|
301
393
|
logger.debug("Advertisement received")
|
|
302
394
|
res = {}
|
|
303
|
-
res["public_key"] =
|
|
395
|
+
res["public_key"] = dbuf.read(32).hex()
|
|
304
396
|
await self.dispatcher.dispatch(Event(EventType.ADVERTISEMENT, res, res))
|
|
305
397
|
|
|
306
398
|
elif packet_type_value == PacketType.PATH_UPDATE.value:
|
|
307
399
|
logger.debug("Code path update")
|
|
308
400
|
res = {}
|
|
309
|
-
res["public_key"] =
|
|
401
|
+
res["public_key"] = dbuf.read(32).hex()
|
|
310
402
|
await self.dispatcher.dispatch(Event(EventType.PATH_UPDATE, res, res))
|
|
311
403
|
|
|
312
404
|
elif packet_type_value == PacketType.ACK.value:
|
|
@@ -314,7 +406,7 @@ class MessageReader:
|
|
|
314
406
|
ack_data = {}
|
|
315
407
|
|
|
316
408
|
if len(data) >= 5:
|
|
317
|
-
ack_data["code"] =
|
|
409
|
+
ack_data["code"] = dbuf.read(4).hex()
|
|
318
410
|
|
|
319
411
|
attributes = {"code": ack_data.get("code", "")}
|
|
320
412
|
|
|
@@ -326,23 +418,24 @@ class MessageReader:
|
|
|
326
418
|
|
|
327
419
|
elif packet_type_value == PacketType.RAW_DATA.value:
|
|
328
420
|
res = {}
|
|
329
|
-
res["SNR"] =
|
|
330
|
-
res["RSSI"] =
|
|
331
|
-
res["payload"] =
|
|
421
|
+
res["SNR"] = int.from_bytes(dbuf.read(1), byteorder="little", signed=True) / 4
|
|
422
|
+
res["RSSI"] = int.from_bytes(dbuf.read(1), byteorder="little", signed=True)
|
|
423
|
+
res["payload"] = dbuf.read(4).hex()
|
|
332
424
|
logger.debug("Received raw data")
|
|
333
425
|
print(res)
|
|
334
426
|
await self.dispatcher.dispatch(Event(EventType.RAW_DATA, res))
|
|
335
427
|
|
|
336
428
|
elif packet_type_value == PacketType.LOGIN_SUCCESS.value:
|
|
337
429
|
res = {}
|
|
430
|
+
attributes = {}
|
|
338
431
|
if len(data) > 1:
|
|
339
|
-
|
|
340
|
-
res["
|
|
432
|
+
perms = dbuf.read(1)[0]
|
|
433
|
+
res["permissions"] = perms
|
|
434
|
+
res["is_admin"] = (perms & 1) == 1 # Check if admin bit is set
|
|
341
435
|
|
|
342
|
-
|
|
343
|
-
res["pubkey_prefix"] = data[2:8].hex()
|
|
436
|
+
res["pubkey_prefix"] = dbuf.read(6).hex()
|
|
344
437
|
|
|
345
|
-
|
|
438
|
+
attributes = {"pubkey_prefix": res.get("pubkey_prefix")}
|
|
346
439
|
|
|
347
440
|
await self.dispatcher.dispatch(
|
|
348
441
|
Event(EventType.LOGIN_SUCCESS, res, attributes)
|
|
@@ -350,11 +443,14 @@ class MessageReader:
|
|
|
350
443
|
|
|
351
444
|
elif packet_type_value == PacketType.LOGIN_FAILED.value:
|
|
352
445
|
res = {}
|
|
446
|
+
attributes = {}
|
|
447
|
+
|
|
448
|
+
pbuf.read(1)
|
|
353
449
|
|
|
354
450
|
if len(data) > 7:
|
|
355
|
-
res["pubkey_prefix"] =
|
|
451
|
+
res["pubkey_prefix"] = pbuf.read(6).hex()
|
|
356
452
|
|
|
357
|
-
|
|
453
|
+
attributes = {"pubkey_prefix": res.get("pubkey_prefix")}
|
|
358
454
|
|
|
359
455
|
await self.dispatcher.dispatch(
|
|
360
456
|
Event(EventType.LOGIN_FAILED, res, attributes)
|
|
@@ -368,12 +464,7 @@ class MessageReader:
|
|
|
368
464
|
attributes = {
|
|
369
465
|
"pubkey_prefix": res["pubkey_pre"],
|
|
370
466
|
}
|
|
371
|
-
data_hex = data[8:].hex()
|
|
372
|
-
logger.debug(f"Status response: {data_hex}")
|
|
373
467
|
|
|
374
|
-
attributes = {
|
|
375
|
-
"pubkey_prefix": res["pubkey_pre"],
|
|
376
|
-
}
|
|
377
468
|
await self.dispatcher.dispatch(
|
|
378
469
|
Event(EventType.STATUS_RESPONSE, res, attributes)
|
|
379
470
|
)
|
|
@@ -386,22 +477,23 @@ class MessageReader:
|
|
|
386
477
|
|
|
387
478
|
# First byte is SNR (signed byte, multiplied by 4)
|
|
388
479
|
if len(data) > 1:
|
|
389
|
-
snr_byte =
|
|
480
|
+
snr_byte = dbuf.read(1)[0]
|
|
390
481
|
# Convert to signed value
|
|
391
482
|
snr = (snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0
|
|
392
483
|
log_data["snr"] = snr
|
|
393
484
|
|
|
394
485
|
# Second byte is RSSI (signed byte)
|
|
395
486
|
if len(data) > 2:
|
|
396
|
-
rssi_byte =
|
|
487
|
+
rssi_byte = dbuf.read(1)[0]
|
|
397
488
|
# Convert to signed value
|
|
398
489
|
rssi = rssi_byte if rssi_byte < 128 else rssi_byte - 256
|
|
399
490
|
log_data["rssi"] = rssi
|
|
400
491
|
|
|
401
492
|
# Remaining bytes are the raw data payload
|
|
402
493
|
if len(data) > 3:
|
|
403
|
-
|
|
404
|
-
log_data["
|
|
494
|
+
payload=dbuf.read()
|
|
495
|
+
log_data["payload"] = payload.hex()
|
|
496
|
+
log_data["payload_length"] = len(payload)
|
|
405
497
|
|
|
406
498
|
attributes = {
|
|
407
499
|
"pubkey_prefix": log_data["raw_hex"],
|
|
@@ -470,8 +562,10 @@ class MessageReader:
|
|
|
470
562
|
logger.debug(f"Received telemetry data: {data.hex()}")
|
|
471
563
|
res = {}
|
|
472
564
|
|
|
473
|
-
|
|
474
|
-
|
|
565
|
+
dbuf.read(1)
|
|
566
|
+
|
|
567
|
+
res["pubkey_pre"] = dbuf.read(6).hex()
|
|
568
|
+
buf = dbuf.read()
|
|
475
569
|
|
|
476
570
|
"""Parse a given byte string and return as a LppFrame object."""
|
|
477
571
|
i = 0
|
|
@@ -498,29 +592,31 @@ class MessageReader:
|
|
|
498
592
|
|
|
499
593
|
elif packet_type_value == PacketType.BINARY_RESPONSE.value:
|
|
500
594
|
logger.debug(f"Received binary data: {data.hex()}")
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
595
|
+
dbuf.read(1)
|
|
596
|
+
tag = dbuf.read(4).hex()
|
|
597
|
+
response_data = dbuf.read()
|
|
598
|
+
|
|
504
599
|
# Always dispatch generic BINARY_RESPONSE
|
|
505
600
|
binary_res = {"tag": tag, "data": response_data.hex()}
|
|
506
601
|
await self.dispatcher.dispatch(
|
|
507
602
|
Event(EventType.BINARY_RESPONSE, binary_res, {"tag": tag})
|
|
508
603
|
)
|
|
509
|
-
|
|
604
|
+
|
|
510
605
|
# Check for tracked request type and dispatch specific response
|
|
511
606
|
if tag in self.pending_binary_requests:
|
|
512
607
|
request_type = self.pending_binary_requests[tag]["request_type"]
|
|
513
608
|
pubkey_prefix = self.pending_binary_requests[tag]["pubkey_prefix"]
|
|
609
|
+
context = self.pending_binary_requests[tag]["context"]
|
|
514
610
|
del self.pending_binary_requests[tag]
|
|
515
611
|
logger.debug(f"Processing binary response for tag {tag}, type {request_type}, pubkey_prefix {pubkey_prefix}")
|
|
516
|
-
|
|
612
|
+
|
|
517
613
|
if request_type == BinaryReqType.STATUS and len(response_data) >= 52:
|
|
518
614
|
res = {}
|
|
519
615
|
res = parse_status(response_data, pubkey_prefix=pubkey_prefix)
|
|
520
616
|
await self.dispatcher.dispatch(
|
|
521
617
|
Event(EventType.STATUS_RESPONSE, res, {"pubkey_prefix": res["pubkey_pre"], "tag": tag})
|
|
522
618
|
)
|
|
523
|
-
|
|
619
|
+
|
|
524
620
|
elif request_type == BinaryReqType.TELEMETRY:
|
|
525
621
|
try:
|
|
526
622
|
lpp = lpp_parse(response_data)
|
|
@@ -530,7 +626,7 @@ class MessageReader:
|
|
|
530
626
|
)
|
|
531
627
|
except Exception as e:
|
|
532
628
|
logger.error(f"Error parsing binary telemetry response: {e}")
|
|
533
|
-
|
|
629
|
+
|
|
534
630
|
elif request_type == BinaryReqType.MMA:
|
|
535
631
|
try:
|
|
536
632
|
mma_result = lpp_parse_mma(response_data[4:]) # Skip 4-byte header
|
|
@@ -540,7 +636,7 @@ class MessageReader:
|
|
|
540
636
|
)
|
|
541
637
|
except Exception as e:
|
|
542
638
|
logger.error(f"Error parsing binary MMA response: {e}")
|
|
543
|
-
|
|
639
|
+
|
|
544
640
|
elif request_type == BinaryReqType.ACL:
|
|
545
641
|
try:
|
|
546
642
|
acl_result = parse_acl(response_data)
|
|
@@ -550,19 +646,54 @@ class MessageReader:
|
|
|
550
646
|
)
|
|
551
647
|
except Exception as e:
|
|
552
648
|
logger.error(f"Error parsing binary ACL response: {e}")
|
|
649
|
+
|
|
650
|
+
elif request_type == BinaryReqType.NEIGHBOURS:
|
|
651
|
+
try:
|
|
652
|
+
pk_plen = context["pubkey_prefix_length"]
|
|
653
|
+
bbuf = io.BytesIO(response_data)
|
|
654
|
+
|
|
655
|
+
res = {
|
|
656
|
+
"pubkey_prefix": pubkey_prefix,
|
|
657
|
+
"tag": tag
|
|
658
|
+
}
|
|
659
|
+
res.update(context) # add context in result
|
|
660
|
+
|
|
661
|
+
res["neighbours_count"] = int.from_bytes(bbuf.read(2), "little", signed=True)
|
|
662
|
+
results_count = int.from_bytes(bbuf.read(2), "little", signed=True)
|
|
663
|
+
res["results_count"] = results_count
|
|
664
|
+
|
|
665
|
+
neighbours_list = []
|
|
666
|
+
|
|
667
|
+
for _ in range (results_count):
|
|
668
|
+
neighb = {}
|
|
669
|
+
neighb["pubkey"] = bbuf.read(pk_plen).hex()
|
|
670
|
+
neighb["secs_ago"] = int.from_bytes(bbuf.read(4), "little", signed=True)
|
|
671
|
+
neighb["snr"] = int.from_bytes(bbuf.read(1), "little", signed=True) / 4
|
|
672
|
+
neighbours_list.append(neighb)
|
|
673
|
+
|
|
674
|
+
res["neighbours"] = neighbours_list
|
|
675
|
+
|
|
676
|
+
await self.dispatcher.dispatch(
|
|
677
|
+
Event(EventType.NEIGHBOURS_RESPONSE, res, {"tag": tag, "pubkey_prefix": pubkey_prefix})
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
except Exception as e:
|
|
681
|
+
logger.error(f"Error parsing binary NEIGHBOURS response: {e}")
|
|
682
|
+
|
|
553
683
|
else:
|
|
554
684
|
logger.debug(f"No tracked request found for binary response tag {tag}")
|
|
555
685
|
|
|
556
686
|
elif packet_type_value == PacketType.PATH_DISCOVERY_RESPONSE.value:
|
|
557
687
|
logger.debug(f"Received path discovery response: {data.hex()}")
|
|
558
688
|
res = {}
|
|
559
|
-
|
|
560
|
-
|
|
689
|
+
dbuf.read(1)
|
|
690
|
+
res["pubkey_pre"] = dbuf.read(6).hex()
|
|
691
|
+
opl = dbuf.read(1)[0]
|
|
561
692
|
res["out_path_len"] = opl
|
|
562
|
-
res["out_path"] =
|
|
563
|
-
ipl =
|
|
693
|
+
res["out_path"] = dbuf.read(opl).hex()
|
|
694
|
+
ipl = dbuf.read(1)[0]
|
|
564
695
|
res["in_path_len"] = ipl
|
|
565
|
-
res["in_path"] =
|
|
696
|
+
res["in_path"] = dbuf.read(ipl).hex()
|
|
566
697
|
|
|
567
698
|
attributes = {"pubkey_pre": res["pubkey_pre"]}
|
|
568
699
|
|
|
@@ -573,7 +704,7 @@ class MessageReader:
|
|
|
573
704
|
elif packet_type_value == PacketType.PRIVATE_KEY.value:
|
|
574
705
|
logger.debug(f"Received private key response: {data.hex()}")
|
|
575
706
|
if len(data) >= 65: # 1 byte response code + 64 bytes private key
|
|
576
|
-
private_key =
|
|
707
|
+
private_key = dbuf.read(64) # Extract 64-byte private key
|
|
577
708
|
res = {"private_key": private_key}
|
|
578
709
|
await self.dispatcher.dispatch(Event(EventType.PRIVATE_KEY, res))
|
|
579
710
|
else:
|
|
@@ -584,6 +715,50 @@ class MessageReader:
|
|
|
584
715
|
res = {"reason": "private_key_export_disabled"}
|
|
585
716
|
await self.dispatcher.dispatch(Event(EventType.DISABLED, res))
|
|
586
717
|
|
|
718
|
+
elif packet_type_value == PacketType.CONTROL_DATA.value:
|
|
719
|
+
logger.debug("Received control data packet")
|
|
720
|
+
res={}
|
|
721
|
+
res["SNR"] = int.from_bytes(dbuf.read(1), byteorder="little", signed=True) / 4
|
|
722
|
+
res["RSSI"] = int.from_bytes(dbuf.read(1), byteorder="little", signed=True)
|
|
723
|
+
res["path_len"] = dbuf.read(1)[0]
|
|
724
|
+
payload = dbuf.read()
|
|
725
|
+
payload_type = payload[0]
|
|
726
|
+
res["payload_type"] = payload_type
|
|
727
|
+
res["payload"] = payload
|
|
728
|
+
|
|
729
|
+
attributes = {"payload_type": payload_type}
|
|
730
|
+
await self.dispatcher.dispatch(
|
|
731
|
+
Event(EventType.CONTROL_DATA, res, attributes)
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
# decode NODE_DISCOVER_RESP
|
|
735
|
+
if payload_type & 0xF0 == ControlType.NODE_DISCOVER_RESP.value:
|
|
736
|
+
pbuf = io.BytesIO(payload[1:])
|
|
737
|
+
ndr = dict(res)
|
|
738
|
+
del ndr["payload_type"]
|
|
739
|
+
del ndr["payload"]
|
|
740
|
+
ndr["node_type"] = payload_type & 0x0F
|
|
741
|
+
ndr["SNR_in"] = int.from_bytes(pbuf.read(1), byteorder="little", signed=True)/4
|
|
742
|
+
ndr["tag"] = pbuf.read(4).hex()
|
|
743
|
+
|
|
744
|
+
pubkey = pbuf.read()
|
|
745
|
+
if len(pubkey) < 32:
|
|
746
|
+
pubkey = pubkey[0:8]
|
|
747
|
+
else:
|
|
748
|
+
pubkey = pubkey[0:32]
|
|
749
|
+
|
|
750
|
+
ndr["pubkey"] = pubkey.hex()
|
|
751
|
+
|
|
752
|
+
attributes = {
|
|
753
|
+
"node_type" : ndr["node_type"],
|
|
754
|
+
"tag" : ndr["tag"],
|
|
755
|
+
"pubkey" : ndr["pubkey"],
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
await self.dispatcher.dispatch(
|
|
759
|
+
Event(EventType.DISCOVER_RESPONSE, ndr, attributes)
|
|
760
|
+
)
|
|
761
|
+
|
|
587
762
|
else:
|
|
588
763
|
logger.debug(f"Unhandled data received {data}")
|
|
589
764
|
logger.debug(f"Unhandled packet type: {packet_type_value}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshcore
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.2
|
|
4
4
|
Summary: Base classes for communicating with meshcore companion radios
|
|
5
5
|
Project-URL: Homepage, https://github.com/fdlamotte/meshcore_py
|
|
6
6
|
Project-URL: Issues, https://github.com/fdlamotte/meshcore_py/issues
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
meshcore/__init__.py,sha256=55PdT8gZ9Lf727s7BdSuCt3lIBlAhRzX28A8i0DHaRc,602
|
|
2
|
+
meshcore/ble_cx.py,sha256=S5uanBI88Mxwcs1zWbdQGVJ6GC4kdWz8-Z_2aQVpeGI,6864
|
|
3
|
+
meshcore/connection_manager.py,sha256=3U0TWuHDvL5FfwEwXRy5HjlCyV3nsWhhrEC7TVxFzZI,5899
|
|
4
|
+
meshcore/events.py,sha256=w1Ug5ugO2hAcMs7cV3WRi_4-hv0Lw_Y7NuZzZv0LPl8,8238
|
|
5
|
+
meshcore/lpp_json_encoder.py,sha256=vyn7z3VYWOo_B9xmCzqblh5xCa0QKWKcMi2eOqw18RE,1875
|
|
6
|
+
meshcore/meshcore.py,sha256=n8fH7AEx7DR8BHOYL_Qex9ns6ACKklNNqYbPnNnvqyE,16110
|
|
7
|
+
meshcore/packets.py,sha256=3irQww-HBAe_O03oHPAdp8Z6pwenjsVX_u3F19fHKyc,1294
|
|
8
|
+
meshcore/parsing.py,sha256=48PQkig-sqvsRlkF9zkvWhJoSq6ERCbGb_aRuCND5NI,4044
|
|
9
|
+
meshcore/reader.py,sha256=YNCP4-ll5JEKW6cvbRMfjFLdG4RtVKt-npEDr8hnpwM,33939
|
|
10
|
+
meshcore/serial_cx.py,sha256=-kaqnqk7Ydufu2DECFfPDv4Xs-7gHUBuz8v0xf8fvvE,3969
|
|
11
|
+
meshcore/tcp_cx.py,sha256=05YRVMnjY5aVBTJcHa0uG4VfFKGbV6hQ1pPIsJg4CDI,4227
|
|
12
|
+
meshcore/commands/__init__.py,sha256=qfGPzrha7pZQYHOXLZyDsK-QVRjiz5zxAEj8kMxO9uU,536
|
|
13
|
+
meshcore/commands/base.py,sha256=yKfKSmdnIh0DhAZ34TtioE38huxXc0QGB5Hm_wbcOqg,7398
|
|
14
|
+
meshcore/commands/binary.py,sha256=MihRjG4IppPYPeu2KMpctcBPac_hdClp6PgGirQfydg,8412
|
|
15
|
+
meshcore/commands/contact.py,sha256=7X4e17M2YxUZsfUhaN18XZG2inTuXknXJOBAoPblydw,5880
|
|
16
|
+
meshcore/commands/control_data.py,sha256=sXp1RoEw6Z0zPr0Nn5XBovEY6r9sePbWQwsbY0iYyXc,1512
|
|
17
|
+
meshcore/commands/device.py,sha256=Ub8sS0xtJu3TJwENyswk8lYmYorMH3W33ppHEpSNpoU,9437
|
|
18
|
+
meshcore/commands/messaging.py,sha256=Mglog1xCz_DhKJU1vEv0AD7bBo5_OhEul1MpSY7dnXc,9806
|
|
19
|
+
meshcore-2.2.2.dist-info/METADATA,sha256=rgQoS5tlPN59QZPkaYmKja9Ah_M3pt0g95Xt_D7VlwE,25316
|
|
20
|
+
meshcore-2.2.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
21
|
+
meshcore-2.2.2.dist-info/licenses/LICENSE,sha256=o62-JWT_C-ZqEtzb1Gl_PPtPr0pVT8KDmgji_Y_bejI,1075
|
|
22
|
+
meshcore-2.2.2.dist-info/RECORD,,
|
meshcore-2.1.17.dist-info/RECORD
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
meshcore/__init__.py,sha256=55PdT8gZ9Lf727s7BdSuCt3lIBlAhRzX28A8i0DHaRc,602
|
|
2
|
-
meshcore/ble_cx.py,sha256=S5uanBI88Mxwcs1zWbdQGVJ6GC4kdWz8-Z_2aQVpeGI,6864
|
|
3
|
-
meshcore/connection_manager.py,sha256=3U0TWuHDvL5FfwEwXRy5HjlCyV3nsWhhrEC7TVxFzZI,5899
|
|
4
|
-
meshcore/events.py,sha256=ZaVy2cCjujM3--xUDoUZqHZTq09R1rR8y3-_rLL8-fQ,8014
|
|
5
|
-
meshcore/lpp_json_encoder.py,sha256=vyn7z3VYWOo_B9xmCzqblh5xCa0QKWKcMi2eOqw18RE,1875
|
|
6
|
-
meshcore/meshcore.py,sha256=n8fH7AEx7DR8BHOYL_Qex9ns6ACKklNNqYbPnNnvqyE,16110
|
|
7
|
-
meshcore/packets.py,sha256=2soo3H6FEbVdYk-ZQANIM3YpyCwsaIOrB4QIEJLLw0c,1072
|
|
8
|
-
meshcore/parsing.py,sha256=48PQkig-sqvsRlkF9zkvWhJoSq6ERCbGb_aRuCND5NI,4044
|
|
9
|
-
meshcore/reader.py,sha256=mIewSrfsgt-qe-acoxGQncHZhEhRr_3CEZuN1afnr7E,24784
|
|
10
|
-
meshcore/serial_cx.py,sha256=-kaqnqk7Ydufu2DECFfPDv4Xs-7gHUBuz8v0xf8fvvE,3969
|
|
11
|
-
meshcore/tcp_cx.py,sha256=05YRVMnjY5aVBTJcHa0uG4VfFKGbV6hQ1pPIsJg4CDI,4227
|
|
12
|
-
meshcore/commands/__init__.py,sha256=NNmkTEcL-DLyuwKLUagEzpqe3C6ui2tETbu_mUd4p-I,441
|
|
13
|
-
meshcore/commands/base.py,sha256=0bDHcNGlKoej03xJqzo_wL5ctoPQU5kcK1Ca5sZh1pk,7097
|
|
14
|
-
meshcore/commands/binary.py,sha256=MVKXrT4pFSlzFkVFLBX988Eh57tRwunw4XPKBumbQNU,4890
|
|
15
|
-
meshcore/commands/contact.py,sha256=-dRY68xZkDFP6-i53WujyWhEsXZsFRk98B3j3zBzaL4,5860
|
|
16
|
-
meshcore/commands/device.py,sha256=vstyy1crvhGbDgCAtyCWyjj3X8F2CpMw1LFTOwoHh6M,8273
|
|
17
|
-
meshcore/commands/messaging.py,sha256=MIVGM5G8J16qimKjCHdrlEiVJ4g_MsBaZKPvRJHJcLU,8306
|
|
18
|
-
meshcore-2.1.17.dist-info/METADATA,sha256=ImPYOzshM7CKAhB-wRgqE3ZoN_0wIwE4Ksk3mRykENg,25317
|
|
19
|
-
meshcore-2.1.17.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
20
|
-
meshcore-2.1.17.dist-info/licenses/LICENSE,sha256=o62-JWT_C-ZqEtzb1Gl_PPtPr0pVT8KDmgji_Y_bejI,1075
|
|
21
|
-
meshcore-2.1.17.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|