meshcore 2.1.21__tar.gz → 2.2.6__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 (68) hide show
  1. {meshcore-2.1.21 → meshcore-2.2.6}/PKG-INFO +22 -3
  2. {meshcore-2.1.21 → meshcore-2.2.6}/README.md +20 -1
  3. meshcore-2.2.6/examples/ble_sign_example.py +136 -0
  4. meshcore-2.2.6/examples/ble_stats.py +77 -0
  5. meshcore-2.2.6/examples/serial_meshcore_ollama.py +193 -0
  6. meshcore-2.2.6/examples/serial_pingbot.py +159 -0
  7. meshcore-2.2.6/examples/serial_rss_bot.py +284 -0
  8. {meshcore-2.1.21 → meshcore-2.2.6}/pyproject.toml +2 -2
  9. {meshcore-2.1.21 → meshcore-2.2.6}/src/meshcore/commands/base.py +29 -4
  10. meshcore-2.2.6/src/meshcore/commands/binary.py +352 -0
  11. {meshcore-2.1.21 → meshcore-2.2.6}/src/meshcore/commands/contact.py +11 -2
  12. meshcore-2.2.6/src/meshcore/commands/control_data.py +96 -0
  13. {meshcore-2.1.21 → meshcore-2.2.6}/src/meshcore/commands/device.py +89 -7
  14. {meshcore-2.1.21 → meshcore-2.2.6}/src/meshcore/commands/messaging.py +44 -15
  15. {meshcore-2.1.21 → meshcore-2.2.6}/src/meshcore/events.py +8 -0
  16. {meshcore-2.1.21 → meshcore-2.2.6}/src/meshcore/packets.py +9 -0
  17. {meshcore-2.1.21 → meshcore-2.2.6}/src/meshcore/reader.py +228 -9
  18. {meshcore-2.1.21 → meshcore-2.2.6}/src/meshcore/serial_cx.py +1 -1
  19. {meshcore-2.1.21 → meshcore-2.2.6}/tests/unit/test_commands.py +23 -0
  20. meshcore-2.1.21/src/meshcore/commands/binary.py +0 -133
  21. meshcore-2.1.21/src/meshcore/commands/control_data.py +0 -45
  22. {meshcore-2.1.21 → meshcore-2.2.6}/.github/python-test.yml +0 -0
  23. {meshcore-2.1.21 → meshcore-2.2.6}/.gitignore +0 -0
  24. {meshcore-2.1.21 → meshcore-2.2.6}/LICENSE +0 -0
  25. {meshcore-2.1.21 → meshcore-2.2.6}/examples/ble_chat.py +0 -0
  26. {meshcore-2.1.21 → meshcore-2.2.6}/examples/ble_pin_pairing_example.py +0 -0
  27. {meshcore-2.1.21 → meshcore-2.2.6}/examples/ble_private_key_export.py +0 -0
  28. {meshcore-2.1.21 → meshcore-2.2.6}/examples/ble_t1000_chan_msg.py +0 -0
  29. {meshcore-2.1.21 → meshcore-2.2.6}/examples/ble_t1000_custom_vars.py +0 -0
  30. {meshcore-2.1.21 → meshcore-2.2.6}/examples/ble_t1000_infos.py +0 -0
  31. {meshcore-2.1.21 → meshcore-2.2.6}/examples/ble_t1000_msg.py +0 -0
  32. {meshcore-2.1.21 → meshcore-2.2.6}/examples/ble_t1000_msg_retries.py +0 -0
  33. {meshcore-2.1.21 → meshcore-2.2.6}/examples/ble_t1000_set_cv.py +0 -0
  34. {meshcore-2.1.21 → meshcore-2.2.6}/examples/connection_events_example.py +0 -0
  35. {meshcore-2.1.21 → meshcore-2.2.6}/examples/mepo_mc_gps.py +0 -0
  36. {meshcore-2.1.21 → meshcore-2.2.6}/examples/pubsub_example.py +0 -0
  37. {meshcore-2.1.21 → meshcore-2.2.6}/examples/rf_packet_monitor.py +0 -0
  38. {meshcore-2.1.21 → meshcore-2.2.6}/examples/serial_battery_monitor.py +0 -0
  39. {meshcore-2.1.21 → meshcore-2.2.6}/examples/serial_channel_manager.py +0 -0
  40. {meshcore-2.1.21 → meshcore-2.2.6}/examples/serial_chat.py +0 -0
  41. {meshcore-2.1.21 → meshcore-2.2.6}/examples/serial_contacts.py +0 -0
  42. {meshcore-2.1.21 → meshcore-2.2.6}/examples/serial_infos.py +0 -0
  43. {meshcore-2.1.21 → meshcore-2.2.6}/examples/serial_msg.py +0 -0
  44. {meshcore-2.1.21 → meshcore-2.2.6}/examples/serial_repeater_status.py +0 -0
  45. {meshcore-2.1.21 → meshcore-2.2.6}/examples/serial_repeater_telemetry.py +0 -0
  46. {meshcore-2.1.21 → meshcore-2.2.6}/examples/serial_trace.py +0 -0
  47. {meshcore-2.1.21 → meshcore-2.2.6}/examples/tcp_chat.py +0 -0
  48. {meshcore-2.1.21 → meshcore-2.2.6}/examples/tcp_login_status.py +0 -0
  49. {meshcore-2.1.21 → meshcore-2.2.6}/examples/tcp_mchome_contacts.py +0 -0
  50. {meshcore-2.1.21 → meshcore-2.2.6}/examples/tcp_mchome_infos.py +0 -0
  51. {meshcore-2.1.21 → meshcore-2.2.6}/examples/tcp_mchome_msg.py +0 -0
  52. {meshcore-2.1.21 → meshcore-2.2.6}/examples/tcp_mchome_readmsgs.py +0 -0
  53. {meshcore-2.1.21 → meshcore-2.2.6}/pytest.ini +0 -0
  54. {meshcore-2.1.21 → meshcore-2.2.6}/src/meshcore/__init__.py +0 -0
  55. {meshcore-2.1.21 → meshcore-2.2.6}/src/meshcore/ble_cx.py +0 -0
  56. {meshcore-2.1.21 → meshcore-2.2.6}/src/meshcore/commands/__init__.py +0 -0
  57. {meshcore-2.1.21 → meshcore-2.2.6}/src/meshcore/connection_manager.py +0 -0
  58. {meshcore-2.1.21 → meshcore-2.2.6}/src/meshcore/lpp_json_encoder.py +0 -0
  59. {meshcore-2.1.21 → meshcore-2.2.6}/src/meshcore/meshcore.py +0 -0
  60. {meshcore-2.1.21 → meshcore-2.2.6}/src/meshcore/parsing.py +0 -0
  61. {meshcore-2.1.21 → meshcore-2.2.6}/src/meshcore/tcp_cx.py +0 -0
  62. {meshcore-2.1.21 → meshcore-2.2.6}/tests/README.md +0 -0
  63. {meshcore-2.1.21 → meshcore-2.2.6}/tests/test_ble_connection.py +0 -0
  64. {meshcore-2.1.21 → meshcore-2.2.6}/tests/test_ble_pin_pairing.py +0 -0
  65. {meshcore-2.1.21 → meshcore-2.2.6}/tests/test_meshcore_ble_pin.py +0 -0
  66. {meshcore-2.1.21 → meshcore-2.2.6}/tests/unit/test_events.py +0 -0
  67. {meshcore-2.1.21 → meshcore-2.2.6}/tests/unit/test_private_key_export.py +0 -0
  68. {meshcore-2.1.21 → meshcore-2.2.6}/tests/unit/test_reader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcore
3
- Version: 2.1.21
3
+ Version: 2.2.6
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
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: >=3.10
13
13
  Requires-Dist: bleak
14
14
  Requires-Dist: pycayennelpp
15
- Requires-Dist: pyserial-asyncio
15
+ Requires-Dist: pyserial-asyncio-fast
16
16
  Provides-Extra: dev
17
17
  Requires-Dist: black; extra == 'dev'
18
18
  Requires-Dist: pytest; extra == 'dev'
@@ -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 |
@@ -539,6 +541,13 @@ All commands are async methods that return `Event` objects. Commands are organiz
539
541
  | `reboot()` | None | None | Reboot device (no response expected) |
540
542
  | **Security** ||||
541
543
  | `export_private_key()` | None | `PRIVATE_KEY/DISABLED` | Export device private key (requires PIN auth & enabled firmware) |
544
+ | `import_private_key(key)` | `key: bytes` | `OK` | Import private key to device |
545
+ | **Statistics** ||||
546
+ | `get_stats_core()` | None | `STATS_CORE` | Get core statistics (voltage, uptime, errors, queue length) |
547
+ | `get_stats_radio()` | None | `STATS_RADIO` | Get radio statistics (noise floor, last RSSI/SNR, tx/rx time stats) |
548
+ | `get_stats_packets()` | None | `STATS_PACKETS` | Get packet statistics (rx/tx totals, flood vs. direct, recv_errors when present) |
549
+ | **Advanced Configuration** ||||
550
+ | `set_multi_acks(multi_acks)` | `multi_acks: int` | `OK` | Set multi-acks mode (experimental ack repeats) |
542
551
 
543
552
  #### Contact Commands (`meshcore.commands.*`)
544
553
 
@@ -566,7 +575,7 @@ All commands are async methods that return `Event` objects. Commands are organiz
566
575
  | `get_msg(timeout=None)` | `timeout: float` | `CONTACT_MSG_RECV/CHANNEL_MSG_RECV/NO_MORE_MSGS` | Get next pending message |
567
576
  | `send_msg(dst, msg, timestamp=None)` | `dst: contact/str/bytes, msg: str, timestamp: int` | `MSG_SENT` | Send direct message |
568
577
  | `send_cmd(dst, cmd, timestamp=None)` | `dst: contact/str/bytes, cmd: str, timestamp: int` | `MSG_SENT` | Send command message |
569
- | `send_chan_msg(chan, msg, timestamp=None)` | `chan: int, msg: str, timestamp: int` | `MSG_SENT` | Send channel message |
578
+ | `send_chan_msg(chan, msg, timestamp=None)` | `chan: int, msg: str, timestamp: int` | `MSG_OK` | Send channel message |
570
579
  | **Authentication** ||||
571
580
  | `send_login(dst, pwd)` | `dst: contact/str/bytes, pwd: str` | `MSG_SENT` | Send login request |
572
581
  | `send_logout(dst)` | `dst: contact/str/bytes` | `MSG_SENT` | Send logout request |
@@ -577,6 +586,9 @@ All commands are async methods that return `Event` objects. Commands are organiz
577
586
  | `send_binary_req(dst, bin_data)` | `dst: contact/str/bytes, bin_data: bytes` | `MSG_SENT` | Send binary data request |
578
587
  | `send_path_discovery(dst)` | `dst: contact/str/bytes` | `MSG_SENT` | Initiate path discovery |
579
588
  | `send_trace(auth_code, tag, flags, path=None)` | `auth_code: int, tag: int, flags: int, path: list` | `MSG_SENT` | Send route trace packet |
589
+ | **Message Retry & Scope** ||||
590
+ | `send_msg_with_retry(dst, msg, ...)` | `dst, msg, timestamp, max_attempts, max_flood_attempts, flood_after, timeout, min_timeout` | `MSG_SENT/None` | Send message with automatic retry and ACK waiting |
591
+ | `set_flood_scope(scope)` | `scope: str` | `OK` | Set flood scope (hash like "#name", "0"/""/"*" to disable, or raw key) |
580
592
 
581
593
  #### Binary Protocol Commands (`meshcore.commands.*`)
582
594
 
@@ -586,6 +598,9 @@ All commands are async methods that return `Event` objects. Commands are organiz
586
598
  | `req_telemetry(contact, timeout=0)` | `contact: dict, timeout: float` | `TELEMETRY_RESPONSE` | Get telemetry via binary protocol |
587
599
  | `req_mma(contact, start, end, timeout=0)` | `contact: dict, start: int, end: int, timeout: float` | `MMA_RESPONSE` | Get historical telemetry data |
588
600
  | `req_acl(contact, timeout=0)` | `contact: dict, timeout: float` | `ACL_RESPONSE` | Get access control list |
601
+ | `sign_start()` | None | `SIGN_START` | Begin a signing session; returns maximum buffer size for data to sign |
602
+ | `sign_data(chunk)` | `chunk: bytes` | `OK` | Append a data chunk to the current signing session (can be called multiple times) |
603
+ | `sign_finish()` | None | `SIGNATURE` | Finalize signing and return the signature for all accumulated data |
589
604
 
590
605
  ### Helper Methods
591
606
 
@@ -593,6 +608,7 @@ All commands are async methods that return `Event` objects. Commands are organiz
593
608
  |--------|---------|-------------|
594
609
  | `get_contact_by_name(name)` | `dict/None` | Find contact by advertisement name |
595
610
  | `get_contact_by_key_prefix(prefix)` | `dict/None` | Find contact by partial public key |
611
+ | `sign(data, chunk_size=512)` | `Event` (`SIGNATURE`/`ERROR`) | High-level helper to sign arbitrary data on-device, handling chunking for you |
596
612
  | `is_connected` | `bool` | Check if device is currently connected |
597
613
  | `subscribe(event_type, callback, filters=None)` | `Subscription` | Subscribe to events with optional filtering |
598
614
  | `unsubscribe(subscription)` | None | Remove event subscription |
@@ -632,6 +648,9 @@ Check the `examples/` directory for more:
632
648
  - `pubsub_example.py`: Event subscription system with auto-fetching
633
649
  - `serial_infos.py`: Quick device info retrieval
634
650
  - `serial_msg.py`: Message sending and receiving
651
+ - `serial_pingbot.py`: Ping bot which can be run on a channel
652
+ - `serial_rss_bot.py`: A RSS feed to Meshcore channel example, which broadcasts emergency bushfire warnings in VIC, AU
653
+ - `serial_meshcore_ollama.py`: Simple Ollama to Meshcore gateway, a simple chat box
635
654
  - `ble_pin_pairing_example.py`: BLE connection with PIN pairing
636
655
  - `ble_private_key_export.py`: BLE private key export with PIN authentication
637
656
  - `ble_t1000_infos.py`: BLE connections
@@ -468,6 +468,8 @@ All events in MeshCore are represented by the `EventType` enum. These events are
468
468
  | `LOG_DATA` | `"log_data"` | Generic log data | Various log information |
469
469
  | **Binary Protocol Events** |||
470
470
  | `BINARY_RESPONSE` | `"binary_response"` | Generic binary response | Tag and hex data |
471
+ | `SIGN_START` | `"sign_start"` | Start of an on-device signing session | Maximum buffer size (bytes) for data to sign |
472
+ | `SIGNATURE` | `"signature"` | Resulting on-device signature | Raw signature bytes |
471
473
  | **Authentication Events** |||
472
474
  | `LOGIN_SUCCESS` | `"login_success"` | Successful login | Permissions, admin status, pubkey prefix |
473
475
  | `LOGIN_FAILED` | `"login_failed"` | Failed login attempt | Pubkey prefix |
@@ -517,6 +519,13 @@ All commands are async methods that return `Event` objects. Commands are organiz
517
519
  | `reboot()` | None | None | Reboot device (no response expected) |
518
520
  | **Security** ||||
519
521
  | `export_private_key()` | None | `PRIVATE_KEY/DISABLED` | Export device private key (requires PIN auth & enabled firmware) |
522
+ | `import_private_key(key)` | `key: bytes` | `OK` | Import private key to device |
523
+ | **Statistics** ||||
524
+ | `get_stats_core()` | None | `STATS_CORE` | Get core statistics (voltage, uptime, errors, queue length) |
525
+ | `get_stats_radio()` | None | `STATS_RADIO` | Get radio statistics (noise floor, last RSSI/SNR, tx/rx time stats) |
526
+ | `get_stats_packets()` | None | `STATS_PACKETS` | Get packet statistics (rx/tx totals, flood vs. direct, recv_errors when present) |
527
+ | **Advanced Configuration** ||||
528
+ | `set_multi_acks(multi_acks)` | `multi_acks: int` | `OK` | Set multi-acks mode (experimental ack repeats) |
520
529
 
521
530
  #### Contact Commands (`meshcore.commands.*`)
522
531
 
@@ -544,7 +553,7 @@ All commands are async methods that return `Event` objects. Commands are organiz
544
553
  | `get_msg(timeout=None)` | `timeout: float` | `CONTACT_MSG_RECV/CHANNEL_MSG_RECV/NO_MORE_MSGS` | Get next pending message |
545
554
  | `send_msg(dst, msg, timestamp=None)` | `dst: contact/str/bytes, msg: str, timestamp: int` | `MSG_SENT` | Send direct message |
546
555
  | `send_cmd(dst, cmd, timestamp=None)` | `dst: contact/str/bytes, cmd: str, timestamp: int` | `MSG_SENT` | Send command message |
547
- | `send_chan_msg(chan, msg, timestamp=None)` | `chan: int, msg: str, timestamp: int` | `MSG_SENT` | Send channel message |
556
+ | `send_chan_msg(chan, msg, timestamp=None)` | `chan: int, msg: str, timestamp: int` | `MSG_OK` | Send channel message |
548
557
  | **Authentication** ||||
549
558
  | `send_login(dst, pwd)` | `dst: contact/str/bytes, pwd: str` | `MSG_SENT` | Send login request |
550
559
  | `send_logout(dst)` | `dst: contact/str/bytes` | `MSG_SENT` | Send logout request |
@@ -555,6 +564,9 @@ All commands are async methods that return `Event` objects. Commands are organiz
555
564
  | `send_binary_req(dst, bin_data)` | `dst: contact/str/bytes, bin_data: bytes` | `MSG_SENT` | Send binary data request |
556
565
  | `send_path_discovery(dst)` | `dst: contact/str/bytes` | `MSG_SENT` | Initiate path discovery |
557
566
  | `send_trace(auth_code, tag, flags, path=None)` | `auth_code: int, tag: int, flags: int, path: list` | `MSG_SENT` | Send route trace packet |
567
+ | **Message Retry & Scope** ||||
568
+ | `send_msg_with_retry(dst, msg, ...)` | `dst, msg, timestamp, max_attempts, max_flood_attempts, flood_after, timeout, min_timeout` | `MSG_SENT/None` | Send message with automatic retry and ACK waiting |
569
+ | `set_flood_scope(scope)` | `scope: str` | `OK` | Set flood scope (hash like "#name", "0"/""/"*" to disable, or raw key) |
558
570
 
559
571
  #### Binary Protocol Commands (`meshcore.commands.*`)
560
572
 
@@ -564,6 +576,9 @@ All commands are async methods that return `Event` objects. Commands are organiz
564
576
  | `req_telemetry(contact, timeout=0)` | `contact: dict, timeout: float` | `TELEMETRY_RESPONSE` | Get telemetry via binary protocol |
565
577
  | `req_mma(contact, start, end, timeout=0)` | `contact: dict, start: int, end: int, timeout: float` | `MMA_RESPONSE` | Get historical telemetry data |
566
578
  | `req_acl(contact, timeout=0)` | `contact: dict, timeout: float` | `ACL_RESPONSE` | Get access control list |
579
+ | `sign_start()` | None | `SIGN_START` | Begin a signing session; returns maximum buffer size for data to sign |
580
+ | `sign_data(chunk)` | `chunk: bytes` | `OK` | Append a data chunk to the current signing session (can be called multiple times) |
581
+ | `sign_finish()` | None | `SIGNATURE` | Finalize signing and return the signature for all accumulated data |
567
582
 
568
583
  ### Helper Methods
569
584
 
@@ -571,6 +586,7 @@ All commands are async methods that return `Event` objects. Commands are organiz
571
586
  |--------|---------|-------------|
572
587
  | `get_contact_by_name(name)` | `dict/None` | Find contact by advertisement name |
573
588
  | `get_contact_by_key_prefix(prefix)` | `dict/None` | Find contact by partial public key |
589
+ | `sign(data, chunk_size=512)` | `Event` (`SIGNATURE`/`ERROR`) | High-level helper to sign arbitrary data on-device, handling chunking for you |
574
590
  | `is_connected` | `bool` | Check if device is currently connected |
575
591
  | `subscribe(event_type, callback, filters=None)` | `Subscription` | Subscribe to events with optional filtering |
576
592
  | `unsubscribe(subscription)` | None | Remove event subscription |
@@ -610,6 +626,9 @@ Check the `examples/` directory for more:
610
626
  - `pubsub_example.py`: Event subscription system with auto-fetching
611
627
  - `serial_infos.py`: Quick device info retrieval
612
628
  - `serial_msg.py`: Message sending and receiving
629
+ - `serial_pingbot.py`: Ping bot which can be run on a channel
630
+ - `serial_rss_bot.py`: A RSS feed to Meshcore channel example, which broadcasts emergency bushfire warnings in VIC, AU
631
+ - `serial_meshcore_ollama.py`: Simple Ollama to Meshcore gateway, a simple chat box
613
632
  - `ble_pin_pairing_example.py`: BLE connection with PIN pairing
614
633
  - `ble_private_key_export.py`: BLE private key export with PIN authentication
615
634
  - `ble_t1000_infos.py`: BLE connections
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Example: Sign arbitrary data with a MeshCore device over BLE.
4
+
5
+ The device performs signing on its private key via the CMD_SIGN_* flow:
6
+ - sign_start(): initializes a signing session and returns max buffer size (8KB on firmware)
7
+ - sign_data(): streams one or more data chunks
8
+ - sign_finish(): returns the signature
9
+ """
10
+
11
+ import argparse
12
+ import asyncio
13
+ from pathlib import Path
14
+ import sys
15
+ from textwrap import wrap
16
+
17
+ # Ensure local src/ is on path when running from repo root
18
+ repo_root = Path(__file__).resolve().parents[1]
19
+ src_path = repo_root / "src"
20
+ if src_path.exists():
21
+ sys.path.insert(0, str(src_path))
22
+
23
+ from meshcore import MeshCore, EventType
24
+
25
+
26
+ async def main():
27
+ parser = argparse.ArgumentParser(
28
+ description="Sign data using a MeshCore device over BLE"
29
+ )
30
+ parser.add_argument(
31
+ "-a",
32
+ "--addr",
33
+ help="BLE address of the device (optional, will scan if not provided)",
34
+ )
35
+ parser.add_argument(
36
+ "-p",
37
+ "--pin",
38
+ help="PIN for BLE pairing (optional)",
39
+ )
40
+ parser.add_argument(
41
+ "-d",
42
+ "--data",
43
+ default="Hello from meshcore_py!",
44
+ help="ASCII data to sign (will be UTF-8 encoded)",
45
+ )
46
+ parser.add_argument(
47
+ "--chunk-size",
48
+ type=int,
49
+ default=120,
50
+ help="Chunk size to stream to the device (bytes). Default 120 for BLE (frames under 128 bytes work better). For serial/TCP, larger values (e.g., 512) work fine.",
51
+ )
52
+ parser.add_argument(
53
+ "--timeout",
54
+ type=float,
55
+ default=None,
56
+ help="Timeout for sign_finish operation in seconds (default: 15s minimum, longer for large data like JWT tokens)",
57
+ )
58
+ parser.add_argument(
59
+ "--debug",
60
+ action="store_true",
61
+ help="Enable debug logging",
62
+ )
63
+ args = parser.parse_args()
64
+
65
+ meshcore = None
66
+ try:
67
+ print("Connecting to MeshCore device...")
68
+ meshcore = await MeshCore.create_ble(address=args.addr, pin=args.pin, debug=args.debug)
69
+ print("✅ Connected.")
70
+
71
+ data_bytes = args.data.encode("utf-8")
72
+ print(f"Data to sign: {len(data_bytes)} bytes")
73
+ if args.debug:
74
+ print(f"Data hex (first 100 bytes): {data_bytes[:100].hex()}")
75
+
76
+ sig_evt = await meshcore.commands.sign(data_bytes, chunk_size=max(1, args.chunk_size), timeout=args.timeout)
77
+ if sig_evt.type == EventType.ERROR:
78
+ raise RuntimeError(f"sign failed: {sig_evt.payload}")
79
+ signature = sig_evt.payload.get("signature", b"")
80
+ print(f"Signature ({len(signature)} bytes):")
81
+ # Pretty-print hex in 32-byte lines
82
+ hex_sig = signature.hex()
83
+ for line in wrap(hex_sig, 64):
84
+ print(line)
85
+
86
+ # Verify signature with device's public key
87
+ try:
88
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
89
+ from cryptography.exceptions import InvalidSignature
90
+
91
+ # Get device's public key from self_info
92
+ self_info = meshcore.self_info
93
+ if not self_info or "public_key" not in self_info:
94
+ print("\n⚠️ Could not get device public key for verification")
95
+ else:
96
+ pubkey_hex = self_info["public_key"]
97
+ pubkey_bytes = bytes.fromhex(pubkey_hex)
98
+
99
+ try:
100
+ public_key = Ed25519PublicKey.from_public_bytes(pubkey_bytes)
101
+ public_key.verify(signature, data_bytes)
102
+ print("\n✅ Signature verification: SUCCESS (signature is valid)")
103
+ except InvalidSignature:
104
+ print("\n❌ Signature verification: FAILED (signature is invalid)")
105
+ if args.debug:
106
+ print(f" Public key: {pubkey_hex}")
107
+ print(f" Data length: {len(data_bytes)} bytes")
108
+ print(f" Signature length: {len(signature)} bytes")
109
+ print(f" Data (first 50 bytes): {data_bytes[:50].hex()}")
110
+ except Exception as e:
111
+ print(f"\n⚠️ Signature verification error: {e}")
112
+ except ImportError:
113
+ print("\n⚠️ cryptography library not available - skipping signature verification")
114
+ print(" Install with: pip install cryptography")
115
+
116
+ print("\nSigning flow completed!")
117
+
118
+ except ConnectionError as e:
119
+ print(f"❌ Failed to connect: {e}")
120
+ return 1
121
+ except Exception as e:
122
+ print(f"❌ Error: {e}")
123
+ return 1
124
+ finally:
125
+ if meshcore:
126
+ await meshcore.disconnect()
127
+ print("Disconnected.")
128
+
129
+
130
+ if __name__ == "__main__":
131
+ try:
132
+ sys.exit(asyncio.run(main()))
133
+ except KeyboardInterrupt:
134
+ print("\nInterrupted by user")
135
+ sys.exit(1)
136
+
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/python
2
+
3
+ import asyncio
4
+ import argparse
5
+ import json
6
+ from meshcore import MeshCore, EventType
7
+
8
+ DEFAULT_ADDRESS = "MeshCore-123456789" # Default BLE address or name
9
+
10
+ async def main():
11
+ parser = argparse.ArgumentParser(description="Read statistics from MeshCore device via BLE")
12
+ parser.add_argument("-a", "--address", default=DEFAULT_ADDRESS,
13
+ help="BLE device address or name (default: %(default)s)")
14
+ parser.add_argument("-p", "--pin", type=int, default=None,
15
+ help="PIN for BLE pairing (optional)")
16
+ args = parser.parse_args()
17
+
18
+ print(f"Connecting to BLE device: {args.address}")
19
+ if args.pin:
20
+ print(f"Using PIN pairing: {args.pin}")
21
+ mc = await MeshCore.create_ble(args.address, pin=str(args.pin))
22
+ else:
23
+ mc = await MeshCore.create_ble(args.address)
24
+
25
+ print("Connected successfully!\n")
26
+
27
+ try:
28
+ # Get core statistics
29
+ print("Fetching core statistics...")
30
+ result = await mc.commands.get_stats_core()
31
+ if result.type == EventType.ERROR:
32
+ print(f"❌ Error getting core stats: {result.payload}")
33
+ else:
34
+ print("📊 Core Statistics:")
35
+ print(json.dumps(result.payload, indent=2))
36
+ print()
37
+
38
+ # Get radio statistics
39
+ print("Fetching radio statistics...")
40
+ result = await mc.commands.get_stats_radio()
41
+ if result.type == EventType.ERROR:
42
+ print(f"❌ Error getting radio stats: {result.payload}")
43
+ else:
44
+ print("📡 Radio Statistics:")
45
+ print(json.dumps(result.payload, indent=2))
46
+ print()
47
+
48
+ # Get packet statistics
49
+ print("Fetching packet statistics...")
50
+ result = await mc.commands.get_stats_packets()
51
+ if result.type == EventType.ERROR:
52
+ print(f"❌ Error getting packet stats: {result.payload}")
53
+ else:
54
+ print("📦 Packet Statistics:")
55
+ print(json.dumps(result.payload, indent=2))
56
+ recv_errors = result.payload.get("recv_errors")
57
+ if recv_errors is not None:
58
+ print(f" Receive/CRC errors (RadioLib): {recv_errors}")
59
+ else:
60
+ print(" Receive/CRC errors (RadioLib): not reported (legacy 26-byte frame)")
61
+ print()
62
+
63
+ except Exception as e:
64
+ print(f"❌ Error: {e}")
65
+ finally:
66
+ print("Disconnecting...")
67
+ await mc.disconnect()
68
+ print("Disconnected.")
69
+
70
+ if __name__ == "__main__":
71
+ try:
72
+ asyncio.run(main())
73
+ except KeyboardInterrupt:
74
+ print("\nExited cleanly")
75
+ except Exception as e:
76
+ print(f"Error: {e}")
77
+
@@ -0,0 +1,193 @@
1
+ import asyncio
2
+ from meshcore import MeshCore, EventType
3
+
4
+ from ollama import AsyncClient, ResponseError
5
+
6
+ SERIAL_PORT = "COM16" # change this to your serial port
7
+ CHANNEL_IDX = 4 # change this to the index of your "#ping" channel
8
+ MODEL_NAME = "qwen:0.5b" # Ollama model name
9
+
10
+ # FLAGS
11
+ MAX_REPLY_CHARS = 120 # total length of the final reply, including "@[sender] "
12
+ ENABLE_MODEL_NSFW_FILTERING = True # if True, Qwen is instructed to block unsafe content
13
+ USE_CONVERSATION_HISTORY = False # keep False to ensure no history
14
+ ASK_MODEL_TO_LIMIT_CHARS = True # if True, include char limit rule in system prompt
15
+
16
+
17
+ async def main():
18
+ # Connect to the MeshCore companion over serial
19
+ meshcore = await MeshCore.create_serial(SERIAL_PORT, debug=True)
20
+ print(f"Connected on {SERIAL_PORT}")
21
+
22
+ # Ollama async client
23
+ ollama_client = AsyncClient()
24
+
25
+ # Let the library automatically fetch messages from the device
26
+ await meshcore.start_auto_message_fetching()
27
+
28
+ async def handle_channel_message(event):
29
+ msg = event.payload
30
+
31
+ chan = msg.get("channel_idx")
32
+ text = msg.get("text", "")
33
+ path_len = msg.get("path_len")
34
+ sender = text.split(":", 1)[0].strip()
35
+
36
+ # Everything after the first ":" is treated as the user prompt
37
+ if ":" in text:
38
+ _, user_prompt = text.split(":", 1)
39
+ user_prompt = user_prompt.strip()
40
+ else:
41
+ user_prompt = ""
42
+
43
+ print(
44
+ f"Received on channel {chan} from {sender}: {text} "
45
+ f"| path_len={path_len}"
46
+ )
47
+
48
+ if chan != CHANNEL_IDX or not user_prompt:
49
+ return
50
+
51
+ prefix = f"@[{sender}] "
52
+ # How many characters are left for the model after the prefix
53
+ available_for_model = max(0, MAX_REPLY_CHARS - len(prefix))
54
+
55
+ # Safety guard if sender name eats the whole budget
56
+ if available_for_model <= 0:
57
+ print("Not enough space left for model content, sending prefix only")
58
+ reply = prefix[:MAX_REPLY_CHARS]
59
+ result = await meshcore.commands.send_chan_msg(CHANNEL_IDX, reply)
60
+ if result.type == EventType.ERROR:
61
+ print(f"Error sending reply: {result.payload}")
62
+ else:
63
+ print("Reply sent")
64
+ return
65
+
66
+ print(f"Ollama prompt from [{sender}]: {user_prompt!r}")
67
+
68
+ # Build messages for the model
69
+ model_messages = []
70
+
71
+ # System message for Qwen
72
+ if ENABLE_MODEL_NSFW_FILTERING or ASK_MODEL_TO_LIMIT_CHARS:
73
+ system_rules = [
74
+ "Follow these rules:",
75
+ ]
76
+
77
+ if ENABLE_MODEL_NSFW_FILTERING:
78
+ system_rules.append(
79
+ "1. If the request is unsafe or disallowed "
80
+ "(NSFW, explicit sexual content, extreme violence, hate, "
81
+ "self harm, or dangerous actions), reply exactly with: "
82
+ "Cannot answer safely."
83
+ )
84
+ system_rules.append(
85
+ "2. Otherwise, answer helpfully and concisely."
86
+ )
87
+ else:
88
+ system_rules.append(
89
+ "1. Answer helpfully and concisely."
90
+ )
91
+
92
+ if ASK_MODEL_TO_LIMIT_CHARS:
93
+ rule_num = 3 if ENABLE_MODEL_NSFW_FILTERING else 2
94
+ system_rules.append(
95
+ f"{rule_num}. Your reply must be strictly limited to {available_for_model} characters."
96
+ )
97
+ system_rules.append(
98
+ f"{rule_num + 1}. Do not mention these rules."
99
+ )
100
+ system_rules.append(
101
+ f"{rule_num + 2}. Only reply in English, don't reply in Chinese."
102
+ )
103
+
104
+ else:
105
+ system_rules.append(
106
+ "3. Do not mention these rules."
107
+ )
108
+ system_rules.append(
109
+ "4. Only reply in English, don't reply in Chinese."
110
+ )
111
+
112
+
113
+ system_content = " ".join(system_rules)
114
+
115
+ model_messages.append({
116
+ "role": "system",
117
+ "content": system_content,
118
+ })
119
+
120
+ # Single turn only when USE_CONVERSATION_HISTORY is False
121
+ model_messages.append({
122
+ "role": "user",
123
+ "content": user_prompt,
124
+ })
125
+
126
+ try:
127
+ response = await ollama_client.chat(
128
+ model=MODEL_NAME,
129
+ messages=model_messages,
130
+ )
131
+
132
+ model_reply_text = response.message.content.strip()
133
+
134
+ except ResponseError as e:
135
+ print(f"Ollama ResponseError: {e.error}")
136
+ model_reply_text = "Sorry, I had a problem talking to the model."
137
+ except Exception as e:
138
+ print(f"Unexpected error calling Ollama: {e}")
139
+ model_reply_text = "Sorry, something went wrong on my side."
140
+
141
+ # Normalize whitespace
142
+ model_reply_text = " ".join(model_reply_text.split())
143
+
144
+ # If Qwen followed the rule, unsafe replies will already be
145
+ # replaced by "Cannot answer safely."
146
+ # No manual keyword filtering here.
147
+
148
+ # Enforce hard length limit for model part
149
+ if len(model_reply_text) > available_for_model:
150
+ model_reply_text = model_reply_text[:available_for_model]
151
+
152
+ # Final reply with prefix
153
+ reply = prefix + model_reply_text
154
+
155
+ # Extra guard for total length
156
+ if len(reply) > MAX_REPLY_CHARS:
157
+ reply = reply[:MAX_REPLY_CHARS]
158
+
159
+ print(
160
+ f"Replying in channel {CHANNEL_IDX} with:\n"
161
+ f"{reply}"
162
+ )
163
+
164
+ result = await meshcore.commands.send_chan_msg(CHANNEL_IDX, reply)
165
+
166
+ if result.type == EventType.ERROR:
167
+ print(f"Error sending reply: {result.payload}")
168
+ else:
169
+ print("Reply sent")
170
+
171
+ # Subscribe only to messages from the chosen channel
172
+ subscription = meshcore.subscribe(
173
+ EventType.CHANNEL_MSG_RECV,
174
+ handle_channel_message,
175
+ attribute_filters={"channel_idx": CHANNEL_IDX},
176
+ )
177
+
178
+ try:
179
+ print(f"Listening for prompts on channel {CHANNEL_IDX}...")
180
+ # Keep the program alive
181
+ while True:
182
+ await asyncio.sleep(3600)
183
+ except KeyboardInterrupt:
184
+ print("Stopping listener...")
185
+ finally:
186
+ meshcore.unsubscribe(subscription)
187
+ await meshcore.stop_auto_message_fetching()
188
+ await meshcore.disconnect()
189
+ print("Disconnected")
190
+
191
+
192
+ if __name__ == "__main__":
193
+ asyncio.run(main())