meshcore 2.2.31__tar.gz → 2.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. {meshcore-2.2.31 → meshcore-2.3.1}/PKG-INFO +1 -1
  2. {meshcore-2.2.31 → meshcore-2.3.1}/pyproject.toml +1 -1
  3. meshcore-2.3.1/src/meshcore/meshcore_parser.py +190 -0
  4. {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/reader.py +25 -171
  5. meshcore-2.3.1/tests/unit/test_path_discovery_response.py +45 -0
  6. {meshcore-2.2.31 → meshcore-2.3.1}/.github/python-test.yml +0 -0
  7. {meshcore-2.2.31 → meshcore-2.3.1}/.gitignore +0 -0
  8. {meshcore-2.2.31 → meshcore-2.3.1}/LICENSE +0 -0
  9. {meshcore-2.2.31 → meshcore-2.3.1}/README.md +0 -0
  10. {meshcore-2.2.31 → meshcore-2.3.1}/examples/.pubsub_example.py.swp +0 -0
  11. {meshcore-2.2.31 → meshcore-2.3.1}/examples/ble_chat.py +0 -0
  12. {meshcore-2.2.31 → meshcore-2.3.1}/examples/ble_pin_pairing_example.py +0 -0
  13. {meshcore-2.2.31 → meshcore-2.3.1}/examples/ble_private_key_export.py +0 -0
  14. {meshcore-2.2.31 → meshcore-2.3.1}/examples/ble_sign_example.py +0 -0
  15. {meshcore-2.2.31 → meshcore-2.3.1}/examples/ble_stats.py +0 -0
  16. {meshcore-2.2.31 → meshcore-2.3.1}/examples/ble_t1000_chan_msg.py +0 -0
  17. {meshcore-2.2.31 → meshcore-2.3.1}/examples/ble_t1000_custom_vars.py +0 -0
  18. {meshcore-2.2.31 → meshcore-2.3.1}/examples/ble_t1000_infos.py +0 -0
  19. {meshcore-2.2.31 → meshcore-2.3.1}/examples/ble_t1000_msg.py +0 -0
  20. {meshcore-2.2.31 → meshcore-2.3.1}/examples/ble_t1000_msg_retries.py +0 -0
  21. {meshcore-2.2.31 → meshcore-2.3.1}/examples/ble_t1000_set_cv.py +0 -0
  22. {meshcore-2.2.31 → meshcore-2.3.1}/examples/chan_recv_with_path.py +0 -0
  23. {meshcore-2.2.31 → meshcore-2.3.1}/examples/connection_events_example.py +0 -0
  24. {meshcore-2.2.31 → meshcore-2.3.1}/examples/mepo_mc_gps.py +0 -0
  25. {meshcore-2.2.31 → meshcore-2.3.1}/examples/pubsub_example.py +0 -0
  26. {meshcore-2.2.31 → meshcore-2.3.1}/examples/rf_packet_monitor.py +0 -0
  27. {meshcore-2.2.31 → meshcore-2.3.1}/examples/serial_battery_monitor.py +0 -0
  28. {meshcore-2.2.31 → meshcore-2.3.1}/examples/serial_channel_manager.py +0 -0
  29. {meshcore-2.2.31 → meshcore-2.3.1}/examples/serial_chat.py +0 -0
  30. {meshcore-2.2.31 → meshcore-2.3.1}/examples/serial_contacts.py +0 -0
  31. {meshcore-2.2.31 → meshcore-2.3.1}/examples/serial_infos.py +0 -0
  32. {meshcore-2.2.31 → meshcore-2.3.1}/examples/serial_meshcore_ollama.py +0 -0
  33. {meshcore-2.2.31 → meshcore-2.3.1}/examples/serial_msg.py +0 -0
  34. {meshcore-2.2.31 → meshcore-2.3.1}/examples/serial_pingbot.py +0 -0
  35. {meshcore-2.2.31 → meshcore-2.3.1}/examples/serial_repeater_status.py +0 -0
  36. {meshcore-2.2.31 → meshcore-2.3.1}/examples/serial_repeater_telemetry.py +0 -0
  37. {meshcore-2.2.31 → meshcore-2.3.1}/examples/serial_rss_bot.py +0 -0
  38. {meshcore-2.2.31 → meshcore-2.3.1}/examples/serial_trace.py +0 -0
  39. {meshcore-2.2.31 → meshcore-2.3.1}/examples/tcp_chat.py +0 -0
  40. {meshcore-2.2.31 → meshcore-2.3.1}/examples/tcp_login_status.py +0 -0
  41. {meshcore-2.2.31 → meshcore-2.3.1}/examples/tcp_mchome_contacts.py +0 -0
  42. {meshcore-2.2.31 → meshcore-2.3.1}/examples/tcp_mchome_infos.py +0 -0
  43. {meshcore-2.2.31 → meshcore-2.3.1}/examples/tcp_mchome_msg.py +0 -0
  44. {meshcore-2.2.31 → meshcore-2.3.1}/examples/tcp_mchome_readmsgs.py +0 -0
  45. {meshcore-2.2.31 → meshcore-2.3.1}/pytest.ini +0 -0
  46. {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/__init__.py +0 -0
  47. {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/ble_cx.py +0 -0
  48. {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/commands/__init__.py +0 -0
  49. {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/commands/base.py +0 -0
  50. {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/commands/binary.py +0 -0
  51. {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/commands/contact.py +0 -0
  52. {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/commands/control_data.py +0 -0
  53. {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/commands/device.py +0 -0
  54. {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/commands/messaging.py +0 -0
  55. {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/connection_manager.py +0 -0
  56. {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/events.py +0 -0
  57. {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/lpp_json_encoder.py +0 -0
  58. {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/meshcore.py +0 -0
  59. {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/packets.py +0 -0
  60. {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/parsing.py +0 -0
  61. {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/serial_cx.py +0 -0
  62. {meshcore-2.2.31 → meshcore-2.3.1}/src/meshcore/tcp_cx.py +0 -0
  63. {meshcore-2.2.31 → meshcore-2.3.1}/tests/README.md +0 -0
  64. {meshcore-2.2.31 → meshcore-2.3.1}/tests/test_ble_connection.py +0 -0
  65. {meshcore-2.2.31 → meshcore-2.3.1}/tests/test_ble_pin_pairing.py +0 -0
  66. {meshcore-2.2.31 → meshcore-2.3.1}/tests/test_meshcore_ble_pin.py +0 -0
  67. {meshcore-2.2.31 → meshcore-2.3.1}/tests/unit/test_commands.py +0 -0
  68. {meshcore-2.2.31 → meshcore-2.3.1}/tests/unit/test_events.py +0 -0
  69. {meshcore-2.2.31 → meshcore-2.3.1}/tests/unit/test_private_key_export.py +0 -0
  70. {meshcore-2.2.31 → meshcore-2.3.1}/tests/unit/test_reader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcore
3
- Version: 2.2.31
3
+ Version: 2.3.1
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "meshcore"
7
- version = "2.2.31"
7
+ version = "2.3.1"
8
8
  authors = [
9
9
  { name="Florent de Lamotte", email="florent@frizoncorrea.fr" },
10
10
  { name="Alex Wolden", email="awolden@gmail.com" },
@@ -0,0 +1,190 @@
1
+ import logging
2
+ import json
3
+ import time
4
+ import io
5
+ from typing import Any, Dict
6
+ from Crypto.Cipher import AES
7
+ from Crypto.Hash import HMAC, SHA256
8
+
9
+ logger = logging.getLogger("meshcore")
10
+
11
+ PAYLOAD_TYPENAMES = ["REQ", "RESPONSE", "TEXT_MSG", "ACK", "ADVERT", "GRP_TXT", "GRP_DATA", "ANON_REQ", "PATH", "TRACE", "MULTIPART", "CONTROL"]
12
+ ROUTE_TYPENAMES = ["TC_FLOOD", "FLOOD", "DIRECT", "TC_DIRECT"]
13
+ CONTACT_TYPENAMES = ["NONE","CLI","REP","ROOM","SENS"]
14
+
15
+ class MeshcorePacketParser:
16
+ def __init__(self):
17
+ self.channels = [{} for _ in range(40)] # keep our own copy of channels, 40 elements by default
18
+ self.channels_log = [] # stores the channel msg events
19
+ self.decrypt_channels = False
20
+
21
+ async def newChannel(self, chan):
22
+ idx = chan['channel_idx']
23
+ if len(self.channels) <= idx:
24
+ self.channels.extend([{} for _ in range(1 + idx - len(self.channels))])
25
+ self.channels[idx] = chan
26
+
27
+ async def findLogChannelMsg(self, msg_hash):
28
+ # search for the same packet
29
+ return next((l for l in reversed(self.channels_log) if 'msg_hash' in l and l['msg_hash'] == msg_hash), None)
30
+
31
+ async def findLogChannelPkt(self, pkt_hash):
32
+ # search for the same packet
33
+ return next((l for l in reversed(self.channels_log) if 'pkt_hash' in l and l['pkt_hash'] == pkt_hash), None)
34
+
35
+ async def parsePacketPayload(self, payload, log_data={}):
36
+ """ Parses the payload of a log_rx packet
37
+
38
+ Parameters :
39
+ log_data: already gathered log_data we'll expand
40
+ payload: payload of the packet to analyze
41
+
42
+ Returns :
43
+ completed log_data
44
+ """
45
+ pbuf = io.BytesIO(payload)
46
+
47
+ header = pbuf.read(1)[0]
48
+ route_type = header & 0x03
49
+ payload_type = (header & 0x3c) >> 2
50
+ payload_ver = (header & 0xc0) >> 6
51
+
52
+ transport_code = None
53
+ if route_type == 0x00 or route_type == 0x03: # has transport code
54
+ transport_code = pbuf.read(4) # discard transport code
55
+
56
+ path_byte = pbuf.read(1)[0]
57
+ path_hash_size = ((path_byte & 0xC0) >> 6) + 1
58
+ path_len = (path_byte & 0x3F)
59
+ # here path_len is number of hops, not number of bytes
60
+
61
+ path = pbuf.read(path_len*path_hash_size).hex() # Beware of traces where pathes are mixed
62
+
63
+ try :
64
+ route_typename = ROUTE_TYPENAMES[route_type]
65
+ except IndexError:
66
+ logger.debug(f"Unknown route type {route_type}")
67
+ route_typename = "UNK"
68
+
69
+ try :
70
+ payload_typename = PAYLOAD_TYPENAMES[payload_type]
71
+ except IndexError:
72
+ logger.debug(f"Unknown payload type {payload_type}")
73
+ payload_typename = "UNK"
74
+
75
+ pkt_payload = pbuf.read()
76
+ pkt_hash = int.from_bytes(SHA256.new(pkt_payload).digest()[0:4], "little", signed=False)
77
+
78
+ log_data["header"] = header
79
+ log_data["route_type"] = route_type
80
+ log_data["route_typename"] = route_typename
81
+ log_data["payload_type"] = payload_type
82
+ log_data["payload_typename"]= payload_typename
83
+
84
+ log_data["payload_ver"] = payload_ver
85
+
86
+ if not transport_code is None:
87
+ log_data["transport_code"] = transport_code.hex()
88
+
89
+ log_data["path_len"] = path_len
90
+ log_data["path_hash_size"] = path_hash_size
91
+ log_data["path"] = path
92
+
93
+ log_data["pkt_payload"] = pkt_payload
94
+ log_data["pkt_hash"] = pkt_hash
95
+
96
+ if not payload is None and payload_type == 0x05: # flood msg / channel
97
+ pk_buf = io.BytesIO(pkt_payload)
98
+ chan_hash = pk_buf.read(1).hex()
99
+ cipher_mac = pk_buf.read(2)
100
+ msg = pk_buf.read() # until the end of buffer
101
+
102
+ channel = None
103
+ for c in self.channels:
104
+ if "channel_hash" in c and c["channel_hash"] == chan_hash : # validate against MAC
105
+ h = HMAC.new(c["channel_secret"], digestmod=SHA256)
106
+ h.update(msg)
107
+ if h.digest()[0:2] == cipher_mac:
108
+ channel = c
109
+ break
110
+
111
+ chan_name = ""
112
+
113
+ if channel is None :
114
+ chan_name = chan_hash
115
+ else:
116
+ chan_name = channel["channel_name"]
117
+
118
+ log_data["chan_hash"] = chan_hash
119
+ log_data["cipher_mac"] = cipher_mac.hex()
120
+ log_data["crypted"] = msg.hex()
121
+ log_data["chan_name"] = chan_name
122
+
123
+ if not channel is None and self.decrypt_channels:
124
+
125
+ logged = await self.findLogChannelPkt(pkt_hash)
126
+
127
+ if logged is None:
128
+ # not found: decrypt the text and hash it
129
+ aes_key = channel["channel_secret"]
130
+ cipher = AES.new(aes_key, AES.MODE_ECB)
131
+ uncrypted = cipher.decrypt(msg)
132
+ timestamp = int.from_bytes(uncrypted[0:4], "little", signed=False)
133
+ attempt = uncrypted[4] & 3
134
+ txt_type = int.from_bytes(uncrypted[4:4], "little", signed=False) >> 2
135
+ message = uncrypted[5:].strip(b"\0")
136
+ msg_hash = int.from_bytes(SHA256.new(timestamp.to_bytes(4, "little", signed=False) + message).digest()[0:4], "little", signed=False)
137
+ log_data["message"] = message.decode("utf-8", "ignore")
138
+ log_data["msg_hash"] = msg_hash
139
+ log_data["sender_timestamp"] = timestamp
140
+ log_data["attempt"] = attempt
141
+ log_data["txt_type"] = txt_type
142
+ else:
143
+ # found: copy
144
+ log_data["message"] = logged["message"]
145
+ log_data["msg_hash"] = logged["msg_hash"]
146
+ log_data["sender_timestamp"] = logged["sender_timestamp"]
147
+ log_data["attempt"] = logged["attempt"]
148
+ log_data["txt_type"] = logged["txt_type"]
149
+
150
+ self.channels_log.append(log_data)
151
+ if len(self.channels_log) > 100:
152
+ del self.channels_log[:25]
153
+
154
+ elif not payload is None and payload_type == 0x04: # Advert
155
+ pk_buf = io.BytesIO(pkt_payload)
156
+ adv_key = pk_buf.read(32).hex()
157
+ adv_timestamp = int.from_bytes(pk_buf.read(4), "little", signed=False)
158
+ signature = pk_buf.read(64).hex()
159
+ flags = pk_buf.read(1)[0]
160
+ adv_type = flags & 0x0F
161
+ adv_lat = None
162
+ adv_lon = None
163
+ adv_feat1 = None
164
+ adv_feat2 = None
165
+ if flags & 0x10 > 0: #has location
166
+ adv_lat = int.from_bytes(pk_buf.read(4), "little", signed=True)/1000000.0
167
+ adv_lon = int.from_bytes(pk_buf.read(4), "little", signed=True)/1000000.0
168
+ if flags & 0x20 > 0: #has feature1
169
+ adv_feat1 = pk_buf.read(2).hex()
170
+ if flags & 0x40 > 0: #has feature2
171
+ adv_feat2 = pk_buf.read(2).hex()
172
+ if flags & 0x80 > 0: #has name
173
+ adv_name = pk_buf.read().decode("utf-8", "ignore").strip("\x00")
174
+ log_data["adv_name"] = adv_name
175
+
176
+ log_data["adv_key"] = adv_key
177
+ log_data["adv_timestamp"] = adv_timestamp
178
+ log_data["signature"] = signature
179
+ log_data["adv_flags"] = flags
180
+ log_data["adv_type"] = adv_type
181
+ if not adv_lat is None :
182
+ log_data["adv_lat"] = adv_lat
183
+ if not adv_lon is None :
184
+ log_data["adv_lon"] = adv_lon
185
+ if not adv_feat1 is None:
186
+ log_data["adv_feat1"] = adv_feat1
187
+ if not adv_feat2 is None:
188
+ log_data["adv_feat2"] = adv_feat2
189
+
190
+ return log_data
@@ -5,19 +5,15 @@ import time
5
5
  import io
6
6
  from typing import Any, Dict
7
7
  from .events import Event, EventType, EventDispatcher, ErrorMessages
8
+ from .meshcore_parser import MeshcorePacketParser
8
9
  from .packets import BinaryReqType, PacketType, ControlType
9
10
  from .parsing import lpp_parse, lpp_parse_mma, parse_acl, parse_status
10
11
  from cayennelpp import LppFrame, LppData
11
12
  from meshcore.lpp_json_encoder import lpp_json_encoder
12
- from Crypto.Cipher import AES
13
- from Crypto.Hash import HMAC, SHA256
13
+ from Crypto.Hash import SHA256
14
14
 
15
15
  logger = logging.getLogger("meshcore")
16
16
 
17
- PAYLOAD_TYPENAMES = ["REQ", "RESPONSE", "TEXT_MSG", "ACK", "ADVERT", "GRP_TXT", "GRP_DATA", "ANON_REQ", "PATH", "TRACE", "MULTIPART", "CONTROL"]
18
- ROUTE_TYPENAMES = ["TC_FLOOD", "FLOOD", "DIRECT", "TC_DIRECT"]
19
- CONTACT_TYPENAMES = ["NONE","CLI","REP","ROOM","SENS"]
20
-
21
17
  class MessageReader:
22
18
  def __init__(self, dispatcher: EventDispatcher):
23
19
  self.dispatcher = dispatcher
@@ -29,9 +25,15 @@ class MessageReader:
29
25
  # Track pending binary requests by tag for proper response parsing
30
26
  self.pending_binary_requests: Dict[str, Dict[str, Any]] = {} # tag -> {request_type, expires_at}
31
27
 
32
- self.channels = [{} for _ in range(40)] # keep our own copy of channels, 40 elements by default
33
- self.decrypt_channels = True
34
- self.channels_log = [] # stores the channel msg events
28
+ self.packet_parser = MeshcorePacketParser()
29
+
30
+ @property
31
+ def decrypt_channels(self):
32
+ return self.packet_parser.decrypt_channels
33
+
34
+ @decrypt_channels.setter
35
+ def decrypt_channels(self, value):
36
+ self.packet_parser.decrypt_channels = value
35
37
 
36
38
  def register_binary_request(self, prefix: str, tag: str, request_type: BinaryReqType, timeout_seconds: float, context={}, is_anon=False):
37
39
  """Register a pending binary request for proper response parsing"""
@@ -266,7 +268,7 @@ class MessageReader:
266
268
  # search for text in log_channels
267
269
  txt_hash = int.from_bytes(SHA256.new(res["sender_timestamp"].to_bytes(4, "little", signed=False)+text).digest()[0:4], "little", signed=False)
268
270
  if self.decrypt_channels:
269
- logged = next((l for l in reversed(self.channels_log) if 'msg_hash' in l and l['msg_hash'] == txt_hash), None)
271
+ logged = await self.packet_parser.findLogChannelMsg(txt_hash)
270
272
  if not logged is None:
271
273
  res["path"] = logged["path"]
272
274
  res["RSSI"] = logged["rssi"]
@@ -302,11 +304,11 @@ class MessageReader:
302
304
  res["text"] = text.decode("utf-8", "ignore")
303
305
 
304
306
  # search for text in log_channels
305
- txt_hash = int.from_bytes(SHA256.new(res["sender_timestamp"].to_bytes(4, "little", signed=False)+text).digest()[0:4], "little", signed=False)
306
- res["txt_hash"] = txt_hash
307
- logged = next((l for l in reversed(self.channels_log) if 'msg_hash' in l and l['msg_hash'] == txt_hash), None)
308
-
309
307
  if self.decrypt_channels:
308
+ txt_hash = int.from_bytes(SHA256.new(res["sender_timestamp"].to_bytes(4, "little", signed=False)+text).digest()[0:4], "little", signed=False)
309
+ res["txt_hash"] = txt_hash
310
+ logged = await self.packet_parser.findLogChannelMsg(txt_hash)
311
+
310
312
  if not logged is None:
311
313
  res["path"] = logged["path"]
312
314
  res["RSSI"] = logged["rssi"]
@@ -476,9 +478,6 @@ class MessageReader:
476
478
  idx = dbuf.read(1)[0]
477
479
  res["channel_idx"] = idx
478
480
 
479
- if len(self.channels) <= idx:
480
- self.channels.extend([{} for _ in range(1 + idx - len(self.channels))])
481
-
482
481
  # Channel name is null-terminated, so find the first null byte
483
482
  name_bytes = dbuf.read(32)
484
483
  null_pos = name_bytes.find(0)
@@ -490,7 +489,7 @@ class MessageReader:
490
489
  res["channel_secret"] = dbuf.read(16)
491
490
  res["channel_hash"] = SHA256.new(res["channel_secret"]).hexdigest()[0:2]
492
491
 
493
- self.channels[idx] = res
492
+ await self.packet_parser.newChannel(res)
494
493
 
495
494
  await self.dispatcher.dispatch(Event(EventType.CHANNEL_INFO, res, res))
496
495
 
@@ -607,157 +606,12 @@ class MessageReader:
607
606
  log_data["payload"] = payload.hex()
608
607
  log_data["payload_length"] = len(payload)
609
608
 
610
- # Parse payload and get some info from it
611
- if not payload is None:
612
- pbuf = io.BytesIO(payload)
613
-
614
- header = pbuf.read(1)[0]
615
- route_type = header & 0x03
616
- payload_type = (header & 0x3c) >> 2
617
- payload_ver = (header & 0xc0) >> 6
618
-
619
- transport_code = None
620
- if route_type == 0x00 or route_type == 0x03: # has transport code
621
- transport_code = pbuf.read(4) # discard transport code
622
-
623
- path_byte = pbuf.read(1)[0]
624
- path_hash_size = ((path_byte & 0xC0) >> 6) + 1
625
- path_len = (path_byte & 0x3F)
626
- # here path_len is number of hops, not number of bytes
627
-
628
- path = pbuf.read(path_len*path_hash_size).hex() # Beware of traces where pathes are mixed
629
-
630
- try :
631
- route_typename = ROUTE_TYPENAMES[route_type]
632
- except IndexError:
633
- logger.debug(f"Unknown route type {route_type}")
634
- route_typename = "UNK"
635
-
636
- try :
637
- payload_typename = PAYLOAD_TYPENAMES[payload_type]
638
- except IndexError:
639
- logger.debug(f"Unknown payload type {payload_type}")
640
- payload_typename = "UNK"
641
-
642
- pkt_payload = pbuf.read()
643
- pkt_hash = int.from_bytes(SHA256.new(pkt_payload).digest()[0:4], "little", signed=False)
644
-
645
- log_data["header"] = header
646
- log_data["route_type"] = route_type
647
- attributes["route_type"] = route_type
648
- log_data["route_typename"] = route_typename
649
- log_data["payload_type"] = payload_type
650
- attributes["payload_type"] = payload_type
651
- log_data["payload_typename"]= payload_typename
652
-
653
- log_data["payload_ver"] = payload_ver
654
-
655
- if not transport_code is None:
656
- log_data["transport_code"] = transport_code.hex()
657
-
658
- log_data["path_len"] = path_len
659
- attributes["path_len"] = path_len
660
- log_data["path_hash_size"] = path_hash_size
661
- log_data["path"] = path
662
- attributes["path"] = path
663
-
664
- log_data["pkt_payload"] = pkt_payload
665
- log_data["pkt_hash"] = pkt_hash
666
-
667
-
668
- if not payload is None and payload_type == 0x05: # flood msg / channel
669
- pk_buf = io.BytesIO(pkt_payload)
670
- chan_hash = pk_buf.read(1).hex()
671
- cipher_mac = pk_buf.read(2)
672
- msg = pk_buf.read() # until the end of buffer
673
-
674
- channel = None
675
- for c in self.channels:
676
- if "channel_hash" in c and c["channel_hash"] == chan_hash : # validate against MAC
677
- h = HMAC.new(c["channel_secret"], digestmod=SHA256)
678
- h.update(msg)
679
- if h.digest()[0:2] == cipher_mac:
680
- channel = c
681
- break
682
-
683
- chan_name = ""
684
-
685
- if channel is None :
686
- chan_name = chan_hash
687
- else:
688
- chan_name = channel["channel_name"]
689
-
690
- log_data["chan_hash"] = chan_hash
691
- log_data["cipher_mac"] = cipher_mac.hex()
692
- log_data["crypted"] = msg.hex()
693
- log_data["chan_name"] = chan_name
694
-
695
- if not channel is None and self.decrypt_channels:
696
- # search for the same packet
697
- logged = next((l for l in reversed(self.channels_log) if 'pkt_hash' in l and l['pkt_hash'] == pkt_hash), None)
698
-
699
- if logged is None:
700
- # not found: decrypt the text and hash it
701
- aes_key = channel["channel_secret"]
702
- cipher = AES.new(aes_key, AES.MODE_ECB)
703
- uncrypted = cipher.decrypt(msg)
704
- timestamp = int.from_bytes(uncrypted[0:4], "little", signed=False)
705
- attempt = uncrypted[4] & 3
706
- txt_type = int.from_bytes(uncrypted[4:4], "little", signed=False) >> 2
707
- message = uncrypted[5:].strip(b"\0")
708
- msg_hash = int.from_bytes(SHA256.new(timestamp.to_bytes(4, "little", signed=False) + message).digest()[0:4], "little", signed=False)
709
- log_data["message"] = message.decode("utf-8", "ignore")
710
- log_data["msg_hash"] = msg_hash
711
- log_data["sender_timestamp"] = timestamp
712
- log_data["attempt"] = attempt
713
- log_data["txt_type"] = txt_type
714
- else:
715
- # found: copy
716
- log_data["message"] = logged["message"]
717
- log_data["msg_hash"] = logged["msg_hash"]
718
- log_data["sender_timestamp"] = logged["sender_timestamp"]
719
- log_data["attempt"] = logged["attempt"]
720
- log_data["txt_type"] = logged["txt_type"]
721
-
722
- self.channels_log.append(log_data)
723
- if len(self.channels_log) > 100:
724
- del self.channels_log[:25]
725
-
726
- elif not payload is None and payload_type == 0x04: # Advert
727
- pk_buf = io.BytesIO(pkt_payload)
728
- adv_key = pk_buf.read(32).hex()
729
- adv_timestamp = int.from_bytes(pk_buf.read(4), "little", signed=False)
730
- signature = pk_buf.read(64).hex()
731
- flags = pk_buf.read(1)[0]
732
- adv_type = flags & 0x0F
733
- adv_lat = None
734
- adv_lon = None
735
- adv_feat1 = None
736
- adv_feat2 = None
737
- if flags & 0x10 > 0: #has location
738
- adv_lat = int.from_bytes(pk_buf.read(4), "little", signed=True)/1000000.0
739
- adv_lon = int.from_bytes(pk_buf.read(4), "little", signed=True)/1000000.0
740
- if flags & 0x20 > 0: #has feature1
741
- adv_feat1 = pk_buf.read(2).hex()
742
- if flags & 0x40 > 0: #has feature2
743
- adv_feat2 = pk_buf.read(2).hex()
744
- if flags & 0x80 > 0: #has name
745
- adv_name = pk_buf.read().decode("utf-8", "ignore").strip("\x00")
746
- log_data["adv_name"] = adv_name
747
-
748
- log_data["adv_key"] = adv_key
749
- log_data["adv_timestamp"] = adv_timestamp
750
- log_data["signature"] = signature
751
- log_data["adv_flags"] = flags
752
- log_data["adv_type"] = adv_type
753
- if not adv_lat is None :
754
- log_data["adv_lat"] = adv_lat
755
- if not adv_lon is None :
756
- log_data["adv_lon"] = adv_lon
757
- if not adv_feat1 is None:
758
- log_data["adv_feat1"] = adv_feat1
759
- if not adv_feat2 is None:
760
- log_data["adv_feat2"] = adv_feat2
609
+ # Parse payload and get some info from it
610
+ log_data = await self.packet_parser.parsePacketPayload(payload, log_data)
611
+ attributes['route_type'] = log_data['route_type']
612
+ attributes['payload_type'] = log_data['payload_type']
613
+ attributes['path_len'] = log_data['path_len']
614
+ attributes['path'] = log_data['path']
761
615
 
762
616
  # Dispatch as RF log data
763
617
  await self.dispatcher.dispatch(
@@ -973,13 +827,13 @@ class MessageReader:
973
827
  res["pubkey_pre"] = dbuf.read(6).hex()
974
828
  opl = dbuf.read(1)[0]
975
829
  opl_hlen = ((opl & 0xc0) >> 6) + 1
976
- opl = opl & 0xbf
830
+ opl = opl & 0x3f
977
831
  res["out_path_len"] = opl
978
832
  res["out_path_hash_len"] = opl_hlen
979
833
  res["out_path"] = dbuf.read(opl*opl_hlen).hex()
980
834
  ipl = dbuf.read(1)[0]
981
835
  ipl_hlen = ((ipl & 0xc0) >> 6) + 1
982
- ipl = ipl & 0xbf
836
+ ipl = ipl & 0x3f
983
837
  res["in_path_len"] = ipl
984
838
  res["in_path_hash_len"] = ipl_hlen
985
839
  res["in_path"] = dbuf.read(ipl*ipl_hlen).hex()
@@ -0,0 +1,45 @@
1
+ import pytest
2
+
3
+ from meshcore.events import EventType
4
+ from meshcore.reader import MessageReader
5
+
6
+ pytestmark = pytest.mark.asyncio
7
+
8
+
9
+ class MockDispatcher:
10
+ def __init__(self):
11
+ self.dispatched_events = []
12
+
13
+ async def dispatch(self, event):
14
+ self.dispatched_events.append(event)
15
+
16
+
17
+ async def test_path_discovery_response_decodes_3byte_paths():
18
+ dispatcher = MockDispatcher()
19
+ reader = MessageReader(dispatcher)
20
+
21
+ packet = bytearray.fromhex(
22
+ "8d" # PacketType.PATH_DISCOVERY_RESPONSE
23
+ "00" # Reserved byte
24
+ "112233445566" # 6-byte contact public-key prefix
25
+ "82" # Outbound path descriptor: mode=2 (3-byte hops), hop_count=2
26
+ "a1a2a3b1b2b3" # Outbound path bytes: 2 hops * 3 bytes each
27
+ "81" # Inbound path descriptor: mode=2 (3-byte hops), hop_count=1
28
+ "c1c2c3" # Inbound path bytes: 1 hop * 3 bytes
29
+ )
30
+
31
+ await reader.handle_rx(packet)
32
+
33
+ matching = [e for e in dispatcher.dispatched_events if e.type == EventType.PATH_RESPONSE]
34
+ assert len(matching) == 1
35
+
36
+ payload = matching[0].payload
37
+ assert payload["pubkey_pre"] == "112233445566"
38
+
39
+ assert payload["out_path_hash_len"] == 3
40
+ assert payload["out_path_len"] == 2
41
+ assert payload["out_path"] == "a1a2a3b1b2b3"
42
+
43
+ assert payload["in_path_hash_len"] == 3
44
+ assert payload["in_path_len"] == 1
45
+ assert payload["in_path"] == "c1c2c3"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes