meshcore 1.9.8.dev2__tar.gz → 1.9.8.dev5__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 (41) hide show
  1. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/PKG-INFO +1 -1
  2. meshcore-1.9.8.dev5/examples/tcp_login_status.py +94 -0
  3. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/pyproject.toml +1 -1
  4. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/src/meshcore/ble_cx.py +6 -0
  5. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/src/meshcore/commands.py +31 -7
  6. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/src/meshcore/events.py +11 -3
  7. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/src/meshcore/meshcore.py +15 -9
  8. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/src/meshcore/reader.py +23 -7
  9. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/src/meshcore/serial_cx.py +8 -1
  10. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/src/meshcore/tcp_cx.py +7 -0
  11. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/.gitignore +0 -0
  12. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/LICENSE +0 -0
  13. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/README.md +0 -0
  14. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/examples/ble_chat.py +0 -0
  15. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/examples/ble_t1000_chan_msg.py +0 -0
  16. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/examples/ble_t1000_custom_vars.py +0 -0
  17. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/examples/ble_t1000_infos.py +0 -0
  18. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/examples/ble_t1000_msg.py +0 -0
  19. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/examples/ble_t1000_set_cv.py +0 -0
  20. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/examples/mepo_mc_gps.py +0 -0
  21. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/examples/pubsub_example.py +0 -0
  22. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/examples/rf_packet_monitor.py +0 -0
  23. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/examples/serial_battery_monitor.py +0 -0
  24. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/examples/serial_chat.py +0 -0
  25. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/examples/serial_contacts.py +0 -0
  26. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/examples/serial_infos.py +0 -0
  27. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/examples/serial_msg.py +0 -0
  28. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/examples/serial_repeater_status.py +0 -0
  29. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/examples/serial_repeater_telemetry.py +0 -0
  30. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/examples/serial_trace.py +0 -0
  31. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/examples/tcp_chat.py +0 -0
  32. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/examples/tcp_mchome_contacts.py +0 -0
  33. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/examples/tcp_mchome_infos.py +0 -0
  34. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/examples/tcp_mchome_msg.py +0 -0
  35. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/examples/tcp_mchome_readmsgs.py +0 -0
  36. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/pytest.ini +0 -0
  37. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/src/meshcore/__init__.py +0 -0
  38. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/src/meshcore/packets.py +0 -0
  39. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/tests/README.md +0 -0
  40. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/tests/unit/test_commands.py +0 -0
  41. {meshcore-1.9.8.dev2 → meshcore-1.9.8.dev5}/tests/unit/test_events.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcore
3
- Version: 1.9.8.dev2
3
+ Version: 1.9.8.dev5
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,94 @@
1
+ #!/usr/bin/python
2
+
3
+ import asyncio
4
+ import argparse
5
+ import logging
6
+ from math import log
7
+
8
+ from meshcore import MeshCore
9
+ from meshcore.events import EventType
10
+
11
+ # Set up logging
12
+ logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
13
+ logger = logging.getLogger(__name__)
14
+
15
+ async def main():
16
+ # Parse command line arguments
17
+ parser = argparse.ArgumentParser(description='Get status from a repeater via TCP connection')
18
+ parser.add_argument('-host', '--hostname', required=True, help='TCP hostname or IP address')
19
+ parser.add_argument('-port', '--port', type=int, required=True, help='TCP port number')
20
+ parser.add_argument('-r', '--repeater', required=True, help='Repeater name')
21
+ parser.add_argument('-pw', '--password', required=True, help='Password for login')
22
+ args = parser.parse_args()
23
+
24
+ # Connect to the device
25
+ print(f"Connecting to TCP {args.hostname}:{args.port}...")
26
+ mc = await MeshCore.create_tcp(args.hostname, args.port, debug=True)
27
+
28
+ try:
29
+ # Set up a simple event handler to log all events
30
+ async def log_event(event):
31
+ print(f"EVENT: {event.type.name} - Payload: {event.payload}")
32
+
33
+ # Subscribe to login events
34
+ mc.subscribe(EventType.LOGIN_SUCCESS, log_event)
35
+ mc.subscribe(EventType.LOGIN_FAILED, log_event)
36
+ mc.subscribe(EventType.STATUS_RESPONSE, log_event)
37
+
38
+ # Get contacts
39
+ await mc.ensure_contacts()
40
+
41
+ repeater = mc.get_contact_by_name(args.repeater)
42
+
43
+ if repeater is None:
44
+ print(f"Repeater '{args.repeater}' not found in contacts.")
45
+ print(f"Available contacts: {mc.contacts}")
46
+ return
47
+
48
+ print(f"Found repeater: {repeater}")
49
+
50
+ # Send login request
51
+ print(f"Sending login request to '{args.repeater}'...")
52
+ login_cmd = await mc.commands.send_login(repeater, args.password)
53
+ if login_cmd.type == EventType.ERROR:
54
+ print(f"Login failed: {login_cmd.payload}")
55
+ return
56
+
57
+ filter = {"pubkey_prefix": repeater["public_key"][0:12]}
58
+ login_result = await mc.wait_for_event(EventType.LOGIN_SUCCESS, filter, timeout=10)
59
+ print(f"Login result: {login_result}")
60
+
61
+ send_ver = await mc.commands.send_cmd(repeater, "ver")
62
+ if send_ver.type == EventType.ERROR:
63
+ print(f"Error sending version command: {send_ver.payload}")
64
+ return
65
+ await mc.wait_for_event(EventType.MESSAGES_WAITING)
66
+ ver_msg = await mc.commands.get_msg()
67
+ print(f"Version message: {ver_msg.payload}")
68
+
69
+ # Send status request
70
+ print("Sending status request...")
71
+ await mc.commands.send_statusreq(repeater)
72
+
73
+ # Wait for status response
74
+ print("Waiting for status response event...")
75
+ status_event = await mc.wait_for_event(EventType.STATUS_RESPONSE, timeout=5.0)
76
+
77
+ if status_event:
78
+ print(f"Status response received: {status_event.payload}")
79
+ else:
80
+ print("No status response received within timeout")
81
+
82
+ finally:
83
+ # Always disconnect properly
84
+ print("Disconnecting...")
85
+ await mc.disconnect()
86
+ print("Disconnected from device")
87
+
88
+ if __name__ == "__main__":
89
+ try:
90
+ asyncio.run(main())
91
+ except KeyboardInterrupt:
92
+ print("\nOperation cancelled by user")
93
+ except Exception as e:
94
+ print(f"Error: {e}")
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "meshcore"
7
- version = "1.9.8.dev2"
7
+ version = "1.9.8.dev5"
8
8
  authors = [
9
9
  { name="Florent de Lamotte", email="florent@frizoncorrea.fr" },
10
10
  { name="Alex Wolden", email="awolden@gmail.com" },
@@ -91,3 +91,9 @@ class BLEConnection:
91
91
  logger.error("RX characteristic not found")
92
92
  return False
93
93
  await self.client.write_gatt_char(self.rx_char, bytes(data), response=False)
94
+
95
+ async def disconnect(self):
96
+ """Disconnect from the BLE device."""
97
+ if self.client and self.client.is_connected:
98
+ await self.client.disconnect()
99
+ logger.debug("BLE Connection closed")
@@ -195,7 +195,33 @@ class CommandHandler:
195
195
  + int(af).to_bytes(4, 'little')\
196
196
  + int(0).to_bytes(1, 'little')\
197
197
  + int(0).to_bytes(1, 'little'), [EventType.OK, EventType.ERROR])
198
-
198
+
199
+ async def set_other_params(self, manual_add_contacts : bool, telemetry_mode_base : int, telemetry_mode_loc : int) :
200
+ telemetry_mode = (telemetry_mode_base & 0b11) | ((telemetry_mode_loc & 0b11) << 2)
201
+ data = b"\x26" + manual_add_contacts.to_bytes(1) + telemetry_mode.to_bytes(1)
202
+ return await self.send(data, [EventType.OK, EventType.ERROR])
203
+
204
+ async def set_telemetry_mode_base(self, telemetry_mode_base : int) :
205
+ infos = (await self.send_appstart()).payload
206
+ return await self.set_other_params(
207
+ infos["manual_add_contacts"],
208
+ telemetry_mode_base,
209
+ infos["telemetry_mode_loc"])
210
+
211
+ async def set_telemetry_mode_loc(self, telemetry_mode_loc : int) :
212
+ infos = (await self.send_appstart()).payload
213
+ return await self.set_other_params(
214
+ infos["manual_add_contacts"],
215
+ infos["telemetry_mode_base"],
216
+ telemetry_mode_loc)
217
+
218
+ async def set_manual_add_contacts(self, manual_add_contacts:bool) :
219
+ infos = (await self.send_appstart()).payload
220
+ return await self.set_other_params(
221
+ manual_add_contacts,
222
+ infos["telemetry_mode_base"],
223
+ infos["telemetry_mode_loc"])
224
+
199
225
  async def set_devicepin(self, pin: int) -> Event:
200
226
  logger.debug(f"Setting device PIN to: {pin}")
201
227
  return await self.send(b"\x25" \
@@ -227,6 +253,10 @@ class CommandHandler:
227
253
  data = b"\x11"
228
254
  return await self.send(data, [EventType.CONTACT_URI, EventType.ERROR])
229
255
 
256
+ async def import_contact(self, card_data) -> Event:
257
+ data = b"\x12" + card_data
258
+ return await self.send(data, [EventType.OK, EventType.ERROR])
259
+
230
260
  async def remove_contact(self, key: DestinationType) -> Event:
231
261
  key_bytes = _validate_destination(key, prefix_length=32)
232
262
  logger.debug(f"Removing contact: {key_bytes.hex()}")
@@ -306,15 +336,9 @@ class CommandHandler:
306
336
  data = b"\x03\x00" + chan.to_bytes(1, 'little') + timestamp + msg.encode("utf-8")
307
337
  return await self.send(data, [EventType.OK, EventType.ERROR])
308
338
 
309
- async def send_other_params(self, manual_add_contacts : bool, telemetry_mode_base : bool, telemetry_mode_loc : bool) :
310
- telemetry_mode = (telemetry_mode_base & 0b11) | ((telemetry_mode_loc & 0b11) << 2)
311
- data = b"\x26" + manual_add_contacts.to_bytes(1) + telemetry_mode.to_bytes(1)
312
- return await self.send(data, [EventType.OK, EventType.ERROR])
313
-
314
339
  async def send_telemetry_req(self, dst: DestinationType) -> Event :
315
340
  dst_bytes = _validate_destination(dst, prefix_length=32)
316
341
  logger.debug(f"Asking telemetry to {dst_bytes.hex()}")
317
-
318
342
  data = b"\x27\x00\x00\x00" + dst_bytes
319
343
  return await self.send(data, [EventType.MSG_SENT, EventType.ERROR])
320
344
 
@@ -64,6 +64,15 @@ class Event:
64
64
  # Add any keyword arguments to the attributes dictionary
65
65
  if kwargs:
66
66
  self.attributes.update(kwargs)
67
+ def clone(self):
68
+ """
69
+ Create a copy of the event.
70
+
71
+ Returns:
72
+ A new Event object with the same type, payload, and attributes.
73
+ """
74
+ copied_payload = self.payload.copy() if isinstance(self.payload, dict) else self.payload
75
+ return Event(self.type, copied_payload, self.attributes.copy())
67
76
 
68
77
 
69
78
  class Subscription:
@@ -79,7 +88,7 @@ class Subscription:
79
88
 
80
89
  class EventDispatcher:
81
90
  def __init__(self):
82
- self.queue = asyncio.Queue()
91
+ self.queue: asyncio.Queue[Event] = asyncio.Queue()
83
92
  self.subscriptions: List[Subscription] = []
84
93
  self.running = False
85
94
  self._task = None
@@ -118,7 +127,6 @@ class EventDispatcher:
118
127
  event = await self.queue.get()
119
128
  logger.debug(f"Dispatching event: {event.type}, {event.payload}, {event.attributes}")
120
129
  for subscription in self.subscriptions.copy():
121
- logger.debug(f"Checking subscription: {subscription.event_type}, {subscription.attribute_filters}")
122
130
  # Check if event type matches
123
131
  if subscription.event_type is None or subscription.event_type == event.type:
124
132
  # Check if all attribute filters match
@@ -128,7 +136,7 @@ class EventDispatcher:
128
136
  for key, value in subscription.attribute_filters.items()):
129
137
  continue
130
138
  try:
131
- result = subscription.callback(event)
139
+ result = subscription.callback(event.clone())
132
140
  if asyncio.iscoroutine(result):
133
141
  await result
134
142
  except Exception as e:
@@ -5,7 +5,9 @@ from typing import Optional, Dict, Any, Union
5
5
  from .events import EventDispatcher, EventType
6
6
  from .reader import MessageReader
7
7
  from .commands import CommandHandler
8
-
8
+ from .ble_cx import BLEConnection
9
+ from .tcp_cx import TCPConnection
10
+ from .serial_cx import SerialConnection
9
11
 
10
12
  # Setup default logger
11
13
  logger = logging.getLogger("meshcore")
@@ -45,9 +47,7 @@ class MeshCore:
45
47
 
46
48
  @classmethod
47
49
  async def create_tcp(cls, host: str, port: int, debug: bool = False, default_timeout=None) -> 'MeshCore':
48
- """Create and connect a MeshCore instance using TCP connection"""
49
- from .tcp_cx import TCPConnection
50
-
50
+ """Create and connect a MeshCore instance using TCP connection"""
51
51
  connection = TCPConnection(host, port)
52
52
  await connection.connect()
53
53
 
@@ -58,9 +58,6 @@ class MeshCore:
58
58
  @classmethod
59
59
  async def create_serial(cls, port: str, baudrate: int = 115200, debug: bool = False, default_timeout=None) -> 'MeshCore':
60
60
  """Create and connect a MeshCore instance using serial connection"""
61
- from .serial_cx import SerialConnection
62
- import asyncio
63
-
64
61
  connection = SerialConnection(port, baudrate)
65
62
  await connection.connect()
66
63
  await asyncio.sleep(0.1) # Time for transport to establish
@@ -75,7 +72,6 @@ class MeshCore:
75
72
 
76
73
  If address is None, it will scan for and connect to the first available MeshCore device.
77
74
  """
78
- from .ble_cx import BLEConnection
79
75
 
80
76
  connection = BLEConnection(address)
81
77
  result = await connection.connect()
@@ -91,7 +87,17 @@ class MeshCore:
91
87
  return await self.commands.send_appstart()
92
88
 
93
89
  async def disconnect(self):
90
+ """Disconnect from the device and clean up resources."""
91
+ # First stop the dispatcher to prevent any new events
94
92
  await self.dispatcher.stop()
93
+
94
+ # Stop auto message fetching if it's running
95
+ if hasattr(self, '_auto_fetch_subscription') and self._auto_fetch_subscription:
96
+ await self.stop_auto_message_fetching()
97
+
98
+ # Disconnect the connection object
99
+ if self.cx:
100
+ await self.cx.disconnect()
95
101
 
96
102
  def stop(self):
97
103
  """Synchronously stop the event dispatcher task"""
@@ -99,7 +105,7 @@ class MeshCore:
99
105
  self.dispatcher.running = False
100
106
  self.dispatcher._task.cancel()
101
107
 
102
- def subscribe(self, event_type: EventType, callback, attribute_filters: Optional[Dict[str, Any]] = None):
108
+ def subscribe(self, event_type: Union[EventType, None], callback, attribute_filters: Optional[Dict[str, Any]] = None):
103
109
  """
104
110
  Subscribe to events using EventType enum with optional attribute filtering
105
111
 
@@ -256,14 +256,31 @@ class MessageReader:
256
256
  await self.dispatcher.dispatch(Event(EventType.RAW_DATA, res))
257
257
 
258
258
  elif packet_type_value == PacketType.LOGIN_SUCCESS.value:
259
- logger.debug("Login success")
260
- # TODO: Read login attributes
261
- await self.dispatcher.dispatch(Event(EventType.LOGIN_SUCCESS, {}))
259
+ res = {}
260
+ if len(data) > 1:
261
+ res["permissions"] = data[1]
262
+ res["is_admin"] = (data[1] & 1) == 1 # Check if admin bit is set
263
+
264
+ if len(data) > 7:
265
+ res["pubkey_prefix"] = data[2:8].hex()
266
+
267
+ attributes = {
268
+ "pubkey_prefix": res.get("pubkey_prefix")
269
+ }
270
+
271
+ await self.dispatcher.dispatch(Event(EventType.LOGIN_SUCCESS, res, attributes))
262
272
 
263
273
  elif packet_type_value == PacketType.LOGIN_FAILED.value:
264
- logger.debug("Login failed")
265
- # TODO: Read login attributes
266
- await self.dispatcher.dispatch(Event(EventType.LOGIN_FAILED, {}))
274
+ res = {}
275
+
276
+ if len(data) > 7:
277
+ res["pubkey_prefix"] = data[2:8].hex()
278
+
279
+ attributes = {
280
+ "pubkey_prefix": res.get("pubkey_prefix")
281
+ }
282
+
283
+ await self.dispatcher.dispatch(Event(EventType.LOGIN_FAILED, res, attributes))
267
284
 
268
285
  elif packet_type_value == PacketType.STATUS_RESPONSE.value:
269
286
  res = {}
@@ -382,7 +399,6 @@ class MessageReader:
382
399
 
383
400
  res["pubkey_pre"] = data[2:8].hex()
384
401
  buf = data[8:]
385
- res["data"] = buf.hex()
386
402
 
387
403
  """Parse a given byte string and return as a LppFrame object."""
388
404
  i = 0
@@ -86,4 +86,11 @@ class SerialConnection:
86
86
  size = len(data)
87
87
  pkt = b"\x3c" + size.to_bytes(2, byteorder="little") + data
88
88
  logger.debug(f"sending pkt : {pkt}")
89
- self.transport.write(pkt)
89
+ self.transport.write(pkt)
90
+
91
+ async def disconnect(self):
92
+ """Close the serial connection."""
93
+ if self.transport:
94
+ self.transport.close()
95
+ self.transport = None
96
+ logger.debug("Serial Connection closed")
@@ -85,3 +85,10 @@ class TCPConnection:
85
85
  pkt = b"\x3c" + size.to_bytes(2, byteorder="little") + data
86
86
  logger.debug(f"sending pkt : {pkt}")
87
87
  self.transport.write(pkt)
88
+
89
+ async def disconnect(self):
90
+ """Close the TCP connection."""
91
+ if self.transport:
92
+ self.transport.close()
93
+ self.transport = None
94
+ logger.debug("TCP Connection closed")
File without changes
File without changes
File without changes
File without changes