meshcore 2.1.24__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/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}")
@@ -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)
@@ -106,9 +106,9 @@ class DeviceCommands(CommandHandlerBase):
106
106
  )
107
107
  data = (
108
108
  b"\x26"
109
- + manual_add_contacts.to_bytes(1)
110
- + telemetry_mode.to_bytes(1)
111
- + 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
112
  )
113
113
  return await self.send(data, [EventType.OK, EventType.ERROR])
114
114
 
@@ -120,10 +120,10 @@ class DeviceCommands(CommandHandlerBase):
120
120
  )
121
121
  data = (
122
122
  b"\x26"
123
- + infos["manual_add_contacts"].to_bytes(1)
124
- + telemetry_mode.to_bytes(1)
125
- + infos["adv_loc_policy"].to_bytes(1)
126
- + infos["multi_acks"].to_bytes(1)
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")
127
127
  )
128
128
  return await self.send(data, [EventType.OK, EventType.ERROR])
129
129
 
@@ -205,3 +205,18 @@ class DeviceCommands(CommandHandlerBase):
205
205
  async def export_private_key(self) -> Event:
206
206
  logger.debug("Requesting private key export")
207
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])
@@ -86,21 +86,33 @@ class MessagingCommands(CommandHandlerBase):
86
86
  max_attempts=3, max_flood_attempts=2, flood_after=2, timeout=0, min_timeout=0
87
87
  ) -> Event:
88
88
 
89
- dst_bytes = _validate_destination(dst)
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)
90
95
  contact = self._get_contact_by_prefix(dst_bytes.hex())
91
96
 
92
97
  attempts = 0
93
98
  flood_attempts = 0
94
99
  if not contact is None :
95
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)
96
104
  else:
97
- flood = False
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}")
98
110
  res = None
99
111
  while attempts < max_attempts and res is None \
100
112
  and (not flood or flood_attempts < max_flood_attempts):
101
- if attempts == flood_after : # change path to flood
113
+ if attempts == flood_after and not flood: # change path to flood
102
114
  logger.info("Resetting path")
103
- rp_res = await self.reset_path(dst)
115
+ rp_res = await self.reset_path(dst_bytes)
104
116
  if rp_res.type == EventType.ERROR:
105
117
  logger.error(f"Couldn't reset path {rp_res} continuing ...")
106
118
  else:
meshcore/events.py CHANGED
@@ -40,6 +40,9 @@ 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"
meshcore/packets.py CHANGED
@@ -36,6 +36,7 @@ class PacketType(Enum):
36
36
  SIGN_START = 19
37
37
  SIGNATURE = 20
38
38
  CUSTOM_VARS = 21
39
+ STATS = 24
39
40
  BINARY_REQ = 50
40
41
  FACTORY_RESET = 51
41
42
  PATH_DISCOVERY = 52
meshcore/reader.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  import json
3
+ import struct
3
4
  import time
4
5
  import io
5
6
  from typing import Any, Dict
@@ -51,7 +52,11 @@ class MessageReader:
51
52
 
52
53
  async def handle_rx(self, data: bytearray):
53
54
  dbuf = io.BytesIO(data)
54
- packet_type_value = dbuf.read(1)[0]
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
55
60
  logger.debug(f"Received data: {data.hex()}")
56
61
 
57
62
  # Handle command responses
@@ -286,6 +291,87 @@ class MessageReader:
286
291
  logger.debug(f"got custom vars : {res}")
287
292
  await self.dispatcher.dispatch(Event(EventType.CUSTOM_VARS, res))
288
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
+
289
375
  elif packet_type_value == PacketType.CHANNEL_INFO.value:
290
376
  logger.debug(f"received channel info response: {data.hex()}")
291
377
  res = {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcore
3
- Version: 2.1.24
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,,
Binary file
@@ -1,23 +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=P40oq3C9j2cEg6iD9_-SxWK927C3ykWBTpjL8ixygFQ,8140
5
- meshcore/lpp_json_encoder.py,sha256=vyn7z3VYWOo_B9xmCzqblh5xCa0QKWKcMi2eOqw18RE,1875
6
- meshcore/meshcore.py,sha256=n8fH7AEx7DR8BHOYL_Qex9ns6ACKklNNqYbPnNnvqyE,16110
7
- meshcore/packets.py,sha256=FT6GwH8rveLBWuFVMkbqnAuaymrk6uhd_t46Qj98uf4,1279
8
- meshcore/parsing.py,sha256=48PQkig-sqvsRlkF9zkvWhJoSq6ERCbGb_aRuCND5NI,4044
9
- meshcore/reader.py,sha256=inhn0Xsc7J0VKKyPVWwoM1LXfAKO-SJ4Th1c3sN4yy8,28645
10
- meshcore/serial_cx.py,sha256=-kaqnqk7Ydufu2DECFfPDv4Xs-7gHUBuz8v0xf8fvvE,3969
11
- meshcore/tcp_cx.py,sha256=05YRVMnjY5aVBTJcHa0uG4VfFKGbV6hQ1pPIsJg4CDI,4227
12
- meshcore/commands/.binary.py.swp,sha256=SXeYOAsKheJnezxgvzUUHL43qUQWEPyc-VZUMBRsGV8,24576
13
- meshcore/commands/__init__.py,sha256=qfGPzrha7pZQYHOXLZyDsK-QVRjiz5zxAEj8kMxO9uU,536
14
- meshcore/commands/base.py,sha256=jO6KWSxBciEMaP0weE8OAbN6Tf-kWSaljb9py3gvihg,7126
15
- meshcore/commands/binary.py,sha256=MihRjG4IppPYPeu2KMpctcBPac_hdClp6PgGirQfydg,8412
16
- meshcore/commands/contact.py,sha256=-dRY68xZkDFP6-i53WujyWhEsXZsFRk98B3j3zBzaL4,5860
17
- meshcore/commands/control_data.py,sha256=sXp1RoEw6Z0zPr0Nn5XBovEY6r9sePbWQwsbY0iYyXc,1512
18
- meshcore/commands/device.py,sha256=V-gxA88We1KLDtzJ-rsmxv5LH04sUOacqnBc6TwB_gg,8662
19
- meshcore/commands/messaging.py,sha256=5h0JecOT3KJV1AFdasVLs889xtXY4J5gguJ7KbUoZOE,9073
20
- meshcore-2.1.24.dist-info/METADATA,sha256=9Ks9G4ws_jzHVCUps3JJmpWjWZ7LoKj9pbCpSViIA2c,25317
21
- meshcore-2.1.24.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
22
- meshcore-2.1.24.dist-info/licenses/LICENSE,sha256=o62-JWT_C-ZqEtzb1Gl_PPtPr0pVT8KDmgji_Y_bejI,1075
23
- meshcore-2.1.24.dist-info/RECORD,,