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.
@@ -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, ContactCommands, MessagingCommands, BinaryCommandHandler
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
@@ -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
@@ -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
@@ -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
- self,
92
- manual_add_contacts: bool,
93
- telemetry_mode_base: int,
94
- telemetry_mode_loc: int,
95
- telemetry_mode_env: int,
96
- advert_loc_policy: int,
97
- ) -> Event:
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
- return await self.set_other_params(
114
- infos["manual_add_contacts"],
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
- return await self.set_other_params(
124
- infos["manual_add_contacts"],
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
- return await self.set_other_params(
134
- infos["manual_add_contacts"],
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
- return await self.set_other_params(
144
- manual_add_contacts,
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
- return await self.set_other_params(
154
- infos["manual_add_contacts"],
155
- infos["telemetry_mode_base"],
156
- infos["telemetry_mode_loc"],
157
- infos["telemetry_mode_env"],
158
- advert_loc_policy,
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])
@@ -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
- 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)
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 = 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}")
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(dst)
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
- packet_type_value = data[0]
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"] = data[1:33].hex()
82
- c["type"] = data[33]
83
- c["flags"] = data[34]
84
- c["out_path_len"] = int.from_bytes(data[35:36], signed=True, byteorder="little")
85
- plen = int.from_bytes(data[35:36], signed=True, byteorder="little")
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
- c["out_path"] = data[36 : 36 + plen].hex()
89
- c["adv_name"] = data[100:132].decode("utf-8", "ignore").replace("\0", "")
90
- c["last_advert"] = int.from_bytes(data[132:136], byteorder="little")
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(data[136:140], byteorder="little", signed=True) / 1e6
101
+ int.from_bytes(dbuf.read(4), byteorder="little", signed=True) / 1e6
93
102
  )
94
103
  c["adv_lon"] = (
95
- int.from_bytes(data[140:144], byteorder="little", signed=True) / 1e6
104
+ int.from_bytes(dbuf.read(4), byteorder="little", signed=True) / 1e6
96
105
  )
97
- c["lastmod"] = int.from_bytes(data[144:148], byteorder="little")
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(data[1:5], byteorder="little")
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"] = data[1]
117
- self_info["tx_power"] = data[2]
118
- self_info["max_tx_power"] = data[3]
119
- self_info["public_key"] = data[4:36].hex()
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(data[36:40], byteorder="little", signed=True) / 1e6
130
+ int.from_bytes(dbuf.read(4), byteorder="little", signed=True) / 1e6
122
131
  )
123
132
  self_info["adv_lon"] = (
124
- int.from_bytes(data[40:44], byteorder="little", signed=True) / 1e6
133
+ int.from_bytes(dbuf.read(4), byteorder="little", signed=True) / 1e6
125
134
  )
126
- self_info["adv_loc_policy"] = data[45]
127
- self_info["telemetry_mode_env"] = (data[46] >> 4) & 0b11
128
- self_info["telemetry_mode_loc"] = (data[46] >> 2) & 0b11
129
- self_info["telemetry_mode_base"] = (data[46]) & 0b11
130
- self_info["manual_add_contacts"] = data[47] > 0
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(data[48:52], byteorder="little") / 1000
143
+ int.from_bytes(dbuf.read(4), byteorder="little") / 1000
133
144
  )
134
145
  self_info["radio_bw"] = (
135
- int.from_bytes(data[52:56], byteorder="little") / 1000
146
+ int.from_bytes(dbuf.read(4), byteorder="little") / 1000
136
147
  )
137
- self_info["radio_sf"] = data[56]
138
- self_info["radio_cr"] = data[57]
139
- self_info["name"] = data[58:].decode("utf-8", "ignore")
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"] = data[1]
145
- res["expected_ack"] = bytes(data[2:6])
146
- res["suggested_timeout"] = int.from_bytes(data[6:10], byteorder="little")
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"] = data[1:7].hex()
159
- res["path_len"] = data[7]
160
- res["txt_type"] = data[8]
161
- res["sender_timestamp"] = int.from_bytes(data[9:13], byteorder="little")
162
- if data[8] == 2:
163
- res["signature"] = data[13:17].hex()
164
- res["text"] = data[17:].decode("utf-8", "ignore")
165
- else:
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(data[1:2], byteorder="little", signed=True) / 4
181
- res["pubkey_prefix"] = data[4:10].hex()
182
- res["path_len"] = data[10]
183
- res["txt_type"] = data[11]
184
- res["sender_timestamp"] = int.from_bytes(data[12:16], byteorder="little")
185
- if data[11] == 2:
186
- res["signature"] = data[16:20].hex()
187
- res["text"] = data[20:].decode("utf-8", "ignore")
188
- else:
189
- res["text"] = data[16:].decode("utf-8", "ignore")
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"] = data[1]
204
- res["path_len"] = data[2]
205
- res["txt_type"] = data[3]
206
- res["sender_timestamp"] = int.from_bytes(data[4:8], byteorder="little")
207
- res["text"] = data[8:].decode("utf-8", "ignore")
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(data[1:2], byteorder="little", signed=True) / 4
222
- res["channel_idx"] = data[4]
223
- res["path_len"] = data[5]
224
- res["txt_type"] = data[6]
225
- res["sender_timestamp"] = int.from_bytes(data[7:11], byteorder="little")
226
- res["text"] = data[11:].decode("utf-8", "ignore")
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(data[1:5], byteorder="little")
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://" + data[1:].hex()
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(data[1:3], byteorder="little")
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(data[3:7], byteorder="little")
256
- result["total_kb"] = int.from_bytes(data[7:11], byteorder="little")
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"] = data[1]
272
+ res["fw ver"] = dbuf.read(1)[0]
262
273
  if data[1] >= 3:
263
- res["max_contacts"] = data[2] * 2
264
- res["max_channels"] = data[3]
265
- res["ble_pin"] = int.from_bytes(data[4:8], byteorder="little")
266
- res["fw_build"] = data[8:20].decode("utf-8", "ignore").replace("\0", "")
267
- res["model"] = data[20:60].decode("utf-8", "ignore").replace("\0", "")
268
- res["ver"] = data[60:80].decode("utf-8", "ignore").replace("\0", "")
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 = data[1:].decode("utf-8", "ignore")
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"] = data[1]
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 = data[2:34]
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"] = data[34:50]
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"] = data[1:33].hex()
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"] = data[1:33].hex()
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"] = bytes(data[1:5]).hex()
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"] = data[1] / 4
330
- res["RSSI"] = data[2]
331
- res["payload"] = data[4:].hex()
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
- res["permissions"] = data[1]
340
- res["is_admin"] = (data[1] & 1) == 1 # Check if admin bit is set
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
- if len(data) > 7:
343
- res["pubkey_prefix"] = data[2:8].hex()
436
+ res["pubkey_prefix"] = dbuf.read(6).hex()
344
437
 
345
- attributes = {"pubkey_prefix": res.get("pubkey_prefix")}
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"] = data[2:8].hex()
451
+ res["pubkey_prefix"] = pbuf.read(6).hex()
356
452
 
357
- attributes = {"pubkey_prefix": res.get("pubkey_prefix")}
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 = data[1]
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 = data[2]
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
- log_data["payload"] = data[3:].hex()
404
- log_data["payload_length"] = len(data) - 3
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
- res["pubkey_pre"] = data[2:8].hex()
474
- buf = data[8:]
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
- tag = data[2:6].hex()
502
- response_data = data[6:]
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
- res["pubkey_pre"] = data[2:8].hex()
560
- opl = data[8]
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"] = data[9 : 9 + opl].hex()
563
- ipl = data[9 + opl]
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"] = data[10 + opl : 10 + opl + ipl].hex()
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 = data[1:65] # Extract 64-byte 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.1.17
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,,
@@ -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,,