mirror of
https://github.com/meshtastic/python.git
synced 2025-12-30 19:37:52 -05:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4721bc5adf | ||
|
|
bc67546019 | ||
|
|
dc35ffa12e | ||
|
|
0a8a193081 |
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -10,7 +10,7 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "meshtastic",
|
"module": "meshtastic",
|
||||||
"justMyCode": false,
|
"justMyCode": false,
|
||||||
"args": ["--debug", "--ble", "--device", "24:62:AB:DD:DF:3A"]
|
"args": ["--debug", "--ble", "24:62:AB:DD:DF:3A"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "meshtastic admin",
|
"name": "meshtastic admin",
|
||||||
|
|||||||
8
TODO.md
8
TODO.md
@@ -5,7 +5,6 @@ Basic functionality is complete now.
|
|||||||
## Eventual tasks
|
## Eventual tasks
|
||||||
|
|
||||||
- Improve documentation on properties/fields
|
- Improve documentation on properties/fields
|
||||||
- change back to Bleak for BLE support - now that they fixed https://github.com/hbldh/bleak/issues/139#event-3499535304
|
|
||||||
- include more examples: textchat.py, replymessage.py all as one little demo
|
- include more examples: textchat.py, replymessage.py all as one little demo
|
||||||
|
|
||||||
- possibly use tk to make a multiwindow test console: https://stackoverflow.com/questions/12351786/how-to-redirect-print-statements-to-tkinter-text-widget
|
- possibly use tk to make a multiwindow test console: https://stackoverflow.com/questions/12351786/how-to-redirect-print-statements-to-tkinter-text-widget
|
||||||
@@ -17,11 +16,8 @@ Basic functionality is complete now.
|
|||||||
|
|
||||||
## Bluetooth support
|
## Bluetooth support
|
||||||
|
|
||||||
(Pre-alpha level feature - you probably don't want this one yet)
|
- ./bin/run.sh --ble-scan # To look for Meshtastic devices
|
||||||
|
- ./bin/run.sh --ble 24:62:AB:DD:DF:3A --info
|
||||||
- This library supports connecting to Meshtastic devices over either USB (serial) or Bluetooth. Before connecting to the device you must [pair](https://docs.ubuntu.com/core/en/stacks/bluetooth/bluez/docs/reference/pairing/outbound.html) your PC with it.
|
|
||||||
- We use the pip3 install "pygatt[GATTTOOL]"
|
|
||||||
- ./bin/run.sh --debug --ble --device 24:62:AB:DD:DF:3A
|
|
||||||
|
|
||||||
## Done
|
## Done
|
||||||
|
|
||||||
|
|||||||
@@ -945,7 +945,17 @@ def common():
|
|||||||
our_globals.set_logfile(logfile)
|
our_globals.set_logfile(logfile)
|
||||||
|
|
||||||
subscribe()
|
subscribe()
|
||||||
if args.ble:
|
if args.ble_scan:
|
||||||
|
logging.debug("BLE scan starting")
|
||||||
|
client = BLEInterface(None, debugOut=logfile, noProto=args.noproto)
|
||||||
|
try:
|
||||||
|
for x in client.scan():
|
||||||
|
print(f"Found: name='{x[1].local_name}' address='{x[0].address}'")
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
meshtastic.util.our_exit("BLE scan finished", 0)
|
||||||
|
return
|
||||||
|
elif args.ble:
|
||||||
client = BLEInterface(args.ble, debugOut=logfile, noProto=args.noproto)
|
client = BLEInterface(args.ble, debugOut=logfile, noProto=args.noproto)
|
||||||
elif args.host:
|
elif args.host:
|
||||||
try:
|
try:
|
||||||
@@ -1310,9 +1320,14 @@ def initParser():
|
|||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--ble",
|
"--ble",
|
||||||
help="BLE mac address to connect to (BLE is not yet supported for this tool)",
|
help="BLE device address or name to connect to",
|
||||||
default=None,
|
default=None,
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ble-scan",
|
||||||
|
help="Scan for Meshtastic BLE devices",
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--noproto",
|
"--noproto",
|
||||||
|
|||||||
@@ -1,66 +1,224 @@
|
|||||||
"""Bluetooth interface
|
"""Bluetooth interface
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import platform
|
import time
|
||||||
|
import struct
|
||||||
|
from threading import Thread, Event
|
||||||
from meshtastic.mesh_interface import MeshInterface
|
from meshtastic.mesh_interface import MeshInterface
|
||||||
from meshtastic.util import our_exit
|
from meshtastic.util import our_exit
|
||||||
|
from bleak import BleakScanner, BleakClient
|
||||||
if platform.system() == "Linux":
|
import asyncio
|
||||||
# pylint: disable=E0401
|
|
||||||
import pygatt
|
|
||||||
|
|
||||||
|
|
||||||
# Our standard BLE characteristics
|
SERVICE_UUID = "6ba1b218-15a8-461f-9fa8-5dcae273eafd"
|
||||||
TORADIO_UUID = "f75c76d2-129e-4dad-a1dd-7866124401e7"
|
TORADIO_UUID = "f75c76d2-129e-4dad-a1dd-7866124401e7"
|
||||||
FROMRADIO_UUID = "8ba2bcc2-ee02-4a55-a531-c525c5e454d5"
|
FROMRADIO_UUID = "2c55e69e-4993-11ed-b878-0242ac120002"
|
||||||
FROMNUM_UUID = "ed9da18c-a800-4f66-a670-aa7547e34453"
|
FROMNUM_UUID = "ed9da18c-a800-4f66-a670-aa7547e34453"
|
||||||
|
|
||||||
|
|
||||||
class BLEInterface(MeshInterface):
|
class BLEInterface(MeshInterface):
|
||||||
"""A not quite ready - FIXME - BLE interface to devices"""
|
class BLEError(Exception):
|
||||||
|
def __init__(self, message):
|
||||||
|
self.message = message
|
||||||
|
super().__init__(self.message)
|
||||||
|
|
||||||
def __init__(self, address, noProto=False, debugOut=None):
|
|
||||||
if platform.system() != "Linux":
|
|
||||||
our_exit("Linux is the only platform with experimental BLE support.", 1)
|
|
||||||
self.address = address
|
|
||||||
if not noProto:
|
|
||||||
self.adapter = pygatt.GATTToolBackend() # BGAPIBackend()
|
|
||||||
self.adapter.start()
|
|
||||||
logging.debug(f"Connecting to {self.address}")
|
|
||||||
self.device = self.adapter.connect(address)
|
|
||||||
else:
|
|
||||||
self.adapter = None
|
|
||||||
self.device = None
|
|
||||||
logging.debug("Connected to device")
|
|
||||||
# fromradio = self.device.char_read(FROMRADIO_UUID)
|
|
||||||
MeshInterface.__init__(self, debugOut=debugOut, noProto=noProto)
|
|
||||||
|
|
||||||
self._readFromRadio() # read the initial responses
|
class BLEState():
|
||||||
|
THREADS = False
|
||||||
|
BLE = False
|
||||||
|
MESH = False
|
||||||
|
|
||||||
def handle_data(handle, data): # pylint: disable=W0613
|
|
||||||
self._handleFromRadio(data)
|
|
||||||
|
|
||||||
if self.device:
|
def __init__(self, address, noProto = False, debugOut = None):
|
||||||
self.device.subscribe(FROMNUM_UUID, callback=handle_data)
|
self.state = BLEInterface.BLEState()
|
||||||
|
|
||||||
|
if not address:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.should_read = False
|
||||||
|
|
||||||
|
logging.debug("Threads starting")
|
||||||
|
self._receiveThread = Thread(target = self._receiveFromRadioImpl)
|
||||||
|
self._receiveThread_started = Event()
|
||||||
|
self._receiveThread_stopped = Event()
|
||||||
|
self._receiveThread.start()
|
||||||
|
self._receiveThread_started.wait(1)
|
||||||
|
self.state.THREADS = True
|
||||||
|
logging.debug("Threads running")
|
||||||
|
|
||||||
|
try:
|
||||||
|
logging.debug(f"BLE connecting to: {address}")
|
||||||
|
self.client = self.connect(address)
|
||||||
|
self.state.BLE = True
|
||||||
|
logging.debug("BLE connected")
|
||||||
|
except BLEInterface.BLEError as e:
|
||||||
|
self.close()
|
||||||
|
our_exit(e.message, 1)
|
||||||
|
return
|
||||||
|
|
||||||
|
logging.debug("Mesh init starting")
|
||||||
|
MeshInterface.__init__(self, debugOut = debugOut, noProto = noProto)
|
||||||
|
self._startConfig()
|
||||||
|
if not self.noProto:
|
||||||
|
self._waitConnected()
|
||||||
|
self.waitForConfig()
|
||||||
|
self.state.MESH = True
|
||||||
|
logging.debug("Mesh init finished")
|
||||||
|
|
||||||
|
logging.debug("Register FROMNUM notify callback")
|
||||||
|
self.client.start_notify(FROMNUM_UUID, self.from_num_handler)
|
||||||
|
|
||||||
|
|
||||||
|
async def from_num_handler(self, _, b):
|
||||||
|
from_num = struct.unpack('<I', bytes(b))[0]
|
||||||
|
logging.debug(f"FROMNUM notify: {from_num}")
|
||||||
|
self.should_read = True
|
||||||
|
|
||||||
|
|
||||||
|
def scan(self):
|
||||||
|
with BLEClient() as client:
|
||||||
|
return [
|
||||||
|
(x[0], x[1]) for x in (client.discover(
|
||||||
|
return_adv = True,
|
||||||
|
service_uuids = [ SERVICE_UUID ]
|
||||||
|
)).values()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def find_device(self, address):
|
||||||
|
meshtastic_devices = self.scan()
|
||||||
|
|
||||||
|
addressed_devices = list(filter(lambda x: address == x[1].local_name or address == x[0].name, meshtastic_devices))
|
||||||
|
# If nothing is found try on the address
|
||||||
|
if len(addressed_devices) == 0:
|
||||||
|
addressed_devices = list(filter(lambda x: BLEInterface._sanitize_address(address) == BLEInterface._sanitize_address(x[0].address), meshtastic_devices))
|
||||||
|
|
||||||
|
if len(addressed_devices) == 0:
|
||||||
|
raise BLEInterface.BLEError(f"No Meshtastic BLE peripheral with identifier or address '{address}' found. Try --ble-scan to find it.")
|
||||||
|
if len(addressed_devices) > 1:
|
||||||
|
raise BLEInterface.BLEError(f"More than one Meshtastic BLE peripheral with identifier or address '{address}' found.")
|
||||||
|
return addressed_devices[0][0]
|
||||||
|
|
||||||
|
def _sanitize_address(address):
|
||||||
|
return address \
|
||||||
|
.replace("-", "") \
|
||||||
|
.replace("_", "") \
|
||||||
|
.replace(":", "") \
|
||||||
|
.lower()
|
||||||
|
|
||||||
|
def connect(self, address):
|
||||||
|
device = self.find_device(address)
|
||||||
|
client = BLEClient(device.address)
|
||||||
|
client.connect()
|
||||||
|
try:
|
||||||
|
client.pair()
|
||||||
|
except NotImplementedError:
|
||||||
|
# Some bluetooth backends do not require explicit pairing.
|
||||||
|
# See Bleak docs for details on this.
|
||||||
|
pass
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
def _receiveFromRadioImpl(self):
|
||||||
|
self._receiveThread_started.set()
|
||||||
|
while self._receiveThread_started.is_set():
|
||||||
|
if self.should_read:
|
||||||
|
self.should_read = False
|
||||||
|
while True:
|
||||||
|
b = bytes(self.client.read_gatt_char(FROMRADIO_UUID))
|
||||||
|
if not b:
|
||||||
|
break
|
||||||
|
logging.debug(f"FROMRADIO read: {b.hex()}")
|
||||||
|
self._handleFromRadio(b)
|
||||||
|
else:
|
||||||
|
time.sleep(0.1)
|
||||||
|
self._receiveThread_stopped.set()
|
||||||
|
|
||||||
def _sendToRadioImpl(self, toRadio):
|
def _sendToRadioImpl(self, toRadio):
|
||||||
"""Send a ToRadio protobuf to the device"""
|
|
||||||
# logging.debug(f"Sending: {stripnl(toRadio)}")
|
|
||||||
b = toRadio.SerializeToString()
|
b = toRadio.SerializeToString()
|
||||||
self.device.char_write(TORADIO_UUID, b)
|
if b:
|
||||||
|
logging.debug(f"TORADIO write: {b.hex()}")
|
||||||
|
self.client.write_gatt_char(TORADIO_UUID, b, response = True)
|
||||||
|
# Allow to propagate and then make sure we read
|
||||||
|
time.sleep(0.1)
|
||||||
|
self.should_read = True
|
||||||
|
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
MeshInterface.close(self)
|
if self.state.MESH:
|
||||||
if self.adapter:
|
MeshInterface.close(self)
|
||||||
self.adapter.stop()
|
|
||||||
|
|
||||||
def _readFromRadio(self):
|
if self.state.THREADS:
|
||||||
if not self.noProto:
|
self._receiveThread_started.clear()
|
||||||
wasEmpty = False
|
self._receiveThread_stopped.wait(5)
|
||||||
while not wasEmpty:
|
|
||||||
if self.device:
|
if self.state.BLE:
|
||||||
b = self.device.char_read(FROMRADIO_UUID)
|
self.client.disconnect()
|
||||||
wasEmpty = len(b) == 0
|
self.client.close()
|
||||||
if not wasEmpty:
|
|
||||||
self._handleFromRadio(b)
|
|
||||||
|
class BLEClient():
|
||||||
|
def __init__(self, address = None, **kwargs):
|
||||||
|
self._eventThread = Thread(target = self._run_event_loop)
|
||||||
|
self._eventThread_started = Event()
|
||||||
|
self._eventThread_stopped = Event()
|
||||||
|
self._eventThread.start()
|
||||||
|
self._eventThread_started.wait(1)
|
||||||
|
|
||||||
|
if not address:
|
||||||
|
logging.debug("No address provided - only discover method will work.")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.bleak_client = BleakClient(address, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def discover(self, **kwargs):
|
||||||
|
return self.async_await(BleakScanner.discover(**kwargs))
|
||||||
|
|
||||||
|
def pair(self, **kwargs):
|
||||||
|
return self.async_await(self.bleak_client.pair(**kwargs))
|
||||||
|
|
||||||
|
def connect(self, **kwargs):
|
||||||
|
return self.async_await(self.bleak_client.connect(**kwargs))
|
||||||
|
|
||||||
|
def disconnect(self, **kwargs):
|
||||||
|
self.async_await(self.bleak_client.disconnect(**kwargs))
|
||||||
|
|
||||||
|
def read_gatt_char(self, *args, **kwargs):
|
||||||
|
return self.async_await(self.bleak_client.read_gatt_char(*args, **kwargs))
|
||||||
|
|
||||||
|
def write_gatt_char(self, *args, **kwargs):
|
||||||
|
self.async_await(self.bleak_client.write_gatt_char(*args, **kwargs))
|
||||||
|
|
||||||
|
def start_notify(self, *args, **kwargs):
|
||||||
|
self.async_await(self.bleak_client.start_notify(*args, **kwargs))
|
||||||
|
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.async_run(self._stop_event_loop())
|
||||||
|
self._eventThread_stopped.wait(5)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, type, value, traceback):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
|
def async_await(self, coro, timeout = None):
|
||||||
|
return self.async_run(coro).result(timeout)
|
||||||
|
|
||||||
|
def async_run(self, coro):
|
||||||
|
return asyncio.run_coroutine_threadsafe(coro, self._eventLoop)
|
||||||
|
|
||||||
|
def _run_event_loop(self):
|
||||||
|
self._eventLoop = asyncio.new_event_loop()
|
||||||
|
self._eventThread_started.set()
|
||||||
|
try:
|
||||||
|
self._eventLoop.run_forever()
|
||||||
|
finally:
|
||||||
|
self._eventLoop.close()
|
||||||
|
self._eventThread_stopped.set()
|
||||||
|
|
||||||
|
async def _stop_event_loop(self):
|
||||||
|
self._eventLoop.stop()
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,17 +0,0 @@
|
|||||||
"""Meshtastic unit tests for ble_interface.py"""
|
|
||||||
|
|
||||||
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from ..ble_interface import BLEInterface
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
@patch("platform.system", return_value="Linux")
|
|
||||||
def test_BLEInterface(mock_platform):
|
|
||||||
"""Test that we can instantiate a BLEInterface"""
|
|
||||||
iface = BLEInterface("foo", debugOut=True, noProto=True)
|
|
||||||
iface.close()
|
|
||||||
mock_platform.assert_called()
|
|
||||||
@@ -283,26 +283,6 @@ def test_main_info_with_tcp_interface(capsys):
|
|||||||
mo.assert_called()
|
mo.assert_called()
|
||||||
|
|
||||||
|
|
||||||
# TODO: comment out ble (for now)
|
|
||||||
# @pytest.mark.unit
|
|
||||||
# def test_main_info_with_ble_interface(capsys):
|
|
||||||
# """Test --info"""
|
|
||||||
# sys.argv = ['', '--info', '--ble', 'foo']
|
|
||||||
# Globals.getInstance().set_args(sys.argv)
|
|
||||||
#
|
|
||||||
# iface = MagicMock(autospec=BLEInterface)
|
|
||||||
# def mock_showInfo():
|
|
||||||
# print('inside mocked showInfo')
|
|
||||||
# iface.showInfo.side_effect = mock_showInfo
|
|
||||||
# with patch('meshtastic.ble_interface.BLEInterface', return_value=iface) as mo:
|
|
||||||
# main()
|
|
||||||
# out, err = capsys.readouterr()
|
|
||||||
# assert re.search(r'Connected to radio', out, re.MULTILINE)
|
|
||||||
# assert re.search(r'inside mocked showInfo', out, re.MULTILINE)
|
|
||||||
# assert err == ''
|
|
||||||
# mo.assert_called()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
@pytest.mark.usefixtures("reset_globals")
|
@pytest.mark.usefixtures("reset_globals")
|
||||||
def test_main_no_proto(capsys):
|
def test_main_no_proto(capsys):
|
||||||
|
|||||||
Submodule protobufs updated: 4a1d3766e8...2ccf73428d
@@ -18,4 +18,4 @@ pyyaml
|
|||||||
pytap2
|
pytap2
|
||||||
pdoc3
|
pdoc3
|
||||||
pypubsub
|
pypubsub
|
||||||
pygatt; platform_system == "Linux"
|
bleak
|
||||||
|
|||||||
4
setup.py
4
setup.py
@@ -13,7 +13,7 @@ with open("README.md", "r") as fh:
|
|||||||
# This call to setup() does all the work
|
# This call to setup() does all the work
|
||||||
setup(
|
setup(
|
||||||
name="meshtastic",
|
name="meshtastic",
|
||||||
version="2.2.17",
|
version="2.2.18",
|
||||||
description="Python API & client shell for talking to Meshtastic devices",
|
description="Python API & client shell for talking to Meshtastic devices",
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
@@ -43,7 +43,7 @@ setup(
|
|||||||
"tabulate>=0.8.9",
|
"tabulate>=0.8.9",
|
||||||
"timeago>=1.0.15",
|
"timeago>=1.0.15",
|
||||||
"pyyaml",
|
"pyyaml",
|
||||||
"pygatt>=4.0.5 ; platform_system=='Linux'",
|
"bleak>=0.21.1",
|
||||||
],
|
],
|
||||||
extras_require={"tunnel": ["pytap2>=2.0.0"]},
|
extras_require={"tunnel": ["pytap2>=2.0.0"]},
|
||||||
python_requires=">=3.7",
|
python_requires=">=3.7",
|
||||||
|
|||||||
Reference in New Issue
Block a user