meshcore 1.9.8.dev3__tar.gz → 1.9.8.dev6__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 (42) hide show
  1. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/PKG-INFO +1 -1
  2. meshcore-1.9.8.dev6/examples/tcp_login_status.py +94 -0
  3. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/pyproject.toml +1 -1
  4. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/src/meshcore/ble_cx.py +6 -0
  5. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/src/meshcore/commands.py +4 -0
  6. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/src/meshcore/events.py +11 -3
  7. meshcore-1.9.8.dev6/src/meshcore/lpp_json_encoder.py +56 -0
  8. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/src/meshcore/meshcore.py +15 -9
  9. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/src/meshcore/reader.py +26 -8
  10. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/src/meshcore/serial_cx.py +8 -1
  11. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/src/meshcore/tcp_cx.py +7 -0
  12. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/.gitignore +0 -0
  13. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/LICENSE +0 -0
  14. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/README.md +0 -0
  15. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/examples/ble_chat.py +0 -0
  16. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/examples/ble_t1000_chan_msg.py +0 -0
  17. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/examples/ble_t1000_custom_vars.py +0 -0
  18. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/examples/ble_t1000_infos.py +0 -0
  19. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/examples/ble_t1000_msg.py +0 -0
  20. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/examples/ble_t1000_set_cv.py +0 -0
  21. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/examples/mepo_mc_gps.py +0 -0
  22. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/examples/pubsub_example.py +0 -0
  23. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/examples/rf_packet_monitor.py +0 -0
  24. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/examples/serial_battery_monitor.py +0 -0
  25. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/examples/serial_chat.py +0 -0
  26. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/examples/serial_contacts.py +0 -0
  27. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/examples/serial_infos.py +0 -0
  28. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/examples/serial_msg.py +0 -0
  29. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/examples/serial_repeater_status.py +0 -0
  30. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/examples/serial_repeater_telemetry.py +0 -0
  31. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/examples/serial_trace.py +0 -0
  32. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/examples/tcp_chat.py +0 -0
  33. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/examples/tcp_mchome_contacts.py +0 -0
  34. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/examples/tcp_mchome_infos.py +0 -0
  35. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/examples/tcp_mchome_msg.py +0 -0
  36. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/examples/tcp_mchome_readmsgs.py +0 -0
  37. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/pytest.ini +0 -0
  38. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/src/meshcore/__init__.py +0 -0
  39. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/src/meshcore/packets.py +0 -0
  40. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/tests/README.md +0 -0
  41. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/tests/unit/test_commands.py +0 -0
  42. {meshcore-1.9.8.dev3 → meshcore-1.9.8.dev6}/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.dev3
3
+ Version: 1.9.8.dev6
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.dev3"
7
+ version = "1.9.8.dev6"
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")
@@ -253,6 +253,10 @@ class CommandHandler:
253
253
  data = b"\x11"
254
254
  return await self.send(data, [EventType.CONTACT_URI, EventType.ERROR])
255
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
+
256
260
  async def remove_contact(self, key: DestinationType) -> Event:
257
261
  key_bytes = _validate_destination(key, prefix_length=32)
258
262
  logger.debug(f"Removing contact: {key_bytes.hex()}")
@@ -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:
@@ -0,0 +1,56 @@
1
+ from cayennelpp import LppFrame, LppData
2
+ from cayennelpp.lpp_type import LppType
3
+
4
+ # type name "how to display vale"
5
+ # display : None: (use lib default), []: only one value to display, ["field1", "field2" ...]: meaning of each field
6
+ my_lpp_types = {
7
+ 0: ('digital input', []),
8
+ 1: ('digital output', []),
9
+ 2: ('analog input', []),
10
+ 3: ('analog output', []),
11
+ 100: ('generic sensor', []),
12
+ 101: ('illuminance', []),
13
+ 102: ('presence', []),
14
+ 103: ('temperature', []),
15
+ 104: ('humidity', []),
16
+ 113: ('accelerometer', ["acc_x", "acc_y", "acc_z"]),
17
+ 115: ('barometer', []),
18
+ 116: ('voltage', []),
19
+ 117: ('current', []),
20
+ 118: ('frequency', []),
21
+ 120: ('percentage', []),
22
+ 121: ('altitude', []),
23
+ 122: ('load', []),
24
+ 125: ('concentration', []),
25
+ 128: ('power', []),
26
+ 130: ('distance', []),
27
+ 131: ('energy', []),
28
+ 132: ('direction', None),
29
+ 133: ('time', []),
30
+ 134: ('gyrometer', None),
31
+ 135: ('colour', ["red", "green", "blue"]),
32
+ 136: ('gps', ["latitude", "longitude", "altitude"]),
33
+ 142: ('switch', []),
34
+ }
35
+
36
+ def lpp_json_encoder (obj) :
37
+ """Encode LppType, LppData, and LppFrame to JSON."""
38
+ if isinstance(obj, LppFrame):
39
+ return obj.data
40
+ if isinstance(obj, LppType):
41
+ return my_lpp_types[obj.type][0]
42
+ if isinstance(obj, LppData):
43
+ d = {"channel" : obj.channel, "type" : obj.type}
44
+ if my_lpp_types[obj.type.type][1] is None :
45
+ d["value"] = obj.value
46
+ elif len(my_lpp_types[obj.type.type][1]) == 0 :
47
+ d["value"] = obj.value[0]
48
+ else :
49
+ val_dict = {}
50
+ i = 0
51
+ for t in my_lpp_types[obj.type.type][1] :
52
+ val_dict[t] = obj.value[i]
53
+ i = i + 1
54
+ d["value"] = val_dict
55
+ return d
56
+ raise TypeError(repr(obj) + " is not JSON serialized")
@@ -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
 
@@ -2,10 +2,11 @@ import sys
2
2
  import logging
3
3
  import asyncio
4
4
  import json
5
- from cayennelpp import LppFrame, LppData, LppUtil
5
+ from cayennelpp import LppFrame, LppData
6
6
  from typing import Any, Optional, Dict
7
7
  from .events import Event, EventType, EventDispatcher
8
8
  from .packets import PacketType
9
+ from meshcore.lpp_json_encoder import lpp_json_encoder
9
10
 
10
11
  logger = logging.getLogger("meshcore")
11
12
 
@@ -256,14 +257,31 @@ class MessageReader:
256
257
  await self.dispatcher.dispatch(Event(EventType.RAW_DATA, res))
257
258
 
258
259
  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, {}))
260
+ res = {}
261
+ if len(data) > 1:
262
+ res["permissions"] = data[1]
263
+ res["is_admin"] = (data[1] & 1) == 1 # Check if admin bit is set
264
+
265
+ if len(data) > 7:
266
+ res["pubkey_prefix"] = data[2:8].hex()
267
+
268
+ attributes = {
269
+ "pubkey_prefix": res.get("pubkey_prefix")
270
+ }
271
+
272
+ await self.dispatcher.dispatch(Event(EventType.LOGIN_SUCCESS, res, attributes))
262
273
 
263
274
  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, {}))
275
+ res = {}
276
+
277
+ if len(data) > 7:
278
+ res["pubkey_prefix"] = data[2:8].hex()
279
+
280
+ attributes = {
281
+ "pubkey_prefix": res.get("pubkey_prefix")
282
+ }
283
+
284
+ await self.dispatcher.dispatch(Event(EventType.LOGIN_FAILED, res, attributes))
267
285
 
268
286
  elif packet_type_value == PacketType.STATUS_RESPONSE.value:
269
287
  res = {}
@@ -391,7 +409,7 @@ class MessageReader:
391
409
  data.append(lppdata)
392
410
  i = i + len(lppdata)
393
411
 
394
- lpp = json.loads(json.dumps(LppFrame(data), default=LppUtil.json_encode_type_str))
412
+ lpp = json.loads(json.dumps(LppFrame(data), default=lpp_json_encoder))
395
413
 
396
414
  res["lpp"] = lpp
397
415
 
@@ -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