meshcore 2.2.2__py3-none-any.whl → 2.2.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
meshcore/commands/base.py CHANGED
@@ -122,7 +122,7 @@ class CommandHandlerBase:
122
122
  # Create an error event when no event is received
123
123
  return Event(EventType.ERROR, {"reason": "no_event_received"})
124
124
  except asyncio.TimeoutError:
125
- logger.debug(f"Command timed out {data}")
125
+ logger.debug(f"Command timed out waiting for events {expected_events}")
126
126
  return Event(EventType.ERROR, {"reason": "timeout"})
127
127
  except Exception as e:
128
128
  logger.debug(f"Command error: {e}")
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  import logging
2
3
  from hashlib import sha256
3
4
  from typing import Optional
@@ -206,6 +207,72 @@ class DeviceCommands(CommandHandlerBase):
206
207
  logger.debug("Requesting private key export")
207
208
  return await self.send(b"\x17", [EventType.PRIVATE_KEY, EventType.DISABLED, EventType.ERROR])
208
209
 
210
+ async def import_private_key(self, key) -> Event:
211
+ logger.debug("Requesting private key import")
212
+ data = b"\x18" + key
213
+ return await self.send(data, [EventType.OK, EventType.ERROR])
214
+
215
+ async def sign_start(self) -> Event:
216
+ logger.debug("Starting signing session on device")
217
+ return await self.send(b"\x21", [EventType.SIGN_START, EventType.ERROR])
218
+
219
+ async def sign_data(self, chunk: bytes) -> Event:
220
+ if not isinstance(chunk, (bytes, bytearray)):
221
+ raise TypeError("chunk must be bytes-like")
222
+ logger.debug(f"Sending signing data chunk ({len(chunk)} bytes)")
223
+ data = b"\x22" + bytes(chunk)
224
+ result = await self.send(data, [EventType.OK, EventType.ERROR], timeout=5.0)
225
+
226
+ # If we got an error (not just timeout), return it immediately
227
+ if result.type == EventType.ERROR:
228
+ # If it's a timeout/no_event, log a warning but continue - the data may have been received
229
+ if result.payload.get("reason") in ("timeout", "no_event_received"):
230
+ logger.warning(
231
+ f"sign_data OK response not received (timeout), but continuing - "
232
+ f"data may have been processed by device"
233
+ )
234
+ return Event(EventType.OK, {})
235
+ # For actual errors (bad state, table full, etc.), return the error
236
+ return result
237
+
238
+ return result
239
+
240
+ async def sign_finish(self, timeout: Optional[float] = None, data_size: int = 0) -> Event:
241
+ logger.debug("Finalizing signing session on device")
242
+ if timeout is None:
243
+ base_timeout = max(self.default_timeout * 3, 15.0)
244
+ size_bonus = min(data_size / 2048.0, 5.0)
245
+ timeout = base_timeout + size_bonus
246
+ logger.debug(f"sign_finish using timeout={timeout:.1f} seconds (data_size={data_size} bytes)")
247
+ return await self.send(b"\x23", [EventType.SIGNATURE, EventType.ERROR], timeout=timeout)
248
+
249
+ async def sign(self, data: bytes, chunk_size: int = 120, timeout: Optional[float] = None) -> Event:
250
+ if not isinstance(data, (bytes, bytearray)):
251
+ raise TypeError("data must be bytes-like")
252
+ if chunk_size <= 0:
253
+ raise ValueError("chunk_size must be > 0")
254
+
255
+ start_evt = await self.sign_start()
256
+ if start_evt.type == EventType.ERROR:
257
+ return start_evt
258
+
259
+ max_len = start_evt.payload.get("max_length", 0)
260
+ if max_len and len(data) > max_len:
261
+ return Event(EventType.ERROR, {"reason": "data_too_large", "max_length": max_len, "len": len(data)})
262
+
263
+ for idx in range(0, len(data), chunk_size):
264
+ chunk = data[idx : idx + chunk_size]
265
+ chunk_num = (idx // chunk_size) + 1
266
+ total_chunks = (len(data) + chunk_size - 1) // chunk_size
267
+ logger.debug(f"Sending chunk {chunk_num}/{total_chunks} ({len(chunk)} bytes)")
268
+ evt = await self.sign_data(chunk)
269
+ if evt.type == EventType.ERROR:
270
+ logger.error(f"Error sending chunk {chunk_num}/{total_chunks}: {evt.payload}")
271
+ return evt
272
+ logger.debug(f"Chunk {chunk_num}/{total_chunks} sent successfully")
273
+
274
+ return await self.sign_finish(timeout=timeout, data_size=len(data))
275
+
209
276
  async def get_stats_core(self) -> Event:
210
277
  logger.debug("Getting core statistics")
211
278
  # CMD_GET_STATS (56) + STATS_TYPE_CORE (0)
meshcore/events.py CHANGED
@@ -50,6 +50,8 @@ class EventType(Enum):
50
50
  CONTROL_DATA = "control_data"
51
51
  DISCOVER_RESPONSE = "discover_response"
52
52
  NEIGHBOURS_RESPONSE = "neighbours_response"
53
+ SIGN_START = "sign_start"
54
+ SIGNATURE = "signature"
53
55
 
54
56
  # Command response types
55
57
  OK = "command_ok"
meshcore/reader.py CHANGED
@@ -710,6 +710,20 @@ class MessageReader:
710
710
  else:
711
711
  logger.error(f"Invalid private key response length: {len(data)}")
712
712
 
713
+ elif packet_type_value == PacketType.SIGN_START.value:
714
+ logger.debug(f"Received sign start response: {data.hex()}")
715
+ # Payload: 1 reserved byte, 4-byte max length
716
+ dbuf.read(1)
717
+ max_len = int.from_bytes(dbuf.read(4), "little")
718
+ res = {"max_length": max_len}
719
+ await self.dispatcher.dispatch(Event(EventType.SIGN_START, res))
720
+
721
+ elif packet_type_value == PacketType.SIGNATURE.value:
722
+ logger.debug(f"Received signature: {data.hex()}")
723
+ signature = dbuf.read()
724
+ res = {"signature": signature}
725
+ await self.dispatcher.dispatch(Event(EventType.SIGNATURE, res))
726
+
713
727
  elif packet_type_value == PacketType.DISABLED.value:
714
728
  logger.debug("Received disabled response")
715
729
  res = {"reason": "private_key_export_disabled"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcore
3
- Version: 2.2.2
3
+ Version: 2.2.4
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
@@ -490,6 +490,8 @@ All events in MeshCore are represented by the `EventType` enum. These events are
490
490
  | `LOG_DATA` | `"log_data"` | Generic log data | Various log information |
491
491
  | **Binary Protocol Events** |||
492
492
  | `BINARY_RESPONSE` | `"binary_response"` | Generic binary response | Tag and hex data |
493
+ | `SIGN_START` | `"sign_start"` | Start of an on-device signing session | Maximum buffer size (bytes) for data to sign |
494
+ | `SIGNATURE` | `"signature"` | Resulting on-device signature | Raw signature bytes |
493
495
  | **Authentication Events** |||
494
496
  | `LOGIN_SUCCESS` | `"login_success"` | Successful login | Permissions, admin status, pubkey prefix |
495
497
  | `LOGIN_FAILED` | `"login_failed"` | Failed login attempt | Pubkey prefix |
@@ -586,6 +588,9 @@ All commands are async methods that return `Event` objects. Commands are organiz
586
588
  | `req_telemetry(contact, timeout=0)` | `contact: dict, timeout: float` | `TELEMETRY_RESPONSE` | Get telemetry via binary protocol |
587
589
  | `req_mma(contact, start, end, timeout=0)` | `contact: dict, start: int, end: int, timeout: float` | `MMA_RESPONSE` | Get historical telemetry data |
588
590
  | `req_acl(contact, timeout=0)` | `contact: dict, timeout: float` | `ACL_RESPONSE` | Get access control list |
591
+ | `sign_start()` | None | `SIGN_START` | Begin a signing session; returns maximum buffer size for data to sign |
592
+ | `sign_data(chunk)` | `chunk: bytes` | `OK` | Append a data chunk to the current signing session (can be called multiple times) |
593
+ | `sign_finish()` | None | `SIGNATURE` | Finalize signing and return the signature for all accumulated data |
589
594
 
590
595
  ### Helper Methods
591
596
 
@@ -593,6 +598,7 @@ All commands are async methods that return `Event` objects. Commands are organiz
593
598
  |--------|---------|-------------|
594
599
  | `get_contact_by_name(name)` | `dict/None` | Find contact by advertisement name |
595
600
  | `get_contact_by_key_prefix(prefix)` | `dict/None` | Find contact by partial public key |
601
+ | `sign(data, chunk_size=512)` | `Event` (`SIGNATURE`/`ERROR`) | High-level helper to sign arbitrary data on-device, handling chunking for you |
596
602
  | `is_connected` | `bool` | Check if device is currently connected |
597
603
  | `subscribe(event_type, callback, filters=None)` | `Subscription` | Subscribe to events with optional filtering |
598
604
  | `unsubscribe(subscription)` | None | Remove event subscription |
@@ -632,6 +638,8 @@ Check the `examples/` directory for more:
632
638
  - `pubsub_example.py`: Event subscription system with auto-fetching
633
639
  - `serial_infos.py`: Quick device info retrieval
634
640
  - `serial_msg.py`: Message sending and receiving
641
+ - `serial_pingbot.py`: Ping bot which can be run on a channel
642
+ - `serial_meshcore_ollama.py`: Simple Ollama to Meshcore gateway, a simple chat box
635
643
  - `ble_pin_pairing_example.py`: BLE connection with PIN pairing
636
644
  - `ble_private_key_export.py`: BLE private key export with PIN authentication
637
645
  - `ble_t1000_infos.py`: BLE connections
@@ -1,22 +1,22 @@
1
1
  meshcore/__init__.py,sha256=55PdT8gZ9Lf727s7BdSuCt3lIBlAhRzX28A8i0DHaRc,602
2
2
  meshcore/ble_cx.py,sha256=S5uanBI88Mxwcs1zWbdQGVJ6GC4kdWz8-Z_2aQVpeGI,6864
3
3
  meshcore/connection_manager.py,sha256=3U0TWuHDvL5FfwEwXRy5HjlCyV3nsWhhrEC7TVxFzZI,5899
4
- meshcore/events.py,sha256=w1Ug5ugO2hAcMs7cV3WRi_4-hv0Lw_Y7NuZzZv0LPl8,8238
4
+ meshcore/events.py,sha256=7LFVxsF_U2rdS2ZfNl9QsgYOqQh3rYHYKV50LLg3wkg,8296
5
5
  meshcore/lpp_json_encoder.py,sha256=vyn7z3VYWOo_B9xmCzqblh5xCa0QKWKcMi2eOqw18RE,1875
6
6
  meshcore/meshcore.py,sha256=n8fH7AEx7DR8BHOYL_Qex9ns6ACKklNNqYbPnNnvqyE,16110
7
7
  meshcore/packets.py,sha256=3irQww-HBAe_O03oHPAdp8Z6pwenjsVX_u3F19fHKyc,1294
8
8
  meshcore/parsing.py,sha256=48PQkig-sqvsRlkF9zkvWhJoSq6ERCbGb_aRuCND5NI,4044
9
- meshcore/reader.py,sha256=YNCP4-ll5JEKW6cvbRMfjFLdG4RtVKt-npEDr8hnpwM,33939
9
+ meshcore/reader.py,sha256=1fYsTODW8csC2NxntXHACC4SJv7PlSjtUEazLCcbfTs,34618
10
10
  meshcore/serial_cx.py,sha256=-kaqnqk7Ydufu2DECFfPDv4Xs-7gHUBuz8v0xf8fvvE,3969
11
11
  meshcore/tcp_cx.py,sha256=05YRVMnjY5aVBTJcHa0uG4VfFKGbV6hQ1pPIsJg4CDI,4227
12
12
  meshcore/commands/__init__.py,sha256=qfGPzrha7pZQYHOXLZyDsK-QVRjiz5zxAEj8kMxO9uU,536
13
- meshcore/commands/base.py,sha256=yKfKSmdnIh0DhAZ34TtioE38huxXc0QGB5Hm_wbcOqg,7398
13
+ meshcore/commands/base.py,sha256=78_fyKb80Rg4xr_-MRakSh5x2d2dJFwV9db86cQ4iGo,7428
14
14
  meshcore/commands/binary.py,sha256=MihRjG4IppPYPeu2KMpctcBPac_hdClp6PgGirQfydg,8412
15
15
  meshcore/commands/contact.py,sha256=7X4e17M2YxUZsfUhaN18XZG2inTuXknXJOBAoPblydw,5880
16
16
  meshcore/commands/control_data.py,sha256=sXp1RoEw6Z0zPr0Nn5XBovEY6r9sePbWQwsbY0iYyXc,1512
17
- meshcore/commands/device.py,sha256=Ub8sS0xtJu3TJwENyswk8lYmYorMH3W33ppHEpSNpoU,9437
17
+ meshcore/commands/device.py,sha256=LEMEOQyJIcDNh5fBgX9GmFiD2AWTFhm3qaY7MTpQ5Es,12754
18
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,,
19
+ meshcore-2.2.4.dist-info/METADATA,sha256=MCwlYln0nWR6L_kVUp-M5y_ToMN4O31UaGO1O40KK1Q,26169
20
+ meshcore-2.2.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
21
+ meshcore-2.2.4.dist-info/licenses/LICENSE,sha256=o62-JWT_C-ZqEtzb1Gl_PPtPr0pVT8KDmgji_Y_bejI,1075
22
+ meshcore-2.2.4.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any