tools: commit the bundled production runtime (python-embed + NSpyocd + DFP pack)
Per request the full portable environment is now in git: a fresh clone's tools/ runs everything with zero installation. Only regenerable release zips and the dev venv stay ignored.
This commit is contained in:
parent
57762e50be
commit
1948e4b1ba
6
.gitignore
vendored
6
.gitignore
vendored
@ -6,8 +6,6 @@ mcm-ddc-ble/MDK-ARM/build.log
|
|||||||
mcm-ddc-ble/MDK-ARM/build-app2.log
|
mcm-ddc-ble/MDK-ARM/build-app2.log
|
||||||
mcm-ddc-ble/MDK-ARM/Objects-app2
|
mcm-ddc-ble/MDK-ARM/Objects-app2
|
||||||
|
|
||||||
# portable production runtime (bundled, not committed)
|
# release zips are regenerable via tools/make_release.bat
|
||||||
tools/python-embed/
|
|
||||||
tools/NSpyocd/
|
|
||||||
tools/N32WB03x_DFP.1.4.0.pack
|
|
||||||
tools/out/mothercup_tools_*.zip
|
tools/out/mothercup_tools_*.zip
|
||||||
|
tools/python-embed/Lib/site-packages/__pycache__/
|
||||||
|
|||||||
BIN
tools/N32WB03x_DFP.1.4.0.pack
Normal file
BIN
tools/N32WB03x_DFP.1.4.0.pack
Normal file
Binary file not shown.
BIN
tools/NSpyocd/NSpyocd.exe
Normal file
BIN
tools/NSpyocd/NSpyocd.exe
Normal file
Binary file not shown.
702
tools/python-embed/LICENSE.txt
Normal file
702
tools/python-embed/LICENSE.txt
Normal file
@ -0,0 +1,702 @@
|
|||||||
|
A. HISTORY OF THE SOFTWARE
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Python was created in the early 1990s by Guido van Rossum at Stichting
|
||||||
|
Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands
|
||||||
|
as a successor of a language called ABC. Guido remains Python's
|
||||||
|
principal author, although it includes many contributions from others.
|
||||||
|
|
||||||
|
In 1995, Guido continued his work on Python at the Corporation for
|
||||||
|
National Research Initiatives (CNRI, see https://www.cnri.reston.va.us)
|
||||||
|
in Reston, Virginia where he released several versions of the
|
||||||
|
software.
|
||||||
|
|
||||||
|
In May 2000, Guido and the Python core development team moved to
|
||||||
|
BeOpen.com to form the BeOpen PythonLabs team. In October of the same
|
||||||
|
year, the PythonLabs team moved to Digital Creations, which became
|
||||||
|
Zope Corporation. In 2001, the Python Software Foundation (PSF, see
|
||||||
|
https://www.python.org/psf/) was formed, a non-profit organization
|
||||||
|
created specifically to own Python-related Intellectual Property.
|
||||||
|
Zope Corporation was a sponsoring member of the PSF.
|
||||||
|
|
||||||
|
All Python releases are Open Source (see https://opensource.org for
|
||||||
|
the Open Source Definition). Historically, most, but not all, Python
|
||||||
|
releases have also been GPL-compatible; the table below summarizes
|
||||||
|
the various releases.
|
||||||
|
|
||||||
|
Release Derived Year Owner GPL-
|
||||||
|
from compatible? (1)
|
||||||
|
|
||||||
|
0.9.0 thru 1.2 1991-1995 CWI yes
|
||||||
|
1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
|
||||||
|
1.6 1.5.2 2000 CNRI no
|
||||||
|
2.0 1.6 2000 BeOpen.com no
|
||||||
|
1.6.1 1.6 2001 CNRI yes (2)
|
||||||
|
2.1 2.0+1.6.1 2001 PSF no
|
||||||
|
2.0.1 2.0+1.6.1 2001 PSF yes
|
||||||
|
2.1.1 2.1+2.0.1 2001 PSF yes
|
||||||
|
2.1.2 2.1.1 2002 PSF yes
|
||||||
|
2.1.3 2.1.2 2002 PSF yes
|
||||||
|
2.2 and above 2.1.1 2001-now PSF yes
|
||||||
|
|
||||||
|
Footnotes:
|
||||||
|
|
||||||
|
(1) GPL-compatible doesn't mean that we're distributing Python under
|
||||||
|
the GPL. All Python licenses, unlike the GPL, let you distribute
|
||||||
|
a modified version without making your changes open source. The
|
||||||
|
GPL-compatible licenses make it possible to combine Python with
|
||||||
|
other software that is released under the GPL; the others don't.
|
||||||
|
|
||||||
|
(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
|
||||||
|
because its license has a choice of law clause. According to
|
||||||
|
CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
|
||||||
|
is "not incompatible" with the GPL.
|
||||||
|
|
||||||
|
Thanks to the many outside volunteers who have worked under Guido's
|
||||||
|
direction to make these releases possible.
|
||||||
|
|
||||||
|
|
||||||
|
B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
|
||||||
|
===============================================================
|
||||||
|
|
||||||
|
Python software and documentation are licensed under the
|
||||||
|
Python Software Foundation License Version 2.
|
||||||
|
|
||||||
|
Starting with Python 3.8.6, examples, recipes, and other code in
|
||||||
|
the documentation are dual licensed under the PSF License Version 2
|
||||||
|
and the Zero-Clause BSD license.
|
||||||
|
|
||||||
|
Some software incorporated into Python is under different licenses.
|
||||||
|
The licenses are listed with code falling under that license.
|
||||||
|
|
||||||
|
|
||||||
|
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
1. This LICENSE AGREEMENT is between the Python Software Foundation
|
||||||
|
("PSF"), and the Individual or Organization ("Licensee") accessing and
|
||||||
|
otherwise using this software ("Python") in source or binary form and
|
||||||
|
its associated documentation.
|
||||||
|
|
||||||
|
2. Subject to the terms and conditions of this License Agreement, PSF hereby
|
||||||
|
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
|
||||||
|
analyze, test, perform and/or display publicly, prepare derivative works,
|
||||||
|
distribute, and otherwise use Python alone or in any derivative version,
|
||||||
|
provided, however, that PSF's License Agreement and PSF's notice of copyright,
|
||||||
|
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
|
||||||
|
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation;
|
||||||
|
All Rights Reserved" are retained in Python alone or in any derivative version
|
||||||
|
prepared by Licensee.
|
||||||
|
|
||||||
|
3. In the event Licensee prepares a derivative work that is based on
|
||||||
|
or incorporates Python or any part thereof, and wants to make
|
||||||
|
the derivative work available to others as provided herein, then
|
||||||
|
Licensee hereby agrees to include in any such work a brief summary of
|
||||||
|
the changes made to Python.
|
||||||
|
|
||||||
|
4. PSF is making Python available to Licensee on an "AS IS"
|
||||||
|
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||||
|
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
|
||||||
|
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||||
|
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
|
||||||
|
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||||
|
|
||||||
|
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||||
|
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||||
|
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
|
||||||
|
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||||
|
|
||||||
|
6. This License Agreement will automatically terminate upon a material
|
||||||
|
breach of its terms and conditions.
|
||||||
|
|
||||||
|
7. Nothing in this License Agreement shall be deemed to create any
|
||||||
|
relationship of agency, partnership, or joint venture between PSF and
|
||||||
|
Licensee. This License Agreement does not grant permission to use PSF
|
||||||
|
trademarks or trade name in a trademark sense to endorse or promote
|
||||||
|
products or services of Licensee, or any third party.
|
||||||
|
|
||||||
|
8. By copying, installing or otherwise using Python, Licensee
|
||||||
|
agrees to be bound by the terms and conditions of this License
|
||||||
|
Agreement.
|
||||||
|
|
||||||
|
|
||||||
|
BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
|
||||||
|
|
||||||
|
1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
|
||||||
|
office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
|
||||||
|
Individual or Organization ("Licensee") accessing and otherwise using
|
||||||
|
this software in source or binary form and its associated
|
||||||
|
documentation ("the Software").
|
||||||
|
|
||||||
|
2. Subject to the terms and conditions of this BeOpen Python License
|
||||||
|
Agreement, BeOpen hereby grants Licensee a non-exclusive,
|
||||||
|
royalty-free, world-wide license to reproduce, analyze, test, perform
|
||||||
|
and/or display publicly, prepare derivative works, distribute, and
|
||||||
|
otherwise use the Software alone or in any derivative version,
|
||||||
|
provided, however, that the BeOpen Python License is retained in the
|
||||||
|
Software, alone or in any derivative version prepared by Licensee.
|
||||||
|
|
||||||
|
3. BeOpen is making the Software available to Licensee on an "AS IS"
|
||||||
|
basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||||
|
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
|
||||||
|
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||||
|
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
|
||||||
|
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||||
|
|
||||||
|
4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
|
||||||
|
SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
|
||||||
|
AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
|
||||||
|
DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||||
|
|
||||||
|
5. This License Agreement will automatically terminate upon a material
|
||||||
|
breach of its terms and conditions.
|
||||||
|
|
||||||
|
6. This License Agreement shall be governed by and interpreted in all
|
||||||
|
respects by the law of the State of California, excluding conflict of
|
||||||
|
law provisions. Nothing in this License Agreement shall be deemed to
|
||||||
|
create any relationship of agency, partnership, or joint venture
|
||||||
|
between BeOpen and Licensee. This License Agreement does not grant
|
||||||
|
permission to use BeOpen trademarks or trade names in a trademark
|
||||||
|
sense to endorse or promote products or services of Licensee, or any
|
||||||
|
third party. As an exception, the "BeOpen Python" logos available at
|
||||||
|
http://www.pythonlabs.com/logos.html may be used according to the
|
||||||
|
permissions granted on that web page.
|
||||||
|
|
||||||
|
7. By copying, installing or otherwise using the software, Licensee
|
||||||
|
agrees to be bound by the terms and conditions of this License
|
||||||
|
Agreement.
|
||||||
|
|
||||||
|
|
||||||
|
CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
1. This LICENSE AGREEMENT is between the Corporation for National
|
||||||
|
Research Initiatives, having an office at 1895 Preston White Drive,
|
||||||
|
Reston, VA 20191 ("CNRI"), and the Individual or Organization
|
||||||
|
("Licensee") accessing and otherwise using Python 1.6.1 software in
|
||||||
|
source or binary form and its associated documentation.
|
||||||
|
|
||||||
|
2. Subject to the terms and conditions of this License Agreement, CNRI
|
||||||
|
hereby grants Licensee a nonexclusive, royalty-free, world-wide
|
||||||
|
license to reproduce, analyze, test, perform and/or display publicly,
|
||||||
|
prepare derivative works, distribute, and otherwise use Python 1.6.1
|
||||||
|
alone or in any derivative version, provided, however, that CNRI's
|
||||||
|
License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
|
||||||
|
1995-2001 Corporation for National Research Initiatives; All Rights
|
||||||
|
Reserved" are retained in Python 1.6.1 alone or in any derivative
|
||||||
|
version prepared by Licensee. Alternately, in lieu of CNRI's License
|
||||||
|
Agreement, Licensee may substitute the following text (omitting the
|
||||||
|
quotes): "Python 1.6.1 is made available subject to the terms and
|
||||||
|
conditions in CNRI's License Agreement. This Agreement together with
|
||||||
|
Python 1.6.1 may be located on the internet using the following
|
||||||
|
unique, persistent identifier (known as a handle): 1895.22/1013. This
|
||||||
|
Agreement may also be obtained from a proxy server on the internet
|
||||||
|
using the following URL: http://hdl.handle.net/1895.22/1013".
|
||||||
|
|
||||||
|
3. In the event Licensee prepares a derivative work that is based on
|
||||||
|
or incorporates Python 1.6.1 or any part thereof, and wants to make
|
||||||
|
the derivative work available to others as provided herein, then
|
||||||
|
Licensee hereby agrees to include in any such work a brief summary of
|
||||||
|
the changes made to Python 1.6.1.
|
||||||
|
|
||||||
|
4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
|
||||||
|
basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||||
|
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
|
||||||
|
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||||
|
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
|
||||||
|
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||||
|
|
||||||
|
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||||
|
1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||||
|
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
|
||||||
|
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||||
|
|
||||||
|
6. This License Agreement will automatically terminate upon a material
|
||||||
|
breach of its terms and conditions.
|
||||||
|
|
||||||
|
7. This License Agreement shall be governed by the federal
|
||||||
|
intellectual property law of the United States, including without
|
||||||
|
limitation the federal copyright law, and, to the extent such
|
||||||
|
U.S. federal law does not apply, by the law of the Commonwealth of
|
||||||
|
Virginia, excluding Virginia's conflict of law provisions.
|
||||||
|
Notwithstanding the foregoing, with regard to derivative works based
|
||||||
|
on Python 1.6.1 that incorporate non-separable material that was
|
||||||
|
previously distributed under the GNU General Public License (GPL), the
|
||||||
|
law of the Commonwealth of Virginia shall govern this License
|
||||||
|
Agreement only as to issues arising under or with respect to
|
||||||
|
Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
|
||||||
|
License Agreement shall be deemed to create any relationship of
|
||||||
|
agency, partnership, or joint venture between CNRI and Licensee. This
|
||||||
|
License Agreement does not grant permission to use CNRI trademarks or
|
||||||
|
trade name in a trademark sense to endorse or promote products or
|
||||||
|
services of Licensee, or any third party.
|
||||||
|
|
||||||
|
8. By clicking on the "ACCEPT" button where indicated, or by copying,
|
||||||
|
installing or otherwise using Python 1.6.1, Licensee agrees to be
|
||||||
|
bound by the terms and conditions of this License Agreement.
|
||||||
|
|
||||||
|
ACCEPT
|
||||||
|
|
||||||
|
|
||||||
|
CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
|
||||||
|
The Netherlands. All rights reserved.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and distribute this software and its
|
||||||
|
documentation for any purpose and without fee is hereby granted,
|
||||||
|
provided that the above copyright notice appear in all copies and that
|
||||||
|
both that copyright notice and this permission notice appear in
|
||||||
|
supporting documentation, and that the name of Stichting Mathematisch
|
||||||
|
Centrum or CWI not be used in advertising or publicity pertaining to
|
||||||
|
distribution of the software without specific, written prior
|
||||||
|
permission.
|
||||||
|
|
||||||
|
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||||
|
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||||
|
FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
|
||||||
|
FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||||
|
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Additional Conditions for this Windows binary build
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
This program is linked with and uses Microsoft Distributable Code,
|
||||||
|
copyrighted by Microsoft Corporation. The Microsoft Distributable Code
|
||||||
|
is embedded in each .exe, .dll and .pyd file as a result of running
|
||||||
|
the code through a linker.
|
||||||
|
|
||||||
|
If you further distribute programs that include the Microsoft
|
||||||
|
Distributable Code, you must comply with the restrictions on
|
||||||
|
distribution specified by Microsoft. In particular, you must require
|
||||||
|
distributors and external end users to agree to terms that protect the
|
||||||
|
Microsoft Distributable Code at least as much as Microsoft's own
|
||||||
|
requirements for the Distributable Code. See Microsoft's documentation
|
||||||
|
(included in its developer tools and on its website at microsoft.com)
|
||||||
|
for specific details.
|
||||||
|
|
||||||
|
Redistribution of the Windows binary build of the Python interpreter
|
||||||
|
complies with this agreement, provided that you do not:
|
||||||
|
|
||||||
|
- alter any copyright, trademark or patent notice in Microsoft's
|
||||||
|
Distributable Code;
|
||||||
|
|
||||||
|
- use Microsoft's trademarks in your programs' names or in a way that
|
||||||
|
suggests your programs come from or are endorsed by Microsoft;
|
||||||
|
|
||||||
|
- distribute Microsoft's Distributable Code to run on a platform other
|
||||||
|
than Microsoft operating systems, run-time technologies or application
|
||||||
|
platforms; or
|
||||||
|
|
||||||
|
- include Microsoft Distributable Code in malicious, deceptive or
|
||||||
|
unlawful programs.
|
||||||
|
|
||||||
|
These restrictions apply only to the Microsoft Distributable Code as
|
||||||
|
defined above, not to Python itself or any programs running on the
|
||||||
|
Python interpreter. The redistribution of the Python interpreter and
|
||||||
|
libraries is governed by the Python Software License included with this
|
||||||
|
file, or by other licenses as marked.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
This program, "bzip2", the associated library "libbzip2", and all
|
||||||
|
documentation, are copyright (C) 1996-2019 Julian R Seward. All
|
||||||
|
rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. The origin of this software must not be misrepresented; you must
|
||||||
|
not claim that you wrote the original software. If you use this
|
||||||
|
software in a product, an acknowledgment in the product
|
||||||
|
documentation would be appreciated but is not required.
|
||||||
|
|
||||||
|
3. Altered source versions must be plainly marked as such, and must
|
||||||
|
not be misrepresented as being the original software.
|
||||||
|
|
||||||
|
4. The name of the author may not be used to endorse or promote
|
||||||
|
products derived from this software without specific prior written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
|
||||||
|
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||||
|
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||||
|
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||||
|
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
Julian Seward, jseward@acm.org
|
||||||
|
bzip2/libbzip2 version 1.0.8 of 13 July 2019
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
libffi - Copyright (c) 1996-2022 Anthony Green, Red Hat, Inc and others.
|
||||||
|
See source files for details.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
``Software''), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
https://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
This software is copyrighted by the Regents of the University of
|
||||||
|
California, Sun Microsystems, Inc., Scriptics Corporation, ActiveState
|
||||||
|
Corporation and other parties. The following terms apply to all files
|
||||||
|
associated with the software unless explicitly disclaimed in
|
||||||
|
individual files.
|
||||||
|
|
||||||
|
The authors hereby grant permission to use, copy, modify, distribute,
|
||||||
|
and license this software and its documentation for any purpose, provided
|
||||||
|
that existing copyright notices are retained in all copies and that this
|
||||||
|
notice is included verbatim in any distributions. No written agreement,
|
||||||
|
license, or royalty fee is required for any of the authorized uses.
|
||||||
|
Modifications to this software may be copyrighted by their authors
|
||||||
|
and need not follow the licensing terms described here, provided that
|
||||||
|
the new terms are clearly indicated on the first page of each file where
|
||||||
|
they apply.
|
||||||
|
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY
|
||||||
|
FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
|
||||||
|
ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
|
||||||
|
DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
|
||||||
|
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE
|
||||||
|
IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE
|
||||||
|
NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
|
||||||
|
MODIFICATIONS.
|
||||||
|
|
||||||
|
GOVERNMENT USE: If you are acquiring this software on behalf of the
|
||||||
|
U.S. government, the Government shall have only "Restricted Rights"
|
||||||
|
in the software and related documentation as defined in the Federal
|
||||||
|
Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you
|
||||||
|
are acquiring the software on behalf of the Department of Defense, the
|
||||||
|
software shall be classified as "Commercial Computer Software" and the
|
||||||
|
Government shall have only "Restricted Rights" as defined in Clause
|
||||||
|
252.227-7014 (b) (3) of DFARs. Notwithstanding the foregoing, the
|
||||||
|
authors grant the U.S. Government and others acting in its behalf
|
||||||
|
permission to use and distribute the software in accordance with the
|
||||||
|
terms specified in this license.
|
||||||
|
|
||||||
|
This software is copyrighted by the Regents of the University of
|
||||||
|
California, Sun Microsystems, Inc., Scriptics Corporation, ActiveState
|
||||||
|
Corporation, Apple Inc. and other parties. The following terms apply to
|
||||||
|
all files associated with the software unless explicitly disclaimed in
|
||||||
|
individual files.
|
||||||
|
|
||||||
|
The authors hereby grant permission to use, copy, modify, distribute,
|
||||||
|
and license this software and its documentation for any purpose, provided
|
||||||
|
that existing copyright notices are retained in all copies and that this
|
||||||
|
notice is included verbatim in any distributions. No written agreement,
|
||||||
|
license, or royalty fee is required for any of the authorized uses.
|
||||||
|
Modifications to this software may be copyrighted by their authors
|
||||||
|
and need not follow the licensing terms described here, provided that
|
||||||
|
the new terms are clearly indicated on the first page of each file where
|
||||||
|
they apply.
|
||||||
|
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY
|
||||||
|
FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
|
||||||
|
ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
|
||||||
|
DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
|
||||||
|
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE
|
||||||
|
IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE
|
||||||
|
NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
|
||||||
|
MODIFICATIONS.
|
||||||
|
|
||||||
|
GOVERNMENT USE: If you are acquiring this software on behalf of the
|
||||||
|
U.S. government, the Government shall have only "Restricted Rights"
|
||||||
|
in the software and related documentation as defined in the Federal
|
||||||
|
Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you
|
||||||
|
are acquiring the software on behalf of the Department of Defense, the
|
||||||
|
software shall be classified as "Commercial Computer Software" and the
|
||||||
|
Government shall have only "Restricted Rights" as defined in Clause
|
||||||
|
252.227-7013 (b) (3) of DFARs. Notwithstanding the foregoing, the
|
||||||
|
authors grant the U.S. Government and others acting in its behalf
|
||||||
|
permission to use and distribute the software in accordance with the
|
||||||
|
terms specified in this license.
|
||||||
|
|
||||||
|
Copyright (c) 1993-1999 Ioi Kim Lam.
|
||||||
|
Copyright (c) 2000-2001 Tix Project Group.
|
||||||
|
Copyright (c) 2004 ActiveState
|
||||||
|
|
||||||
|
This software is copyrighted by the above entities
|
||||||
|
and other parties. The following terms apply to all files associated
|
||||||
|
with the software unless explicitly disclaimed in individual files.
|
||||||
|
|
||||||
|
The authors hereby grant permission to use, copy, modify, distribute,
|
||||||
|
and license this software and its documentation for any purpose, provided
|
||||||
|
that existing copyright notices are retained in all copies and that this
|
||||||
|
notice is included verbatim in any distributions. No written agreement,
|
||||||
|
license, or royalty fee is required for any of the authorized uses.
|
||||||
|
Modifications to this software may be copyrighted by their authors
|
||||||
|
and need not follow the licensing terms described here, provided that
|
||||||
|
the new terms are clearly indicated on the first page of each file where
|
||||||
|
they apply.
|
||||||
|
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY
|
||||||
|
FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
|
||||||
|
ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
|
||||||
|
DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
|
||||||
|
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE
|
||||||
|
IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE
|
||||||
|
NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
|
||||||
|
MODIFICATIONS.
|
||||||
|
|
||||||
|
GOVERNMENT USE: If you are acquiring this software on behalf of the
|
||||||
|
U.S. government, the Government shall have only "Restricted Rights"
|
||||||
|
in the software and related documentation as defined in the Federal
|
||||||
|
Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you
|
||||||
|
are acquiring the software on behalf of the Department of Defense, the
|
||||||
|
software shall be classified as "Commercial Computer Software" and the
|
||||||
|
Government shall have only "Restricted Rights" as defined in Clause
|
||||||
|
252.227-7013 (c) (1) of DFARs. Notwithstanding the foregoing, the
|
||||||
|
authors grant the U.S. Government and others acting in its behalf
|
||||||
|
permission to use and distribute the software in accordance with the
|
||||||
|
terms specified in this license.
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
Parts of this software are based on the Tcl/Tk software copyrighted by
|
||||||
|
the Regents of the University of California, Sun Microsystems, Inc.,
|
||||||
|
and other parties. The original license terms of the Tcl/Tk software
|
||||||
|
distribution is included in the file docs/license.tcltk.
|
||||||
|
|
||||||
|
Parts of this software are based on the HTML Library software
|
||||||
|
copyrighted by Sun Microsystems, Inc. The original license terms of
|
||||||
|
the HTML Library software distribution is included in the file
|
||||||
|
docs/license.html_lib.
|
||||||
|
|
||||||
BIN
tools/python-embed/Lib/site-packages/bin/pyserial-miniterm.exe
Normal file
BIN
tools/python-embed/Lib/site-packages/bin/pyserial-miniterm.exe
Normal file
Binary file not shown.
BIN
tools/python-embed/Lib/site-packages/bin/pyserial-ports.exe
Normal file
BIN
tools/python-embed/Lib/site-packages/bin/pyserial-ports.exe
Normal file
Binary file not shown.
@ -0,0 +1 @@
|
|||||||
|
pip
|
||||||
@ -0,0 +1,132 @@
|
|||||||
|
Metadata-Version: 2.4
|
||||||
|
Name: bleak
|
||||||
|
Version: 3.0.2
|
||||||
|
Summary: Bluetooth Low Energy platform Agnostic Klient
|
||||||
|
Author: Henrik Blidh
|
||||||
|
Author-email: Henrik Blidh <henrik.blidh@nedomkull.com>
|
||||||
|
License-Expression: MIT
|
||||||
|
License-File: LICENSE
|
||||||
|
Classifier: Framework :: AsyncIO
|
||||||
|
Classifier: Operating System :: Microsoft :: Windows :: Windows 11
|
||||||
|
Classifier: Operating System :: POSIX :: Linux
|
||||||
|
Classifier: Operating System :: MacOS :: MacOS X
|
||||||
|
Classifier: Operating System :: Android
|
||||||
|
Requires-Dist: async-timeout>=3.0.0 ; python_full_version < '3.11'
|
||||||
|
Requires-Dist: typing-extensions>=4.7.0 ; python_full_version < '3.12'
|
||||||
|
Requires-Dist: pyobjc-core>=10.3 ; sys_platform == 'darwin'
|
||||||
|
Requires-Dist: pyobjc-framework-corebluetooth>=10.3 ; sys_platform == 'darwin'
|
||||||
|
Requires-Dist: pyobjc-framework-libdispatch>=10.3 ; sys_platform == 'darwin'
|
||||||
|
Requires-Dist: winrt-runtime>=3.1 ; sys_platform == 'win32'
|
||||||
|
Requires-Dist: winrt-windows-devices-bluetooth>=3.1 ; sys_platform == 'win32'
|
||||||
|
Requires-Dist: winrt-windows-devices-bluetooth-advertisement>=3.1 ; sys_platform == 'win32'
|
||||||
|
Requires-Dist: winrt-windows-devices-bluetooth-genericattributeprofile>=3.1 ; sys_platform == 'win32'
|
||||||
|
Requires-Dist: winrt-windows-devices-enumeration>=3.1 ; sys_platform == 'win32'
|
||||||
|
Requires-Dist: winrt-windows-devices-radios>=3.1 ; sys_platform == 'win32'
|
||||||
|
Requires-Dist: winrt-windows-foundation>=3.1 ; sys_platform == 'win32'
|
||||||
|
Requires-Dist: winrt-windows-foundation-collections>=3.1 ; sys_platform == 'win32'
|
||||||
|
Requires-Dist: winrt-windows-storage-streams>=3.1 ; sys_platform == 'win32'
|
||||||
|
Requires-Dist: dbus-fast>=1.83.0 ; sys_platform == 'linux'
|
||||||
|
Requires-Dist: bleak-pythonista>=0.1.1 ; extra == 'pythonista'
|
||||||
|
Requires-Python: >=3.10
|
||||||
|
Project-URL: Homepage, https://github.com/hbldh/bleak
|
||||||
|
Project-URL: Documentation, https://bleak.readthedocs.io
|
||||||
|
Project-URL: Changelog, https://github.com/hbldh/bleak/blob/develop/CHANGELOG.rst
|
||||||
|
Project-URL: Support, https://github.com/hbldh/bleak/discussions
|
||||||
|
Project-URL: Issues, https://github.com/hbldh/bleak/issues
|
||||||
|
Provides-Extra: pythonista
|
||||||
|
Description-Content-Type: text/x-rst
|
||||||
|
|
||||||
|
=====
|
||||||
|
Bleak
|
||||||
|
=====
|
||||||
|
|
||||||
|
.. image:: https://github.com/hbldh/bleak/workflows/Build%20and%20Test/badge.svg
|
||||||
|
:target: https://github.com/hbldh/bleak/actions?query=workflow%3A%22Build+and+Test%22
|
||||||
|
:alt: Build and Test
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/v/bleak.svg
|
||||||
|
:target: https://pypi.python.org/pypi/bleak
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/dm/bleak.svg
|
||||||
|
:target: https://pypi.python.org/pypi/bleak
|
||||||
|
:alt: PyPI - Downloads
|
||||||
|
|
||||||
|
.. image:: https://readthedocs.org/projects/bleak/badge/?version=latest
|
||||||
|
:target: https://bleak.readthedocs.io/en/latest/?badge=latest
|
||||||
|
:alt: Documentation Status
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||||
|
:target: https://github.com/psf/black
|
||||||
|
|
||||||
|
.. container::
|
||||||
|
|
||||||
|
.. image:: Bleak_logo2.png
|
||||||
|
:target: https://github.com/hbldh/bleak
|
||||||
|
:alt: Bleak Logo
|
||||||
|
|
||||||
|
Bleak is an acronym for Bluetooth Low Energy platform Agnostic Klient.
|
||||||
|
|
||||||
|
* Free software: MIT license
|
||||||
|
* Documentation: https://bleak.readthedocs.io.
|
||||||
|
|
||||||
|
Bleak is a GATT client software, capable of connecting to BLE devices
|
||||||
|
acting as GATT servers. It is designed to provide a asynchronous,
|
||||||
|
cross-platform Python API to connect and communicate with e.g. sensors.
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ pip install bleak
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
* Supports Windows 11, version 22000 and greater
|
||||||
|
* Supports Linux distributions with BlueZ >= 5.55
|
||||||
|
* OS X/macOS support via Core Bluetooth API, from at least OS X version 10.15
|
||||||
|
* Android backend compatible with python-for-android
|
||||||
|
|
||||||
|
Bleak supports reading, writing and getting notifications from
|
||||||
|
GATT servers, as well as a function for discovering BLE devices.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
To discover Bluetooth devices that can be connected to:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from bleak import BleakScanner
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
devices = await BleakScanner.discover()
|
||||||
|
for d in devices:
|
||||||
|
print(d)
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
|
|
||||||
|
|
||||||
|
Connect to a Bluetooth device and read its model number:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from bleak import BleakClient
|
||||||
|
|
||||||
|
address = "24:71:89:cc:09:05"
|
||||||
|
MODEL_NBR_UUID = "2A24"
|
||||||
|
|
||||||
|
async def main(address):
|
||||||
|
async with BleakClient(address) as client:
|
||||||
|
model_number = await client.read_gatt_char(MODEL_NBR_UUID)
|
||||||
|
print(f"Model Number: {model_number.decode()}")
|
||||||
|
|
||||||
|
asyncio.run(main(address))
|
||||||
|
|
||||||
|
DO NOT NAME YOUR SCRIPT ``bleak.py``! It will cause a circular import error.
|
||||||
|
|
||||||
|
See examples folder for more code, for instance example code for connecting to a
|
||||||
|
`TI SensorTag CC2650 <http://www.ti.com/ww/en/wireless_connectivity/sensortag/>`_
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
bleak-3.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
bleak-3.0.2.dist-info/METADATA,sha256=-J8V2qKsJBXOvDOVL73TrXv4U67jyiDmbopo7BXa9AE,4698
|
||||||
|
bleak-3.0.2.dist-info/RECORD,,
|
||||||
|
bleak-3.0.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
bleak-3.0.2.dist-info/WHEEL,sha256=q5IF0q2xCp3ktUFRCVWsQLjl2ChNlWXBJtnI1LCGdJ8,80
|
||||||
|
bleak-3.0.2.dist-info/licenses/LICENSE,sha256=xAKaK2Ozgkq2i-hB9BSt293iDLec2-Jy-oDAsqvmU3Q,1072
|
||||||
|
bleak/__init__.py,sha256=ht6iQq9epvqQ7hz6A8KlQcw7fvT0efdqNvtaxTJyg2M,37965
|
||||||
|
bleak/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
bleak/__pycache__/_compat.cpython-312.pyc,,
|
||||||
|
bleak/__pycache__/assigned_numbers.cpython-312.pyc,,
|
||||||
|
bleak/__pycache__/exc.cpython-312.pyc,,
|
||||||
|
bleak/__pycache__/uuids.cpython-312.pyc,,
|
||||||
|
bleak/_compat.py,sha256=Vd1TpIlUIlg33OAsJjWy1H-SNZYrf7PkjPI2IFqfI60,988
|
||||||
|
bleak/args/__init__.py,sha256=B60fxxXFh_1F0t56BQl0i2rYW2iQaMhMWk7CPEAGG4s,594
|
||||||
|
bleak/args/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
bleak/args/__pycache__/bluez.cpython-312.pyc,,
|
||||||
|
bleak/args/__pycache__/corebluetooth.cpython-312.pyc,,
|
||||||
|
bleak/args/__pycache__/winrt.cpython-312.pyc,,
|
||||||
|
bleak/args/bluez.py,sha256=FXldHcGZ4aFtKA70kwX39dLBJNUJhk0p2gJobpx8vko,4122
|
||||||
|
bleak/args/corebluetooth.py,sha256=HUlwGXtw2M6F_mGj07obYlwUALnWqR7o8BmDWKzuKuk,1226
|
||||||
|
bleak/args/winrt.py,sha256=RPdm8I316SFKbkf_8w_FSb7uTKuD5wXHLGe6bcii97k,974
|
||||||
|
bleak/assigned_numbers.py,sha256=jC3uihHGvozGmCdIZ34zwk-jeDyYj65shj8RvtI-lZw,2504
|
||||||
|
bleak/backends/__init__.py,sha256=PSTvMjyTVGzk92mtxVW5Q7wlKcvocL9pK4y834eeK_8,1798
|
||||||
|
bleak/backends/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
bleak/backends/__pycache__/_manufacturers.cpython-312.pyc,,
|
||||||
|
bleak/backends/__pycache__/_utils.cpython-312.pyc,,
|
||||||
|
bleak/backends/__pycache__/characteristic.cpython-312.pyc,,
|
||||||
|
bleak/backends/__pycache__/client.cpython-312.pyc,,
|
||||||
|
bleak/backends/__pycache__/descriptor.cpython-312.pyc,,
|
||||||
|
bleak/backends/__pycache__/device.cpython-312.pyc,,
|
||||||
|
bleak/backends/__pycache__/scanner.cpython-312.pyc,,
|
||||||
|
bleak/backends/__pycache__/service.cpython-312.pyc,,
|
||||||
|
bleak/backends/_manufacturers.py,sha256=NjCqNI7SCtSzNf5OmGiFM7ayq9t_5eKNecMnyF4kehU,67669
|
||||||
|
bleak/backends/_utils.py,sha256=KkWbkLbOhawaQ-3iKcmvJVen2Adt4QxstcsvE0l-mII,1047
|
||||||
|
bleak/backends/bluezdbus/__init__.py,sha256=isYF5VYdJI9EUUD9Z0AbQ3WDv4IBuaLBv2epV2DS8FM,21
|
||||||
|
bleak/backends/bluezdbus/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
bleak/backends/bluezdbus/__pycache__/advertisement_monitor.cpython-312.pyc,,
|
||||||
|
bleak/backends/bluezdbus/__pycache__/client.cpython-312.pyc,,
|
||||||
|
bleak/backends/bluezdbus/__pycache__/defs.cpython-312.pyc,,
|
||||||
|
bleak/backends/bluezdbus/__pycache__/manager.cpython-312.pyc,,
|
||||||
|
bleak/backends/bluezdbus/__pycache__/scanner.cpython-312.pyc,,
|
||||||
|
bleak/backends/bluezdbus/__pycache__/signals.cpython-312.pyc,,
|
||||||
|
bleak/backends/bluezdbus/__pycache__/utils.cpython-312.pyc,,
|
||||||
|
bleak/backends/bluezdbus/__pycache__/version.cpython-312.pyc,,
|
||||||
|
bleak/backends/bluezdbus/advertisement_monitor.py,sha256=hfwHUzA8kNCv9OyUJSAUJ2mU0Kayd2yplfhLVL2ief4,3974
|
||||||
|
bleak/backends/bluezdbus/client.py,sha256=72X6l4RBBSHBDVyeyi9jMQ4pnX9CeLr3Uw-B1XiIz5I,39932
|
||||||
|
bleak/backends/bluezdbus/defs.py,sha256=OwPfz0ZlMx7vBWI-8t_1tfvqu05PG8dgUG_c2JMtXSY,4373
|
||||||
|
bleak/backends/bluezdbus/manager.py,sha256=Kn4g7zqWsBroPfX8v8IwiEnv6qtikhaSx2ymmighO9s,44905
|
||||||
|
bleak/backends/bluezdbus/scanner.py,sha256=u3NJaQWRBQqYWRXr_Pe8lu_LoETdm1R0Oh2MGdRfzzo,8234
|
||||||
|
bleak/backends/bluezdbus/signals.py,sha256=2iOPVBtHSQBIACqGhC5v8Bk_oFgkPUuR6kd845sIM2w,6252
|
||||||
|
bleak/backends/bluezdbus/utils.py,sha256=eZNw8GK5WlMVbGJ3NdloSgC3twpl5y6Y7VL1eA-QfMI,4933
|
||||||
|
bleak/backends/bluezdbus/version.py,sha256=GQbwpYMBfv8LCGpEjynUyvk1sIudceLz_FNgVfxrE4I,1957
|
||||||
|
bleak/backends/characteristic.py,sha256=jhVkm_Ce3REfmKcjArDHUOTdWxfAugfSr0EcZY6BALc,5112
|
||||||
|
bleak/backends/client.py,sha256=_wSF61BwfMWO0kzgcQjhLBinxZKK1fZZiyyX2LjTrGE,8442
|
||||||
|
bleak/backends/corebluetooth/CentralManagerDelegate.py,sha256=3YvbHNQ3jVJ4JqVJK7ujWy2ZsROwBnpawTjSf1tZfJM,14226
|
||||||
|
bleak/backends/corebluetooth/PeripheralDelegate.py,sha256=KdcZ2KbSmiJvULmFqdpT1fZZeHATzz5T7rnoHA8kn0c,24743
|
||||||
|
bleak/backends/corebluetooth/__init__.py,sha256=e3h7IH-yrKjTvT4WoK2g-Azrewm18Rg0ln6VsmZ3hbU,284
|
||||||
|
bleak/backends/corebluetooth/__pycache__/CentralManagerDelegate.cpython-312.pyc,,
|
||||||
|
bleak/backends/corebluetooth/__pycache__/PeripheralDelegate.cpython-312.pyc,,
|
||||||
|
bleak/backends/corebluetooth/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
bleak/backends/corebluetooth/__pycache__/client.cpython-312.pyc,,
|
||||||
|
bleak/backends/corebluetooth/__pycache__/scanner.cpython-312.pyc,,
|
||||||
|
bleak/backends/corebluetooth/__pycache__/utils.cpython-312.pyc,,
|
||||||
|
bleak/backends/corebluetooth/client.py,sha256=HIcltkuujrV7X027AzuzUWqWcJxAYf6F_Qw5t8kpul8,14472
|
||||||
|
bleak/backends/corebluetooth/scanner.py,sha256=taMAE0h2DzeypUC7HJehgFg5XNfho6RqjyvCUQYU-sI,6554
|
||||||
|
bleak/backends/corebluetooth/utils.py,sha256=EhtJKMbLvBfBO_yVdYF4PZ4EN8drBpKZlzf38h3iVm8,3602
|
||||||
|
bleak/backends/descriptor.py,sha256=ZxSU30c7aN_N6Bd_vVjF9OmmDvDsRdQ-kEODgTNHsUI,4501
|
||||||
|
bleak/backends/device.py,sha256=ip3sfYapIlwwl2VcSD8F4l2v9WV5PBEg2hgSqk1re3Y,1230
|
||||||
|
bleak/backends/p4android/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
bleak/backends/p4android/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
bleak/backends/p4android/__pycache__/client.cpython-312.pyc,,
|
||||||
|
bleak/backends/p4android/__pycache__/defs.cpython-312.pyc,,
|
||||||
|
bleak/backends/p4android/__pycache__/scanner.cpython-312.pyc,,
|
||||||
|
bleak/backends/p4android/__pycache__/utils.cpython-312.pyc,,
|
||||||
|
bleak/backends/p4android/client.py,sha256=WCgYmdwOPSGNeKJFXH21j7tVkroIQ5lRle8cXtMroQA,17814
|
||||||
|
bleak/backends/p4android/defs.py,sha256=pO4gpTx8tu2n7kXgV5zDWwd26_Hh-myPAy73TYQsS-4,3283
|
||||||
|
bleak/backends/p4android/java/com/github/hbldh/bleak/PythonBluetoothGattCallback.java,sha256=2bZPGMQB3EZt7uW2EV7ST2EVwtWnDa04iTOtOWHuWqc,2897
|
||||||
|
bleak/backends/p4android/java/com/github/hbldh/bleak/PythonScanCallback.java,sha256=vtiCeSA4Sn8ScjqDiCgXG21v1ADQD8SEekRdJQ3QAB8,909
|
||||||
|
bleak/backends/p4android/recipes/bleak/__init__.py,sha256=pnT6pxILzrII_cl8sAFIN-_it4dPRbXbW5mfsx4sotA,2022
|
||||||
|
bleak/backends/p4android/recipes/bleak/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
bleak/backends/p4android/recipes/bleak/__pycache__/fix_setup.cpython-312.pyc,,
|
||||||
|
bleak/backends/p4android/recipes/bleak/fix_setup.py,sha256=KMfVhr4Um91aKxBAYxXIGcjH98CiTFXBRJwEAwUJriA,244
|
||||||
|
bleak/backends/p4android/scanner.py,sha256=cXCMTlYRotPFUBzlauYebfSy0FmHKuMEzS9edYsYG-0,10842
|
||||||
|
bleak/backends/p4android/utils.py,sha256=mkrjzJBHnkUri41Yl6qgKjllz-LXQqNULMj6DW5HDwo,3330
|
||||||
|
bleak/backends/scanner.py,sha256=zryWb4-YwRoEmVV1fc93u_qtiAOVl-ku8nP-Mlg650k,10184
|
||||||
|
bleak/backends/service.py,sha256=hhjKGkmfgsKoNSi3Iwhef5UWF8PC5-zhWDi0nB0IFeI,7576
|
||||||
|
bleak/backends/winrt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
bleak/backends/winrt/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
bleak/backends/winrt/__pycache__/client.cpython-312.pyc,,
|
||||||
|
bleak/backends/winrt/__pycache__/scanner.cpython-312.pyc,,
|
||||||
|
bleak/backends/winrt/__pycache__/util.cpython-312.pyc,,
|
||||||
|
bleak/backends/winrt/client.py,sha256=5QiEcCYjNZvYF6nvIGxatNEinJPo9ed34YT_bIdMMtQ,43313
|
||||||
|
bleak/backends/winrt/scanner.py,sha256=Y6nlml8jTO_2Ci6JjsZpeLdeAMuAOXxE458hHg-H68E,13046
|
||||||
|
bleak/backends/winrt/util.py,sha256=bNT0ivkVEoO4oteB1cRBBTADHolluccx7AEVgjLa4RM,6486
|
||||||
|
bleak/exc.py,sha256=voa8y6eWdkvMJcEKAO7rZqgAgDgGtCEOM5rZ3JECVq4,11504
|
||||||
|
bleak/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
bleak/uuids.py,sha256=ind-60OjNfoEXLzhCfYJWTGhLuTgwJuN0vLPnvZL8Oc,49038
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: uv 0.11.8
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py3-none-any
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020, Henrik Blidh
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
1008
tools/python-embed/Lib/site-packages/bleak/__init__.py
Normal file
1008
tools/python-embed/Lib/site-packages/bleak/__init__.py
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
27
tools/python-embed/Lib/site-packages/bleak/_compat.py
Normal file
27
tools/python-embed/Lib/site-packages/bleak/_compat.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
"""
|
||||||
|
Python version compatibility imports.
|
||||||
|
|
||||||
|
These will be removed when support for older Python versions is dropped.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if sys.version_info < (3, 11):
|
||||||
|
from async_timeout import timeout as timeout
|
||||||
|
from typing_extensions import Never as Never
|
||||||
|
from typing_extensions import Self as Self
|
||||||
|
from typing_extensions import TypeVarTuple as TypeVarTuple
|
||||||
|
from typing_extensions import Unpack as Unpack
|
||||||
|
from typing_extensions import assert_never as assert_never
|
||||||
|
else:
|
||||||
|
from asyncio import timeout as timeout # noqa: F401
|
||||||
|
from typing import Never as Never # noqa: F401
|
||||||
|
from typing import Self as Self # noqa: F401
|
||||||
|
from typing import TypeVarTuple as TypeVarTuple # noqa: F401
|
||||||
|
from typing import Unpack as Unpack # noqa: F401
|
||||||
|
from typing import assert_never as assert_never # noqa: F401
|
||||||
|
|
||||||
|
if sys.version_info < (3, 12):
|
||||||
|
from typing_extensions import override as override
|
||||||
|
else:
|
||||||
|
from typing import override as override # noqa: F401
|
||||||
21
tools/python-embed/Lib/site-packages/bleak/args/__init__.py
Normal file
21
tools/python-embed/Lib/site-packages/bleak/args/__init__.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import sys
|
||||||
|
from collections.abc import Sized
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
if sys.version_info < (3, 12):
|
||||||
|
# mypy and pyright object to Buffer being both ABC and Protocol. Work around
|
||||||
|
# by just inheriting from Protocol here. typing_extensions.Buffer is just
|
||||||
|
# abc.ABC.
|
||||||
|
class BufferProtocol(Protocol):
|
||||||
|
def __buffer__(self, flags: int, /) -> memoryview: ...
|
||||||
|
|
||||||
|
else:
|
||||||
|
from collections.abc import Buffer as BufferProtocol
|
||||||
|
|
||||||
|
|
||||||
|
class SizedBuffer(BufferProtocol, Sized, Protocol):
|
||||||
|
"""
|
||||||
|
Protocol for types that are both Buffer and Sized.
|
||||||
|
|
||||||
|
.. versionadded:: 2.1
|
||||||
|
"""
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
150
tools/python-embed/Lib/site-packages/bleak/args/bluez.py
Normal file
150
tools/python-embed/Lib/site-packages/bleak/args/bluez.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
"""
|
||||||
|
-----------------------
|
||||||
|
BlueZ backend arguments
|
||||||
|
-----------------------
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import NamedTuple, TypedDict, Union
|
||||||
|
|
||||||
|
from bleak.assigned_numbers import AdvertisementDataType
|
||||||
|
|
||||||
|
|
||||||
|
class BlueZDiscoveryFilters(TypedDict, total=False):
|
||||||
|
"""
|
||||||
|
Dictionary of arguments for the ``org.bluez.Adapter1.SetDiscoveryFilter``
|
||||||
|
D-Bus method.
|
||||||
|
|
||||||
|
https://github.com/bluez/bluez/blob/master/doc/org.bluez.Adapter.rst#void-setdiscoveryfilterdict-filter
|
||||||
|
"""
|
||||||
|
|
||||||
|
UUIDs: list[str]
|
||||||
|
"""
|
||||||
|
Filter by service UUIDs, empty means match _any_ UUID.
|
||||||
|
|
||||||
|
Normally, the ``service_uuids`` argument of :class:`bleak.BleakScanner`
|
||||||
|
is used instead.
|
||||||
|
"""
|
||||||
|
RSSI: int
|
||||||
|
"""
|
||||||
|
RSSI threshold value.
|
||||||
|
"""
|
||||||
|
Pathloss: int
|
||||||
|
"""
|
||||||
|
Pathloss threshold value.
|
||||||
|
"""
|
||||||
|
Transport: str
|
||||||
|
"""
|
||||||
|
Transport parameter determines the type of scan.
|
||||||
|
|
||||||
|
This should not be used since it is required to be set to ``"le"``.
|
||||||
|
"""
|
||||||
|
DuplicateData: bool
|
||||||
|
"""
|
||||||
|
Disables duplicate detection of advertisement data.
|
||||||
|
|
||||||
|
This does not affect the ``Filter Duplicates`` parameter of the ``LE Set Scan Enable``
|
||||||
|
HCI command to the Bluetooth adapter!
|
||||||
|
|
||||||
|
Although the default value for BlueZ is ``True``, Bleak sets this to ``False`` by default.
|
||||||
|
"""
|
||||||
|
Discoverable: bool
|
||||||
|
"""
|
||||||
|
Make adapter discoverable while discovering,
|
||||||
|
if the adapter is already discoverable setting
|
||||||
|
this filter won't do anything.
|
||||||
|
"""
|
||||||
|
Pattern: str
|
||||||
|
"""
|
||||||
|
Discover devices where the pattern matches
|
||||||
|
either the prefix of the address or
|
||||||
|
device name which is convenient way to limited
|
||||||
|
the number of device objects created during a
|
||||||
|
discovery.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class OrPattern(NamedTuple):
|
||||||
|
"""
|
||||||
|
BlueZ advertisement monitor or-pattern.
|
||||||
|
|
||||||
|
https://github.com/bluez/bluez/blob/master/doc/org.bluez.AdvertisementMonitor.rst#arrayuint8-uint8-arraybyte-patterns-read-only-optional
|
||||||
|
"""
|
||||||
|
|
||||||
|
start_position: int
|
||||||
|
ad_data_type: AdvertisementDataType
|
||||||
|
content_of_pattern: bytes
|
||||||
|
|
||||||
|
|
||||||
|
# Windows has a similar structure, so we allow generic tuple for cross-platform compatibility
|
||||||
|
OrPatternLike = Union[OrPattern, tuple[int, AdvertisementDataType, bytes]]
|
||||||
|
|
||||||
|
|
||||||
|
class BlueZScannerArgs(TypedDict, total=False):
|
||||||
|
"""
|
||||||
|
:class:`BleakScanner` args that are specific to the BlueZ backend.
|
||||||
|
"""
|
||||||
|
|
||||||
|
adapter: str
|
||||||
|
"""
|
||||||
|
Bluetooth adapter to use for discovery, e.g. "hci0".
|
||||||
|
|
||||||
|
.. tip:: If you have multiple Bluetooth adapters, they may not always be
|
||||||
|
assigned the same ``hciX`` name across reboots. In that case, you can
|
||||||
|
use udev to look up the name based on other properties like the USB
|
||||||
|
vendor and product ID.
|
||||||
|
|
||||||
|
.. versionadded:: 3.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
filters: BlueZDiscoveryFilters
|
||||||
|
"""
|
||||||
|
Filters to pass to the adapter SetDiscoveryFilter D-Bus method.
|
||||||
|
|
||||||
|
Only used for active scanning.
|
||||||
|
"""
|
||||||
|
|
||||||
|
or_patterns: list[OrPatternLike]
|
||||||
|
"""
|
||||||
|
Or patterns to pass to the AdvertisementMonitor1 D-Bus interface.
|
||||||
|
|
||||||
|
Only used for passive scanning.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class BlueZClientArgs(TypedDict, total=False):
|
||||||
|
"""
|
||||||
|
:class:`bleak.BleakClient` args that are specific to the BlueZ backend.
|
||||||
|
|
||||||
|
.. versionadded:: 3.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
adapter: str
|
||||||
|
"""
|
||||||
|
Bluetooth adapter to use for connection, e.g. "hci0".
|
||||||
|
|
||||||
|
.. tip:: If you have multiple Bluetooth adapters, they may not always be
|
||||||
|
assigned the same ``hciX`` name across reboots. In that case, you can
|
||||||
|
use udev to look up the name based on other properties like the USB
|
||||||
|
vendor and product ID.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class BlueZNotifyArgs(TypedDict, total=False):
|
||||||
|
"""
|
||||||
|
:meth:`bleak.BleakClient.start_notify` method args that are specific to the
|
||||||
|
BlueZ backend.
|
||||||
|
|
||||||
|
.. versionadded:: 2.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
use_start_notify: bool
|
||||||
|
"""
|
||||||
|
If false, use the "AcquireNotify" D-Bus method instead of "StartNotify" to
|
||||||
|
subscribe to notifications. The default is to use "StartNotify" for better
|
||||||
|
compatibility with most BLE devices.
|
||||||
|
|
||||||
|
see :ref:`linux-start-notify` for more details.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0.2
|
||||||
|
The default value was changed from ``False`` to ``True``.
|
||||||
|
"""
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
"""
|
||||||
|
-------------------------------
|
||||||
|
CoreBluetooth backend arguments
|
||||||
|
-------------------------------
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Optional, TypedDict
|
||||||
|
|
||||||
|
|
||||||
|
class CBScannerArgs(TypedDict, total=False):
|
||||||
|
"""
|
||||||
|
Platform-specific :class:`BleakScanner` args for the CoreBluetooth backend.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use_bdaddr: bool
|
||||||
|
"""
|
||||||
|
If true, use Bluetooth address instead of UUID.
|
||||||
|
|
||||||
|
.. warning:: This uses an undocumented IOBluetooth API to get the Bluetooth
|
||||||
|
address and may break in the future macOS releases. `It is known to not
|
||||||
|
work on macOS 10.15 <https://github.com/hbldh/bleak/issues/1286>`_.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
NotificationDiscriminator = Callable[[bytes], bool]
|
||||||
|
|
||||||
|
|
||||||
|
class CBStartNotifyArgs(TypedDict, total=False):
|
||||||
|
"""CoreBluetooth backend-specific dictionary of arguments for the
|
||||||
|
:meth:`bleak.BleakClient.start_notify` method.
|
||||||
|
"""
|
||||||
|
|
||||||
|
notification_discriminator: Optional[NotificationDiscriminator]
|
||||||
|
"""
|
||||||
|
A function that takes a single argument of a characteristic value
|
||||||
|
and returns ``True`` if the value is from a notification or
|
||||||
|
``False`` if the value is from a read response.
|
||||||
|
|
||||||
|
.. seealso:: :ref:`cb-notification-discriminator` for more info.
|
||||||
|
"""
|
||||||
32
tools/python-embed/Lib/site-packages/bleak/args/winrt.py
Normal file
32
tools/python-embed/Lib/site-packages/bleak/args/winrt.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"""
|
||||||
|
-----------------------
|
||||||
|
WinRT backend arguments
|
||||||
|
-----------------------
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Literal, TypedDict
|
||||||
|
|
||||||
|
|
||||||
|
class WinRTClientArgs(TypedDict, total=False):
|
||||||
|
"""
|
||||||
|
Windows-specific arguments for :class:`BleakClient`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
address_type: Literal["public", "random"]
|
||||||
|
"""
|
||||||
|
Can either be ``"public"`` or ``"random"``, depending on the required address
|
||||||
|
type needed to connect to your device.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use_cached_services: bool
|
||||||
|
"""
|
||||||
|
``True`` allows Windows to fetch the services, characteristics and descriptors
|
||||||
|
from the Windows cache instead of reading them from the device. Can be very
|
||||||
|
much faster for known, unchanging devices, but not recommended for DIY peripherals
|
||||||
|
where the GATT layout can change between connections.
|
||||||
|
|
||||||
|
``False`` will force the attribute database to be read from the remote device
|
||||||
|
instead of using the OS cache.
|
||||||
|
|
||||||
|
If omitted, the OS Bluetooth stack will do what it thinks is best.
|
||||||
|
"""
|
||||||
@ -0,0 +1,93 @@
|
|||||||
|
"""
|
||||||
|
Bluetooth Assigned Numbers
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
This module contains useful assigned numbers from the Bluetooth spec.
|
||||||
|
|
||||||
|
See <https://www.bluetooth.com/specifications/assigned-numbers/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from enum import IntEnum
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
|
||||||
|
class AdvertisementDataType(IntEnum):
|
||||||
|
"""
|
||||||
|
Generic Access Profile advertisement data types.
|
||||||
|
|
||||||
|
`Source <https://btprodspecificationrefs.blob.core.windows.net/assigned-numbers/Assigned%20Number%20Types/Generic%20Access%20Profile.pdf>`.
|
||||||
|
|
||||||
|
.. versionadded:: 0.15
|
||||||
|
"""
|
||||||
|
|
||||||
|
FLAGS = 0x01
|
||||||
|
INCOMPLETE_LIST_SERVICE_UUID16 = 0x02
|
||||||
|
COMPLETE_LIST_SERVICE_UUID16 = 0x03
|
||||||
|
INCOMPLETE_LIST_SERVICE_UUID32 = 0x04
|
||||||
|
COMPLETE_LIST_SERVICE_UUID32 = 0x05
|
||||||
|
INCOMPLETE_LIST_SERVICE_UUID128 = 0x06
|
||||||
|
COMPLETE_LIST_SERVICE_UUID128 = 0x07
|
||||||
|
SHORTENED_LOCAL_NAME = 0x08
|
||||||
|
COMPLETE_LOCAL_NAME = 0x09
|
||||||
|
TX_POWER_LEVEL = 0x0A
|
||||||
|
CLASS_OF_DEVICE = 0x0D
|
||||||
|
|
||||||
|
SERVICE_DATA_UUID16 = 0x16
|
||||||
|
SERVICE_DATA_UUID32 = 0x20
|
||||||
|
SERVICE_DATA_UUID128 = 0x21
|
||||||
|
|
||||||
|
MANUFACTURER_SPECIFIC_DATA = 0xFF
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: these must match BlueZ name mapping
|
||||||
|
CharacteristicPropertyName = Literal[
|
||||||
|
"broadcast",
|
||||||
|
"read",
|
||||||
|
"write-without-response",
|
||||||
|
"write",
|
||||||
|
"notify",
|
||||||
|
"indicate",
|
||||||
|
"authenticated-signed-writes",
|
||||||
|
"extended-properties",
|
||||||
|
"reliable-write",
|
||||||
|
"writable-auxiliaries",
|
||||||
|
"encrypt-read",
|
||||||
|
"encrypt-write",
|
||||||
|
# "encrypt-notify" and "encrypt-indicate" are server-only
|
||||||
|
"encrypt-authenticated-read",
|
||||||
|
"encrypt-authenticated-write",
|
||||||
|
# "encrypt-authenticated-notify", "encrypt-authenticated-indicate",
|
||||||
|
# "secure-read", "secure-write", "secure-notify", "secure-indicate"
|
||||||
|
# are server-only
|
||||||
|
"authorize",
|
||||||
|
]
|
||||||
|
|
||||||
|
CHARACTERISTIC_PROPERTIES: dict[int, CharacteristicPropertyName] = {
|
||||||
|
0x1: "broadcast",
|
||||||
|
0x2: "read",
|
||||||
|
0x4: "write-without-response",
|
||||||
|
0x8: "write",
|
||||||
|
0x10: "notify",
|
||||||
|
0x20: "indicate",
|
||||||
|
0x40: "authenticated-signed-writes",
|
||||||
|
0x80: "extended-properties",
|
||||||
|
0x100: "reliable-write",
|
||||||
|
0x200: "writable-auxiliaries",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def gatt_char_props_to_strs(
|
||||||
|
props: int,
|
||||||
|
) -> frozenset[CharacteristicPropertyName]:
|
||||||
|
"""
|
||||||
|
Convert a GATT characteristic properties bitmask to a set of strings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
props: The GATT characteristic properties bitmask.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A set of strings representing the GATT characteristic properties.
|
||||||
|
"""
|
||||||
|
return frozenset(
|
||||||
|
CHARACTERISTIC_PROPERTIES[i] for i in (1 << n for n in range(16)) if props & i
|
||||||
|
)
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
"""
|
||||||
|
Communicating with Bluetooth hardware requires calling OS-specific APIs. These
|
||||||
|
are abstracted as "backends" in Bleak.
|
||||||
|
|
||||||
|
The backend will be automatically selected based on the operating system Bleak
|
||||||
|
is running on. In some cases, this may also depend on a specific runtime, like
|
||||||
|
Pythonista on iOS.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import enum
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from bleak.exc import BleakError
|
||||||
|
|
||||||
|
|
||||||
|
class BleakBackend(str, enum.Enum):
|
||||||
|
"""
|
||||||
|
Identifiers for available built-in Bleak backends.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
P4ANDROID = "p4android"
|
||||||
|
"""
|
||||||
|
Python for Android backend.
|
||||||
|
"""
|
||||||
|
|
||||||
|
BLUEZ_DBUS = "bluez_dbus"
|
||||||
|
"""
|
||||||
|
BlueZ D-Bus backend for Linux.
|
||||||
|
"""
|
||||||
|
|
||||||
|
PYTHONISTA_CB = "pythonista_cb"
|
||||||
|
"""
|
||||||
|
Pythonista CoreBluetooth backend for iOS and macOS.
|
||||||
|
"""
|
||||||
|
|
||||||
|
CORE_BLUETOOTH = "core_bluetooth"
|
||||||
|
"""
|
||||||
|
CoreBluetooth backend for macOS.
|
||||||
|
"""
|
||||||
|
|
||||||
|
WIN_RT = "win_rt"
|
||||||
|
"""
|
||||||
|
Windows Runtime backend for Windows.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_backend() -> BleakBackend:
|
||||||
|
"""
|
||||||
|
Returns the preferred backend for the current platform/environment.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
if os.environ.get("P4A_BOOTSTRAP") is not None:
|
||||||
|
return BleakBackend.P4ANDROID
|
||||||
|
|
||||||
|
if platform.system() == "Linux":
|
||||||
|
return BleakBackend.BLUEZ_DBUS
|
||||||
|
|
||||||
|
if sys.platform == "ios" and "Pythonista3.app" in sys.executable:
|
||||||
|
# Must be resolved before checking for "Darwin" (macOS),
|
||||||
|
# as both the Pythonista app for iOS and macOS
|
||||||
|
# return "Darwin" from platform.system()
|
||||||
|
return BleakBackend.PYTHONISTA_CB
|
||||||
|
|
||||||
|
if platform.system() == "Darwin":
|
||||||
|
return BleakBackend.CORE_BLUETOOTH
|
||||||
|
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
return BleakBackend.WIN_RT
|
||||||
|
|
||||||
|
raise BleakError(f"Unsupported platform: {platform.system()}")
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,38 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Any, ParamSpec, TypeVar
|
||||||
|
|
||||||
|
from bleak._compat import TypeVarTuple, Unpack
|
||||||
|
|
||||||
|
_Ts = TypeVarTuple("_Ts")
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def try_call_soon_threadsafe(
|
||||||
|
event_loop: asyncio.AbstractEventLoop,
|
||||||
|
callback: Callable[[Unpack[_Ts]], Any],
|
||||||
|
*args: Unpack[_Ts],
|
||||||
|
) -> None:
|
||||||
|
"""Call a callback on the event loop thread, handling closed loop errors gracefully."""
|
||||||
|
try:
|
||||||
|
event_loop.call_soon_threadsafe(callback, *args)
|
||||||
|
except RuntimeError:
|
||||||
|
# Likely caused by loop being closed
|
||||||
|
logger.debug("unraisable exception", exc_info=True) # pragma: no cover
|
||||||
|
|
||||||
|
|
||||||
|
_P = ParamSpec("_P")
|
||||||
|
_TReturn = TypeVar("_TReturn")
|
||||||
|
|
||||||
|
|
||||||
|
def external_thread_callback(func: Callable[_P, _TReturn]) -> Callable[_P, _TReturn]:
|
||||||
|
"""
|
||||||
|
Decorator for callbacks invoked from external non-Python thread.
|
||||||
|
|
||||||
|
This is a no-op placeholder that can be overridden in tests to enable
|
||||||
|
coverage tracing for external threads.
|
||||||
|
"""
|
||||||
|
return func
|
||||||
@ -0,0 +1 @@
|
|||||||
|
"""BlueZ backend."""
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,131 @@
|
|||||||
|
"""
|
||||||
|
Advertisement Monitor
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
This module contains types associated with the BlueZ D-Bus `advertisement
|
||||||
|
monitor api <https://github.com/bluez/bluez/blob/master/doc/org.bluez.AdvertisementMonitor.rst>`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
if sys.platform != "linux":
|
||||||
|
assert False, "This backend is only available on Linux"
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from collections.abc import Iterable
|
||||||
|
from typing import Any, no_type_check
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
|
from dbus_fast import PropertyAccess
|
||||||
|
from dbus_fast.service import ServiceInterface, dbus_property, method
|
||||||
|
|
||||||
|
from bleak.args.bluez import OrPattern as _OrPattern
|
||||||
|
from bleak.args.bluez import OrPatternLike as _OrPatternLike
|
||||||
|
from bleak.backends.bluezdbus import defs
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_DEPRECATED: dict[str, Any] = {
|
||||||
|
"OrPattern": _OrPattern,
|
||||||
|
"OrPatternLike": _OrPatternLike,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name: str):
|
||||||
|
if value := _DEPRECATED.get(name):
|
||||||
|
warn(
|
||||||
|
f"importing {name} from bleak.backends.bluezdbus.advertisement_monitor is deprecated, use bleak.args.bluez instead",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||||
|
|
||||||
|
|
||||||
|
class AdvertisementMonitor(ServiceInterface):
|
||||||
|
"""
|
||||||
|
Implementation of the org.bluez.AdvertisementMonitor1 D-Bus interface.
|
||||||
|
|
||||||
|
The BlueZ advertisement monitor API design seems to be just for device
|
||||||
|
presence (is it in range or out of range), but this isn't really what
|
||||||
|
we want in Bleak, we want to monitor changes in advertisement data, just
|
||||||
|
like in active scanning.
|
||||||
|
|
||||||
|
So the only thing we are using here is the "or_patterns" since it is
|
||||||
|
currently required, but really we don't need that either. Hopefully an
|
||||||
|
"all" "Type" could be added to BlueZ in the future.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
or_patterns: Iterable[_OrPatternLike],
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
or_patterns:
|
||||||
|
List of or patterns that will be returned by the ``Patterns`` property.
|
||||||
|
"""
|
||||||
|
super().__init__(defs.ADVERTISEMENT_MONITOR_INTERFACE)
|
||||||
|
# dbus_fast marshaling requires list instead of tuple
|
||||||
|
self._or_patterns = [list(p) for p in or_patterns]
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def Release(self):
|
||||||
|
logger.debug("Release")
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def Activate(self):
|
||||||
|
logger.debug("Activate")
|
||||||
|
|
||||||
|
# REVISIT: mypy is broke, so we have to add redundant @no_type_check
|
||||||
|
# https://github.com/python/mypy/issues/6583
|
||||||
|
|
||||||
|
@method()
|
||||||
|
@no_type_check
|
||||||
|
def DeviceFound(self, device: "o"): # noqa: F821
|
||||||
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
|
logger.debug("DeviceFound %s", device)
|
||||||
|
|
||||||
|
@method()
|
||||||
|
@no_type_check
|
||||||
|
def DeviceLost(self, device: "o"): # noqa: F821
|
||||||
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
|
logger.debug("DeviceLost %s", device)
|
||||||
|
|
||||||
|
@dbus_property(PropertyAccess.READ)
|
||||||
|
@no_type_check
|
||||||
|
def Type(self) -> "s": # noqa: F821
|
||||||
|
# this is currently the only type supported in BlueZ
|
||||||
|
return "or_patterns"
|
||||||
|
|
||||||
|
@dbus_property(PropertyAccess.READ, disabled=True)
|
||||||
|
@no_type_check
|
||||||
|
def RSSILowThreshold(self) -> "n": # noqa: F821
|
||||||
|
...
|
||||||
|
|
||||||
|
@dbus_property(PropertyAccess.READ, disabled=True)
|
||||||
|
@no_type_check
|
||||||
|
def RSSIHighThreshold(self) -> "n": # noqa: F821
|
||||||
|
...
|
||||||
|
|
||||||
|
@dbus_property(PropertyAccess.READ, disabled=True)
|
||||||
|
@no_type_check
|
||||||
|
def RSSILowTimeout(self) -> "q": # noqa: F821
|
||||||
|
...
|
||||||
|
|
||||||
|
@dbus_property(PropertyAccess.READ, disabled=True)
|
||||||
|
@no_type_check
|
||||||
|
def RSSIHighTimeout(self) -> "q": # noqa: F821
|
||||||
|
...
|
||||||
|
|
||||||
|
@dbus_property(PropertyAccess.READ, disabled=True)
|
||||||
|
@no_type_check
|
||||||
|
def RSSISamplingPeriod(self) -> "q": # noqa: F821
|
||||||
|
...
|
||||||
|
|
||||||
|
@dbus_property(PropertyAccess.READ)
|
||||||
|
@no_type_check
|
||||||
|
def Patterns(self) -> "a(yyay)": # noqa: F821
|
||||||
|
return self._or_patterns
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,158 @@
|
|||||||
|
from typing import Literal, TypedDict
|
||||||
|
|
||||||
|
from bleak.assigned_numbers import CharacteristicPropertyName
|
||||||
|
|
||||||
|
# DBus Interfaces
|
||||||
|
OBJECT_MANAGER_INTERFACE = "org.freedesktop.DBus.ObjectManager"
|
||||||
|
PROPERTIES_INTERFACE = "org.freedesktop.DBus.Properties"
|
||||||
|
|
||||||
|
# Bluez specific DBUS
|
||||||
|
BLUEZ_SERVICE = "org.bluez"
|
||||||
|
ADAPTER_INTERFACE = "org.bluez.Adapter1"
|
||||||
|
ADVERTISEMENT_MONITOR_INTERFACE = "org.bluez.AdvertisementMonitor1"
|
||||||
|
ADVERTISEMENT_MONITOR_MANAGER_INTERFACE = "org.bluez.AdvertisementMonitorManager1"
|
||||||
|
DEVICE_INTERFACE = "org.bluez.Device1"
|
||||||
|
BATTERY_INTERFACE = "org.bluez.Battery1"
|
||||||
|
|
||||||
|
# GATT interfaces
|
||||||
|
GATT_MANAGER_INTERFACE = "org.bluez.GattManager1"
|
||||||
|
GATT_PROFILE_INTERFACE = "org.bluez.GattProfile1"
|
||||||
|
GATT_SERVICE_INTERFACE = "org.bluez.GattService1"
|
||||||
|
GATT_CHARACTERISTIC_INTERFACE = "org.bluez.GattCharacteristic1"
|
||||||
|
GATT_DESCRIPTOR_INTERFACE = "org.bluez.GattDescriptor1"
|
||||||
|
|
||||||
|
# BlueZ error names
|
||||||
|
BLUEZ_ERROR_DOES_NOT_EXIST = "org.bluez.Error.DoesNotExist"
|
||||||
|
BLUEZ_ERROR_FAILED = "org.bluez.Error.Failed"
|
||||||
|
BLUEZ_ERROR_IMPROPERLY_CONFIGURED = "org.bluez.Error.ImproperlyConfigured"
|
||||||
|
BLUEZ_ERROR_IN_PROGRESS = "org.bluez.Error.InProgress"
|
||||||
|
BLUEZ_ERROR_INVALID_ARGUMENT = "org.bluez.Error.InvalidArguments"
|
||||||
|
BLUEZ_ERROR_INVALID_OFFSET = "org.bluez.Error.InvalidOffset"
|
||||||
|
BLUEZ_ERROR_INVALID_VALUE_LENGTH = "org.bluez.Error.InvalidValueLength"
|
||||||
|
BLUEZ_ERROR_NOT_AUTHORIZED = "org.bluez.Error.NotAuthorized"
|
||||||
|
BLUEZ_ERROR_NOT_PERMITTED = "org.bluez.Error.NotPermitted"
|
||||||
|
BLUEZ_ERROR_NOT_READY = "org.bluez.Error.NotReady"
|
||||||
|
BLUEZ_ERROR_NOT_SUPPORTED = "org.bluez.Error.NotSupported"
|
||||||
|
|
||||||
|
# D-Bus properties for interfaces
|
||||||
|
# https://github.com/bluez/bluez/blob/master/doc/org.bluez.Adapter.rst
|
||||||
|
|
||||||
|
|
||||||
|
class Adapter1(TypedDict):
|
||||||
|
Address: str
|
||||||
|
Name: str
|
||||||
|
Alias: str
|
||||||
|
Class: int
|
||||||
|
Powered: bool
|
||||||
|
Discoverable: bool
|
||||||
|
Pairable: bool
|
||||||
|
PairableTimeout: int
|
||||||
|
DiscoverableTimeout: int
|
||||||
|
Discovering: int
|
||||||
|
UUIDs: list[str]
|
||||||
|
Modalias: str
|
||||||
|
Roles: list[str]
|
||||||
|
ExperimentalFeatures: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
# https://github.com/bluez/bluez/blob/master/doc/org.bluez.AdvertisementMonitor.rst
|
||||||
|
|
||||||
|
|
||||||
|
class AdvertisementMonitor1(TypedDict):
|
||||||
|
Type: str
|
||||||
|
RSSILowThreshold: int
|
||||||
|
RSSIHighThreshold: int
|
||||||
|
RSSILowTimeout: int
|
||||||
|
RSSIHighTimeout: int
|
||||||
|
RSSISamplingPeriod: int
|
||||||
|
Patterns: list[tuple[int, int, bytes]]
|
||||||
|
|
||||||
|
|
||||||
|
# https://github.com/bluez/bluez/blob/master/doc/org.bluez.AdvertisementMonitorManager.rst
|
||||||
|
|
||||||
|
|
||||||
|
class AdvertisementMonitorManager1(TypedDict):
|
||||||
|
SupportedMonitorTypes: list[str]
|
||||||
|
SupportedFeatures: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
# https://github.com/bluez/bluez/blob/master/doc/org.bluez.Battery.rst
|
||||||
|
|
||||||
|
|
||||||
|
class Battery1(TypedDict):
|
||||||
|
SupportedMonitorTypes: list[str]
|
||||||
|
SupportedFeatures: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
# https://github.com/bluez/bluez/blob/master/doc/org.bluez.Device.rst
|
||||||
|
|
||||||
|
|
||||||
|
class Device1(TypedDict):
|
||||||
|
Address: str
|
||||||
|
AddressType: str
|
||||||
|
Name: str
|
||||||
|
Icon: str
|
||||||
|
Class: int
|
||||||
|
Appearance: int
|
||||||
|
UUIDs: list[str]
|
||||||
|
Paired: bool
|
||||||
|
Bonded: bool
|
||||||
|
Connected: bool
|
||||||
|
Trusted: bool
|
||||||
|
Blocked: bool
|
||||||
|
WakeAllowed: bool
|
||||||
|
Alias: str
|
||||||
|
Adapter: str
|
||||||
|
LegacyPairing: bool
|
||||||
|
Modalias: str
|
||||||
|
RSSI: int
|
||||||
|
TxPower: int
|
||||||
|
ManufacturerData: dict[int, bytes]
|
||||||
|
ServiceData: dict[str, bytes]
|
||||||
|
ServicesResolved: bool
|
||||||
|
AdvertisingFlags: bytes
|
||||||
|
AdvertisingData: dict[int, bytes]
|
||||||
|
|
||||||
|
|
||||||
|
# https://github.com/bluez/bluez/blob/master/doc/org.bluez.GattService.rst
|
||||||
|
|
||||||
|
|
||||||
|
class GattService1(TypedDict):
|
||||||
|
UUID: str
|
||||||
|
Primary: bool
|
||||||
|
Device: str
|
||||||
|
Includes: list[str]
|
||||||
|
# Handle is server-only and not available in Bleak
|
||||||
|
|
||||||
|
|
||||||
|
class GattCharacteristic1(TypedDict):
|
||||||
|
UUID: str
|
||||||
|
Service: str
|
||||||
|
Value: bytes
|
||||||
|
WriteAcquired: bool
|
||||||
|
NotifyAcquired: bool
|
||||||
|
Notifying: bool
|
||||||
|
Flags: list[CharacteristicPropertyName]
|
||||||
|
# "MTU" property was added in BlueZ 5.62.
|
||||||
|
# It may missing when operating with an older stack.
|
||||||
|
MTU: int
|
||||||
|
# Handle is server-only and not available in Bleak
|
||||||
|
|
||||||
|
|
||||||
|
class GattDescriptor1(TypedDict):
|
||||||
|
UUID: str
|
||||||
|
Characteristic: str
|
||||||
|
Value: bytes
|
||||||
|
Flags: list[
|
||||||
|
Literal[
|
||||||
|
"read",
|
||||||
|
"write",
|
||||||
|
"encrypt-read",
|
||||||
|
"encrypt-write",
|
||||||
|
"encrypt-authenticated-read",
|
||||||
|
"encrypt-authenticated-write",
|
||||||
|
# "secure-read" and "secure-write" are server-only and not available in Bleak
|
||||||
|
"authorize",
|
||||||
|
]
|
||||||
|
]
|
||||||
|
# Handle is server-only and not available in Bleak
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,239 @@
|
|||||||
|
import sys
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
if sys.platform != "linux":
|
||||||
|
assert False, "This backend is only available on Linux"
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from collections.abc import Callable, Coroutine
|
||||||
|
from typing import Any, Literal, Optional
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
|
from dbus_fast import Variant
|
||||||
|
|
||||||
|
from bleak._compat import override
|
||||||
|
from bleak.args.bluez import BlueZDiscoveryFilters as _BlueZDiscoveryFilters
|
||||||
|
from bleak.args.bluez import BlueZScannerArgs as _BlueZScannerArgs
|
||||||
|
from bleak.backends.bluezdbus.defs import Device1
|
||||||
|
from bleak.backends.bluezdbus.manager import get_global_bluez_manager
|
||||||
|
from bleak.backends.scanner import (
|
||||||
|
AdvertisementData,
|
||||||
|
AdvertisementDataCallback,
|
||||||
|
BaseBleakScanner,
|
||||||
|
)
|
||||||
|
from bleak.exc import BleakError
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
_DEPRECATED: dict[str, Any] = {
|
||||||
|
"BlueZDiscoveryFilters": _BlueZDiscoveryFilters,
|
||||||
|
"BlueZScannerArgs": _BlueZScannerArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name: str):
|
||||||
|
if value := _DEPRECATED.get(name):
|
||||||
|
warn(
|
||||||
|
f"importing {name} from bleak.backends.bluezdbus.scanner is deprecated, use bleak.args.bluez instead",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||||
|
|
||||||
|
|
||||||
|
class BleakScannerBlueZDBus(BaseBleakScanner):
|
||||||
|
"""The native Linux Bleak BLE Scanner.
|
||||||
|
|
||||||
|
For possible values for `filters`, see the parameters to the
|
||||||
|
``SetDiscoveryFilter`` method in the `BlueZ docs
|
||||||
|
<https://github.com/bluez/bluez/blob/master/doc/org.bluez.Adapter.rst#void-setdiscoveryfilterdict-filter>`_
|
||||||
|
|
||||||
|
Args:
|
||||||
|
detection_callback:
|
||||||
|
Optional function that will be called each time a device is
|
||||||
|
discovered or advertising data has changed.
|
||||||
|
service_uuids:
|
||||||
|
Optional list of service UUIDs to filter on. Only advertisements
|
||||||
|
containing this advertising data will be received. Specifying this
|
||||||
|
also enables scanning while the screen is off on Android.
|
||||||
|
scanning_mode:
|
||||||
|
Set to ``"passive"`` to avoid the ``"active"`` scanning mode.
|
||||||
|
**bluez:
|
||||||
|
Dictionary of arguments specific to the BlueZ backend.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
detection_callback: Optional[AdvertisementDataCallback],
|
||||||
|
service_uuids: Optional[list[str]],
|
||||||
|
scanning_mode: Literal["active", "passive"],
|
||||||
|
*,
|
||||||
|
bluez: _BlueZScannerArgs,
|
||||||
|
**kwargs: Any,
|
||||||
|
):
|
||||||
|
super().__init__(detection_callback, service_uuids)
|
||||||
|
|
||||||
|
self._scanning_mode = scanning_mode
|
||||||
|
self._adapter = bluez.get("adapter")
|
||||||
|
|
||||||
|
# callback from manager for stopping scanning if it has been started
|
||||||
|
self._stop: Optional[Callable[[], Coroutine[Any, Any, None]]] = None
|
||||||
|
|
||||||
|
# Discovery filters
|
||||||
|
|
||||||
|
self._filters: dict[str, Variant] = {}
|
||||||
|
|
||||||
|
self._filters["Transport"] = Variant("s", "le")
|
||||||
|
self._filters["DuplicateData"] = Variant("b", False)
|
||||||
|
|
||||||
|
if self._service_uuids:
|
||||||
|
self._filters["UUIDs"] = Variant("as", self._service_uuids)
|
||||||
|
|
||||||
|
filters = bluez.get("filters")
|
||||||
|
|
||||||
|
if filters is not None:
|
||||||
|
self.set_scanning_filter(filters=filters)
|
||||||
|
|
||||||
|
self._or_patterns = bluez.get("or_patterns")
|
||||||
|
|
||||||
|
if self._scanning_mode == "passive" and service_uuids:
|
||||||
|
logger.warning(
|
||||||
|
"service uuid filtering is not implemented for passive scanning, use bluez or_patterns as a workaround"
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._scanning_mode == "passive" and not self._or_patterns:
|
||||||
|
raise BleakError("passive scanning mode requires bluez or_patterns")
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def start(self) -> None:
|
||||||
|
manager = await get_global_bluez_manager()
|
||||||
|
|
||||||
|
if self._adapter:
|
||||||
|
adapter_path = f"/org/bluez/{self._adapter}"
|
||||||
|
else:
|
||||||
|
adapter_path = manager.get_default_adapter()
|
||||||
|
|
||||||
|
self.seen_devices = {}
|
||||||
|
|
||||||
|
if self._scanning_mode == "passive":
|
||||||
|
assert self._or_patterns is not None # should be checked in __init__
|
||||||
|
|
||||||
|
self._stop = await manager.passive_scan(
|
||||||
|
adapter_path,
|
||||||
|
self._or_patterns,
|
||||||
|
self._handle_advertising_data,
|
||||||
|
self._handle_device_removed,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._stop = await manager.active_scan(
|
||||||
|
adapter_path,
|
||||||
|
self._filters,
|
||||||
|
self._handle_advertising_data,
|
||||||
|
self._handle_device_removed,
|
||||||
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def stop(self) -> None:
|
||||||
|
if self._stop:
|
||||||
|
# avoid reentrancy
|
||||||
|
stop, self._stop = self._stop, None
|
||||||
|
|
||||||
|
await stop()
|
||||||
|
|
||||||
|
def set_scanning_filter(self, **kwargs: Any) -> None:
|
||||||
|
"""Sets OS level scanning filters for the BleakScanner.
|
||||||
|
|
||||||
|
For possible values for `filters`, see the parameters to the
|
||||||
|
``SetDiscoveryFilter`` method in the `BlueZ docs
|
||||||
|
<https://github.com/bluez/bluez/blob/master/doc/org.bluez.Adapter.rst#void-setdiscoveryfilterdict-filter>`_
|
||||||
|
|
||||||
|
See variant types here: <https://python-dbus-next.readthedocs.io/en/latest/type-system/>
|
||||||
|
|
||||||
|
Keyword Args:
|
||||||
|
filters (dict): A dict of filters to be applied on discovery.
|
||||||
|
|
||||||
|
"""
|
||||||
|
for k, v in kwargs.get("filters", {}).items():
|
||||||
|
if k == "UUIDs":
|
||||||
|
self._filters[k] = Variant("as", v)
|
||||||
|
elif k == "RSSI":
|
||||||
|
self._filters[k] = Variant("n", v)
|
||||||
|
elif k == "Pathloss":
|
||||||
|
self._filters[k] = Variant("n", v)
|
||||||
|
elif k == "Transport":
|
||||||
|
self._filters[k] = Variant("s", v)
|
||||||
|
elif k == "DuplicateData":
|
||||||
|
self._filters[k] = Variant("b", v)
|
||||||
|
elif k == "Discoverable":
|
||||||
|
self._filters[k] = Variant("b", v)
|
||||||
|
elif k == "Pattern":
|
||||||
|
self._filters[k] = Variant("s", v)
|
||||||
|
else:
|
||||||
|
logger.warning("Filter '%s' is not currently supported.", k)
|
||||||
|
|
||||||
|
# Helper methods
|
||||||
|
|
||||||
|
def _handle_advertising_data(self, path: str, props: Device1) -> None:
|
||||||
|
"""
|
||||||
|
Handles advertising data received from the BlueZ manager instance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: The D-Bus object path of the device.
|
||||||
|
props: The D-Bus object properties of the device.
|
||||||
|
"""
|
||||||
|
_service_uuids = props.get("UUIDs", [])
|
||||||
|
|
||||||
|
if not self.is_allowed_uuid(_service_uuids):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get all the information wanted to pack in the advertisement data
|
||||||
|
_local_name = props.get("Name")
|
||||||
|
_manufacturer_data = {
|
||||||
|
k: bytes(v) for k, v in props.get("ManufacturerData", {}).items()
|
||||||
|
}
|
||||||
|
_service_data = {k: bytes(v) for k, v in props.get("ServiceData", {}).items()}
|
||||||
|
|
||||||
|
# Get tx power data
|
||||||
|
tx_power = props.get("TxPower")
|
||||||
|
|
||||||
|
# Pack the advertisement data
|
||||||
|
advertisement_data = AdvertisementData(
|
||||||
|
local_name=_local_name,
|
||||||
|
manufacturer_data=_manufacturer_data,
|
||||||
|
service_data=_service_data,
|
||||||
|
service_uuids=_service_uuids,
|
||||||
|
tx_power=tx_power,
|
||||||
|
rssi=props.get("RSSI", -127),
|
||||||
|
platform_data=(path, props),
|
||||||
|
)
|
||||||
|
|
||||||
|
device = self.create_or_update_device(
|
||||||
|
path,
|
||||||
|
props["Address"],
|
||||||
|
# BlueZ generates a name based on the address if no name is available.
|
||||||
|
# To match other backends, we replace this with None.
|
||||||
|
(
|
||||||
|
None
|
||||||
|
if props["Alias"] == props["Address"].replace(":", "-")
|
||||||
|
else props["Alias"]
|
||||||
|
),
|
||||||
|
{"path": path, "props": props},
|
||||||
|
advertisement_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.call_detection_callbacks(device, advertisement_data)
|
||||||
|
|
||||||
|
def _handle_device_removed(self, device_path: str) -> None:
|
||||||
|
"""
|
||||||
|
Handles a device being removed from BlueZ.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
del self.seen_devices[device_path]
|
||||||
|
except KeyError:
|
||||||
|
# The device will not have been added to self.seen_devices if no
|
||||||
|
# advertising data was received, so this is expected to happen
|
||||||
|
# occasionally.
|
||||||
|
pass
|
||||||
@ -0,0 +1,212 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
if sys.platform != "linux":
|
||||||
|
assert False, "This backend is only available on Linux"
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from dbus_fast.aio.message_bus import MessageBus
|
||||||
|
from dbus_fast.errors import InvalidObjectPathError
|
||||||
|
from dbus_fast.message import Message
|
||||||
|
from dbus_fast.validators import (
|
||||||
|
assert_interface_name_valid,
|
||||||
|
assert_member_name_valid,
|
||||||
|
assert_object_path_valid,
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: this stuff should be improved and submitted upstream to dbus-next
|
||||||
|
# https://github.com/altdesktop/python-dbus-next/issues/53
|
||||||
|
|
||||||
|
_message_types = ["signal", "method_call", "method_return", "error"]
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidMessageTypeError(TypeError):
|
||||||
|
def __init__(self, type: str):
|
||||||
|
super().__init__(f"invalid message type: {type}")
|
||||||
|
|
||||||
|
|
||||||
|
def is_message_type_valid(type: str) -> bool:
|
||||||
|
"""Whether this is a valid message type.
|
||||||
|
|
||||||
|
.. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-bus-routing-match-rules
|
||||||
|
|
||||||
|
:param type: The message type to validate.
|
||||||
|
:type name: str
|
||||||
|
|
||||||
|
:returns: Whether the name is a valid message type.
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
|
return type in _message_types
|
||||||
|
|
||||||
|
|
||||||
|
def assert_bus_name_valid(type: str) -> None:
|
||||||
|
"""Raise an error if this is not a valid message type.
|
||||||
|
|
||||||
|
.. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-bus-routing-match-rules
|
||||||
|
|
||||||
|
:param type: The message type to validate.
|
||||||
|
:type name: str
|
||||||
|
|
||||||
|
:raises:
|
||||||
|
- :class:`InvalidMessageTypeError` - If this is not a valid message type.
|
||||||
|
"""
|
||||||
|
if not is_message_type_valid(type):
|
||||||
|
raise InvalidMessageTypeError(type)
|
||||||
|
|
||||||
|
|
||||||
|
class MatchRules:
|
||||||
|
"""D-Bus signal match rules.
|
||||||
|
|
||||||
|
.. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-bus-routing-match-rules
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
type: str = "signal",
|
||||||
|
sender: Optional[str] = None,
|
||||||
|
interface: Optional[str] = None,
|
||||||
|
member: Optional[str] = None,
|
||||||
|
path: Optional[str] = None,
|
||||||
|
path_namespace: Optional[str] = None,
|
||||||
|
destination: Optional[str] = None,
|
||||||
|
arg0namespace: Optional[str] = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
):
|
||||||
|
assert_bus_name_valid(type)
|
||||||
|
self.type: str = type
|
||||||
|
|
||||||
|
if sender:
|
||||||
|
assert_bus_name_valid(sender)
|
||||||
|
self.sender: Optional[str] = sender
|
||||||
|
else:
|
||||||
|
self.sender = None
|
||||||
|
|
||||||
|
if interface:
|
||||||
|
assert_interface_name_valid(interface)
|
||||||
|
self.interface: Optional[str] = interface
|
||||||
|
else:
|
||||||
|
self.interface = None
|
||||||
|
|
||||||
|
if member:
|
||||||
|
assert_member_name_valid(member)
|
||||||
|
self.member: Optional[str] = member
|
||||||
|
else:
|
||||||
|
self.member = None
|
||||||
|
|
||||||
|
if path:
|
||||||
|
assert_object_path_valid(path)
|
||||||
|
self.path: Optional[str] = path
|
||||||
|
else:
|
||||||
|
self.path = None
|
||||||
|
|
||||||
|
if path_namespace:
|
||||||
|
assert_object_path_valid(path_namespace)
|
||||||
|
self.path_namespace: Optional[str] = path_namespace
|
||||||
|
else:
|
||||||
|
self.path_namespace = None
|
||||||
|
|
||||||
|
if path and path_namespace:
|
||||||
|
raise TypeError(
|
||||||
|
"message rules cannot have both 'path' and 'path_namespace' at the same time"
|
||||||
|
)
|
||||||
|
|
||||||
|
if destination:
|
||||||
|
assert_bus_name_valid(destination)
|
||||||
|
self.destination: Optional[str] = destination
|
||||||
|
else:
|
||||||
|
self.destination = None
|
||||||
|
|
||||||
|
if arg0namespace:
|
||||||
|
assert_bus_name_valid(arg0namespace)
|
||||||
|
self.arg0namespace: Optional[str] = arg0namespace
|
||||||
|
else:
|
||||||
|
self.arg0namespace = None
|
||||||
|
|
||||||
|
if kwargs:
|
||||||
|
for k, v in kwargs.items():
|
||||||
|
if re.match(r"^arg\d+$", k):
|
||||||
|
if not isinstance(v, str):
|
||||||
|
raise TypeError(f"kwarg '{k}' must have a str value")
|
||||||
|
elif re.match(r"^arg\d+path$", k):
|
||||||
|
if not isinstance(v, str):
|
||||||
|
raise InvalidObjectPathError(v)
|
||||||
|
assert_object_path_valid(v[:-1] if v.endswith("/") else v)
|
||||||
|
else:
|
||||||
|
raise ValueError("kwargs must be in the form 'arg0' or 'arg0path'")
|
||||||
|
self.args: Optional[dict[str, str]] = kwargs
|
||||||
|
else:
|
||||||
|
self.args = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse(rules: str) -> MatchRules:
|
||||||
|
return MatchRules(**dict(r.split("=") for r in rules.split(",")))
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
rules = [f"type={self.type}"]
|
||||||
|
|
||||||
|
if self.sender:
|
||||||
|
rules.append(f"sender={self.sender}")
|
||||||
|
|
||||||
|
if self.interface:
|
||||||
|
rules.append(f"interface={self.interface}")
|
||||||
|
|
||||||
|
if self.member:
|
||||||
|
rules.append(f"member={self.member}")
|
||||||
|
|
||||||
|
if self.path:
|
||||||
|
rules.append(f"path={self.path}")
|
||||||
|
|
||||||
|
if self.path_namespace:
|
||||||
|
rules.append(f"path_namespace={self.path_namespace}")
|
||||||
|
|
||||||
|
if self.destination:
|
||||||
|
rules.append(f"destination={self.destination}")
|
||||||
|
|
||||||
|
if self.args:
|
||||||
|
for k, v in self.args.items():
|
||||||
|
rules.append(f"{k}={v}")
|
||||||
|
|
||||||
|
if self.arg0namespace:
|
||||||
|
rules.append(f"arg0namespace={self.arg0namespace}")
|
||||||
|
|
||||||
|
return ",".join(rules)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"MatchRules({self})"
|
||||||
|
|
||||||
|
|
||||||
|
async def add_match(bus: MessageBus, rules: MatchRules) -> Message:
|
||||||
|
"""Calls org.freedesktop.DBus.AddMatch using ``rules``."""
|
||||||
|
reply = await bus.call(
|
||||||
|
Message(
|
||||||
|
destination="org.freedesktop.DBus",
|
||||||
|
interface="org.freedesktop.DBus",
|
||||||
|
path="/org/freedesktop/DBus",
|
||||||
|
member="AddMatch",
|
||||||
|
signature="s",
|
||||||
|
body=[str(rules)],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return reply
|
||||||
|
|
||||||
|
|
||||||
|
async def remove_match(bus: MessageBus, rules: MatchRules) -> Message:
|
||||||
|
"""Calls org.freedesktop.DBus.RemoveMatch using ``rules``."""
|
||||||
|
reply = await bus.call(
|
||||||
|
Message(
|
||||||
|
destination="org.freedesktop.DBus",
|
||||||
|
interface="org.freedesktop.DBus",
|
||||||
|
path="/org/freedesktop/DBus",
|
||||||
|
member="RemoveMatch",
|
||||||
|
signature="s",
|
||||||
|
body=[str(rules)],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return reply
|
||||||
@ -0,0 +1,147 @@
|
|||||||
|
import sys
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
if sys.platform != "linux":
|
||||||
|
assert False, "This backend is only available on Linux"
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from dbus_fast.auth import AuthExternal
|
||||||
|
from dbus_fast.constants import MessageType
|
||||||
|
from dbus_fast.message import Message
|
||||||
|
|
||||||
|
from bleak.backends.bluezdbus import defs
|
||||||
|
from bleak.exc import (
|
||||||
|
BleakDBusError,
|
||||||
|
BleakError,
|
||||||
|
BleakGATTProtocolError,
|
||||||
|
BleakGATTProtocolErrorCode,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_reply(reply: Message) -> None:
|
||||||
|
"""Checks that a D-Bus message is a valid reply.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
BleakDBusError: if the message type is ``MessageType.ERROR``
|
||||||
|
AssertionError: if the message type is not ``MessageType.METHOD_RETURN``
|
||||||
|
"""
|
||||||
|
if reply.message_type == MessageType.ERROR:
|
||||||
|
assert reply.error_name
|
||||||
|
raise BleakDBusError(reply.error_name, reply.body)
|
||||||
|
assert reply.message_type == MessageType.METHOD_RETURN
|
||||||
|
|
||||||
|
|
||||||
|
def assert_gatt_reply(reply: Message, start_notify: bool = False) -> None:
|
||||||
|
"""
|
||||||
|
Checks that a D-Bus message is a valid reply.
|
||||||
|
|
||||||
|
Like :func:`assert_reply`, but has special handling for GATT protocol errors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reply: The D-Bus message to check.
|
||||||
|
start_notify: Whether this reply is for a StartNotify call.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
BleakGATTProtocolError: for specific GATT protocol errors.
|
||||||
|
BleakDBusError: if the message type is ``MessageType.ERROR``
|
||||||
|
AssertionError: if the message type is not ``MessageType.METHOD_RETURN``
|
||||||
|
"""
|
||||||
|
|
||||||
|
# BlueZ has specific errors for some GATT protocol errors, so we
|
||||||
|
# have to turn them back into the generic BleakGATTProtocolError
|
||||||
|
# with the correct code. See create_gatt_dbus_error() in BlueZ source.
|
||||||
|
|
||||||
|
if reply.error_name == defs.BLUEZ_ERROR_NOT_PERMITTED:
|
||||||
|
# Same error is used for both read and write not permitted, so we have
|
||||||
|
# to use the message to discriminate.
|
||||||
|
if reply.body and reply.body[0] == "Read not permitted":
|
||||||
|
raise BleakGATTProtocolError(BleakGATTProtocolErrorCode.READ_NOT_PERMITTED)
|
||||||
|
|
||||||
|
if reply.body and reply.body[0] == "Write not permitted":
|
||||||
|
raise BleakGATTProtocolError(BleakGATTProtocolErrorCode.WRITE_NOT_PERMITTED)
|
||||||
|
|
||||||
|
# REVISIT: could also be "Not paired" which could be any of:
|
||||||
|
# INSUFFICIENT_AUTHENTICATION, INSUFFICIENT_ENCRYPTION, or
|
||||||
|
# INSUFFICIENT_ENCRYPTION_KEY_SIZE
|
||||||
|
|
||||||
|
# "StartNotify" will return BLUEZ_ERROR_NOT_SUPPORTED if the characteristic
|
||||||
|
# does not support notifications before even trying, so it is not a GATT
|
||||||
|
# error in this case.
|
||||||
|
if not start_notify and reply.error_name == defs.BLUEZ_ERROR_NOT_SUPPORTED:
|
||||||
|
raise BleakGATTProtocolError(BleakGATTProtocolErrorCode.REQUEST_NOT_SUPPORTED)
|
||||||
|
|
||||||
|
if reply.error_name == defs.BLUEZ_ERROR_NOT_AUTHORIZED:
|
||||||
|
raise BleakGATTProtocolError(
|
||||||
|
BleakGATTProtocolErrorCode.INSUFFICIENT_AUTHORIZATION
|
||||||
|
)
|
||||||
|
|
||||||
|
if reply.error_name == defs.BLUEZ_ERROR_INVALID_ARGUMENT:
|
||||||
|
if reply.body and reply.body[0] == "Invalid offset":
|
||||||
|
raise BleakGATTProtocolError(BleakGATTProtocolErrorCode.INVALID_OFFSET)
|
||||||
|
|
||||||
|
if reply.body and reply.body[0] == "Invalid Length":
|
||||||
|
raise BleakGATTProtocolError(
|
||||||
|
BleakGATTProtocolErrorCode.INVALID_ATTRIBUTE_VALUE_LENGTH
|
||||||
|
)
|
||||||
|
|
||||||
|
if reply.error_name == defs.BLUEZ_ERROR_IMPROPERLY_CONFIGURED:
|
||||||
|
raise BleakGATTProtocolError(
|
||||||
|
BleakGATTProtocolErrorCode.CCCD_IMPROPERLY_CONFIGURED
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
reply.error_name == defs.BLUEZ_ERROR_FAILED
|
||||||
|
and reply.body
|
||||||
|
and (
|
||||||
|
# Unfortunately, BlueZ makes us scrape the string to get the
|
||||||
|
# error code in this case.
|
||||||
|
match := re.match(
|
||||||
|
r"^Operation failed with ATT error: (0x[0-9a-fA-F]+)",
|
||||||
|
reply.body[0],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
raise BleakGATTProtocolError(
|
||||||
|
BleakGATTProtocolErrorCode(int(match.group(1), 16))
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_reply(reply)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_service_handle_from_path(path: str) -> int:
|
||||||
|
try:
|
||||||
|
return int(path[-4:], 16)
|
||||||
|
except Exception as e:
|
||||||
|
raise BleakError(f"Could not parse service handle from path: {path}") from e
|
||||||
|
|
||||||
|
|
||||||
|
def device_path_from_characteristic_path(characteristic_path: str) -> str:
|
||||||
|
"""
|
||||||
|
Scrape the device path from a D-Bus characteristic path.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
characteristic_path: The D-Bus object path of the characteristic.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A D-Bus object path of the device.
|
||||||
|
"""
|
||||||
|
# /org/bluez/hci1/dev_FA_23_9D_AA_45_46/service000c/char000d
|
||||||
|
return characteristic_path[:-21]
|
||||||
|
|
||||||
|
|
||||||
|
def get_dbus_authenticator() -> Optional[AuthExternal]:
|
||||||
|
uid = None
|
||||||
|
try:
|
||||||
|
uid = int(os.environ.get("BLEAK_DBUS_AUTH_UID", ""))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
auth = None
|
||||||
|
if uid is not None:
|
||||||
|
auth = AuthExternal(uid=uid)
|
||||||
|
|
||||||
|
return auth
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
import sys
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
if sys.platform != "linux":
|
||||||
|
assert False, "This backend is only available on Linux"
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import contextlib
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_bluetoothctl_version() -> Optional[re.Match[bytes]]:
|
||||||
|
"""Get the version of bluetoothctl."""
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
"bluetoothctl", "--version", stdout=asyncio.subprocess.PIPE
|
||||||
|
)
|
||||||
|
assert proc.stdout
|
||||||
|
out = await proc.stdout.read()
|
||||||
|
version = re.search(b"(\\d+).(\\d+)", out.strip(b"'"))
|
||||||
|
await proc.wait()
|
||||||
|
return version
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class BlueZFeatures:
|
||||||
|
"""Check which features are supported by the BlueZ backend."""
|
||||||
|
|
||||||
|
checked_bluez_version = False
|
||||||
|
supported_version = True
|
||||||
|
_check_bluez_event: Optional[asyncio.Event] = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def check_bluez_version(cls) -> None:
|
||||||
|
"""Check the bluez version."""
|
||||||
|
if cls._check_bluez_event:
|
||||||
|
# If there is already a check in progress
|
||||||
|
# it wins, wait for it instead
|
||||||
|
await cls._check_bluez_event.wait()
|
||||||
|
return
|
||||||
|
cls._check_bluez_event = asyncio.Event()
|
||||||
|
version_output = await _get_bluetoothctl_version()
|
||||||
|
if version_output:
|
||||||
|
major, minor = tuple(map(int, version_output.groups()))
|
||||||
|
cls.supported_version = major == 5 and minor >= 55
|
||||||
|
else:
|
||||||
|
# Its possible they may be running inside a container where
|
||||||
|
# bluetoothctl is not available and they only have access to the
|
||||||
|
# BlueZ D-Bus API.
|
||||||
|
logger.warning(
|
||||||
|
"Could not determine BlueZ version, bluetoothctl not available, assuming 5.55+"
|
||||||
|
)
|
||||||
|
|
||||||
|
cls._check_bluez_event.set()
|
||||||
|
cls.checked_bluez_version = True
|
||||||
@ -0,0 +1,157 @@
|
|||||||
|
# Created on 2019-03-19 by hbldh <henrik.blidh@nedomkull.com>
|
||||||
|
"""
|
||||||
|
Interface class for the Bleak representation of a GATT Characteristic
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import enum
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import TYPE_CHECKING, Any, Union
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from bleak.assigned_numbers import CharacteristicPropertyName
|
||||||
|
from bleak.backends.descriptor import BleakGATTDescriptor
|
||||||
|
from bleak.uuids import normalize_uuid_str, uuidstr_to_str
|
||||||
|
|
||||||
|
# to prevent circular import
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from bleak.backends.service import BleakGATTService
|
||||||
|
|
||||||
|
|
||||||
|
class GattCharacteristicsFlags(enum.Enum):
|
||||||
|
broadcast = 0x0001
|
||||||
|
read = 0x0002
|
||||||
|
write_without_response = 0x0004
|
||||||
|
write = 0x0008
|
||||||
|
notify = 0x0010
|
||||||
|
indicate = 0x0020
|
||||||
|
authenticated_signed_writes = 0x0040
|
||||||
|
extended_properties = 0x0080
|
||||||
|
reliable_write = 0x0100
|
||||||
|
writable_auxiliaries = 0x0200
|
||||||
|
|
||||||
|
|
||||||
|
class BleakGATTCharacteristic:
|
||||||
|
"""The Bleak representation of a GATT Characteristic"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
obj: Any,
|
||||||
|
handle: int,
|
||||||
|
uuid: str,
|
||||||
|
properties: list[CharacteristicPropertyName],
|
||||||
|
max_write_without_response_size: Callable[[], int],
|
||||||
|
service: BleakGATTService,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
obj:
|
||||||
|
A platform-specific object for this characteristic.
|
||||||
|
max_write_without_response_size:
|
||||||
|
The maximum size in bytes that can be written to the
|
||||||
|
characteristic in a single write without response command.
|
||||||
|
service:
|
||||||
|
The service this characteristic belongs to.
|
||||||
|
"""
|
||||||
|
self.obj = obj
|
||||||
|
self._handle = handle
|
||||||
|
self._uuid = uuid
|
||||||
|
self._properties = properties
|
||||||
|
self._max_write_without_response_size = max_write_without_response_size
|
||||||
|
self._service = service
|
||||||
|
self._descriptors: dict[int, BleakGATTDescriptor] = {}
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.uuid} (Handle: {self.handle}): {self.description}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def service_uuid(self) -> str:
|
||||||
|
"""The UUID of the Service containing this characteristic"""
|
||||||
|
return self._service.uuid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def service_handle(self) -> int:
|
||||||
|
"""The integer handle of the Service containing this characteristic"""
|
||||||
|
return self._service.handle
|
||||||
|
|
||||||
|
@property
|
||||||
|
def handle(self) -> int:
|
||||||
|
"""The handle for this characteristic"""
|
||||||
|
return self._handle
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uuid(self) -> str:
|
||||||
|
"""The UUID for this characteristic"""
|
||||||
|
return self._uuid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self) -> str:
|
||||||
|
"""Description for this characteristic"""
|
||||||
|
return uuidstr_to_str(self.uuid)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def properties(self) -> list[CharacteristicPropertyName]:
|
||||||
|
"""Properties of this characteristic"""
|
||||||
|
return self._properties
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_write_without_response_size(self) -> int:
|
||||||
|
"""
|
||||||
|
Gets the maximum size in bytes that can be used for the *data* argument
|
||||||
|
of :meth:`BleakClient.write_gatt_char()` when ``response=False``.
|
||||||
|
|
||||||
|
In rare cases, a device may take a long time to update this value, so
|
||||||
|
reading this property may return the default value of ``20`` and reading
|
||||||
|
it again after a some time may return the expected higher value.
|
||||||
|
|
||||||
|
If you *really* need to wait for a higher value, you can do something
|
||||||
|
like this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async with asyncio.timeout(10):
|
||||||
|
while char.max_write_without_response_size == 20:
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
.. warning:: Linux quirk: For BlueZ versions < 5.62, this property
|
||||||
|
will always return ``20``.
|
||||||
|
|
||||||
|
.. versionadded:: 0.16
|
||||||
|
"""
|
||||||
|
|
||||||
|
# for backwards compatibility
|
||||||
|
if isinstance(self._max_write_without_response_size, int):
|
||||||
|
return self._max_write_without_response_size
|
||||||
|
|
||||||
|
return self._max_write_without_response_size()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def descriptors(self) -> list[BleakGATTDescriptor]:
|
||||||
|
"""List of descriptors for this service"""
|
||||||
|
return list(self._descriptors.values())
|
||||||
|
|
||||||
|
def get_descriptor(
|
||||||
|
self, specifier: Union[int, str, UUID]
|
||||||
|
) -> Union[BleakGATTDescriptor, None]:
|
||||||
|
"""Get a descriptor by handle (int) or UUID (str or uuid.UUID)"""
|
||||||
|
if isinstance(specifier, int):
|
||||||
|
return self._descriptors.get(specifier)
|
||||||
|
|
||||||
|
uuid = normalize_uuid_str(str(specifier))
|
||||||
|
for descriptor in self._descriptors.values():
|
||||||
|
if descriptor.uuid == uuid:
|
||||||
|
return descriptor
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def add_descriptor(self, descriptor: BleakGATTDescriptor) -> None:
|
||||||
|
"""Add a :py:class:`~BleakGATTDescriptor` to the characteristic.
|
||||||
|
|
||||||
|
Should not be used by end user, but rather by `bleak` itself.
|
||||||
|
"""
|
||||||
|
if descriptor.handle in self._descriptors:
|
||||||
|
raise ValueError(
|
||||||
|
f"Descriptor with handle {descriptor.handle} already exists"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._descriptors[descriptor.handle] = descriptor
|
||||||
258
tools/python-embed/Lib/site-packages/bleak/backends/client.py
Normal file
258
tools/python-embed/Lib/site-packages/bleak/backends/client.py
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
# Created on 2018-04-23 by hbldh <henrik.blidh@nedomkull.com>
|
||||||
|
"""
|
||||||
|
Base class for backend clients.
|
||||||
|
"""
|
||||||
|
import abc
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
|
from bleak.args import SizedBuffer
|
||||||
|
from bleak.backends import BleakBackend, get_default_backend
|
||||||
|
from bleak.backends.characteristic import BleakGATTCharacteristic
|
||||||
|
from bleak.backends.descriptor import BleakGATTDescriptor
|
||||||
|
from bleak.backends.device import BLEDevice
|
||||||
|
from bleak.backends.service import BleakGATTServiceCollection
|
||||||
|
from bleak.exc import BleakError
|
||||||
|
|
||||||
|
NotifyCallback = Callable[[bytearray], None]
|
||||||
|
|
||||||
|
|
||||||
|
class BaseBleakClient(abc.ABC):
|
||||||
|
"""The Client Interface for Bleak Backend implementations to implement.
|
||||||
|
|
||||||
|
The documentation of this interface should thus be safe to use as a reference for your implementation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
address_or_ble_device (`BLEDevice` or str): The Bluetooth address of the BLE peripheral to connect to or the `BLEDevice` object representing it.
|
||||||
|
|
||||||
|
Keyword Args:
|
||||||
|
timeout (float): Timeout for required ``discover`` call.
|
||||||
|
disconnected_callback (callable): Callback that will be scheduled in the
|
||||||
|
event loop when the client is disconnected. The callable must take one
|
||||||
|
argument, which will be this client object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs: Any):
|
||||||
|
if isinstance(address_or_ble_device, BLEDevice):
|
||||||
|
self.address = address_or_ble_device.address
|
||||||
|
else:
|
||||||
|
self.address = address_or_ble_device
|
||||||
|
|
||||||
|
self.services: Optional[BleakGATTServiceCollection] = None
|
||||||
|
|
||||||
|
self._timeout = kwargs["timeout"]
|
||||||
|
self._disconnected_callback: Optional[Callable[[], None]] = kwargs.get(
|
||||||
|
"disconnected_callback"
|
||||||
|
)
|
||||||
|
|
||||||
|
# NB: this is not marked as @abc.abstractmethod because that would break
|
||||||
|
# 3rd-party backends. We might change this in the future to make it required.
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""See :meth:`bleak.BleakClient.name`."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abc.abstractmethod
|
||||||
|
def mtu_size(self) -> int:
|
||||||
|
"""Gets the negotiated MTU."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
# Connectivity methods
|
||||||
|
|
||||||
|
def set_disconnected_callback(
|
||||||
|
self, callback: Optional[Callable[[], None]], **kwargs: Any
|
||||||
|
) -> None:
|
||||||
|
"""Set the disconnect callback.
|
||||||
|
The callback will only be called on unsolicited disconnect event.
|
||||||
|
|
||||||
|
Set the callback to ``None`` to remove any existing callback.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
callback: callback to be called on disconnection.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._disconnected_callback = callback
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def connect(self, pair: bool, **kwargs: Any) -> None:
|
||||||
|
"""Connect to the specified GATT server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pair (bool): If the client should attempt to pair with the
|
||||||
|
peripheral before connecting if it is not already paired.
|
||||||
|
|
||||||
|
Backends that can't implement this should make an appropriate
|
||||||
|
log message and ignore the parameter.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def disconnect(self) -> None:
|
||||||
|
"""Disconnect from the specified GATT server."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def pair(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
"""Pair with the peripheral."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def unpair(self) -> None:
|
||||||
|
"""Unpair with the peripheral."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abc.abstractmethod
|
||||||
|
def is_connected(self) -> bool:
|
||||||
|
"""Check connection status between this client and the server.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Boolean representing connection status.
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
# I/O methods
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def read_gatt_char(
|
||||||
|
self,
|
||||||
|
characteristic: BleakGATTCharacteristic,
|
||||||
|
*,
|
||||||
|
use_cached: bool = False,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> bytearray:
|
||||||
|
"""Perform read operation on the specified GATT characteristic.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
characteristic (BleakGATTCharacteristic): The characteristic to read from.
|
||||||
|
use_cached: If True, read the cached value instead of performing a read from the device.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(bytearray) The read data.
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def read_gatt_descriptor(
|
||||||
|
self,
|
||||||
|
descriptor: BleakGATTDescriptor,
|
||||||
|
*,
|
||||||
|
use_cached: bool = False,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> bytearray:
|
||||||
|
"""Perform read operation on the specified GATT descriptor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
descriptor: The descriptor to read from.
|
||||||
|
use_cached: If True, read the cached value instead of performing a read from the device.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The read data.
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def write_gatt_char(
|
||||||
|
self, characteristic: BleakGATTCharacteristic, data: SizedBuffer, response: bool
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Perform a write operation on the specified GATT characteristic.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
characteristic: The characteristic to write to.
|
||||||
|
data: The data to send.
|
||||||
|
response: If write-with-response operation should be done.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def write_gatt_descriptor(
|
||||||
|
self, descriptor: BleakGATTDescriptor, data: SizedBuffer
|
||||||
|
) -> None:
|
||||||
|
"""Perform a write operation on the specified GATT descriptor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
descriptor: The descriptor to read from.
|
||||||
|
data: The data to send (any bytes-like object).
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def start_notify(
|
||||||
|
self,
|
||||||
|
characteristic: BleakGATTCharacteristic,
|
||||||
|
callback: NotifyCallback,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Activate notifications/indications on a characteristic.
|
||||||
|
|
||||||
|
Implementers should call the OS function to enable notifications or
|
||||||
|
indications on the characteristic.
|
||||||
|
|
||||||
|
To keep things the same cross-platform, notifications should be preferred
|
||||||
|
over indications if possible when a characteristic supports both.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def stop_notify(self, characteristic: BleakGATTCharacteristic) -> None:
|
||||||
|
"""Deactivate notification/indication on a specified characteristic.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
characteristic (BleakGATTCharacteristic): The characteristic to deactivate
|
||||||
|
notification/indication on.
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
def get_platform_client_backend_type() -> tuple[type[BaseBleakClient], BleakBackend]:
|
||||||
|
"""
|
||||||
|
Gets the platform-specific :class:`BaseBleakClient` type.
|
||||||
|
"""
|
||||||
|
backend = get_default_backend()
|
||||||
|
match backend:
|
||||||
|
case BleakBackend.P4ANDROID:
|
||||||
|
from bleak.backends.p4android.client import (
|
||||||
|
BleakClientP4Android, # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
return (BleakClientP4Android, backend) # type: ignore
|
||||||
|
|
||||||
|
case BleakBackend.BLUEZ_DBUS:
|
||||||
|
from bleak.backends.bluezdbus.client import (
|
||||||
|
BleakClientBlueZDBus, # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
return (BleakClientBlueZDBus, backend) # type: ignore
|
||||||
|
|
||||||
|
case BleakBackend.PYTHONISTA_CB:
|
||||||
|
try:
|
||||||
|
from bleak_pythonista import BleakClientPythonistaCB # type: ignore
|
||||||
|
|
||||||
|
return (BleakClientPythonistaCB, backend) # type: ignore
|
||||||
|
except ImportError as e:
|
||||||
|
raise ImportError(
|
||||||
|
"Ensure you have `bleak-pythonista` package installed."
|
||||||
|
) from e
|
||||||
|
|
||||||
|
case BleakBackend.CORE_BLUETOOTH:
|
||||||
|
from bleak.backends.corebluetooth.client import (
|
||||||
|
BleakClientCoreBluetooth, # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
return (BleakClientCoreBluetooth, backend) # type: ignore
|
||||||
|
|
||||||
|
case BleakBackend.WIN_RT:
|
||||||
|
from bleak.backends.winrt.client import BleakClientWinRT # type: ignore
|
||||||
|
|
||||||
|
return (BleakClientWinRT, backend) # type: ignore
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise BleakError(f"Unsupported backend: {backend}")
|
||||||
@ -0,0 +1,402 @@
|
|||||||
|
# Created on June, 25 2019 by kevincar <kevincarrolldavis@gmail.com>
|
||||||
|
"""
|
||||||
|
CentralManagerDelegate will implement the CBCentralManagerDelegate protocol to
|
||||||
|
manage CoreBluetooth services and resources on the Central End
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import weakref
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
if sys.platform != "darwin":
|
||||||
|
assert False, "This backend is only available on macOS"
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Any, Optional, TypedDict, cast
|
||||||
|
|
||||||
|
import objc
|
||||||
|
from CoreBluetooth import (
|
||||||
|
CBUUID,
|
||||||
|
CBCentralManager,
|
||||||
|
CBManagerAuthorizationDenied,
|
||||||
|
CBManagerAuthorizationRestricted,
|
||||||
|
CBManagerStatePoweredOff,
|
||||||
|
CBManagerStatePoweredOn,
|
||||||
|
CBManagerStateResetting,
|
||||||
|
CBManagerStateUnauthorized,
|
||||||
|
CBManagerStateUnsupported,
|
||||||
|
CBPeripheral,
|
||||||
|
)
|
||||||
|
from Foundation import (
|
||||||
|
NSUUID,
|
||||||
|
NSArray,
|
||||||
|
NSData,
|
||||||
|
NSDictionary,
|
||||||
|
NSError,
|
||||||
|
NSNumber,
|
||||||
|
NSObject,
|
||||||
|
NSString,
|
||||||
|
)
|
||||||
|
from libdispatch import DISPATCH_QUEUE_SERIAL, dispatch_queue_create
|
||||||
|
|
||||||
|
from bleak._compat import Self
|
||||||
|
from bleak._compat import timeout as async_timeout
|
||||||
|
from bleak.backends._utils import external_thread_callback, try_call_soon_threadsafe
|
||||||
|
from bleak.backends.corebluetooth.utils import cb_manager_state_message
|
||||||
|
from bleak.exc import (
|
||||||
|
BleakBluetoothNotAvailableError,
|
||||||
|
BleakBluetoothNotAvailableReason,
|
||||||
|
BleakError,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
CBCentralManagerDelegate = objc.protocolNamed("CBCentralManagerDelegate")
|
||||||
|
|
||||||
|
|
||||||
|
DisconnectCallback = Callable[[], None]
|
||||||
|
|
||||||
|
|
||||||
|
class CBAdvertisementData(TypedDict, total=False):
|
||||||
|
kCBAdvDataLocalName: NSString
|
||||||
|
kCBAdvDataManufacturerData: NSData
|
||||||
|
kCBAdvDataServiceData: dict[CBUUID, NSData]
|
||||||
|
kCBAdvDataServiceUUIDs: NSArray[CBUUID]
|
||||||
|
kCBAdvertisementDataOverflowServiceUUIDsKey: NSArray[CBUUID]
|
||||||
|
kCBAdvDataTxPowerLevel: NSNumber
|
||||||
|
kCBAdvertisementDataIsConnectable: NSNumber
|
||||||
|
kCBAdvDataOverflowServiceUUIDs: NSArray[CBUUID]
|
||||||
|
|
||||||
|
|
||||||
|
class ObjcCentralManagerDelegate(NSObject, protocols=[CBCentralManagerDelegate]):
|
||||||
|
"""
|
||||||
|
CoreBluetooth central manager delegate for bridging callbacks to asyncio.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def initWithPyDelegate_(
|
||||||
|
self, py_delegate: weakref.ReferenceType["CentralManagerDelegate"]
|
||||||
|
) -> Optional[Self]:
|
||||||
|
"""macOS init function for NSObject"""
|
||||||
|
self = objc.super(ObjcCentralManagerDelegate, self).init() # type: ignore[assignment]
|
||||||
|
|
||||||
|
if self is None:
|
||||||
|
return None # pragma: no cover
|
||||||
|
|
||||||
|
self.py_delegate = py_delegate
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
# Protocol Functions
|
||||||
|
|
||||||
|
@external_thread_callback
|
||||||
|
def centralManagerDidUpdateState_(self, centralManager: CBCentralManager) -> None:
|
||||||
|
logger.debug("centralManagerDidUpdateState_")
|
||||||
|
logger.debug(cb_manager_state_message(centralManager.state()))
|
||||||
|
|
||||||
|
if (py_delegate := self.py_delegate()) is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try_call_soon_threadsafe(
|
||||||
|
py_delegate.event_loop,
|
||||||
|
py_delegate.did_update_state_event.set,
|
||||||
|
)
|
||||||
|
|
||||||
|
@external_thread_callback
|
||||||
|
def centralManager_didDiscoverPeripheral_advertisementData_RSSI_(
|
||||||
|
self,
|
||||||
|
central: CBCentralManager,
|
||||||
|
peripheral: CBPeripheral,
|
||||||
|
advertisementData: NSDictionary[str, Any],
|
||||||
|
RSSI: NSNumber,
|
||||||
|
) -> None:
|
||||||
|
logger.debug("centralManager_didDiscoverPeripheral_advertisementData_RSSI_")
|
||||||
|
|
||||||
|
if (py_delegate := self.py_delegate()) is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try_call_soon_threadsafe(
|
||||||
|
py_delegate.event_loop,
|
||||||
|
py_delegate.did_discover_peripheral,
|
||||||
|
central,
|
||||||
|
peripheral,
|
||||||
|
advertisementData,
|
||||||
|
RSSI,
|
||||||
|
)
|
||||||
|
|
||||||
|
@external_thread_callback
|
||||||
|
def centralManager_didConnectPeripheral_(
|
||||||
|
self, central: CBCentralManager, peripheral: CBPeripheral
|
||||||
|
) -> None:
|
||||||
|
logger.debug("centralManager_didConnectPeripheral_")
|
||||||
|
|
||||||
|
if (py_delegate := self.py_delegate()) is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try_call_soon_threadsafe(
|
||||||
|
py_delegate.event_loop,
|
||||||
|
py_delegate.did_connect_peripheral,
|
||||||
|
central,
|
||||||
|
peripheral,
|
||||||
|
)
|
||||||
|
|
||||||
|
@external_thread_callback
|
||||||
|
def centralManager_didFailToConnectPeripheral_error_(
|
||||||
|
self,
|
||||||
|
centralManager: CBCentralManager,
|
||||||
|
peripheral: CBPeripheral,
|
||||||
|
error: Optional[NSError],
|
||||||
|
) -> None:
|
||||||
|
logger.debug("centralManager_didFailToConnectPeripheral_error_")
|
||||||
|
|
||||||
|
if (py_delegate := self.py_delegate()) is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try_call_soon_threadsafe(
|
||||||
|
py_delegate.event_loop,
|
||||||
|
py_delegate.did_fail_to_connect_peripheral,
|
||||||
|
centralManager,
|
||||||
|
peripheral,
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
|
||||||
|
@external_thread_callback
|
||||||
|
def centralManager_didDisconnectPeripheral_error_(
|
||||||
|
self,
|
||||||
|
central: CBCentralManager,
|
||||||
|
peripheral: CBPeripheral,
|
||||||
|
error: Optional[NSError],
|
||||||
|
) -> None:
|
||||||
|
logger.debug("centralManager_didDisconnectPeripheral_error_")
|
||||||
|
|
||||||
|
if (py_delegate := self.py_delegate()) is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try_call_soon_threadsafe(
|
||||||
|
py_delegate.event_loop,
|
||||||
|
py_delegate.did_disconnect_peripheral,
|
||||||
|
central,
|
||||||
|
peripheral,
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CentralManagerDelegate:
|
||||||
|
"""
|
||||||
|
macOS conforming python class for managing the CentralManger for BLE
|
||||||
|
|
||||||
|
Before this object can be used, the :method:`wait_until_ready` method has to
|
||||||
|
be called. This can take a while, when the OS asks the user for permissions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""macOS init function for NSObject"""
|
||||||
|
delegate = ObjcCentralManagerDelegate.alloc().initWithPyDelegate_(
|
||||||
|
weakref.ref(self)
|
||||||
|
)
|
||||||
|
assert delegate is not None
|
||||||
|
self.objc_delegate = delegate
|
||||||
|
|
||||||
|
self.event_loop = asyncio.get_running_loop()
|
||||||
|
self._connect_futures: dict[NSUUID, asyncio.Future[bool]] = {}
|
||||||
|
|
||||||
|
self.callbacks: dict[
|
||||||
|
int,
|
||||||
|
Callable[[CBPeripheral, CBAdvertisementData, NSNumber], None],
|
||||||
|
] = {}
|
||||||
|
self._disconnect_callbacks: dict[NSUUID, DisconnectCallback] = {}
|
||||||
|
self._disconnect_futures: dict[NSUUID, asyncio.Future[None]] = {}
|
||||||
|
|
||||||
|
self.did_update_state_event = asyncio.Event()
|
||||||
|
self.central_manager = CBCentralManager.alloc().initWithDelegate_queue_(
|
||||||
|
self.objc_delegate,
|
||||||
|
dispatch_queue_create(b"bleak.corebluetooth", DISPATCH_QUEUE_SERIAL),
|
||||||
|
)
|
||||||
|
|
||||||
|
# User defined functions
|
||||||
|
@objc.python_method
|
||||||
|
async def wait_until_ready(self):
|
||||||
|
# According to CoreBluetooth docs, it is not valid to call CBCentral
|
||||||
|
# methods until the centralManagerDidUpdateState_() delegate method
|
||||||
|
# is called and the current state is CBManagerStatePoweredOn.
|
||||||
|
# Wait until the callback occurs. This normally should not take too long,
|
||||||
|
# but if the app currently has no permission to access the Bluetooth peripheral,
|
||||||
|
# there is automatically a dialog shown by the OS. The user has to accept or deny
|
||||||
|
# the Bluetooth access. This may take infinite time until the user clicks something.
|
||||||
|
await self.did_update_state_event.wait()
|
||||||
|
|
||||||
|
state = self.central_manager.state()
|
||||||
|
if state == CBManagerStateUnsupported:
|
||||||
|
raise BleakBluetoothNotAvailableError(
|
||||||
|
"Bluetooth is unsupported",
|
||||||
|
BleakBluetoothNotAvailableReason.NO_BLUETOOTH,
|
||||||
|
)
|
||||||
|
elif state == CBManagerStateUnauthorized:
|
||||||
|
authorization = self.central_manager.authorization()
|
||||||
|
if authorization == CBManagerAuthorizationDenied:
|
||||||
|
raise BleakBluetoothNotAvailableError(
|
||||||
|
"Bluetooth access is denied by the user for the current application. Check macOS privacy settings.",
|
||||||
|
BleakBluetoothNotAvailableReason.DENIED_BY_USER,
|
||||||
|
)
|
||||||
|
elif authorization == CBManagerAuthorizationRestricted:
|
||||||
|
raise BleakBluetoothNotAvailableError(
|
||||||
|
"Bluetooth access is restricted for the current application, e.g. by parental controls. Ask the admin to remove this restriction.",
|
||||||
|
BleakBluetoothNotAvailableReason.DENIED_BY_SYSTEM,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise BleakBluetoothNotAvailableError(
|
||||||
|
"Bluetooth is not authorized for an unknown reason. Check macOS privacy settings.",
|
||||||
|
BleakBluetoothNotAvailableReason.DENIED_BY_UNKNOWN,
|
||||||
|
)
|
||||||
|
elif state == CBManagerStatePoweredOff:
|
||||||
|
raise BleakBluetoothNotAvailableError(
|
||||||
|
"Bluetooth device is turned off",
|
||||||
|
BleakBluetoothNotAvailableReason.POWERED_OFF,
|
||||||
|
)
|
||||||
|
elif state == CBManagerStateResetting:
|
||||||
|
raise BleakBluetoothNotAvailableError(
|
||||||
|
"Connection to the Bluetooth system service was lost. Currently trying to reconnect...",
|
||||||
|
BleakBluetoothNotAvailableReason.UNKNOWN,
|
||||||
|
)
|
||||||
|
elif state != CBManagerStatePoweredOn:
|
||||||
|
raise BleakBluetoothNotAvailableError(
|
||||||
|
"Bluetooth state is unknown",
|
||||||
|
BleakBluetoothNotAvailableReason.UNKNOWN,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def start_scan(self, service_uuids: Optional[list[str]]) -> None:
|
||||||
|
_service_uuids = (
|
||||||
|
NSArray[CBUUID]
|
||||||
|
.alloc()
|
||||||
|
.initWithArray_(list(map(CBUUID.UUIDWithString_, service_uuids)))
|
||||||
|
if service_uuids
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
self.central_manager.scanForPeripheralsWithServices_options_(
|
||||||
|
_service_uuids, None
|
||||||
|
)
|
||||||
|
|
||||||
|
async def stop_scan(self) -> None:
|
||||||
|
self.central_manager.stopScan()
|
||||||
|
|
||||||
|
async def connect(
|
||||||
|
self,
|
||||||
|
peripheral: CBPeripheral,
|
||||||
|
disconnect_callback: DisconnectCallback,
|
||||||
|
timeout: float = 10.0,
|
||||||
|
) -> None:
|
||||||
|
try:
|
||||||
|
self._disconnect_callbacks[peripheral.identifier()] = disconnect_callback
|
||||||
|
future = self.event_loop.create_future()
|
||||||
|
|
||||||
|
self._connect_futures[peripheral.identifier()] = future
|
||||||
|
try:
|
||||||
|
self.central_manager.connectPeripheral_options_(peripheral, None)
|
||||||
|
async with async_timeout(timeout):
|
||||||
|
await future
|
||||||
|
finally:
|
||||||
|
del self._connect_futures[peripheral.identifier()]
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logger.debug(f"Connection timed out after {timeout} seconds.")
|
||||||
|
del self._disconnect_callbacks[peripheral.identifier()]
|
||||||
|
future = self.event_loop.create_future()
|
||||||
|
|
||||||
|
self._disconnect_futures[peripheral.identifier()] = future
|
||||||
|
try:
|
||||||
|
self.central_manager.cancelPeripheralConnection_(peripheral)
|
||||||
|
await future
|
||||||
|
finally:
|
||||||
|
del self._disconnect_futures[peripheral.identifier()]
|
||||||
|
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def disconnect(self, peripheral: CBPeripheral) -> None:
|
||||||
|
future = self.event_loop.create_future()
|
||||||
|
|
||||||
|
self._disconnect_futures[peripheral.identifier()] = future
|
||||||
|
try:
|
||||||
|
self.central_manager.cancelPeripheralConnection_(peripheral)
|
||||||
|
await future
|
||||||
|
finally:
|
||||||
|
del self._disconnect_futures[peripheral.identifier()]
|
||||||
|
|
||||||
|
# Protocol Functions
|
||||||
|
|
||||||
|
def did_discover_peripheral(
|
||||||
|
self,
|
||||||
|
central: CBCentralManager,
|
||||||
|
peripheral: CBPeripheral,
|
||||||
|
advertisementData: NSDictionary[str, Any],
|
||||||
|
RSSI: NSNumber,
|
||||||
|
) -> None:
|
||||||
|
# Note: this function might be called several times for same device.
|
||||||
|
# This can happen for instance when an active scan is done, and the
|
||||||
|
# second call with contain the data from the BLE scan response.
|
||||||
|
# Example a first time with the following keys in advertisementData:
|
||||||
|
# ['kCBAdvDataLocalName', 'kCBAdvDataIsConnectable', 'kCBAdvDataChannel']
|
||||||
|
# ... and later a second time with other keys (and values) such as:
|
||||||
|
# ['kCBAdvDataServiceUUIDs', 'kCBAdvDataIsConnectable', 'kCBAdvDataChannel']
|
||||||
|
#
|
||||||
|
# i.e it is best not to trust advertisementData for later use and data
|
||||||
|
# from it should be copied.
|
||||||
|
#
|
||||||
|
# This behaviour could be affected by the
|
||||||
|
# CBCentralManagerScanOptionAllowDuplicatesKey global setting.
|
||||||
|
|
||||||
|
uuid_string = peripheral.identifier().UUIDString()
|
||||||
|
|
||||||
|
for callback in self.callbacks.values():
|
||||||
|
callback(peripheral, cast(CBAdvertisementData, advertisementData), RSSI)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"Discovered device %s: %s @ RSSI: %d (kCBAdvData %r) and Central: %r",
|
||||||
|
uuid_string,
|
||||||
|
peripheral.name(),
|
||||||
|
RSSI,
|
||||||
|
advertisementData.keys(),
|
||||||
|
central,
|
||||||
|
)
|
||||||
|
|
||||||
|
def did_connect_peripheral(
|
||||||
|
self, central: CBCentralManager, peripheral: CBPeripheral
|
||||||
|
) -> None:
|
||||||
|
future = self._connect_futures.get(peripheral.identifier(), None)
|
||||||
|
if future is not None:
|
||||||
|
future.set_result(True)
|
||||||
|
|
||||||
|
def did_fail_to_connect_peripheral(
|
||||||
|
self,
|
||||||
|
centralManager: CBCentralManager,
|
||||||
|
peripheral: CBPeripheral,
|
||||||
|
error: Optional[NSError],
|
||||||
|
) -> None:
|
||||||
|
future = self._connect_futures.get(peripheral.identifier(), None)
|
||||||
|
if future is not None:
|
||||||
|
if error is not None:
|
||||||
|
future.set_exception(BleakError(f"failed to connect: {error}"))
|
||||||
|
else:
|
||||||
|
future.set_result(False)
|
||||||
|
|
||||||
|
def did_disconnect_peripheral(
|
||||||
|
self,
|
||||||
|
central: CBCentralManager,
|
||||||
|
peripheral: CBPeripheral,
|
||||||
|
error: Optional[NSError],
|
||||||
|
) -> None:
|
||||||
|
logger.debug("Peripheral Device disconnected!")
|
||||||
|
|
||||||
|
future = self._disconnect_futures.get(peripheral.identifier(), None)
|
||||||
|
if future is not None:
|
||||||
|
if error is not None:
|
||||||
|
future.set_exception(BleakError(f"disconnect failed: {error}"))
|
||||||
|
else:
|
||||||
|
future.set_result(None)
|
||||||
|
|
||||||
|
callback = self._disconnect_callbacks.pop(peripheral.identifier(), None)
|
||||||
|
|
||||||
|
if callback is not None:
|
||||||
|
callback()
|
||||||
@ -0,0 +1,727 @@
|
|||||||
|
"""
|
||||||
|
|
||||||
|
PeripheralDelegate
|
||||||
|
|
||||||
|
Created by kevincar <kevincarrolldavis@gmail.com>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import weakref
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
if sys.platform != "darwin":
|
||||||
|
assert False, "This backend is only available on macOS"
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import itertools
|
||||||
|
import logging
|
||||||
|
from collections.abc import Iterable
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
import objc
|
||||||
|
from CoreBluetooth import (
|
||||||
|
CBUUID,
|
||||||
|
CBATTErrorDomain,
|
||||||
|
CBCharacteristic,
|
||||||
|
CBCharacteristicWriteType,
|
||||||
|
CBCharacteristicWriteWithResponse,
|
||||||
|
CBDescriptor,
|
||||||
|
CBPeripheral,
|
||||||
|
CBService,
|
||||||
|
)
|
||||||
|
from Foundation import NSUUID, NSArray, NSData, NSError, NSNumber, NSObject
|
||||||
|
|
||||||
|
from bleak._compat import Self
|
||||||
|
from bleak._compat import timeout as async_timeout
|
||||||
|
from bleak.args.corebluetooth import NotificationDiscriminator
|
||||||
|
from bleak.backends._utils import external_thread_callback, try_call_soon_threadsafe
|
||||||
|
from bleak.backends.client import NotifyCallback
|
||||||
|
from bleak.exc import BleakError, BleakGATTProtocolError
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
CBPeripheralDelegate = objc.protocolNamed("CBPeripheralDelegate")
|
||||||
|
|
||||||
|
|
||||||
|
class ObjcPeripheralDelegate(NSObject, protocols=[CBPeripheralDelegate]):
|
||||||
|
"""
|
||||||
|
CoreBluetooth peripheral manager delegate for bridging callbacks to asyncio.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def initWithPyDelegate_(
|
||||||
|
self, py_delegate: weakref.ReferenceType["PeripheralDelegate"]
|
||||||
|
) -> Optional[Self]:
|
||||||
|
"""macOS init function for NSObject"""
|
||||||
|
self = objc.super(ObjcPeripheralDelegate, self).init() # type: ignore[assignment]
|
||||||
|
|
||||||
|
if self is None:
|
||||||
|
return None # pragma: no cover
|
||||||
|
|
||||||
|
self.py_delegate = py_delegate
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
# Protocol Functions
|
||||||
|
|
||||||
|
@external_thread_callback
|
||||||
|
def peripheral_didDiscoverServices_(
|
||||||
|
self, peripheral: CBPeripheral, error: Optional[NSError]
|
||||||
|
) -> None:
|
||||||
|
logger.debug("peripheral_didDiscoverServices_")
|
||||||
|
|
||||||
|
if (py_delegate := self.py_delegate()) is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try_call_soon_threadsafe(
|
||||||
|
py_delegate.event_loop,
|
||||||
|
py_delegate.did_discover_services,
|
||||||
|
peripheral,
|
||||||
|
peripheral.services(),
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
|
||||||
|
@external_thread_callback
|
||||||
|
def peripheral_didDiscoverIncludedServicesForService_error_( # pragma: no cover
|
||||||
|
self, peripheral: CBPeripheral, service: CBService, error: Optional[NSError]
|
||||||
|
) -> None:
|
||||||
|
logger.debug("peripheral_didDiscoverIncludedServicesForService_error_")
|
||||||
|
# Currently not used in Bleak
|
||||||
|
|
||||||
|
@external_thread_callback
|
||||||
|
def peripheral_didDiscoverCharacteristicsForService_error_(
|
||||||
|
self, peripheral: CBPeripheral, service: CBService, error: Optional[NSError]
|
||||||
|
) -> None:
|
||||||
|
logger.debug("peripheral_didDiscoverCharacteristicsForService_error_")
|
||||||
|
|
||||||
|
if (py_delegate := self.py_delegate()) is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try_call_soon_threadsafe(
|
||||||
|
py_delegate.event_loop,
|
||||||
|
py_delegate.did_discover_characteristics_for_service,
|
||||||
|
peripheral,
|
||||||
|
service,
|
||||||
|
service.characteristics(),
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
|
||||||
|
@external_thread_callback
|
||||||
|
def peripheral_didDiscoverDescriptorsForCharacteristic_error_(
|
||||||
|
self,
|
||||||
|
peripheral: CBPeripheral,
|
||||||
|
characteristic: CBCharacteristic,
|
||||||
|
error: Optional[NSError],
|
||||||
|
) -> None:
|
||||||
|
logger.debug("peripheral_didDiscoverDescriptorsForCharacteristic_error_")
|
||||||
|
|
||||||
|
if (py_delegate := self.py_delegate()) is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try_call_soon_threadsafe(
|
||||||
|
py_delegate.event_loop,
|
||||||
|
py_delegate.did_discover_descriptors_for_characteristic,
|
||||||
|
peripheral,
|
||||||
|
characteristic,
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
|
||||||
|
@external_thread_callback
|
||||||
|
def peripheral_didUpdateValueForCharacteristic_error_(
|
||||||
|
self,
|
||||||
|
peripheral: CBPeripheral,
|
||||||
|
characteristic: CBCharacteristic,
|
||||||
|
error: Optional[NSError],
|
||||||
|
) -> None:
|
||||||
|
logger.debug("peripheral_didUpdateValueForCharacteristic_error_")
|
||||||
|
|
||||||
|
if (py_delegate := self.py_delegate()) is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try_call_soon_threadsafe(
|
||||||
|
py_delegate.event_loop,
|
||||||
|
py_delegate.did_update_value_for_characteristic,
|
||||||
|
peripheral,
|
||||||
|
characteristic,
|
||||||
|
characteristic.value(),
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
|
||||||
|
@external_thread_callback
|
||||||
|
def peripheral_didUpdateValueForDescriptor_error_(
|
||||||
|
self,
|
||||||
|
peripheral: CBPeripheral,
|
||||||
|
descriptor: CBDescriptor,
|
||||||
|
error: Optional[NSError],
|
||||||
|
) -> None:
|
||||||
|
logger.debug("peripheral_didUpdateValueForDescriptor_error_")
|
||||||
|
|
||||||
|
if (py_delegate := self.py_delegate()) is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try_call_soon_threadsafe(
|
||||||
|
py_delegate.event_loop,
|
||||||
|
py_delegate.did_update_value_for_descriptor,
|
||||||
|
peripheral,
|
||||||
|
descriptor,
|
||||||
|
descriptor.value(),
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
|
||||||
|
@external_thread_callback
|
||||||
|
def peripheral_didWriteValueForCharacteristic_error_(
|
||||||
|
self,
|
||||||
|
peripheral: CBPeripheral,
|
||||||
|
characteristic: CBCharacteristic,
|
||||||
|
error: Optional[NSError],
|
||||||
|
) -> None:
|
||||||
|
logger.debug("peripheral_didWriteValueForCharacteristic_error_")
|
||||||
|
|
||||||
|
if (py_delegate := self.py_delegate()) is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try_call_soon_threadsafe(
|
||||||
|
py_delegate.event_loop,
|
||||||
|
py_delegate.did_write_value_for_characteristic,
|
||||||
|
peripheral,
|
||||||
|
characteristic,
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
|
||||||
|
@external_thread_callback
|
||||||
|
def peripheral_didWriteValueForDescriptor_error_(
|
||||||
|
self,
|
||||||
|
peripheral: CBPeripheral,
|
||||||
|
descriptor: CBDescriptor,
|
||||||
|
error: Optional[NSError],
|
||||||
|
) -> None:
|
||||||
|
logger.debug("peripheral_didWriteValueForDescriptor_error_")
|
||||||
|
|
||||||
|
if (py_delegate := self.py_delegate()) is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try_call_soon_threadsafe(
|
||||||
|
py_delegate.event_loop,
|
||||||
|
py_delegate.did_write_value_for_descriptor,
|
||||||
|
peripheral,
|
||||||
|
descriptor,
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
|
||||||
|
@external_thread_callback
|
||||||
|
def peripheralIsReadyToSendWriteWithoutResponse_( # pragma: no cover
|
||||||
|
self, peripheral: CBPeripheral
|
||||||
|
) -> None:
|
||||||
|
logger.debug("peripheralIsReadyToSendWriteWithoutResponse_")
|
||||||
|
# Currently not used in Bleak
|
||||||
|
|
||||||
|
@external_thread_callback
|
||||||
|
def peripheral_didUpdateNotificationStateForCharacteristic_error_(
|
||||||
|
self,
|
||||||
|
peripheral: CBPeripheral,
|
||||||
|
characteristic: CBCharacteristic,
|
||||||
|
error: Optional[NSError],
|
||||||
|
) -> None:
|
||||||
|
logger.debug("peripheral_didUpdateNotificationStateForCharacteristic_error_")
|
||||||
|
|
||||||
|
if (py_delegate := self.py_delegate()) is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try_call_soon_threadsafe(
|
||||||
|
py_delegate.event_loop,
|
||||||
|
py_delegate.did_update_notification_for_characteristic,
|
||||||
|
peripheral,
|
||||||
|
characteristic,
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
|
||||||
|
@external_thread_callback
|
||||||
|
def peripheral_didReadRSSI_error_(
|
||||||
|
self,
|
||||||
|
peripheral: CBPeripheral,
|
||||||
|
rssi: NSNumber,
|
||||||
|
error: Optional[NSError],
|
||||||
|
) -> None:
|
||||||
|
logger.debug("peripheral_didReadRSSI_error_")
|
||||||
|
|
||||||
|
if (py_delegate := self.py_delegate()) is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try_call_soon_threadsafe(
|
||||||
|
py_delegate.event_loop,
|
||||||
|
py_delegate.did_read_rssi,
|
||||||
|
peripheral,
|
||||||
|
int(rssi),
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
|
||||||
|
@external_thread_callback
|
||||||
|
def peripheralDidUpdateName_( # pragma: no cover
|
||||||
|
self, peripheral: CBPeripheral
|
||||||
|
) -> None:
|
||||||
|
logger.debug("peripheralDidUpdateName_")
|
||||||
|
logger.debug(
|
||||||
|
f"name of {peripheral.identifier()} changed to {peripheral.name()}"
|
||||||
|
)
|
||||||
|
# Currently not used in Bleak
|
||||||
|
|
||||||
|
@external_thread_callback
|
||||||
|
def peripheral_didModifyServices_( # pragma: no cover
|
||||||
|
self, peripheral: CBPeripheral, invalidatedServices: NSArray[CBService]
|
||||||
|
) -> None:
|
||||||
|
logger.debug("peripheral_didModifyServices_")
|
||||||
|
logger.debug(
|
||||||
|
f"{peripheral.identifier()} invalidated services: {invalidatedServices}"
|
||||||
|
)
|
||||||
|
# Currently not used in Bleak
|
||||||
|
|
||||||
|
|
||||||
|
class PeripheralDelegate:
|
||||||
|
"""macOS conforming python class for managing the PeripheralDelegate for BLE"""
|
||||||
|
|
||||||
|
def __init__(self, peripheral: CBPeripheral) -> None:
|
||||||
|
delegate = ObjcPeripheralDelegate.alloc().initWithPyDelegate_(weakref.ref(self))
|
||||||
|
assert delegate is not None
|
||||||
|
self.objc_delegate = delegate
|
||||||
|
|
||||||
|
self.peripheral = peripheral
|
||||||
|
self.peripheral.setDelegate_(self.objc_delegate)
|
||||||
|
|
||||||
|
self.event_loop = asyncio.get_running_loop()
|
||||||
|
self._services_discovered_future = self.event_loop.create_future()
|
||||||
|
|
||||||
|
self._service_characteristic_discovered_futures: dict[
|
||||||
|
int, asyncio.Future[NSArray[CBCharacteristic]]
|
||||||
|
] = {}
|
||||||
|
self._characteristic_descriptor_discover_futures: dict[
|
||||||
|
int, asyncio.Future[None]
|
||||||
|
] = {}
|
||||||
|
|
||||||
|
self._characteristic_read_futures: dict[int, asyncio.Future[NSData]] = {}
|
||||||
|
self._characteristic_write_futures: dict[int, asyncio.Future[None]] = {}
|
||||||
|
|
||||||
|
self._descriptor_read_futures: dict[int, asyncio.Future[NSObject]] = {}
|
||||||
|
self._descriptor_write_futures: dict[int, asyncio.Future[None]] = {}
|
||||||
|
|
||||||
|
self._characteristic_notify_change_futures: dict[int, asyncio.Future[None]] = {}
|
||||||
|
self._characteristic_notify_callbacks: dict[int, NotifyCallback] = {}
|
||||||
|
self._characteristic_notification_discriminators: dict[
|
||||||
|
int, Optional[NotificationDiscriminator]
|
||||||
|
] = {}
|
||||||
|
|
||||||
|
self._read_rssi_futures: dict[NSUUID, asyncio.Future[int]] = {}
|
||||||
|
|
||||||
|
def futures(self) -> Iterable[asyncio.Future[Any]]:
|
||||||
|
"""
|
||||||
|
Gets all futures for this delegate.
|
||||||
|
|
||||||
|
These can be used to handle any pending futures when a peripheral is disconnected.
|
||||||
|
"""
|
||||||
|
services_discovered_future = (
|
||||||
|
(self._services_discovered_future,)
|
||||||
|
if hasattr(self, "_services_discovered_future")
|
||||||
|
else ()
|
||||||
|
)
|
||||||
|
|
||||||
|
return itertools.chain(
|
||||||
|
services_discovered_future,
|
||||||
|
self._service_characteristic_discovered_futures.values(),
|
||||||
|
self._characteristic_descriptor_discover_futures.values(),
|
||||||
|
self._characteristic_read_futures.values(),
|
||||||
|
self._characteristic_write_futures.values(),
|
||||||
|
self._descriptor_read_futures.values(),
|
||||||
|
self._descriptor_write_futures.values(),
|
||||||
|
self._characteristic_notify_change_futures.values(),
|
||||||
|
self._read_rssi_futures.values(),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def discover_services(
|
||||||
|
self, services: Optional[NSArray[CBUUID]] = None
|
||||||
|
) -> NSArray[CBService]:
|
||||||
|
future = self.event_loop.create_future()
|
||||||
|
|
||||||
|
self._services_discovered_future = future
|
||||||
|
try:
|
||||||
|
self.peripheral.discoverServices_(services)
|
||||||
|
return await future
|
||||||
|
finally:
|
||||||
|
del self._services_discovered_future
|
||||||
|
|
||||||
|
async def discover_characteristics(
|
||||||
|
self, service: CBService
|
||||||
|
) -> NSArray[CBCharacteristic]:
|
||||||
|
future = self.event_loop.create_future()
|
||||||
|
|
||||||
|
self._service_characteristic_discovered_futures[service.startHandle()] = future
|
||||||
|
try:
|
||||||
|
self.peripheral.discoverCharacteristics_forService_(None, service)
|
||||||
|
return await future
|
||||||
|
finally:
|
||||||
|
del self._service_characteristic_discovered_futures[service.startHandle()]
|
||||||
|
|
||||||
|
async def discover_descriptors(
|
||||||
|
self, characteristic: CBCharacteristic
|
||||||
|
) -> NSArray[CBDescriptor]:
|
||||||
|
future = self.event_loop.create_future()
|
||||||
|
|
||||||
|
self._characteristic_descriptor_discover_futures[characteristic.handle()] = (
|
||||||
|
future
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
self.peripheral.discoverDescriptorsForCharacteristic_(characteristic)
|
||||||
|
await future
|
||||||
|
finally:
|
||||||
|
del self._characteristic_descriptor_discover_futures[
|
||||||
|
characteristic.handle()
|
||||||
|
]
|
||||||
|
|
||||||
|
return characteristic.descriptors()
|
||||||
|
|
||||||
|
async def read_characteristic(
|
||||||
|
self,
|
||||||
|
characteristic: CBCharacteristic,
|
||||||
|
use_cached: bool,
|
||||||
|
timeout: int = 20,
|
||||||
|
) -> NSData:
|
||||||
|
value = characteristic.value()
|
||||||
|
if value is not None and use_cached:
|
||||||
|
return value
|
||||||
|
|
||||||
|
future = self.event_loop.create_future()
|
||||||
|
|
||||||
|
self._characteristic_read_futures[characteristic.handle()] = future
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.peripheral.readValueForCharacteristic_(characteristic)
|
||||||
|
async with async_timeout(timeout):
|
||||||
|
return await future
|
||||||
|
finally:
|
||||||
|
del self._characteristic_read_futures[characteristic.handle()]
|
||||||
|
|
||||||
|
async def read_descriptor(
|
||||||
|
self, descriptor: CBDescriptor, use_cached: bool = True
|
||||||
|
) -> Any:
|
||||||
|
value = descriptor.value()
|
||||||
|
if value is not None and use_cached:
|
||||||
|
return value
|
||||||
|
|
||||||
|
future = self.event_loop.create_future()
|
||||||
|
|
||||||
|
self._descriptor_read_futures[descriptor.handle()] = future
|
||||||
|
try:
|
||||||
|
self.peripheral.readValueForDescriptor_(descriptor)
|
||||||
|
return await future
|
||||||
|
finally:
|
||||||
|
del self._descriptor_read_futures[descriptor.handle()]
|
||||||
|
|
||||||
|
async def write_characteristic(
|
||||||
|
self,
|
||||||
|
characteristic: CBCharacteristic,
|
||||||
|
value: NSData,
|
||||||
|
response: CBCharacteristicWriteType,
|
||||||
|
) -> None:
|
||||||
|
# in CoreBluetooth there is no indication of success or failure of
|
||||||
|
# CBCharacteristicWriteWithoutResponse
|
||||||
|
if response == CBCharacteristicWriteWithResponse:
|
||||||
|
future = self.event_loop.create_future()
|
||||||
|
|
||||||
|
self._characteristic_write_futures[characteristic.handle()] = future
|
||||||
|
try:
|
||||||
|
self.peripheral.writeValue_forCharacteristic_type_(
|
||||||
|
value, characteristic, response
|
||||||
|
)
|
||||||
|
await future
|
||||||
|
finally:
|
||||||
|
del self._characteristic_write_futures[characteristic.handle()]
|
||||||
|
else:
|
||||||
|
self.peripheral.writeValue_forCharacteristic_type_(
|
||||||
|
value, characteristic, response
|
||||||
|
)
|
||||||
|
|
||||||
|
async def write_descriptor(self, descriptor: CBDescriptor, value: NSData) -> None:
|
||||||
|
future = self.event_loop.create_future()
|
||||||
|
|
||||||
|
self._descriptor_write_futures[descriptor.handle()] = future
|
||||||
|
try:
|
||||||
|
self.peripheral.writeValue_forDescriptor_(value, descriptor)
|
||||||
|
await future
|
||||||
|
finally:
|
||||||
|
del self._descriptor_write_futures[descriptor.handle()]
|
||||||
|
|
||||||
|
async def start_notifications(
|
||||||
|
self,
|
||||||
|
characteristic: CBCharacteristic,
|
||||||
|
callback: NotifyCallback,
|
||||||
|
notification_discriminator: Optional[NotificationDiscriminator] = None,
|
||||||
|
) -> None:
|
||||||
|
c_handle = characteristic.handle()
|
||||||
|
if c_handle in self._characteristic_notify_callbacks:
|
||||||
|
raise ValueError("Characteristic notifications already started")
|
||||||
|
|
||||||
|
self._characteristic_notify_callbacks[c_handle] = callback
|
||||||
|
self._characteristic_notification_discriminators[c_handle] = (
|
||||||
|
notification_discriminator
|
||||||
|
)
|
||||||
|
|
||||||
|
future = self.event_loop.create_future()
|
||||||
|
|
||||||
|
self._characteristic_notify_change_futures[c_handle] = future
|
||||||
|
try:
|
||||||
|
self.peripheral.setNotifyValue_forCharacteristic_(True, characteristic)
|
||||||
|
await future
|
||||||
|
finally:
|
||||||
|
del self._characteristic_notify_change_futures[c_handle]
|
||||||
|
|
||||||
|
async def stop_notifications(self, characteristic: CBCharacteristic) -> None:
|
||||||
|
c_handle = characteristic.handle()
|
||||||
|
if c_handle not in self._characteristic_notify_callbacks:
|
||||||
|
raise ValueError("Characteristic notification never started")
|
||||||
|
|
||||||
|
future = self.event_loop.create_future()
|
||||||
|
|
||||||
|
self._characteristic_notify_change_futures[c_handle] = future
|
||||||
|
try:
|
||||||
|
self.peripheral.setNotifyValue_forCharacteristic_(False, characteristic)
|
||||||
|
await future
|
||||||
|
finally:
|
||||||
|
del self._characteristic_notify_change_futures[c_handle]
|
||||||
|
|
||||||
|
self._characteristic_notify_callbacks.pop(c_handle)
|
||||||
|
self._characteristic_notification_discriminators.pop(c_handle)
|
||||||
|
|
||||||
|
async def read_rssi(self) -> int:
|
||||||
|
future = self.event_loop.create_future()
|
||||||
|
|
||||||
|
self._read_rssi_futures[self.peripheral.identifier()] = future
|
||||||
|
try:
|
||||||
|
self.peripheral.readRSSI()
|
||||||
|
return await future
|
||||||
|
finally:
|
||||||
|
del self._read_rssi_futures[self.peripheral.identifier()]
|
||||||
|
|
||||||
|
# Protocol Functions
|
||||||
|
|
||||||
|
def did_discover_services(
|
||||||
|
self,
|
||||||
|
peripheral: CBPeripheral,
|
||||||
|
services: NSArray[CBService],
|
||||||
|
error: Optional[NSError],
|
||||||
|
) -> None:
|
||||||
|
future = self._services_discovered_future
|
||||||
|
if error is not None:
|
||||||
|
exception = BleakError(f"Failed to discover services {error}")
|
||||||
|
future.set_exception(exception)
|
||||||
|
else:
|
||||||
|
logger.debug("Services discovered")
|
||||||
|
future.set_result(services)
|
||||||
|
|
||||||
|
def did_discover_characteristics_for_service(
|
||||||
|
self,
|
||||||
|
peripheral: CBPeripheral,
|
||||||
|
service: CBService,
|
||||||
|
characteristics: NSArray[CBCharacteristic],
|
||||||
|
error: Optional[NSError],
|
||||||
|
) -> None:
|
||||||
|
future = self._service_characteristic_discovered_futures.get(
|
||||||
|
service.startHandle()
|
||||||
|
)
|
||||||
|
if not future:
|
||||||
|
logger.debug(
|
||||||
|
f"Unexpected event didDiscoverCharacteristicsForService for {service.startHandle()}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if error is not None:
|
||||||
|
exception = BleakError(
|
||||||
|
f"Failed to discover characteristics for service {service.startHandle()}: {error}"
|
||||||
|
)
|
||||||
|
future.set_exception(exception)
|
||||||
|
else:
|
||||||
|
logger.debug("Characteristics discovered")
|
||||||
|
future.set_result(characteristics)
|
||||||
|
|
||||||
|
def did_discover_descriptors_for_characteristic(
|
||||||
|
self,
|
||||||
|
peripheral: CBPeripheral,
|
||||||
|
characteristic: CBCharacteristic,
|
||||||
|
error: Optional[NSError],
|
||||||
|
) -> None:
|
||||||
|
future = self._characteristic_descriptor_discover_futures.get(
|
||||||
|
characteristic.handle()
|
||||||
|
)
|
||||||
|
if not future:
|
||||||
|
logger.warning(
|
||||||
|
f"Unexpected event didDiscoverDescriptorsForCharacteristic for {characteristic.handle()}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if error is not None:
|
||||||
|
exception = BleakError(
|
||||||
|
f"Failed to discover descriptors for characteristic {characteristic.handle()}: {error}"
|
||||||
|
)
|
||||||
|
future.set_exception(exception)
|
||||||
|
else:
|
||||||
|
logger.debug(f"Descriptor discovered {characteristic.handle()}")
|
||||||
|
future.set_result(None)
|
||||||
|
|
||||||
|
def did_update_value_for_characteristic(
|
||||||
|
self,
|
||||||
|
peripheral: CBPeripheral,
|
||||||
|
characteristic: CBCharacteristic,
|
||||||
|
value: Optional[NSData],
|
||||||
|
error: Optional[NSError],
|
||||||
|
) -> None:
|
||||||
|
c_handle = characteristic.handle()
|
||||||
|
|
||||||
|
future = self._characteristic_read_futures.get(c_handle)
|
||||||
|
|
||||||
|
# If error is set, then we know this was a read response.
|
||||||
|
# Otherwise, if there is a pending read request, we can't tell if this is a read response or notification.
|
||||||
|
# If the user provided a notification discriminator, we can use that to
|
||||||
|
# identify if this callback is due to a notification by analyzing the value.
|
||||||
|
# If not, and there is a future (pending read request), we assume it is a read response but can't know for sure.
|
||||||
|
if not error:
|
||||||
|
assert value is not None
|
||||||
|
|
||||||
|
notification_discriminator = (
|
||||||
|
self._characteristic_notification_discriminators.get(c_handle)
|
||||||
|
)
|
||||||
|
if not future or (
|
||||||
|
notification_discriminator and notification_discriminator(bytes(value))
|
||||||
|
):
|
||||||
|
notify_callback = self._characteristic_notify_callbacks.get(c_handle)
|
||||||
|
|
||||||
|
if notify_callback:
|
||||||
|
notify_callback(bytearray(value))
|
||||||
|
return
|
||||||
|
|
||||||
|
if not future:
|
||||||
|
logger.warning(
|
||||||
|
"Unexpected event didUpdateValueForCharacteristic for 0x%04x with value: %r and error: %r",
|
||||||
|
c_handle,
|
||||||
|
value,
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if error is not None:
|
||||||
|
exception = (
|
||||||
|
BleakGATTProtocolError(error.code())
|
||||||
|
if error.domain() == CBATTErrorDomain
|
||||||
|
else BleakError(f"Failed to read characteristic {c_handle}: {error}")
|
||||||
|
)
|
||||||
|
future.set_exception(exception)
|
||||||
|
else:
|
||||||
|
logger.debug("Read characteristic value")
|
||||||
|
assert value is not None
|
||||||
|
future.set_result(value)
|
||||||
|
|
||||||
|
def did_update_value_for_descriptor(
|
||||||
|
self,
|
||||||
|
peripheral: CBPeripheral,
|
||||||
|
descriptor: CBDescriptor,
|
||||||
|
value: Optional[Any],
|
||||||
|
error: Optional[NSError],
|
||||||
|
) -> None:
|
||||||
|
future = self._descriptor_read_futures.get(descriptor.handle())
|
||||||
|
if not future:
|
||||||
|
logger.warning("Unexpected event didUpdateValueForDescriptor")
|
||||||
|
return
|
||||||
|
if error is not None:
|
||||||
|
exception = (
|
||||||
|
BleakGATTProtocolError(error.code())
|
||||||
|
if error.domain() == CBATTErrorDomain
|
||||||
|
else BleakError(
|
||||||
|
f"Failed to read descriptor {descriptor.handle()}: {error}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
future.set_exception(exception)
|
||||||
|
else:
|
||||||
|
logger.debug("Read descriptor value")
|
||||||
|
assert value is not None
|
||||||
|
future.set_result(value)
|
||||||
|
|
||||||
|
def did_write_value_for_characteristic(
|
||||||
|
self,
|
||||||
|
peripheral: CBPeripheral,
|
||||||
|
characteristic: CBCharacteristic,
|
||||||
|
error: Optional[NSError],
|
||||||
|
) -> None:
|
||||||
|
future = self._characteristic_write_futures.get(characteristic.handle(), None)
|
||||||
|
if not future:
|
||||||
|
return # event only expected on write with response
|
||||||
|
if error is not None:
|
||||||
|
exception = (
|
||||||
|
BleakGATTProtocolError(error.code())
|
||||||
|
if error.domain() == CBATTErrorDomain
|
||||||
|
else BleakError(
|
||||||
|
f"Failed to write characteristic {characteristic.handle()}: {error}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
future.set_exception(exception)
|
||||||
|
else:
|
||||||
|
logger.debug("Write Characteristic Value")
|
||||||
|
future.set_result(None)
|
||||||
|
|
||||||
|
def did_write_value_for_descriptor(
|
||||||
|
self,
|
||||||
|
peripheral: CBPeripheral,
|
||||||
|
descriptor: CBDescriptor,
|
||||||
|
error: Optional[NSError],
|
||||||
|
) -> None:
|
||||||
|
future = self._descriptor_write_futures.get(descriptor.handle())
|
||||||
|
if not future:
|
||||||
|
logger.warning("Unexpected event didWriteValueForDescriptor")
|
||||||
|
return
|
||||||
|
if error is not None:
|
||||||
|
exception = (
|
||||||
|
BleakGATTProtocolError(error.code())
|
||||||
|
if error.domain() == CBATTErrorDomain
|
||||||
|
else BleakError(
|
||||||
|
f"Failed to write descriptor {descriptor.handle()}: {error}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
future.set_exception(exception)
|
||||||
|
else:
|
||||||
|
logger.debug("Write Descriptor Value")
|
||||||
|
future.set_result(None)
|
||||||
|
|
||||||
|
def did_update_notification_for_characteristic(
|
||||||
|
self,
|
||||||
|
peripheral: CBPeripheral,
|
||||||
|
characteristic: CBCharacteristic,
|
||||||
|
error: Optional[NSError],
|
||||||
|
) -> None:
|
||||||
|
c_handle = characteristic.handle()
|
||||||
|
future = self._characteristic_notify_change_futures.get(c_handle)
|
||||||
|
if not future:
|
||||||
|
logger.warning(
|
||||||
|
"Unexpected event didUpdateNotificationStateForCharacteristic"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if error is not None:
|
||||||
|
exception = BleakError(
|
||||||
|
f"Failed to update the notification status for characteristic {c_handle}: {error}"
|
||||||
|
)
|
||||||
|
future.set_exception(exception)
|
||||||
|
else:
|
||||||
|
logger.debug("Character Notify Update")
|
||||||
|
future.set_result(None)
|
||||||
|
|
||||||
|
def did_read_rssi(
|
||||||
|
self, peripheral: CBPeripheral, rssi: int, error: Optional[NSError]
|
||||||
|
) -> None:
|
||||||
|
future = self._read_rssi_futures.get(peripheral.identifier(), None)
|
||||||
|
|
||||||
|
if not future:
|
||||||
|
logger.warning("Unexpected event did_read_rssi")
|
||||||
|
return
|
||||||
|
|
||||||
|
if error is not None:
|
||||||
|
exception = BleakError(f"Failed to read RSSI: {error}")
|
||||||
|
future.set_exception(exception)
|
||||||
|
else:
|
||||||
|
future.set_result(rssi)
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
# Created on 2017-11-19 by hbldh <henrik.blidh@nedomkull.com>
|
||||||
|
"""
|
||||||
|
__init__.py
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
if sys.platform != "darwin":
|
||||||
|
assert False, "This backend is only available on macOS"
|
||||||
|
|
||||||
|
import objc
|
||||||
|
|
||||||
|
objc.options.verbose = True
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,402 @@
|
|||||||
|
# Created on 2019-06-26 by kevincar <kevincarrolldavis@gmail.com>
|
||||||
|
"""
|
||||||
|
BLE Client for CoreBluetooth on macOS
|
||||||
|
"""
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import sys
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
if sys.platform != "darwin":
|
||||||
|
assert False, "This backend is only available on macOS"
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
|
from CoreBluetooth import (
|
||||||
|
CBUUID,
|
||||||
|
CBCharacteristicWriteWithoutResponse,
|
||||||
|
CBCharacteristicWriteWithResponse,
|
||||||
|
CBPeripheral,
|
||||||
|
CBPeripheralStateConnected,
|
||||||
|
)
|
||||||
|
from Foundation import NSArray, NSData
|
||||||
|
|
||||||
|
from bleak import BleakScanner
|
||||||
|
from bleak._compat import override
|
||||||
|
from bleak.args import SizedBuffer
|
||||||
|
from bleak.args.corebluetooth import CBStartNotifyArgs
|
||||||
|
from bleak.assigned_numbers import gatt_char_props_to_strs
|
||||||
|
from bleak.backends.characteristic import BleakGATTCharacteristic
|
||||||
|
from bleak.backends.client import BaseBleakClient, NotifyCallback
|
||||||
|
from bleak.backends.corebluetooth.CentralManagerDelegate import CentralManagerDelegate
|
||||||
|
from bleak.backends.corebluetooth.PeripheralDelegate import PeripheralDelegate
|
||||||
|
from bleak.backends.corebluetooth.scanner import BleakScannerCoreBluetooth
|
||||||
|
from bleak.backends.corebluetooth.utils import (
|
||||||
|
cb_uuid_to_str,
|
||||||
|
is_descriptor_nsnumber,
|
||||||
|
is_descriptor_nsstring,
|
||||||
|
)
|
||||||
|
from bleak.backends.descriptor import BleakGATTDescriptor
|
||||||
|
from bleak.backends.device import BLEDevice
|
||||||
|
from bleak.backends.service import BleakGATTService, BleakGATTServiceCollection
|
||||||
|
from bleak.exc import BleakDeviceNotFoundError, BleakError
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BleakClientCoreBluetooth(BaseBleakClient):
|
||||||
|
"""CoreBluetooth class interface for BleakClient
|
||||||
|
|
||||||
|
Args:
|
||||||
|
address_or_ble_device (`BLEDevice` or str): The Bluetooth address of the BLE peripheral to connect to or the `BLEDevice` object representing it.
|
||||||
|
services: Optional set of service UUIDs that will be used.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
address_or_ble_device: Union[BLEDevice, str],
|
||||||
|
services: Optional[set[str]] = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
):
|
||||||
|
super().__init__(address_or_ble_device, **kwargs)
|
||||||
|
|
||||||
|
self._peripheral: Optional[CBPeripheral] = None
|
||||||
|
self._delegate: Optional[PeripheralDelegate] = None
|
||||||
|
self._central_manager_delegate: Optional[CentralManagerDelegate] = None
|
||||||
|
|
||||||
|
if isinstance(address_or_ble_device, BLEDevice):
|
||||||
|
(
|
||||||
|
self._peripheral,
|
||||||
|
self._central_manager_delegate,
|
||||||
|
) = address_or_ble_device.details
|
||||||
|
|
||||||
|
self._requested_services = (
|
||||||
|
NSArray[CBUUID]
|
||||||
|
.alloc()
|
||||||
|
.initWithArray_(list(map(CBUUID.UUIDWithString_, services)))
|
||||||
|
if services
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"BleakClientCoreBluetooth ({self.address})"
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def connect(self, pair: bool, **kwargs: Any) -> None:
|
||||||
|
"""Connect to a specified Peripheral
|
||||||
|
|
||||||
|
Keyword Args:
|
||||||
|
timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call.
|
||||||
|
"""
|
||||||
|
if pair:
|
||||||
|
logger.debug("Explicit pairing is not available in CoreBluetooth.")
|
||||||
|
|
||||||
|
timeout = kwargs.get("timeout", self._timeout)
|
||||||
|
if self._peripheral is None:
|
||||||
|
device = await BleakScanner.find_device_by_address(
|
||||||
|
self.address, timeout=timeout, backend=BleakScannerCoreBluetooth
|
||||||
|
)
|
||||||
|
|
||||||
|
if device:
|
||||||
|
self._peripheral, self._central_manager_delegate = device.details
|
||||||
|
else:
|
||||||
|
raise BleakDeviceNotFoundError(
|
||||||
|
self.address, f"Device with address {self.address} was not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._delegate is None:
|
||||||
|
assert self._peripheral is not None
|
||||||
|
self._delegate = PeripheralDelegate(self._peripheral)
|
||||||
|
|
||||||
|
def disconnect_callback() -> None:
|
||||||
|
# Ensure that `get_services` retrieves services again, rather
|
||||||
|
# than using the cached object
|
||||||
|
self.services = None
|
||||||
|
|
||||||
|
assert self._delegate is not None
|
||||||
|
|
||||||
|
# If there are any pending futures waiting for delegate callbacks, we
|
||||||
|
# need to raise an exception since the callback will no longer be
|
||||||
|
# called because the device is disconnected.
|
||||||
|
for future in self._delegate.futures():
|
||||||
|
try:
|
||||||
|
future.set_exception(BleakError("disconnected"))
|
||||||
|
except asyncio.InvalidStateError:
|
||||||
|
# the future was already done
|
||||||
|
pass
|
||||||
|
|
||||||
|
if self._disconnected_callback:
|
||||||
|
self._disconnected_callback()
|
||||||
|
|
||||||
|
manager = self._central_manager_delegate
|
||||||
|
assert manager is not None
|
||||||
|
logger.debug("CentralManagerDelegate at %r", manager)
|
||||||
|
logger.debug("Connecting to BLE device @ %s", self.address)
|
||||||
|
assert self._peripheral is not None
|
||||||
|
await manager.connect(self._peripheral, disconnect_callback, timeout=timeout)
|
||||||
|
|
||||||
|
# Now get services
|
||||||
|
await self._get_services()
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def disconnect(self) -> None:
|
||||||
|
"""Disconnect from the peripheral device"""
|
||||||
|
if (
|
||||||
|
self._peripheral is None
|
||||||
|
or self._peripheral.state() != CBPeripheralStateConnected
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
assert self._central_manager_delegate
|
||||||
|
await self._central_manager_delegate.disconnect(self._peripheral)
|
||||||
|
|
||||||
|
@property
|
||||||
|
@override
|
||||||
|
def is_connected(self) -> bool:
|
||||||
|
"""Checks for current active connection"""
|
||||||
|
return (
|
||||||
|
False
|
||||||
|
if self._peripheral is None
|
||||||
|
else self._peripheral.state() == CBPeripheralStateConnected
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
@override
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Get the name of the connected peripheral"""
|
||||||
|
if self._peripheral is None:
|
||||||
|
raise BleakError("Not connected")
|
||||||
|
return self._peripheral.name()
|
||||||
|
|
||||||
|
@property
|
||||||
|
@override
|
||||||
|
def mtu_size(self) -> int:
|
||||||
|
"""Get ATT MTU size for active connection"""
|
||||||
|
# Use type CBCharacteristicWriteWithoutResponse to get maximum write
|
||||||
|
# value length based on the negotiated ATT MTU size. Add the ATT header
|
||||||
|
# length (+3) to get the actual ATT MTU size.
|
||||||
|
assert self._peripheral
|
||||||
|
return (
|
||||||
|
self._peripheral.maximumWriteValueLengthForType_(
|
||||||
|
CBCharacteristicWriteWithoutResponse
|
||||||
|
)
|
||||||
|
+ 3
|
||||||
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def pair(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
"""Attempt to pair with a peripheral.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError:
|
||||||
|
This is not available on macOS since there is not explicit API
|
||||||
|
to do a pairing. Instead, the docs state that it "auto-pairs",
|
||||||
|
when trying to read a characteristic that requires encryption.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
|
||||||
|
- `Apple Docs <https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/BestPracticesForSettingUpYourIOSDeviceAsAPeripheral/BestPracticesForSettingUpYourIOSDeviceAsAPeripheral.html#//apple_ref/doc/uid/TP40013257-CH5-SW1>`_
|
||||||
|
- `Stack Overflow post #1 <https://stackoverflow.com/questions/25254932/can-you-pair-a-bluetooth-le-device-in-an-ios-app>`_
|
||||||
|
- `Stack Overflow post #2 <https://stackoverflow.com/questions/47546690/ios-bluetooth-pairing-request-dialog-can-i-know-the-users-choice>`_
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("Pairing is not available in Core Bluetooth.")
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def unpair(self) -> None:
|
||||||
|
"""
|
||||||
|
Remove pairing information for a peripheral.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError:
|
||||||
|
This is not available on macOS since there is not explicit API
|
||||||
|
to do a pairing.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("Pairing is not available in Core Bluetooth.")
|
||||||
|
|
||||||
|
async def _get_services(self) -> BleakGATTServiceCollection:
|
||||||
|
"""Get all services registered for this GATT server.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A :py:class:`bleak.backends.service.BleakGATTServiceCollection` with this device's services tree.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self.services is not None:
|
||||||
|
return self.services
|
||||||
|
|
||||||
|
services = BleakGATTServiceCollection()
|
||||||
|
|
||||||
|
logger.debug("Retrieving services...")
|
||||||
|
assert self._delegate
|
||||||
|
assert self._peripheral
|
||||||
|
cb_services = await self._delegate.discover_services(self._requested_services)
|
||||||
|
|
||||||
|
for service in cb_services:
|
||||||
|
serv = BleakGATTService(
|
||||||
|
service, service.startHandle(), cb_uuid_to_str(service.UUID())
|
||||||
|
)
|
||||||
|
services.add_service(serv)
|
||||||
|
|
||||||
|
serviceUUID = service.UUID().UUIDString()
|
||||||
|
logger.debug("Retrieving characteristics for service %s", serviceUUID)
|
||||||
|
characteristics = await self._delegate.discover_characteristics(service)
|
||||||
|
|
||||||
|
for characteristic in characteristics:
|
||||||
|
cUUID = characteristic.UUID().UUIDString()
|
||||||
|
logger.debug("Retrieving descriptors for characteristic %s", cUUID)
|
||||||
|
|
||||||
|
char = BleakGATTCharacteristic(
|
||||||
|
characteristic,
|
||||||
|
characteristic.handle(),
|
||||||
|
cb_uuid_to_str(characteristic.UUID()),
|
||||||
|
list(gatt_char_props_to_strs(characteristic.properties())),
|
||||||
|
functools.partial(
|
||||||
|
self._peripheral.maximumWriteValueLengthForType_,
|
||||||
|
CBCharacteristicWriteWithoutResponse,
|
||||||
|
),
|
||||||
|
serv,
|
||||||
|
)
|
||||||
|
services.add_characteristic(char)
|
||||||
|
|
||||||
|
descriptors = await self._delegate.discover_descriptors(characteristic)
|
||||||
|
for descriptor in descriptors:
|
||||||
|
desc = BleakGATTDescriptor(
|
||||||
|
descriptor,
|
||||||
|
int(descriptor.handle()),
|
||||||
|
cb_uuid_to_str(descriptor.UUID()),
|
||||||
|
char,
|
||||||
|
)
|
||||||
|
services.add_descriptor(desc)
|
||||||
|
|
||||||
|
logger.debug("Services resolved for %s", str(self))
|
||||||
|
self.services = services
|
||||||
|
return self.services
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def read_gatt_char(
|
||||||
|
self,
|
||||||
|
characteristic: BleakGATTCharacteristic,
|
||||||
|
*,
|
||||||
|
use_cached: bool = False,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> bytearray:
|
||||||
|
"""Perform read operation on the specified GATT characteristic.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
characteristic (BleakGATTCharacteristic): The characteristic to read from.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(bytearray) The read data.
|
||||||
|
|
||||||
|
"""
|
||||||
|
assert self._delegate
|
||||||
|
output = await self._delegate.read_characteristic(
|
||||||
|
characteristic.obj, use_cached=use_cached
|
||||||
|
)
|
||||||
|
value = bytearray(output)
|
||||||
|
logger.debug("Read Characteristic %s: %r", characteristic.uuid, value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def read_gatt_descriptor(
|
||||||
|
self,
|
||||||
|
descriptor: BleakGATTDescriptor,
|
||||||
|
*,
|
||||||
|
use_cached: bool = False,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> bytearray:
|
||||||
|
"""Perform read operation on the specified GATT descriptor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
handle (int): The handle of the descriptor to read from.
|
||||||
|
use_cached (bool): `False` forces Windows to read the value from the
|
||||||
|
device again and not use its own cached value. Defaults to `False`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(bytearray) The read data.
|
||||||
|
"""
|
||||||
|
assert self._delegate
|
||||||
|
output = await self._delegate.read_descriptor(
|
||||||
|
descriptor.obj, use_cached=use_cached
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_descriptor_nsnumber(output, descriptor.uuid):
|
||||||
|
value = bytearray(int(output).to_bytes(2, byteorder="little"))
|
||||||
|
elif is_descriptor_nsstring(output, descriptor.uuid):
|
||||||
|
value = bytearray(output.encode())
|
||||||
|
else:
|
||||||
|
value = bytearray(output)
|
||||||
|
|
||||||
|
logger.debug("Read Descriptor %d : %r", descriptor.handle, value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def write_gatt_char(
|
||||||
|
self, characteristic: BleakGATTCharacteristic, data: SizedBuffer, response: bool
|
||||||
|
) -> None:
|
||||||
|
value = NSData.alloc().initWithBytes_length_(data, len(data))
|
||||||
|
assert self._delegate
|
||||||
|
await self._delegate.write_characteristic(
|
||||||
|
characteristic.obj,
|
||||||
|
value,
|
||||||
|
(
|
||||||
|
CBCharacteristicWriteWithResponse
|
||||||
|
if response
|
||||||
|
else CBCharacteristicWriteWithoutResponse
|
||||||
|
),
|
||||||
|
)
|
||||||
|
logger.debug(f"Write Characteristic {characteristic.uuid} : {data}")
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def write_gatt_descriptor(
|
||||||
|
self, descriptor: BleakGATTDescriptor, data: SizedBuffer
|
||||||
|
) -> None:
|
||||||
|
"""Perform a write operation on the specified GATT descriptor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
descriptor: The descriptor to read from.
|
||||||
|
data: The data to send (any bytes-like object).
|
||||||
|
|
||||||
|
"""
|
||||||
|
assert self._delegate
|
||||||
|
value = NSData.alloc().initWithBytes_length_(data, len(data))
|
||||||
|
await self._delegate.write_descriptor(descriptor.obj, value)
|
||||||
|
logger.debug("Write Descriptor %d : %r", descriptor.handle, data)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def start_notify(
|
||||||
|
self,
|
||||||
|
characteristic: BleakGATTCharacteristic,
|
||||||
|
callback: NotifyCallback,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Activate notifications/indications on a characteristic.
|
||||||
|
"""
|
||||||
|
assert self._delegate is not None
|
||||||
|
|
||||||
|
cb: CBStartNotifyArgs = kwargs["cb"]
|
||||||
|
|
||||||
|
await self._delegate.start_notifications(
|
||||||
|
characteristic.obj,
|
||||||
|
callback,
|
||||||
|
cb.get("notification_discriminator"),
|
||||||
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def stop_notify(self, characteristic: BleakGATTCharacteristic) -> None:
|
||||||
|
"""Deactivate notification/indication on a specified characteristic.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
characteristic (BleakGATTCharacteristic: The characteristic to deactivate
|
||||||
|
notification/indication on.
|
||||||
|
"""
|
||||||
|
assert self._delegate
|
||||||
|
await self._delegate.stop_notifications(characteristic.obj)
|
||||||
|
|
||||||
|
async def get_rssi(self) -> int:
|
||||||
|
"""To get RSSI value in dBm of the connected Peripheral"""
|
||||||
|
assert self._delegate
|
||||||
|
return int(await self._delegate.read_rssi())
|
||||||
@ -0,0 +1,180 @@
|
|||||||
|
import sys
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
if sys.platform != "darwin":
|
||||||
|
assert False, "This backend is only available on macOS"
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any, Literal, Optional, cast
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
|
import objc
|
||||||
|
from CoreBluetooth import CBPeripheral
|
||||||
|
from Foundation import NSBundle, NSNumber
|
||||||
|
|
||||||
|
from bleak._compat import override
|
||||||
|
from bleak.args.corebluetooth import CBScannerArgs as _CBScannerArgs
|
||||||
|
from bleak.backends.corebluetooth.CentralManagerDelegate import (
|
||||||
|
CBAdvertisementData,
|
||||||
|
CentralManagerDelegate,
|
||||||
|
)
|
||||||
|
from bleak.backends.corebluetooth.utils import (
|
||||||
|
cb_uuid_to_str,
|
||||||
|
to_optional_int,
|
||||||
|
to_optional_str,
|
||||||
|
)
|
||||||
|
from bleak.backends.scanner import (
|
||||||
|
AdvertisementData,
|
||||||
|
AdvertisementDataCallback,
|
||||||
|
BaseBleakScanner,
|
||||||
|
)
|
||||||
|
from bleak.exc import BleakError
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name: str):
|
||||||
|
if name == "CBScannerArgs":
|
||||||
|
warn(
|
||||||
|
"importing CBScannerArgs from bleak.backends.corebluetooth.scanner is deprecated, use bleak.args.corebluetooth instead",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return _CBScannerArgs
|
||||||
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||||
|
|
||||||
|
|
||||||
|
class BleakScannerCoreBluetooth(BaseBleakScanner):
|
||||||
|
"""The native macOS Bleak BLE Scanner.
|
||||||
|
|
||||||
|
Documentation:
|
||||||
|
https://developer.apple.com/documentation/corebluetooth/cbcentralmanager
|
||||||
|
|
||||||
|
CoreBluetooth doesn't explicitly use Bluetooth addresses to identify peripheral
|
||||||
|
devices because private devices may obscure their Bluetooth addresses. To cope
|
||||||
|
with this, CoreBluetooth utilizes UUIDs for each peripheral. Bleak uses
|
||||||
|
this for the BLEDevice address on macOS.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
detection_callback:
|
||||||
|
Optional function that will be called each time a device is
|
||||||
|
discovered or advertising data has changed.
|
||||||
|
service_uuids:
|
||||||
|
Optional list of service UUIDs to filter on. Only advertisements
|
||||||
|
containing this advertising data will be received. Required on
|
||||||
|
macOS >= 12.0, < 12.3 (unless you create an app with ``py2app``).
|
||||||
|
scanning_mode:
|
||||||
|
Set to ``"passive"`` to avoid the ``"active"`` scanning mode. Not
|
||||||
|
supported on macOS! Will raise :class:`BleakError` if set to
|
||||||
|
``"passive"``
|
||||||
|
**timeout (float):
|
||||||
|
The scanning timeout to be used, in case of missing
|
||||||
|
``stopScan_`` method.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
detection_callback: Optional[AdvertisementDataCallback],
|
||||||
|
service_uuids: Optional[list[str]],
|
||||||
|
scanning_mode: Literal["active", "passive"],
|
||||||
|
*,
|
||||||
|
cb: _CBScannerArgs,
|
||||||
|
**kwargs: Any,
|
||||||
|
):
|
||||||
|
super().__init__(detection_callback, service_uuids)
|
||||||
|
|
||||||
|
self._use_bdaddr = cb.get("use_bdaddr", False)
|
||||||
|
|
||||||
|
if scanning_mode == "passive":
|
||||||
|
raise BleakError("macOS does not support passive scanning")
|
||||||
|
|
||||||
|
self._manager = CentralManagerDelegate()
|
||||||
|
self._timeout: float = kwargs.get("timeout", 5.0)
|
||||||
|
if (
|
||||||
|
objc.macos_available(12, 0)
|
||||||
|
and not objc.macos_available(12, 3)
|
||||||
|
and not self._service_uuids
|
||||||
|
):
|
||||||
|
# See https://github.com/hbldh/bleak/issues/720
|
||||||
|
if NSBundle.mainBundle().bundleIdentifier() == "org.python.python":
|
||||||
|
logger.error(
|
||||||
|
"macOS 12.0, 12.1 and 12.2 require non-empty service_uuids kwarg, otherwise no advertisement data will be received"
|
||||||
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def start(self) -> None:
|
||||||
|
await self._manager.wait_until_ready()
|
||||||
|
|
||||||
|
self.seen_devices = {}
|
||||||
|
|
||||||
|
def callback(
|
||||||
|
peripheral: CBPeripheral, adv_data: CBAdvertisementData, rssi: NSNumber
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
service_uuids = [
|
||||||
|
cb_uuid_to_str(u) for u in adv_data.get("kCBAdvDataServiceUUIDs", [])
|
||||||
|
]
|
||||||
|
|
||||||
|
if not self.is_allowed_uuid(service_uuids):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Process service data
|
||||||
|
service_data = {
|
||||||
|
cb_uuid_to_str(k): bytes(v)
|
||||||
|
for k, v in adv_data.get("kCBAdvDataServiceData", {}).items()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Process manufacturer data into a more friendly format
|
||||||
|
manufacturer_binary_data = adv_data.get("kCBAdvDataManufacturerData")
|
||||||
|
manufacturer_data: dict[int, bytes] = {}
|
||||||
|
if manufacturer_binary_data:
|
||||||
|
manufacturer_id = int.from_bytes(
|
||||||
|
manufacturer_binary_data[0:2], byteorder="little"
|
||||||
|
)
|
||||||
|
manufacturer_value = bytes(manufacturer_binary_data[2:])
|
||||||
|
manufacturer_data[manufacturer_id] = manufacturer_value
|
||||||
|
|
||||||
|
advertisement_data = AdvertisementData(
|
||||||
|
local_name=to_optional_str(adv_data.get("kCBAdvDataLocalName")),
|
||||||
|
manufacturer_data=manufacturer_data,
|
||||||
|
service_data=service_data,
|
||||||
|
service_uuids=service_uuids,
|
||||||
|
tx_power=to_optional_int(adv_data.get("kCBAdvDataTxPowerLevel")),
|
||||||
|
rssi=int(rssi),
|
||||||
|
platform_data=(peripheral, adv_data, rssi),
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._use_bdaddr:
|
||||||
|
# HACK: retrieveAddressForPeripheral_ is undocumented but seems to do the trick
|
||||||
|
address_bytes = cast(
|
||||||
|
Optional[bytes],
|
||||||
|
self._manager.central_manager.retrieveAddressForPeripheral_(peripheral), # type: ignore
|
||||||
|
)
|
||||||
|
if address_bytes is None:
|
||||||
|
logger.debug(
|
||||||
|
"Could not get Bluetooth address for %s. Ignoring this device.",
|
||||||
|
peripheral.identifier().UUIDString(),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
address = address_bytes.hex(":").upper()
|
||||||
|
else:
|
||||||
|
address = peripheral.identifier().UUIDString()
|
||||||
|
|
||||||
|
device = self.create_or_update_device(
|
||||||
|
peripheral.identifier().UUIDString(),
|
||||||
|
address,
|
||||||
|
peripheral.name(),
|
||||||
|
(peripheral, self._manager),
|
||||||
|
advertisement_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.call_detection_callbacks(device, advertisement_data)
|
||||||
|
|
||||||
|
self._manager.callbacks[id(self)] = callback
|
||||||
|
await self._manager.start_scan(self._service_uuids)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def stop(self) -> None:
|
||||||
|
await self._manager.stop_scan()
|
||||||
|
self._manager.callbacks.pop(id(self), None)
|
||||||
@ -0,0 +1,123 @@
|
|||||||
|
import sys
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
if sys.platform != "darwin":
|
||||||
|
assert False, "This backend is only available on macOS"
|
||||||
|
|
||||||
|
from typing import Any, Optional, TypeGuard, overload
|
||||||
|
|
||||||
|
from CoreBluetooth import (
|
||||||
|
CBUUID,
|
||||||
|
CBManagerState,
|
||||||
|
CBManagerStatePoweredOff,
|
||||||
|
CBManagerStatePoweredOn,
|
||||||
|
CBManagerStateResetting,
|
||||||
|
CBManagerStateUnauthorized,
|
||||||
|
CBManagerStateUnknown,
|
||||||
|
CBManagerStateUnsupported,
|
||||||
|
CBUUIDCharacteristicExtendedPropertiesString,
|
||||||
|
CBUUIDCharacteristicUserDescriptionString,
|
||||||
|
CBUUIDClientCharacteristicConfigurationString,
|
||||||
|
CBUUIDServerCharacteristicConfigurationString,
|
||||||
|
)
|
||||||
|
from Foundation import NSNumber, NSString
|
||||||
|
|
||||||
|
from bleak.uuids import normalize_uuid_str
|
||||||
|
|
||||||
|
|
||||||
|
def cb_uuid_to_str(uuid: CBUUID) -> str:
|
||||||
|
"""Converts a CoreBluetooth UUID to a Python string.
|
||||||
|
|
||||||
|
If ``uuid`` is a 16-bit UUID, it is assumed to be a Bluetooth GATT UUID
|
||||||
|
(``0000xxxx-0000-1000-8000-00805f9b34fb``).
|
||||||
|
|
||||||
|
Args
|
||||||
|
uuid: The UUID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The UUID as a lower case Python string (``xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx``)
|
||||||
|
"""
|
||||||
|
return normalize_uuid_str(uuid.UUIDString())
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def to_optional_str(value: NSString) -> str: ...
|
||||||
|
@overload
|
||||||
|
def to_optional_str(value: None) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
def to_optional_str(value: Optional[NSString]) -> Optional[str]:
|
||||||
|
"""Converts an NSString to a Python string or None.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: The NSString or None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The Python string or None.
|
||||||
|
"""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def to_optional_int(value: NSNumber) -> int: ...
|
||||||
|
@overload
|
||||||
|
def to_optional_int(value: None) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
def to_optional_int(value: Optional[NSNumber]) -> Optional[int]:
|
||||||
|
"""Converts an NSNumber to a Python int or None.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: The NSNumber or None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The Python int or None.
|
||||||
|
"""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return int(value)
|
||||||
|
|
||||||
|
|
||||||
|
# Most descriptors are returned as NSData (raw bytes), but some of them
|
||||||
|
# are returned as NSNumber or NSString.
|
||||||
|
# See: https://developer.apple.com/documentation/corebluetooth/characteristic-descriptors
|
||||||
|
_DESCRIPTOR_TYPE_NSNUMBER = (
|
||||||
|
normalize_uuid_str(CBUUIDCharacteristicExtendedPropertiesString), # 0x2900
|
||||||
|
normalize_uuid_str(CBUUIDClientCharacteristicConfigurationString), # 0x2902
|
||||||
|
normalize_uuid_str(CBUUIDServerCharacteristicConfigurationString), # 0x2903
|
||||||
|
)
|
||||||
|
_DESCRIPTOR_TYPE_NSSTRING = (
|
||||||
|
normalize_uuid_str(CBUUIDCharacteristicUserDescriptionString), # 0x2901
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_descriptor_nsnumber(value: Any, descriptor_uuid: str) -> TypeGuard[NSNumber]:
|
||||||
|
"""Check if descriptor value is returned as NSNumber by CoreBluetooth."""
|
||||||
|
return descriptor_uuid in _DESCRIPTOR_TYPE_NSNUMBER
|
||||||
|
|
||||||
|
|
||||||
|
def is_descriptor_nsstring(value: Any, descriptor_uuid: str) -> TypeGuard[NSString]:
|
||||||
|
"""Check if descriptor value is returned as NSString by CoreBluetooth."""
|
||||||
|
return descriptor_uuid in _DESCRIPTOR_TYPE_NSSTRING
|
||||||
|
|
||||||
|
|
||||||
|
_CB_MANAGER_STATE_MESSAGE = {
|
||||||
|
CBManagerStateUnknown: "Bluetooth in unknown state",
|
||||||
|
CBManagerStateResetting: "Bluetooth is resetting",
|
||||||
|
CBManagerStateUnsupported: "Bluetooth is unsupported",
|
||||||
|
CBManagerStateUnauthorized: "Bluetooth is unauthorized",
|
||||||
|
CBManagerStatePoweredOff: "Bluetooth powered off",
|
||||||
|
CBManagerStatePoweredOn: "Bluetooth powered on",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def cb_manager_state_message(state: CBManagerState) -> str:
|
||||||
|
"""Get log message for CoreBluetooth manager state."""
|
||||||
|
return _CB_MANAGER_STATE_MESSAGE.get(
|
||||||
|
state, f"Unknown CBManagerState value: {state}"
|
||||||
|
)
|
||||||
@ -0,0 +1,154 @@
|
|||||||
|
# Created on 2019-03-19 by hbldh <henrik.blidh@nedomkull.com>
|
||||||
|
"""
|
||||||
|
Interface class for the Bleak representation of a GATT Descriptor
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
from bleak.uuids import normalize_uuid_16
|
||||||
|
|
||||||
|
# avoid circular import
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from bleak.backends.characteristic import BleakGATTCharacteristic
|
||||||
|
|
||||||
|
|
||||||
|
_descriptor_descriptions = {
|
||||||
|
normalize_uuid_16(0x2905): [
|
||||||
|
"Characteristic Aggregate Format",
|
||||||
|
"org.bluetooth.descriptor.gatt.characteristic_aggregate_format",
|
||||||
|
"0x2905",
|
||||||
|
"GSS",
|
||||||
|
],
|
||||||
|
normalize_uuid_16(0x2900): [
|
||||||
|
"Characteristic Extended Properties",
|
||||||
|
"org.bluetooth.descriptor.gatt.characteristic_extended_properties",
|
||||||
|
"0x2900",
|
||||||
|
"GSS",
|
||||||
|
],
|
||||||
|
normalize_uuid_16(0x2904): [
|
||||||
|
"Characteristic Presentation Format",
|
||||||
|
"org.bluetooth.descriptor.gatt.characteristic_presentation_format",
|
||||||
|
"0x2904",
|
||||||
|
"GSS",
|
||||||
|
],
|
||||||
|
normalize_uuid_16(0x2901): [
|
||||||
|
"Characteristic User Description",
|
||||||
|
"org.bluetooth.descriptor.gatt.characteristic_user_description",
|
||||||
|
"0x2901",
|
||||||
|
"GSS",
|
||||||
|
],
|
||||||
|
normalize_uuid_16(0x2902): [
|
||||||
|
"Client Characteristic Configuration",
|
||||||
|
"org.bluetooth.descriptor.gatt.client_characteristic_configuration",
|
||||||
|
"0x2902",
|
||||||
|
"GSS",
|
||||||
|
],
|
||||||
|
normalize_uuid_16(0x290B): [
|
||||||
|
"Environmental Sensing Configuration",
|
||||||
|
"org.bluetooth.descriptor.es_configuration",
|
||||||
|
"0x290B",
|
||||||
|
"GSS",
|
||||||
|
],
|
||||||
|
normalize_uuid_16(0x290C): [
|
||||||
|
"Environmental Sensing Measurement",
|
||||||
|
"org.bluetooth.descriptor.es_measurement",
|
||||||
|
"0x290C",
|
||||||
|
"GSS",
|
||||||
|
],
|
||||||
|
normalize_uuid_16(0x290D): [
|
||||||
|
"Environmental Sensing Trigger Setting",
|
||||||
|
"org.bluetooth.descriptor.es_trigger_setting",
|
||||||
|
"0x290D",
|
||||||
|
"GSS",
|
||||||
|
],
|
||||||
|
normalize_uuid_16(0x2907): [
|
||||||
|
"External Report Reference",
|
||||||
|
"org.bluetooth.descriptor.external_report_reference",
|
||||||
|
"0x2907",
|
||||||
|
"GSS",
|
||||||
|
],
|
||||||
|
normalize_uuid_16(0x2909): [
|
||||||
|
"Number of Digitals",
|
||||||
|
"org.bluetooth.descriptor.number_of_digitals",
|
||||||
|
"0x2909",
|
||||||
|
"GSS",
|
||||||
|
],
|
||||||
|
normalize_uuid_16(0x2908): [
|
||||||
|
"Report Reference",
|
||||||
|
"org.bluetooth.descriptor.report_reference",
|
||||||
|
"0x2908",
|
||||||
|
"GSS",
|
||||||
|
],
|
||||||
|
normalize_uuid_16(0x2903): [
|
||||||
|
"Server Characteristic Configuration",
|
||||||
|
"org.bluetooth.descriptor.gatt.server_characteristic_configuration",
|
||||||
|
"0x2903",
|
||||||
|
"GSS",
|
||||||
|
],
|
||||||
|
normalize_uuid_16(0x290E): [
|
||||||
|
"Time Trigger Setting",
|
||||||
|
"org.bluetooth.descriptor.time_trigger_setting",
|
||||||
|
"0x290E",
|
||||||
|
"GSS",
|
||||||
|
],
|
||||||
|
normalize_uuid_16(0x2906): [
|
||||||
|
"Valid Range",
|
||||||
|
"org.bluetooth.descriptor.valid_range",
|
||||||
|
"0x2906",
|
||||||
|
"GSS",
|
||||||
|
],
|
||||||
|
normalize_uuid_16(0x290A): [
|
||||||
|
"Value Trigger Setting",
|
||||||
|
"org.bluetooth.descriptor.value_trigger_setting",
|
||||||
|
"0x290A",
|
||||||
|
"GSS",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BleakGATTDescriptor:
|
||||||
|
"""The Bleak representation of a GATT Descriptor"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, obj: Any, handle: int, uuid: str, characteristic: BleakGATTCharacteristic
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
obj: The backend-specific object for the descriptor.
|
||||||
|
handle: The handle of the descriptor.
|
||||||
|
uuid: The UUID of the descriptor.
|
||||||
|
characteristic: The characteristic that this descriptor belongs to.
|
||||||
|
"""
|
||||||
|
self.obj = obj
|
||||||
|
self._handle = handle
|
||||||
|
self._uuid = uuid
|
||||||
|
self._characteristic = characteristic
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.uuid} (Handle: {self.handle}): {self.description}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def characteristic_uuid(self) -> str:
|
||||||
|
"""UUID for the characteristic that this descriptor belongs to"""
|
||||||
|
return self._characteristic.uuid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def characteristic_handle(self) -> int:
|
||||||
|
"""handle for the characteristic that this descriptor belongs to"""
|
||||||
|
return self._characteristic.handle
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uuid(self) -> str:
|
||||||
|
"""UUID for this descriptor"""
|
||||||
|
return self._uuid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def handle(self) -> int:
|
||||||
|
"""Integer handle for this descriptor"""
|
||||||
|
return self._handle
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self) -> str:
|
||||||
|
"""A text description of what this descriptor represents"""
|
||||||
|
return _descriptor_descriptions.get(self.uuid, ["Unknown"])[0]
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
# Created on 2018-04-23 by hbldh <henrik.blidh@nedomkull.com>
|
||||||
|
"""
|
||||||
|
Wrapper class for Bluetooth LE servers returned from calling
|
||||||
|
:py:meth:`bleak.discover`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from typing import Any, Optional
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
|
|
||||||
|
class BLEDevice:
|
||||||
|
"""
|
||||||
|
A simple wrapper class representing a BLE server detected during scanning.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("address", "name", "details")
|
||||||
|
|
||||||
|
def __init__(self, address: str, name: Optional[str], details: Any, **kwargs: Any):
|
||||||
|
#: The Bluetooth address of the device on this machine (UUID on macOS).
|
||||||
|
self.address = address
|
||||||
|
#: The operating system name of the device (not necessarily the local name
|
||||||
|
#: from the advertising data), suitable for display to the user.
|
||||||
|
self.name = name
|
||||||
|
#: The OS native details required for connecting to the device.
|
||||||
|
self.details = details
|
||||||
|
|
||||||
|
if kwargs:
|
||||||
|
warn(
|
||||||
|
"Passing additional arguments for BLEDevice is deprecated and has no effect.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.address}: {self.name}"
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"BLEDevice({self.address}, {self.name})"
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,528 @@
|
|||||||
|
"""
|
||||||
|
BLE Client for python-for-android
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
if sys.platform != "android":
|
||||||
|
assert False, "This backend is only available on Android"
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
import warnings
|
||||||
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
|
from android.broadcast import BroadcastReceiver
|
||||||
|
from jnius import java_method
|
||||||
|
|
||||||
|
from bleak._compat import override
|
||||||
|
from bleak.assigned_numbers import gatt_char_props_to_strs
|
||||||
|
from bleak.backends.characteristic import BleakGATTCharacteristic
|
||||||
|
from bleak.backends.client import BaseBleakClient, NotifyCallback
|
||||||
|
from bleak.backends.descriptor import BleakGATTDescriptor
|
||||||
|
from bleak.backends.device import BLEDevice
|
||||||
|
from bleak.backends.p4android import defs, utils
|
||||||
|
from bleak.backends.service import BleakGATTService, BleakGATTServiceCollection
|
||||||
|
from bleak.exc import BleakError
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BleakClientP4Android(BaseBleakClient):
|
||||||
|
"""A python-for-android Bleak Client
|
||||||
|
|
||||||
|
Args:
|
||||||
|
address_or_ble_device:
|
||||||
|
The Bluetooth address of the BLE peripheral to connect to or the
|
||||||
|
:class:`BLEDevice` object representing it.
|
||||||
|
services:
|
||||||
|
Optional set of services UUIDs to filter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
address_or_ble_device: Union[BLEDevice, str],
|
||||||
|
services: Optional[set[uuid.UUID]],
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
super().__init__(address_or_ble_device, **kwargs)
|
||||||
|
self._requested_services = (
|
||||||
|
set(map(defs.UUID.fromString, services)) if services else None
|
||||||
|
)
|
||||||
|
self.__gatt = None
|
||||||
|
self.__mtu = 23
|
||||||
|
|
||||||
|
self.__callbacks = None
|
||||||
|
|
||||||
|
# Connectivity methods
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def connect(self, pair: bool, **kwargs) -> None:
|
||||||
|
"""Connect to the specified GATT server."""
|
||||||
|
if pair:
|
||||||
|
logger.warning("Pairing during connect is not implemented on Android")
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
|
adapter = defs.BluetoothAdapter.getDefaultAdapter()
|
||||||
|
if adapter is None:
|
||||||
|
raise BleakError("Bluetooth is not supported on this hardware platform")
|
||||||
|
if adapter.getState() != defs.BluetoothAdapter.STATE_ON:
|
||||||
|
raise BleakError("Bluetooth is not turned on")
|
||||||
|
|
||||||
|
self.__device = adapter.getRemoteDevice(self.address)
|
||||||
|
|
||||||
|
self.__callbacks = _PythonBluetoothGattCallback(self, loop)
|
||||||
|
|
||||||
|
self._subscriptions = {}
|
||||||
|
|
||||||
|
logger.debug(f"Connecting to BLE device @ {self.address}")
|
||||||
|
|
||||||
|
(self.__gatt,) = await self.__callbacks.perform_and_wait(
|
||||||
|
dispatchApi=self.__device.connectGatt,
|
||||||
|
dispatchParams=(
|
||||||
|
defs.context,
|
||||||
|
False,
|
||||||
|
self.__callbacks.java,
|
||||||
|
defs.BluetoothDevice.TRANSPORT_LE,
|
||||||
|
),
|
||||||
|
resultApi="onConnectionStateChange",
|
||||||
|
resultExpected=(defs.BluetoothProfile.STATE_CONNECTED,),
|
||||||
|
return_indicates_status=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.debug("Connection successful.")
|
||||||
|
|
||||||
|
# unlike other backends, Android doesn't automatically negotiate
|
||||||
|
# the MTU, so we request the largest size possible like BlueZ
|
||||||
|
logger.debug("requesting mtu...")
|
||||||
|
(self.__mtu,) = await self.__callbacks.perform_and_wait(
|
||||||
|
dispatchApi=self.__gatt.requestMtu,
|
||||||
|
dispatchParams=(517,),
|
||||||
|
resultApi="onMtuChanged",
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug("discovering services...")
|
||||||
|
await self.__callbacks.perform_and_wait(
|
||||||
|
dispatchApi=self.__gatt.discoverServices,
|
||||||
|
dispatchParams=(),
|
||||||
|
resultApi="onServicesDiscovered",
|
||||||
|
)
|
||||||
|
|
||||||
|
await self._get_services()
|
||||||
|
except BaseException:
|
||||||
|
# if connecting is canceled or one of the above fails, we need to
|
||||||
|
# disconnect
|
||||||
|
try:
|
||||||
|
await self.disconnect()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def disconnect(self) -> None:
|
||||||
|
"""Disconnect from the specified GATT server."""
|
||||||
|
logger.debug("Disconnecting from BLE device...")
|
||||||
|
if self.__gatt is None:
|
||||||
|
# No connection exists. Either one hasn't been created or
|
||||||
|
# we have already called disconnect and closed the gatt
|
||||||
|
# connection.
|
||||||
|
logger.debug("already disconnected")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Try to disconnect the actual device/peripheral
|
||||||
|
try:
|
||||||
|
await self.__callbacks.perform_and_wait(
|
||||||
|
dispatchApi=self.__gatt.disconnect,
|
||||||
|
dispatchParams=(),
|
||||||
|
resultApi="onConnectionStateChange",
|
||||||
|
resultExpected=(defs.BluetoothProfile.STATE_DISCONNECTED,),
|
||||||
|
unless_already=True,
|
||||||
|
return_indicates_status=False,
|
||||||
|
)
|
||||||
|
self.__gatt.close()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Attempt to disconnect device failed: {e}")
|
||||||
|
|
||||||
|
self.__gatt = None
|
||||||
|
self.__callbacks = None
|
||||||
|
|
||||||
|
# Reset all stored services.
|
||||||
|
self.services = None
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def pair(self, *args, **kwargs) -> None:
|
||||||
|
"""Pair with the peripheral.
|
||||||
|
|
||||||
|
You can use ConnectDevice method if you already know the MAC address of the device.
|
||||||
|
Else you need to StartDiscovery, Trust, Pair and Connect in sequence.
|
||||||
|
"""
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
|
bondedFuture = loop.create_future()
|
||||||
|
|
||||||
|
def handleBondStateChanged(context, intent):
|
||||||
|
bond_state = intent.getIntExtra(defs.BluetoothDevice.EXTRA_BOND_STATE, -1)
|
||||||
|
if bond_state == -1:
|
||||||
|
loop.call_soon_threadsafe(
|
||||||
|
bondedFuture.set_exception,
|
||||||
|
BleakError(f"Unexpected bond state {bond_state}"),
|
||||||
|
)
|
||||||
|
elif bond_state == defs.BluetoothDevice.BOND_NONE:
|
||||||
|
loop.call_soon_threadsafe(
|
||||||
|
bondedFuture.set_exception,
|
||||||
|
BleakError(
|
||||||
|
f"Device with address {self.address} could not be paired with."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
elif bond_state == defs.BluetoothDevice.BOND_BONDED:
|
||||||
|
loop.call_soon_threadsafe(bondedFuture.set_result, True)
|
||||||
|
|
||||||
|
receiver = BroadcastReceiver(
|
||||||
|
handleBondStateChanged,
|
||||||
|
actions=[defs.BluetoothDevice.ACTION_BOND_STATE_CHANGED],
|
||||||
|
)
|
||||||
|
receiver.start()
|
||||||
|
try:
|
||||||
|
# See if it is already paired.
|
||||||
|
bond_state = self.__device.getBondState()
|
||||||
|
if bond_state == defs.BluetoothDevice.BOND_BONDED:
|
||||||
|
return
|
||||||
|
elif bond_state == defs.BluetoothDevice.BOND_NONE:
|
||||||
|
logger.debug(f"Pairing to BLE device @ {self.address}")
|
||||||
|
if not self.__device.createBond():
|
||||||
|
raise BleakError(
|
||||||
|
f"Could not initiate bonding with device @ {self.address}"
|
||||||
|
)
|
||||||
|
await bondedFuture
|
||||||
|
finally:
|
||||||
|
await receiver.stop()
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def unpair(self) -> None:
|
||||||
|
"""Unpair with the peripheral."""
|
||||||
|
warnings.warn(
|
||||||
|
"Unpairing is seemingly unavailable in the Android API at the moment."
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
@override
|
||||||
|
def is_connected(self) -> bool:
|
||||||
|
"""Check connection status between this client and the server.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Boolean representing connection status.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
self.__callbacks is not None
|
||||||
|
and self.__callbacks.states["onConnectionStateChange"][1]
|
||||||
|
== defs.BluetoothProfile.STATE_CONNECTED
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
@override
|
||||||
|
def mtu_size(self) -> int:
|
||||||
|
return self.__mtu
|
||||||
|
|
||||||
|
# GATT services methods
|
||||||
|
|
||||||
|
async def _get_services(self) -> BleakGATTServiceCollection:
|
||||||
|
"""Get all services registered for this GATT server.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A :py:class:`bleak.backends.service.BleakGATTServiceCollection` with this device's services tree.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self.services is not None:
|
||||||
|
return self.services
|
||||||
|
|
||||||
|
services = BleakGATTServiceCollection()
|
||||||
|
|
||||||
|
logger.debug("Get Services...")
|
||||||
|
for java_service in self.__gatt.getServices():
|
||||||
|
if (
|
||||||
|
self._requested_services is not None
|
||||||
|
and java_service.getUuid() not in self._requested_services
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
service = BleakGATTService(
|
||||||
|
java_service,
|
||||||
|
java_service.getInstanceId(),
|
||||||
|
java_service.getUuid().toString(),
|
||||||
|
)
|
||||||
|
services.add_service(service)
|
||||||
|
|
||||||
|
for java_characteristic in java_service.getCharacteristics():
|
||||||
|
|
||||||
|
characteristic = BleakGATTCharacteristic(
|
||||||
|
java_characteristic,
|
||||||
|
java_characteristic.getInstanceId(),
|
||||||
|
java_characteristic.getUuid().toString(),
|
||||||
|
gatt_char_props_to_strs(java_characteristic.getProperties()),
|
||||||
|
lambda: self.__mtu - 3,
|
||||||
|
service,
|
||||||
|
)
|
||||||
|
services.add_characteristic(characteristic)
|
||||||
|
|
||||||
|
for descriptor_index, java_descriptor in enumerate(
|
||||||
|
java_characteristic.getDescriptors()
|
||||||
|
):
|
||||||
|
|
||||||
|
descriptor = BleakGATTDescriptor(
|
||||||
|
java_descriptor,
|
||||||
|
characteristic.handle + 1 + descriptor_index,
|
||||||
|
java_descriptor.getUuid().toString(),
|
||||||
|
characteristic,
|
||||||
|
)
|
||||||
|
services.add_descriptor(descriptor)
|
||||||
|
|
||||||
|
self.services = services
|
||||||
|
return self.services
|
||||||
|
|
||||||
|
# IO methods
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def read_gatt_char(
|
||||||
|
self,
|
||||||
|
characteristic: BleakGATTCharacteristic,
|
||||||
|
*,
|
||||||
|
use_cached: bool = False,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> bytearray:
|
||||||
|
"""Perform read operation on the specified GATT characteristic.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
characteristic (BleakGATTCharacteristic): The characteristic to read from.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(bytearray) The read data.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if use_cached:
|
||||||
|
logger.debug(
|
||||||
|
"Reading cached characteristic values is not implemented on Android"
|
||||||
|
)
|
||||||
|
|
||||||
|
(value,) = await self.__callbacks.perform_and_wait(
|
||||||
|
dispatchApi=self.__gatt.readCharacteristic,
|
||||||
|
dispatchParams=(characteristic.obj,),
|
||||||
|
resultApi=("onCharacteristicRead", characteristic.handle),
|
||||||
|
)
|
||||||
|
value = bytearray(value)
|
||||||
|
logger.debug(
|
||||||
|
f"Read Characteristic {characteristic.uuid} | {characteristic.handle}: {value}"
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def read_gatt_descriptor(
|
||||||
|
self,
|
||||||
|
descriptor: BleakGATTDescriptor,
|
||||||
|
*,
|
||||||
|
use_cached: bool = False,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> bytearray:
|
||||||
|
"""Perform read operation on the specified GATT descriptor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
descriptor: The descriptor to read from.
|
||||||
|
use_cached: Whether to use cached value.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The read data.
|
||||||
|
"""
|
||||||
|
if use_cached:
|
||||||
|
logger.debug(
|
||||||
|
"Reading cached descriptor values is not implemented on Android"
|
||||||
|
)
|
||||||
|
|
||||||
|
(value,) = await self.__callbacks.perform_and_wait(
|
||||||
|
dispatchApi=self.__gatt.readDescriptor,
|
||||||
|
dispatchParams=(descriptor.obj,),
|
||||||
|
resultApi=("onDescriptorRead", descriptor.uuid),
|
||||||
|
)
|
||||||
|
value = bytearray(value)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"Read Descriptor {descriptor.uuid} | {descriptor.handle}: {value}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def write_gatt_char(
|
||||||
|
self, characteristic: BleakGATTCharacteristic, data: bytearray, response: bool
|
||||||
|
) -> None:
|
||||||
|
if response:
|
||||||
|
characteristic.obj.setWriteType(
|
||||||
|
defs.BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
characteristic.obj.setWriteType(
|
||||||
|
defs.BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
||||||
|
)
|
||||||
|
|
||||||
|
characteristic.obj.setValue(data)
|
||||||
|
|
||||||
|
await self.__callbacks.perform_and_wait(
|
||||||
|
dispatchApi=self.__gatt.writeCharacteristic,
|
||||||
|
dispatchParams=(characteristic.obj,),
|
||||||
|
resultApi=("onCharacteristicWrite", characteristic.handle),
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"Write Characteristic {characteristic.uuid} | {characteristic.handle}: {data}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def write_gatt_descriptor(
|
||||||
|
self,
|
||||||
|
desc_specifier: Union[BleakGATTDescriptor, str, uuid.UUID],
|
||||||
|
data: bytearray,
|
||||||
|
) -> None:
|
||||||
|
"""Perform a write operation on the specified GATT descriptor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
desc_specifier (BleakGATTDescriptor, str or UUID): The descriptor to write
|
||||||
|
to, specified by either UUID or directly by the
|
||||||
|
BleakGATTDescriptor object representing it.
|
||||||
|
data (bytes or bytearray): The data to send.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not isinstance(desc_specifier, BleakGATTDescriptor):
|
||||||
|
descriptor = self.services.get_descriptor(desc_specifier)
|
||||||
|
else:
|
||||||
|
descriptor = desc_specifier
|
||||||
|
|
||||||
|
if not descriptor:
|
||||||
|
raise BleakError(f"Descriptor {desc_specifier} was not found!")
|
||||||
|
|
||||||
|
descriptor.obj.setValue(data)
|
||||||
|
|
||||||
|
await self.__callbacks.perform_and_wait(
|
||||||
|
dispatchApi=self.__gatt.writeDescriptor,
|
||||||
|
dispatchParams=(descriptor.obj,),
|
||||||
|
resultApi=("onDescriptorWrite", descriptor.uuid),
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"Write Descriptor {descriptor.uuid} | {descriptor.handle}: {data}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def start_notify(
|
||||||
|
self,
|
||||||
|
characteristic: BleakGATTCharacteristic,
|
||||||
|
callback: NotifyCallback,
|
||||||
|
**kwargs,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Activate notifications/indications on a characteristic.
|
||||||
|
"""
|
||||||
|
self._subscriptions[characteristic.handle] = callback
|
||||||
|
|
||||||
|
assert self.__gatt is not None
|
||||||
|
|
||||||
|
if not self.__gatt.setCharacteristicNotification(characteristic.obj, True):
|
||||||
|
raise BleakError(
|
||||||
|
f"Failed to enable notification for characteristic {characteristic.uuid}"
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.write_gatt_descriptor(
|
||||||
|
characteristic.get_descriptor(
|
||||||
|
defs.CLIENT_CHARACTERISTIC_CONFIGURATION_UUID
|
||||||
|
),
|
||||||
|
defs.BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE,
|
||||||
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def stop_notify(self, characteristic: BleakGATTCharacteristic) -> None:
|
||||||
|
"""Deactivate notification/indication on a specified characteristic.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
characteristic (BleakGATTCharacteristic): The characteristic to deactivate
|
||||||
|
notification/indication on,.
|
||||||
|
|
||||||
|
"""
|
||||||
|
await self.write_gatt_descriptor(
|
||||||
|
characteristic.get_descriptor(
|
||||||
|
defs.CLIENT_CHARACTERISTIC_CONFIGURATION_UUID
|
||||||
|
),
|
||||||
|
defs.BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self.__gatt.setCharacteristicNotification(characteristic.obj, False):
|
||||||
|
raise BleakError(
|
||||||
|
f"Failed to disable notification for characteristic {characteristic.uuid}"
|
||||||
|
)
|
||||||
|
del self._subscriptions[characteristic.handle]
|
||||||
|
|
||||||
|
|
||||||
|
class _PythonBluetoothGattCallback(utils.AsyncJavaCallbacks):
|
||||||
|
__javainterfaces__ = [
|
||||||
|
"com.github.hbldh.bleak.PythonBluetoothGattCallback$Interface"
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, client, loop):
|
||||||
|
super().__init__(loop)
|
||||||
|
self._client = client
|
||||||
|
self.java = defs.PythonBluetoothGattCallback(self)
|
||||||
|
|
||||||
|
def result_state(self, status, resultApi, *data):
|
||||||
|
if status == defs.BluetoothGatt.GATT_SUCCESS:
|
||||||
|
failure_str = None
|
||||||
|
else:
|
||||||
|
failure_str = defs.GATT_STATUS_STRINGS.get(status, status)
|
||||||
|
self._loop.call_soon_threadsafe(
|
||||||
|
self._result_state_unthreadsafe, failure_str, resultApi, data
|
||||||
|
)
|
||||||
|
|
||||||
|
@java_method("(II)V")
|
||||||
|
def onConnectionStateChange(self, status, new_state):
|
||||||
|
try:
|
||||||
|
self.result_state(status, "onConnectionStateChange", new_state)
|
||||||
|
except BleakError:
|
||||||
|
pass
|
||||||
|
if (
|
||||||
|
new_state == defs.BluetoothProfile.STATE_DISCONNECTED
|
||||||
|
and self._client._disconnected_callback is not None
|
||||||
|
):
|
||||||
|
self._client._disconnected_callback()
|
||||||
|
|
||||||
|
@java_method("(II)V")
|
||||||
|
def onMtuChanged(self, mtu, status):
|
||||||
|
self.result_state(status, "onMtuChanged", mtu)
|
||||||
|
|
||||||
|
@java_method("(I)V")
|
||||||
|
def onServicesDiscovered(self, status):
|
||||||
|
self.result_state(status, "onServicesDiscovered")
|
||||||
|
|
||||||
|
@java_method("(I[B)V")
|
||||||
|
def onCharacteristicChanged(self, handle, value):
|
||||||
|
self._loop.call_soon_threadsafe(
|
||||||
|
self._client._subscriptions[handle], bytearray(value.tolist())
|
||||||
|
)
|
||||||
|
|
||||||
|
@java_method("(II[B)V")
|
||||||
|
def onCharacteristicRead(self, handle, status, value):
|
||||||
|
self.result_state(
|
||||||
|
status, ("onCharacteristicRead", handle), bytes(value.tolist())
|
||||||
|
)
|
||||||
|
|
||||||
|
@java_method("(II)V")
|
||||||
|
def onCharacteristicWrite(self, handle, status):
|
||||||
|
self.result_state(status, ("onCharacteristicWrite", handle))
|
||||||
|
|
||||||
|
@java_method("(Ljava/lang/String;I[B)V")
|
||||||
|
def onDescriptorRead(self, uuid, status, value):
|
||||||
|
self.result_state(status, ("onDescriptorRead", uuid), bytes(value.tolist()))
|
||||||
|
|
||||||
|
@java_method("(Ljava/lang/String;I)V")
|
||||||
|
def onDescriptorWrite(self, uuid, status):
|
||||||
|
self.result_state(status, ("onDescriptorWrite", uuid))
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
import sys
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
if sys.platform != "android":
|
||||||
|
assert False, "This backend is only available on Android"
|
||||||
|
|
||||||
|
import enum
|
||||||
|
|
||||||
|
from jnius import autoclass, cast
|
||||||
|
|
||||||
|
import bleak.exc
|
||||||
|
from bleak.uuids import normalize_uuid_16
|
||||||
|
|
||||||
|
# caching constants avoids unnecessary extra use of the jni-python interface, which can be slow
|
||||||
|
|
||||||
|
List = autoclass("java.util.ArrayList")
|
||||||
|
UUID = autoclass("java.util.UUID")
|
||||||
|
BluetoothAdapter = autoclass("android.bluetooth.BluetoothAdapter")
|
||||||
|
ScanCallback = autoclass("android.bluetooth.le.ScanCallback")
|
||||||
|
ScanFilter = autoclass("android.bluetooth.le.ScanFilter")
|
||||||
|
ScanFilterBuilder = autoclass("android.bluetooth.le.ScanFilter$Builder")
|
||||||
|
ScanSettings = autoclass("android.bluetooth.le.ScanSettings")
|
||||||
|
ScanSettingsBuilder = autoclass("android.bluetooth.le.ScanSettings$Builder")
|
||||||
|
BluetoothDevice = autoclass("android.bluetooth.BluetoothDevice")
|
||||||
|
BluetoothGatt = autoclass("android.bluetooth.BluetoothGatt")
|
||||||
|
BluetoothGattCharacteristic = autoclass("android.bluetooth.BluetoothGattCharacteristic")
|
||||||
|
BluetoothGattDescriptor = autoclass("android.bluetooth.BluetoothGattDescriptor")
|
||||||
|
BluetoothProfile = autoclass("android.bluetooth.BluetoothProfile")
|
||||||
|
|
||||||
|
PythonActivity = autoclass("org.kivy.android.PythonActivity")
|
||||||
|
ParcelUuid = autoclass("android.os.ParcelUuid")
|
||||||
|
activity = cast("android.app.Activity", PythonActivity.mActivity)
|
||||||
|
context = cast("android.content.Context", activity.getApplicationContext())
|
||||||
|
|
||||||
|
ScanResult = autoclass("android.bluetooth.le.ScanResult")
|
||||||
|
|
||||||
|
BLEAK_JNI_NAMESPACE = "com.github.hbldh.bleak"
|
||||||
|
PythonScanCallback = autoclass(BLEAK_JNI_NAMESPACE + ".PythonScanCallback")
|
||||||
|
PythonBluetoothGattCallback = autoclass(
|
||||||
|
BLEAK_JNI_NAMESPACE + ".PythonBluetoothGattCallback"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ScanFailed(enum.IntEnum):
|
||||||
|
ALREADY_STARTED = ScanCallback.SCAN_FAILED_ALREADY_STARTED
|
||||||
|
APPLICATION_REGISTRATION_FAILED = (
|
||||||
|
ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED
|
||||||
|
)
|
||||||
|
FEATURE_UNSUPPORTED = ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED
|
||||||
|
INTERNAL_ERROR = ScanCallback.SCAN_FAILED_INTERNAL_ERROR
|
||||||
|
|
||||||
|
|
||||||
|
GATT_SUCCESS = 0x0000
|
||||||
|
# TODO: we may need different lookups, e.g. one for bleak.exc.CONTROLLER_ERROR_CODES
|
||||||
|
GATT_STATUS_STRINGS = {
|
||||||
|
# https://developer.android.com/reference/android/bluetooth/BluetoothGatt
|
||||||
|
# https://android.googlesource.com/platform/external/bluetooth/bluedroid/+/5738f83aeb59361a0a2eda2460113f6dc9194271/stack/include/gatt_api.h
|
||||||
|
# https://android.googlesource.com/platform/system/bt/+/master/stack/include/gatt_api.h
|
||||||
|
# https://www.bluetooth.com/specifications/bluetooth-core-specification/
|
||||||
|
**bleak.exc.PROTOCOL_ERROR_CODES,
|
||||||
|
0x007F: "Too Short",
|
||||||
|
0x0080: "No Resources",
|
||||||
|
0x0081: "Internal Error",
|
||||||
|
0x0082: "Wrong State",
|
||||||
|
0x0083: "DB Full",
|
||||||
|
0x0084: "Busy",
|
||||||
|
0x0085: "Error",
|
||||||
|
0x0086: "Command Started",
|
||||||
|
0x0087: "Illegal Parameter",
|
||||||
|
0x0088: "Pending",
|
||||||
|
0x0089: "Auth Failure",
|
||||||
|
0x008A: "More",
|
||||||
|
0x008B: "Invalid Configuration",
|
||||||
|
0x008C: "Service Started",
|
||||||
|
0x008D: "Encrypted No MITM",
|
||||||
|
0x008E: "Not Encrypted",
|
||||||
|
0x008F: "Congested",
|
||||||
|
0x0090: "Duplicate Reg",
|
||||||
|
0x0091: "Already Open",
|
||||||
|
0x0092: "Cancel",
|
||||||
|
0x0101: "Failure",
|
||||||
|
}
|
||||||
|
|
||||||
|
CLIENT_CHARACTERISTIC_CONFIGURATION_UUID = normalize_uuid_16(0x2902)
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
package com.github.hbldh.bleak;
|
||||||
|
|
||||||
|
import java.net.ConnectException;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CancellationException;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothGatt;
|
||||||
|
import android.bluetooth.BluetoothGattCallback;
|
||||||
|
import android.bluetooth.BluetoothGattCharacteristic;
|
||||||
|
import android.bluetooth.BluetoothGattDescriptor;
|
||||||
|
import android.bluetooth.BluetoothProfile;
|
||||||
|
|
||||||
|
|
||||||
|
public final class PythonBluetoothGattCallback extends BluetoothGattCallback
|
||||||
|
{
|
||||||
|
public interface Interface
|
||||||
|
{
|
||||||
|
public void onConnectionStateChange(int status, int newState);
|
||||||
|
public void onMtuChanged(int mtu, int status);
|
||||||
|
public void onServicesDiscovered(int status);
|
||||||
|
public void onCharacteristicChanged(int handle, byte[] value);
|
||||||
|
public void onCharacteristicRead(int handle, int status, byte[] value);
|
||||||
|
public void onCharacteristicWrite(int handle, int status);
|
||||||
|
public void onDescriptorRead(String uuid, int status, byte[] value);
|
||||||
|
public void onDescriptorWrite(String uuid, int status);
|
||||||
|
}
|
||||||
|
private Interface callback;
|
||||||
|
|
||||||
|
public PythonBluetoothGattCallback(Interface pythonCallback)
|
||||||
|
{
|
||||||
|
callback = pythonCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState)
|
||||||
|
{
|
||||||
|
callback.onConnectionStateChange(status, newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status)
|
||||||
|
{
|
||||||
|
callback.onMtuChanged(mtu, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServicesDiscovered(BluetoothGatt gatt, int status)
|
||||||
|
{
|
||||||
|
callback.onServicesDiscovered(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status)
|
||||||
|
{
|
||||||
|
callback.onCharacteristicRead(characteristic.getInstanceId(), status, characteristic.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status)
|
||||||
|
{
|
||||||
|
callback.onCharacteristicWrite(characteristic.getInstanceId(), status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic)
|
||||||
|
{
|
||||||
|
callback.onCharacteristicChanged(characteristic.getInstanceId(), characteristic.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status)
|
||||||
|
{
|
||||||
|
callback.onDescriptorRead(descriptor.getUuid().toString(), status, descriptor.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status)
|
||||||
|
{
|
||||||
|
callback.onDescriptorWrite(descriptor.getUuid().toString(), status);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
package com.github.hbldh.bleak;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import android.bluetooth.le.ScanCallback;
|
||||||
|
import android.bluetooth.le.ScanResult;
|
||||||
|
|
||||||
|
public final class PythonScanCallback extends ScanCallback
|
||||||
|
{
|
||||||
|
public interface Interface
|
||||||
|
{
|
||||||
|
public void onScanFailed(int code);
|
||||||
|
public void onScanResult(ScanResult result);
|
||||||
|
}
|
||||||
|
private Interface callback;
|
||||||
|
|
||||||
|
public PythonScanCallback(Interface pythonCallback)
|
||||||
|
{
|
||||||
|
callback = pythonCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBatchScanResults(List<ScanResult> results)
|
||||||
|
{
|
||||||
|
for (ScanResult result : results) {
|
||||||
|
callback.onScanResult(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onScanFailed(int errorCode)
|
||||||
|
{
|
||||||
|
callback.onScanFailed(errorCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onScanResult(int callbackType, ScanResult result)
|
||||||
|
{
|
||||||
|
callback.onScanResult(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
import os
|
||||||
|
from os.path import join
|
||||||
|
|
||||||
|
import sh
|
||||||
|
from pythonforandroid.recipe import PythonRecipe
|
||||||
|
from pythonforandroid.toolchain import info, shprint
|
||||||
|
|
||||||
|
|
||||||
|
class BleakRecipe(PythonRecipe):
|
||||||
|
version = None # Must be none for p4a to correctly clone repo
|
||||||
|
fix_setup_py_version = "bleak develop branch"
|
||||||
|
url = "git+https://github.com/hbldh/bleak.git"
|
||||||
|
name = "bleak"
|
||||||
|
|
||||||
|
depends = ["pyjnius"]
|
||||||
|
call_hostpython_via_targetpython = False
|
||||||
|
|
||||||
|
fix_setup_filename = "fix_setup.py"
|
||||||
|
|
||||||
|
def prepare_build_dir(self, arch):
|
||||||
|
super().prepare_build_dir(arch) # Unpack the url file to the get_build_dir
|
||||||
|
build_dir = self.get_build_dir(arch)
|
||||||
|
|
||||||
|
setup_py_path = join(build_dir, "setup.py")
|
||||||
|
if not os.path.exists(setup_py_path):
|
||||||
|
# Perform the p4a temporary fix
|
||||||
|
# At the moment, p4a recipe installing requires setup.py to be present
|
||||||
|
# So, we create a setup.py file only for android
|
||||||
|
|
||||||
|
fix_setup_py_path = join(self.get_recipe_dir(), self.fix_setup_filename)
|
||||||
|
with open(fix_setup_py_path, "r") as f:
|
||||||
|
contents = f.read()
|
||||||
|
|
||||||
|
# Write to the correct location and fill in the version number
|
||||||
|
with open(setup_py_path, "w") as f:
|
||||||
|
f.write(contents.replace("[VERSION]", self.fix_setup_py_version))
|
||||||
|
else:
|
||||||
|
info("setup.py found in bleak directory, are you installing older version?")
|
||||||
|
|
||||||
|
def get_recipe_env(self, arch=None, with_flags_in_cc=True):
|
||||||
|
env = super().get_recipe_env(arch, with_flags_in_cc)
|
||||||
|
# to find jnius and identify p4a
|
||||||
|
env["PYJNIUS_PACKAGES"] = self.ctx.get_site_packages_dir(arch)
|
||||||
|
return env
|
||||||
|
|
||||||
|
def postbuild_arch(self, arch):
|
||||||
|
super().postbuild_arch(arch)
|
||||||
|
|
||||||
|
info("Copying java files")
|
||||||
|
dest_dir = self.ctx.javaclass_dir
|
||||||
|
path = join(
|
||||||
|
self.get_build_dir(arch.arch), "bleak", "backends", "p4android", "java", "."
|
||||||
|
)
|
||||||
|
|
||||||
|
shprint(sh.cp, "-a", path, dest_dir)
|
||||||
|
|
||||||
|
|
||||||
|
recipe = BleakRecipe()
|
||||||
Binary file not shown.
Binary file not shown.
@ -0,0 +1,10 @@
|
|||||||
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
|
VERSION = "[VERSION]" # Version will be filled in by the bleak recipe
|
||||||
|
NAME = "bleak"
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name=NAME,
|
||||||
|
version=VERSION,
|
||||||
|
packages=find_packages(exclude=("tests", "examples", "docs")),
|
||||||
|
)
|
||||||
@ -0,0 +1,293 @@
|
|||||||
|
import sys
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
if sys.platform != "android":
|
||||||
|
assert False, "This backend is only available on Android"
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import warnings
|
||||||
|
from typing import Literal, Optional
|
||||||
|
|
||||||
|
from android.broadcast import BroadcastReceiver
|
||||||
|
from android.permissions import Permission, request_permissions
|
||||||
|
from jnius import cast, java_method
|
||||||
|
|
||||||
|
from bleak._compat import override
|
||||||
|
from bleak._compat import timeout as async_timeout
|
||||||
|
from bleak.backends.p4android import defs, utils
|
||||||
|
from bleak.backends.scanner import (
|
||||||
|
AdvertisementData,
|
||||||
|
AdvertisementDataCallback,
|
||||||
|
BaseBleakScanner,
|
||||||
|
)
|
||||||
|
from bleak.exc import BleakError
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BleakScannerP4Android(BaseBleakScanner):
|
||||||
|
"""
|
||||||
|
The python-for-android Bleak BLE Scanner.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
detection_callback:
|
||||||
|
Optional function that will be called each time a device is
|
||||||
|
discovered or advertising data has changed.
|
||||||
|
service_uuids:
|
||||||
|
Optional list of service UUIDs to filter on. Only advertisements
|
||||||
|
containing this advertising data will be received. Specifying this
|
||||||
|
also enables scanning while the screen is off on Android.
|
||||||
|
scanning_mode:
|
||||||
|
Set to ``"passive"`` to avoid the ``"active"`` scanning mode.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__scanner = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
detection_callback: Optional[AdvertisementDataCallback],
|
||||||
|
service_uuids: Optional[list[str]],
|
||||||
|
scanning_mode: Literal["active", "passive"],
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
super().__init__(detection_callback, service_uuids)
|
||||||
|
|
||||||
|
if scanning_mode == "passive":
|
||||||
|
self.__scan_mode = defs.ScanSettings.SCAN_MODE_OPPORTUNISTIC
|
||||||
|
else:
|
||||||
|
self.__scan_mode = defs.ScanSettings.SCAN_MODE_LOW_LATENCY
|
||||||
|
|
||||||
|
self.__javascanner = None
|
||||||
|
self.__callback = None
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def start(self) -> None:
|
||||||
|
if BleakScannerP4Android.__scanner is not None:
|
||||||
|
raise BleakError("A BleakScanner is already scanning on this adapter.")
|
||||||
|
|
||||||
|
logger.debug("Starting BTLE scan")
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
|
if self.__javascanner is None:
|
||||||
|
if self.__callback is None:
|
||||||
|
self.__callback = _PythonScanCallback(self, loop)
|
||||||
|
|
||||||
|
permission_acknowledged = loop.create_future()
|
||||||
|
|
||||||
|
def handle_permissions(permissions, grantResults):
|
||||||
|
if any(grantResults):
|
||||||
|
loop.call_soon_threadsafe(
|
||||||
|
permission_acknowledged.set_result, grantResults
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
loop.call_soon_threadsafe(
|
||||||
|
permission_acknowledged.set_exception(
|
||||||
|
BleakError("User denied access to " + str(permissions))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
request_permissions(
|
||||||
|
[
|
||||||
|
Permission.ACCESS_FINE_LOCATION,
|
||||||
|
Permission.ACCESS_COARSE_LOCATION,
|
||||||
|
"android.permission.ACCESS_BACKGROUND_LOCATION",
|
||||||
|
],
|
||||||
|
handle_permissions,
|
||||||
|
)
|
||||||
|
await permission_acknowledged
|
||||||
|
|
||||||
|
adapter = defs.BluetoothAdapter.getDefaultAdapter()
|
||||||
|
if adapter is None:
|
||||||
|
raise BleakError("Bluetooth is not supported on this hardware platform")
|
||||||
|
if adapter.getState() != defs.BluetoothAdapter.STATE_ON:
|
||||||
|
raise BleakError("Bluetooth is not turned on")
|
||||||
|
|
||||||
|
self.__javascanner = adapter.getBluetoothLeScanner()
|
||||||
|
|
||||||
|
BleakScannerP4Android.__scanner = self
|
||||||
|
|
||||||
|
filters = cast("java.util.List", defs.List())
|
||||||
|
if self._service_uuids:
|
||||||
|
for uuid in self._service_uuids:
|
||||||
|
filters.add(
|
||||||
|
defs.ScanFilterBuilder()
|
||||||
|
.setServiceUuid(defs.ParcelUuid.fromString(uuid))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
scanfuture = self.__callback.perform_and_wait(
|
||||||
|
dispatchApi=self.__javascanner.startScan,
|
||||||
|
dispatchParams=(
|
||||||
|
filters,
|
||||||
|
defs.ScanSettingsBuilder()
|
||||||
|
.setScanMode(self.__scan_mode)
|
||||||
|
.setReportDelay(0)
|
||||||
|
.setPhy(defs.ScanSettings.PHY_LE_ALL_SUPPORTED)
|
||||||
|
.setNumOfMatches(defs.ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT)
|
||||||
|
.setMatchMode(defs.ScanSettings.MATCH_MODE_AGGRESSIVE)
|
||||||
|
.setCallbackType(defs.ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
|
||||||
|
.build(),
|
||||||
|
self.__callback.java,
|
||||||
|
),
|
||||||
|
resultApi="onScan",
|
||||||
|
return_indicates_status=False,
|
||||||
|
)
|
||||||
|
self.__javascanner.flushPendingScanResults(self.__callback.java)
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with async_timeout(0.2):
|
||||||
|
await scanfuture
|
||||||
|
except asyncio.exceptions.TimeoutError:
|
||||||
|
pass
|
||||||
|
except BleakError as bleakerror:
|
||||||
|
await self.stop()
|
||||||
|
if bleakerror.args != (
|
||||||
|
"onScan",
|
||||||
|
"SCAN_FAILED_APPLICATION_REGISTRATION_FAILED",
|
||||||
|
):
|
||||||
|
raise bleakerror
|
||||||
|
else:
|
||||||
|
# there might be a clearer solution to this if android source and vendor
|
||||||
|
# documentation are reviewed for the meaning of the error
|
||||||
|
# https://stackoverflow.com/questions/27516399/solution-for-ble-scans-scan-failed-application-registration-failed
|
||||||
|
warnings.warn(
|
||||||
|
"BT API gave SCAN_FAILED_APPLICATION_REGISTRATION_FAILED. Resetting adapter."
|
||||||
|
)
|
||||||
|
|
||||||
|
def handlerWaitingForState(state, stateFuture):
|
||||||
|
def handleAdapterStateChanged(context, intent):
|
||||||
|
adapter_state = intent.getIntExtra(
|
||||||
|
defs.BluetoothAdapter.EXTRA_STATE,
|
||||||
|
defs.BluetoothAdapter.STATE_ERROR,
|
||||||
|
)
|
||||||
|
if adapter_state == defs.BluetoothAdapter.STATE_ERROR:
|
||||||
|
loop.call_soon_threadsafe(
|
||||||
|
stateOffFuture.set_exception,
|
||||||
|
BleakError(f"Unexpected adapter state {adapter_state}"),
|
||||||
|
)
|
||||||
|
elif adapter_state == state:
|
||||||
|
loop.call_soon_threadsafe(
|
||||||
|
stateFuture.set_result, adapter_state
|
||||||
|
)
|
||||||
|
|
||||||
|
return handleAdapterStateChanged
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"disabling bluetooth adapter to handle SCAN_FAILED_APPLICATION_REGSTRATION_FAILED ..."
|
||||||
|
)
|
||||||
|
stateOffFuture = loop.create_future()
|
||||||
|
receiver = BroadcastReceiver(
|
||||||
|
handlerWaitingForState(
|
||||||
|
defs.BluetoothAdapter.STATE_OFF, stateOffFuture
|
||||||
|
),
|
||||||
|
actions=[defs.BluetoothAdapter.ACTION_STATE_CHANGED],
|
||||||
|
)
|
||||||
|
receiver.start()
|
||||||
|
try:
|
||||||
|
adapter.disable()
|
||||||
|
await stateOffFuture
|
||||||
|
finally:
|
||||||
|
receiver.stop()
|
||||||
|
|
||||||
|
logger.info("re-enabling bluetooth adapter ...")
|
||||||
|
stateOnFuture = loop.create_future()
|
||||||
|
receiver = BroadcastReceiver(
|
||||||
|
handlerWaitingForState(
|
||||||
|
defs.BluetoothAdapter.STATE_ON, stateOnFuture
|
||||||
|
),
|
||||||
|
actions=[defs.BluetoothAdapter.ACTION_STATE_CHANGED],
|
||||||
|
)
|
||||||
|
receiver.start()
|
||||||
|
try:
|
||||||
|
adapter.enable()
|
||||||
|
await stateOnFuture
|
||||||
|
finally:
|
||||||
|
receiver.stop()
|
||||||
|
logger.debug("restarting scan ...")
|
||||||
|
|
||||||
|
return await self.start()
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def stop(self) -> None:
|
||||||
|
if self.__javascanner is not None:
|
||||||
|
logger.debug("Stopping BTLE scan")
|
||||||
|
self.__javascanner.stopScan(self.__callback.java)
|
||||||
|
BleakScannerP4Android.__scanner = None
|
||||||
|
self.__javascanner = None
|
||||||
|
else:
|
||||||
|
logger.debug("BTLE scan already stopped")
|
||||||
|
|
||||||
|
def _handle_scan_result(self, result) -> None:
|
||||||
|
native_device = result.getDevice()
|
||||||
|
record = result.getScanRecord()
|
||||||
|
|
||||||
|
service_uuids = record.getServiceUuids()
|
||||||
|
if service_uuids is not None:
|
||||||
|
service_uuids = [service_uuid.toString() for service_uuid in service_uuids]
|
||||||
|
|
||||||
|
if not self.is_allowed_uuid(service_uuids):
|
||||||
|
return
|
||||||
|
|
||||||
|
manufacturer_data = record.getManufacturerSpecificData()
|
||||||
|
manufacturer_data = {
|
||||||
|
manufacturer_data.keyAt(index): bytes(manufacturer_data.valueAt(index))
|
||||||
|
for index in range(manufacturer_data.size())
|
||||||
|
}
|
||||||
|
|
||||||
|
service_data = {
|
||||||
|
entry.getKey().toString(): bytes(entry.getValue())
|
||||||
|
for entry in record.getServiceData().entrySet()
|
||||||
|
}
|
||||||
|
tx_power = record.getTxPowerLevel()
|
||||||
|
|
||||||
|
# change "not present" value to None to match other backends
|
||||||
|
if tx_power == -2147483648: # Integer#MIN_VALUE
|
||||||
|
tx_power = None
|
||||||
|
|
||||||
|
advertisement = AdvertisementData(
|
||||||
|
local_name=record.getDeviceName(),
|
||||||
|
manufacturer_data=manufacturer_data,
|
||||||
|
service_data=service_data,
|
||||||
|
service_uuids=service_uuids,
|
||||||
|
tx_power=tx_power,
|
||||||
|
rssi=result.getRssi(),
|
||||||
|
platform_data=(result,),
|
||||||
|
)
|
||||||
|
|
||||||
|
device = self.create_or_update_device(
|
||||||
|
native_device.getAddress(),
|
||||||
|
native_device.getAddress(),
|
||||||
|
native_device.getName(),
|
||||||
|
native_device,
|
||||||
|
advertisement,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.call_detection_callbacks(device, advertisement)
|
||||||
|
|
||||||
|
|
||||||
|
class _PythonScanCallback(utils.AsyncJavaCallbacks):
|
||||||
|
__javainterfaces__ = ["com.github.hbldh.bleak.PythonScanCallback$Interface"]
|
||||||
|
|
||||||
|
def __init__(self, scanner: BleakScannerP4Android, loop: asyncio.AbstractEventLoop):
|
||||||
|
super().__init__(loop)
|
||||||
|
self._scanner = scanner
|
||||||
|
self.java = defs.PythonScanCallback(self)
|
||||||
|
|
||||||
|
def result_state(self, status_str, name, *data):
|
||||||
|
self._loop.call_soon_threadsafe(
|
||||||
|
self._result_state_unthreadsafe, status_str, name, data
|
||||||
|
)
|
||||||
|
|
||||||
|
@java_method("(I)V")
|
||||||
|
def onScanFailed(self, errorCode):
|
||||||
|
self.result_state(defs.ScanFailed(errorCode).name, "onScan")
|
||||||
|
|
||||||
|
@java_method("(Landroid/bluetooth/le/ScanResult;)V")
|
||||||
|
def onScanResult(self, result):
|
||||||
|
self._loop.call_soon_threadsafe(self._scanner._handle_scan_result, result)
|
||||||
|
|
||||||
|
if "onScan" not in self.states:
|
||||||
|
self.result_state(None, "onScan", result)
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
import sys
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
if sys.platform != "android":
|
||||||
|
assert False, "This backend is only available on Android"
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from jnius import PythonJavaClass
|
||||||
|
|
||||||
|
from bleak.exc import BleakError
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncJavaCallbacks(PythonJavaClass):
|
||||||
|
__javacontext__ = "app"
|
||||||
|
|
||||||
|
def __init__(self, loop: asyncio.AbstractEventLoop):
|
||||||
|
self._loop = loop
|
||||||
|
self.states = {}
|
||||||
|
self.futures = {}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _if_expected(result, expected):
|
||||||
|
if result[: len(expected)] == expected[:]:
|
||||||
|
return result[len(expected) :]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def perform_and_wait(
|
||||||
|
self,
|
||||||
|
dispatchApi,
|
||||||
|
dispatchParams,
|
||||||
|
resultApi,
|
||||||
|
resultExpected=(),
|
||||||
|
unless_already=False,
|
||||||
|
return_indicates_status=True,
|
||||||
|
):
|
||||||
|
result2 = None
|
||||||
|
if unless_already:
|
||||||
|
if resultApi in self.states:
|
||||||
|
result2 = self._if_expected(self.states[resultApi][1:], resultExpected)
|
||||||
|
result1 = True
|
||||||
|
|
||||||
|
if result2 is not None:
|
||||||
|
logger.debug(
|
||||||
|
f"Not waiting for android api {resultApi} because found {resultExpected}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.debug(f"Waiting for android api {resultApi}")
|
||||||
|
|
||||||
|
state = self._loop.create_future()
|
||||||
|
self.futures[resultApi] = state
|
||||||
|
result1 = dispatchApi(*dispatchParams)
|
||||||
|
if return_indicates_status and not result1:
|
||||||
|
del self.futures[resultApi]
|
||||||
|
raise BleakError(f"api call failed, not waiting for {resultApi}")
|
||||||
|
data = await state
|
||||||
|
result2 = self._if_expected(data, resultExpected)
|
||||||
|
if result2 is None:
|
||||||
|
raise BleakError("Expected", resultExpected, "got", data)
|
||||||
|
|
||||||
|
logger.debug(f"{resultApi} succeeded {result2}")
|
||||||
|
|
||||||
|
if return_indicates_status:
|
||||||
|
return result2
|
||||||
|
else:
|
||||||
|
return (result1, *result2)
|
||||||
|
|
||||||
|
def _result_state_unthreadsafe(self, failure_str, source, data):
|
||||||
|
logger.debug(f"Java state transfer {source} error={failure_str} data={data}")
|
||||||
|
self.states[source] = (failure_str, *data)
|
||||||
|
future = self.futures.get(source, None)
|
||||||
|
if future is not None and not future.done():
|
||||||
|
if failure_str is None:
|
||||||
|
future.set_result(data)
|
||||||
|
else:
|
||||||
|
future.set_exception(BleakError(source, failure_str, *data))
|
||||||
|
else:
|
||||||
|
if failure_str is not None:
|
||||||
|
# an error happened with nothing waiting for it
|
||||||
|
exception = BleakError(source, failure_str, *data)
|
||||||
|
namedfutures = [
|
||||||
|
namedfuture
|
||||||
|
for namedfuture in self.futures.items()
|
||||||
|
if not namedfuture[1].done()
|
||||||
|
]
|
||||||
|
if len(namedfutures):
|
||||||
|
# send it on existing requests
|
||||||
|
for name, future in namedfutures:
|
||||||
|
warnings.warn(f"Redirecting error without home to {name}")
|
||||||
|
future.set_exception(exception)
|
||||||
|
else:
|
||||||
|
# send it on the event thread
|
||||||
|
raise exception
|
||||||
324
tools/python-embed/Lib/site-packages/bleak/backends/scanner.py
Normal file
324
tools/python-embed/Lib/site-packages/bleak/backends/scanner.py
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
import abc
|
||||||
|
import asyncio
|
||||||
|
import inspect
|
||||||
|
from collections.abc import Callable, Coroutine, Hashable
|
||||||
|
from typing import Any, NamedTuple, Optional
|
||||||
|
|
||||||
|
from bleak.backends import BleakBackend, get_default_backend
|
||||||
|
from bleak.backends.device import BLEDevice
|
||||||
|
from bleak.exc import BleakError
|
||||||
|
|
||||||
|
# prevent tasks from being garbage collected
|
||||||
|
_background_tasks: set[asyncio.Task[None]] = set()
|
||||||
|
|
||||||
|
|
||||||
|
class AdvertisementData(NamedTuple):
|
||||||
|
"""
|
||||||
|
Wrapper around the advertisement data that each platform returns upon discovery
|
||||||
|
"""
|
||||||
|
|
||||||
|
local_name: Optional[str]
|
||||||
|
"""
|
||||||
|
The local name of the device or ``None`` if not included in advertising data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
manufacturer_data: dict[int, bytes]
|
||||||
|
"""
|
||||||
|
Dictionary of manufacturer data in bytes from the received advertisement data or empty dict if not present.
|
||||||
|
|
||||||
|
The keys are Bluetooth SIG assigned Company Identifiers and the values are bytes.
|
||||||
|
|
||||||
|
https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers/
|
||||||
|
"""
|
||||||
|
|
||||||
|
service_data: dict[str, bytes]
|
||||||
|
"""
|
||||||
|
Dictionary of service data from the received advertisement data or empty dict if not present.
|
||||||
|
"""
|
||||||
|
|
||||||
|
service_uuids: list[str]
|
||||||
|
"""
|
||||||
|
List of service UUIDs from the received advertisement data or empty list if not present.
|
||||||
|
"""
|
||||||
|
|
||||||
|
tx_power: Optional[int]
|
||||||
|
"""
|
||||||
|
TX Power Level of the remote device from the received advertising data or ``None`` if not present.
|
||||||
|
|
||||||
|
.. versionadded:: 0.17
|
||||||
|
"""
|
||||||
|
|
||||||
|
rssi: int
|
||||||
|
"""
|
||||||
|
The Radio Receive Signal Strength (RSSI) in dBm.
|
||||||
|
|
||||||
|
.. versionadded:: 0.19
|
||||||
|
"""
|
||||||
|
|
||||||
|
platform_data: tuple[Any, ...]
|
||||||
|
"""
|
||||||
|
Tuple of platform specific data.
|
||||||
|
|
||||||
|
This is not a stable API. The actual values may change between releases.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
kwargs: list[str] = []
|
||||||
|
if self.local_name:
|
||||||
|
kwargs.append(f"local_name={repr(self.local_name)}")
|
||||||
|
if self.manufacturer_data:
|
||||||
|
kwargs.append(f"manufacturer_data={repr(self.manufacturer_data)}")
|
||||||
|
if self.service_data:
|
||||||
|
kwargs.append(f"service_data={repr(self.service_data)}")
|
||||||
|
if self.service_uuids:
|
||||||
|
kwargs.append(f"service_uuids={repr(self.service_uuids)}")
|
||||||
|
if self.tx_power is not None:
|
||||||
|
kwargs.append(f"tx_power={repr(self.tx_power)}")
|
||||||
|
kwargs.append(f"rssi={repr(self.rssi)}")
|
||||||
|
return f"AdvertisementData({', '.join(kwargs)})"
|
||||||
|
|
||||||
|
|
||||||
|
AdvertisementDataCallback = Callable[
|
||||||
|
[BLEDevice, AdvertisementData],
|
||||||
|
Optional[Coroutine[Any, Any, None]],
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
Type alias for callback called when advertisement data is received.
|
||||||
|
"""
|
||||||
|
|
||||||
|
AdvertisementDataFilter = Callable[
|
||||||
|
[BLEDevice, AdvertisementData],
|
||||||
|
bool,
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
Type alias for an advertisement data filter function.
|
||||||
|
|
||||||
|
Implementations should return ``True`` for matches, otherwise ``False``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class BaseBleakScanner(abc.ABC):
|
||||||
|
"""
|
||||||
|
Interface for Bleak Bluetooth LE Scanners
|
||||||
|
|
||||||
|
Args:
|
||||||
|
detection_callback:
|
||||||
|
Optional function that will be called each time a device is
|
||||||
|
discovered or advertising data has changed.
|
||||||
|
service_uuids:
|
||||||
|
Optional list of service UUIDs to filter on. Only advertisements
|
||||||
|
containing this advertising data will be received.
|
||||||
|
"""
|
||||||
|
|
||||||
|
seen_devices: dict[str, tuple[BLEDevice, AdvertisementData]]
|
||||||
|
"""
|
||||||
|
Map of device identifier to BLEDevice and most recent advertisement data.
|
||||||
|
|
||||||
|
The key is a backend-specific identifier for the device.
|
||||||
|
|
||||||
|
This map must be cleared when scanning starts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
detection_callback: Optional[AdvertisementDataCallback],
|
||||||
|
service_uuids: Optional[list[str]],
|
||||||
|
):
|
||||||
|
self._ad_callbacks: dict[
|
||||||
|
Hashable, Callable[[BLEDevice, AdvertisementData], None]
|
||||||
|
] = {}
|
||||||
|
"""
|
||||||
|
List of callbacks to call when an advertisement is received.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if detection_callback is not None:
|
||||||
|
self.register_detection_callback(detection_callback)
|
||||||
|
|
||||||
|
self._service_uuids: Optional[list[str]] = (
|
||||||
|
[u.lower() for u in service_uuids] if service_uuids is not None else None
|
||||||
|
)
|
||||||
|
|
||||||
|
self.seen_devices = {}
|
||||||
|
|
||||||
|
def register_detection_callback(
|
||||||
|
self, callback: Optional[AdvertisementDataCallback]
|
||||||
|
) -> Callable[[], None]:
|
||||||
|
"""
|
||||||
|
Register a callback that is called when an advertisement event from the
|
||||||
|
OS is received.
|
||||||
|
|
||||||
|
The ``callback`` is a function or coroutine that takes two arguments: :class:`BLEDevice`
|
||||||
|
and :class:`AdvertisementData`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
callback: A function, coroutine or ``None``.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A method that can be called to unregister the callback.
|
||||||
|
"""
|
||||||
|
error_text = "callback must be callable with 2 parameters"
|
||||||
|
|
||||||
|
if not callable(callback):
|
||||||
|
raise TypeError(error_text)
|
||||||
|
|
||||||
|
handler_signature = inspect.signature(callback)
|
||||||
|
|
||||||
|
if len(handler_signature.parameters) != 2:
|
||||||
|
raise TypeError(error_text)
|
||||||
|
|
||||||
|
if inspect.iscoroutinefunction(callback):
|
||||||
|
|
||||||
|
def detection_callback(s: BLEDevice, d: AdvertisementData) -> None:
|
||||||
|
task = asyncio.create_task(callback(s, d))
|
||||||
|
_background_tasks.add(task)
|
||||||
|
task.add_done_callback(_background_tasks.discard)
|
||||||
|
|
||||||
|
else:
|
||||||
|
detection_callback = callback # type: ignore
|
||||||
|
|
||||||
|
token = object()
|
||||||
|
|
||||||
|
self._ad_callbacks[token] = detection_callback
|
||||||
|
|
||||||
|
def remove() -> None:
|
||||||
|
self._ad_callbacks.pop(token, None)
|
||||||
|
|
||||||
|
return remove
|
||||||
|
|
||||||
|
def is_allowed_uuid(self, service_uuids: Optional[list[str]]) -> bool:
|
||||||
|
"""
|
||||||
|
Check if the advertisement data contains any of the service UUIDs
|
||||||
|
matching the filter. If no filter is set, this will always return
|
||||||
|
``True``.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
service_uuids: The service UUIDs from the advertisement data.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
``True`` if the advertisement data should be allowed or ``False``
|
||||||
|
if the advertisement data should be filtered out.
|
||||||
|
"""
|
||||||
|
# Backends will make best effort to filter out advertisements that
|
||||||
|
# don't match the service UUIDs, but if other apps are scanning at the
|
||||||
|
# same time or something like that, we may still receive advertisements
|
||||||
|
# that don't match. So we need to do more filtering here to get the
|
||||||
|
# expected behavior.
|
||||||
|
|
||||||
|
if not self._service_uuids:
|
||||||
|
# if there is no filter, everything is allowed
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not service_uuids:
|
||||||
|
# if there is a filter the advertisement data doesn't contain any
|
||||||
|
# service UUIDs, filter it out
|
||||||
|
return False
|
||||||
|
|
||||||
|
for uuid in service_uuids:
|
||||||
|
if uuid in self._service_uuids:
|
||||||
|
# match was found, keep this advertisement
|
||||||
|
return True
|
||||||
|
|
||||||
|
# there were no matching service uuids, filter this one out
|
||||||
|
return False
|
||||||
|
|
||||||
|
def call_detection_callbacks(
|
||||||
|
self, device: BLEDevice, advertisement_data: AdvertisementData
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Calls all registered detection callbacks.
|
||||||
|
|
||||||
|
Backend implementations should call this method when an advertisement
|
||||||
|
event is received from the OS.
|
||||||
|
"""
|
||||||
|
|
||||||
|
for callback in self._ad_callbacks.values():
|
||||||
|
callback(device, advertisement_data)
|
||||||
|
|
||||||
|
def create_or_update_device(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
address: str,
|
||||||
|
name: Optional[str],
|
||||||
|
details: Any,
|
||||||
|
adv: AdvertisementData,
|
||||||
|
) -> BLEDevice:
|
||||||
|
"""
|
||||||
|
Creates or updates a device in :attr:`seen_devices`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: A backend-specific identifier for the device.
|
||||||
|
address: The Bluetooth address of the device (UUID on macOS).
|
||||||
|
name: The OS display name for the device.
|
||||||
|
details: The platform-specific handle for the device.
|
||||||
|
adv: The most recent advertisement data received.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The updated device.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
device, _ = self.seen_devices[key]
|
||||||
|
|
||||||
|
device.name = name
|
||||||
|
except KeyError:
|
||||||
|
device = BLEDevice(address, name, details)
|
||||||
|
|
||||||
|
self.seen_devices[key] = (device, adv)
|
||||||
|
|
||||||
|
return device
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def start(self) -> None:
|
||||||
|
"""Start scanning for devices"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def stop(self) -> None:
|
||||||
|
"""Stop scanning for devices"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
def get_platform_scanner_backend_type() -> tuple[type[BaseBleakScanner], BleakBackend]:
|
||||||
|
"""
|
||||||
|
Gets the platform-specific :class:`BaseBleakScanner` type.
|
||||||
|
"""
|
||||||
|
backend = get_default_backend()
|
||||||
|
match backend:
|
||||||
|
case BleakBackend.P4ANDROID:
|
||||||
|
from bleak.backends.p4android.scanner import (
|
||||||
|
BleakScannerP4Android, # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
return (BleakScannerP4Android, backend) # type: ignore
|
||||||
|
|
||||||
|
case BleakBackend.BLUEZ_DBUS:
|
||||||
|
from bleak.backends.bluezdbus.scanner import (
|
||||||
|
BleakScannerBlueZDBus, # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
return (BleakScannerBlueZDBus, backend) # type: ignore
|
||||||
|
|
||||||
|
case BleakBackend.PYTHONISTA_CB:
|
||||||
|
try:
|
||||||
|
from bleak_pythonista import BleakScannerPythonistaCB # type: ignore
|
||||||
|
|
||||||
|
return (BleakScannerPythonistaCB, backend) # type: ignore
|
||||||
|
except ImportError as e:
|
||||||
|
raise ImportError(
|
||||||
|
"Ensure you have `bleak-pythonista` package installed."
|
||||||
|
) from e
|
||||||
|
|
||||||
|
case BleakBackend.CORE_BLUETOOTH:
|
||||||
|
from bleak.backends.corebluetooth.scanner import (
|
||||||
|
BleakScannerCoreBluetooth, # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
return (BleakScannerCoreBluetooth, backend) # type: ignore
|
||||||
|
|
||||||
|
case BleakBackend.WIN_RT:
|
||||||
|
from bleak.backends.winrt.scanner import BleakScannerWinRT # type: ignore
|
||||||
|
|
||||||
|
return (BleakScannerWinRT, backend) # type: ignore
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise BleakError(f"Unsupported backend: {backend}")
|
||||||
218
tools/python-embed/Lib/site-packages/bleak/backends/service.py
Normal file
218
tools/python-embed/Lib/site-packages/bleak/backends/service.py
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
# Created on 2019-03-19 by hbldh <henrik.blidh@nedomkull.com>
|
||||||
|
"""
|
||||||
|
Gatt Service Collection class and interface class for the Bleak representation of a GATT Service.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from collections.abc import Iterator
|
||||||
|
from typing import Any, Optional, Union, cast
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from bleak.backends.characteristic import BleakGATTCharacteristic
|
||||||
|
from bleak.backends.descriptor import BleakGATTDescriptor
|
||||||
|
from bleak.exc import BleakError
|
||||||
|
from bleak.uuids import normalize_uuid_str, uuidstr_to_str
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BleakGATTService:
|
||||||
|
"""The Bleak representation of a GATT Service."""
|
||||||
|
|
||||||
|
def __init__(self, obj: Any, handle: int, uuid: str) -> None:
|
||||||
|
self.obj = obj
|
||||||
|
self._handle = handle
|
||||||
|
self._uuid = uuid
|
||||||
|
self._characteristics: dict[int, BleakGATTCharacteristic] = {}
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.uuid} (Handle: {self.handle}): {self.description}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def handle(self) -> int:
|
||||||
|
"""The handle of this service"""
|
||||||
|
return self._handle
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uuid(self) -> str:
|
||||||
|
"""The UUID to this service"""
|
||||||
|
return self._uuid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self) -> str:
|
||||||
|
"""String description for this service"""
|
||||||
|
return uuidstr_to_str(self.uuid)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def characteristics(self) -> list[BleakGATTCharacteristic]:
|
||||||
|
"""List of characteristics for this service"""
|
||||||
|
return list(self._characteristics.values())
|
||||||
|
|
||||||
|
def add_characteristic(self, characteristic: BleakGATTCharacteristic) -> None:
|
||||||
|
"""Add a :py:class:`~BleakGATTCharacteristic` to the service.
|
||||||
|
|
||||||
|
Should not be used by end user, but rather by `bleak` itself.
|
||||||
|
"""
|
||||||
|
if characteristic.handle in self._characteristics:
|
||||||
|
raise BleakError(
|
||||||
|
"The characteristic '%s' is already present in this BleakGATTService!",
|
||||||
|
characteristic.handle,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._characteristics[characteristic.handle] = characteristic
|
||||||
|
|
||||||
|
def get_characteristic(
|
||||||
|
self, uuid: Union[str, UUID]
|
||||||
|
) -> Union[BleakGATTCharacteristic, None]:
|
||||||
|
"""Get a characteristic by UUID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
uuid: The UUID to match.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The first characteristic matching ``uuid`` or ``None`` if no
|
||||||
|
matching characteristic was found.
|
||||||
|
"""
|
||||||
|
uuid = normalize_uuid_str(str(uuid))
|
||||||
|
|
||||||
|
try:
|
||||||
|
return next(
|
||||||
|
filter(lambda x: x.uuid == uuid, self._characteristics.values())
|
||||||
|
)
|
||||||
|
except StopIteration:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class BleakGATTServiceCollection:
|
||||||
|
"""Simple data container for storing the peripheral's service complement."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.__services: dict[int, BleakGATTService] = {}
|
||||||
|
self.__characteristics: dict[int, BleakGATTCharacteristic] = {}
|
||||||
|
self.__descriptors: dict[int, BleakGATTDescriptor] = {}
|
||||||
|
|
||||||
|
def __getitem__(
|
||||||
|
self, item: Union[str, int, UUID]
|
||||||
|
) -> Optional[
|
||||||
|
Union[BleakGATTService, BleakGATTCharacteristic, BleakGATTDescriptor]
|
||||||
|
]:
|
||||||
|
"""Get a service, characteristic or descriptor from uuid or handle"""
|
||||||
|
return (
|
||||||
|
self.get_service(item)
|
||||||
|
or self.get_characteristic(item)
|
||||||
|
or self.get_descriptor(cast(int, item))
|
||||||
|
)
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[BleakGATTService]:
|
||||||
|
"""Returns an iterator over all BleakGATTService objects"""
|
||||||
|
return iter(self.services.values())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def services(self) -> dict[int, BleakGATTService]:
|
||||||
|
"""Returns dictionary of handles mapping to BleakGATTService"""
|
||||||
|
return self.__services
|
||||||
|
|
||||||
|
@property
|
||||||
|
def characteristics(self) -> dict[int, BleakGATTCharacteristic]:
|
||||||
|
"""Returns dictionary of handles mapping to BleakGATTCharacteristic"""
|
||||||
|
return self.__characteristics
|
||||||
|
|
||||||
|
@property
|
||||||
|
def descriptors(self) -> dict[int, BleakGATTDescriptor]:
|
||||||
|
"""Returns a dictionary of integer handles mapping to BleakGATTDescriptor"""
|
||||||
|
return self.__descriptors
|
||||||
|
|
||||||
|
def add_service(self, service: BleakGATTService) -> None:
|
||||||
|
"""Add a :py:class:`~BleakGATTService` to the service collection.
|
||||||
|
|
||||||
|
Should not be used by end user, but rather by `bleak` itself.
|
||||||
|
"""
|
||||||
|
if service.handle not in self.__services:
|
||||||
|
self.__services[service.handle] = service
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
"The service '%s' is already present in this BleakGATTServiceCollection!",
|
||||||
|
service.handle,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_service(
|
||||||
|
self, specifier: Union[int, str, UUID]
|
||||||
|
) -> Optional[BleakGATTService]:
|
||||||
|
"""Get a service by handle (int) or UUID (str or uuid.UUID)"""
|
||||||
|
if isinstance(specifier, int):
|
||||||
|
return self.services.get(specifier)
|
||||||
|
|
||||||
|
uuid = normalize_uuid_str(str(specifier))
|
||||||
|
|
||||||
|
x = list(
|
||||||
|
filter(
|
||||||
|
lambda x: x.uuid == uuid,
|
||||||
|
self.services.values(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(x) > 1:
|
||||||
|
raise BleakError(
|
||||||
|
"Multiple Services with this UUID, refer to your desired service by the `handle` attribute instead."
|
||||||
|
)
|
||||||
|
|
||||||
|
return x[0] if x else None
|
||||||
|
|
||||||
|
def add_characteristic(self, characteristic: BleakGATTCharacteristic) -> None:
|
||||||
|
"""Add a :py:class:`~BleakGATTCharacteristic` to the service collection.
|
||||||
|
|
||||||
|
Should not be used by end user, but rather by `bleak` itself.
|
||||||
|
"""
|
||||||
|
if characteristic.handle not in self.__characteristics:
|
||||||
|
self.__characteristics[characteristic.handle] = characteristic
|
||||||
|
self.__services[characteristic.service_handle].add_characteristic(
|
||||||
|
characteristic
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
"The characteristic '%s' is already present in this BleakGATTServiceCollection!",
|
||||||
|
characteristic.handle,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_characteristic(
|
||||||
|
self, specifier: Union[int, str, UUID]
|
||||||
|
) -> Optional[BleakGATTCharacteristic]:
|
||||||
|
"""Get a characteristic by handle (int) or UUID (str or uuid.UUID)"""
|
||||||
|
if isinstance(specifier, int):
|
||||||
|
return self.characteristics.get(specifier)
|
||||||
|
|
||||||
|
uuid = normalize_uuid_str(str(specifier))
|
||||||
|
|
||||||
|
# Assume uuid usage.
|
||||||
|
x = list(
|
||||||
|
filter(
|
||||||
|
lambda x: x.uuid == uuid,
|
||||||
|
self.characteristics.values(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(x) > 1:
|
||||||
|
raise BleakError(
|
||||||
|
"Multiple Characteristics with this UUID, refer to your desired characteristic by the `handle` attribute instead."
|
||||||
|
)
|
||||||
|
|
||||||
|
return x[0] if x else None
|
||||||
|
|
||||||
|
def add_descriptor(self, descriptor: BleakGATTDescriptor) -> None:
|
||||||
|
"""Add a :py:class:`~BleakGATTDescriptor` to the service collection.
|
||||||
|
|
||||||
|
Should not be used by end user, but rather by `bleak` itself.
|
||||||
|
"""
|
||||||
|
if descriptor.handle not in self.__descriptors:
|
||||||
|
self.__descriptors[descriptor.handle] = descriptor
|
||||||
|
self.__characteristics[descriptor.characteristic_handle].add_descriptor(
|
||||||
|
descriptor
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
"The descriptor '%s' is already present in this BleakGATTServiceCollection!",
|
||||||
|
descriptor.handle,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_descriptor(self, handle: int) -> Optional[BleakGATTDescriptor]:
|
||||||
|
"""Get a descriptor by integer handle"""
|
||||||
|
return self.descriptors.get(handle)
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1198
tools/python-embed/Lib/site-packages/bleak/backends/winrt/client.py
Normal file
1198
tools/python-embed/Lib/site-packages/bleak/backends/winrt/client.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,341 @@
|
|||||||
|
import sys
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
if sys.platform != "win32":
|
||||||
|
assert False, "This backend is only available on Windows"
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import Literal, NamedTuple, Optional
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from winrt.windows.devices.bluetooth import BluetoothAdapter
|
||||||
|
from winrt.windows.devices.bluetooth.advertisement import (
|
||||||
|
BluetoothLEAdvertisementReceivedEventArgs,
|
||||||
|
BluetoothLEAdvertisementType,
|
||||||
|
BluetoothLEAdvertisementWatcher,
|
||||||
|
BluetoothLEAdvertisementWatcherStatus,
|
||||||
|
BluetoothLEAdvertisementWatcherStoppedEventArgs,
|
||||||
|
BluetoothLEScanningMode,
|
||||||
|
)
|
||||||
|
from winrt.windows.devices.radios import RadioState
|
||||||
|
from winrt.windows.foundation import EventRegistrationToken
|
||||||
|
|
||||||
|
from bleak._compat import override
|
||||||
|
from bleak.assigned_numbers import AdvertisementDataType
|
||||||
|
from bleak.backends.scanner import (
|
||||||
|
AdvertisementData,
|
||||||
|
AdvertisementDataCallback,
|
||||||
|
BaseBleakScanner,
|
||||||
|
)
|
||||||
|
from bleak.backends.winrt.util import assert_mta
|
||||||
|
from bleak.exc import (
|
||||||
|
BleakBluetoothNotAvailableError,
|
||||||
|
BleakBluetoothNotAvailableReason,
|
||||||
|
BleakError,
|
||||||
|
)
|
||||||
|
from bleak.uuids import normalize_uuid_str
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_bdaddr(a: int) -> str:
|
||||||
|
return ":".join(f"{x:02X}" for x in a.to_bytes(6, byteorder="big"))
|
||||||
|
|
||||||
|
|
||||||
|
def _format_event_args(e: BluetoothLEAdvertisementReceivedEventArgs) -> str:
|
||||||
|
try:
|
||||||
|
return f"{_format_bdaddr(e.bluetooth_address)}: {e.advertisement.local_name}"
|
||||||
|
except Exception:
|
||||||
|
return _format_bdaddr(e.bluetooth_address)
|
||||||
|
|
||||||
|
|
||||||
|
class RawAdvData(NamedTuple):
|
||||||
|
"""
|
||||||
|
Platform-specific advertisement data.
|
||||||
|
|
||||||
|
Windows does not combine advertising data with type SCAN_RSP with other
|
||||||
|
advertising data like other platforms, so we have to do it ourselves.
|
||||||
|
"""
|
||||||
|
|
||||||
|
adv: Optional[BluetoothLEAdvertisementReceivedEventArgs]
|
||||||
|
"""
|
||||||
|
The advertisement data received from the BluetoothLEAdvertisementWatcher.Received event.
|
||||||
|
"""
|
||||||
|
scan: Optional[BluetoothLEAdvertisementReceivedEventArgs]
|
||||||
|
"""
|
||||||
|
The scan response for the same device as *adv*.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class BleakScannerWinRT(BaseBleakScanner):
|
||||||
|
"""The native Windows Bleak BLE Scanner.
|
||||||
|
|
||||||
|
Implemented using `Python/WinRT <https://github.com/Microsoft/xlang/tree/master/src/package/pywinrt/projection/>`_.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
detection_callback:
|
||||||
|
Optional function that will be called each time a device is
|
||||||
|
discovered or advertising data has changed.
|
||||||
|
service_uuids:
|
||||||
|
Optional list of service UUIDs to filter on. Only advertisements
|
||||||
|
containing this advertising data will be received.
|
||||||
|
scanning_mode:
|
||||||
|
Set to ``"passive"`` to avoid the ``"active"`` scanning mode.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
detection_callback: Optional[AdvertisementDataCallback],
|
||||||
|
service_uuids: Optional[list[str]],
|
||||||
|
scanning_mode: Literal["active", "passive"],
|
||||||
|
**kwargs: Any,
|
||||||
|
):
|
||||||
|
super().__init__(detection_callback, service_uuids)
|
||||||
|
|
||||||
|
self.watcher: Optional[BluetoothLEAdvertisementWatcher] = None
|
||||||
|
self._advertisement_pairs: dict[str, RawAdvData] = {}
|
||||||
|
self._stopped_event: Optional[asyncio.Event] = None
|
||||||
|
|
||||||
|
# case insensitivity is for backwards compatibility on Windows only
|
||||||
|
if scanning_mode.lower() == "passive":
|
||||||
|
self._scanning_mode = BluetoothLEScanningMode.PASSIVE
|
||||||
|
else:
|
||||||
|
self._scanning_mode = BluetoothLEScanningMode.ACTIVE
|
||||||
|
|
||||||
|
# Unfortunately, due to the way Windows handles filtering, we can't
|
||||||
|
# make use of the service_uuids filter here. If we did we would only
|
||||||
|
# get the advertisement data or the scan data, but not both, so would
|
||||||
|
# miss out on other essential data. Advanced users can pass their own
|
||||||
|
# filters though if they want to.
|
||||||
|
self._signal_strength_filter = kwargs.get("SignalStrengthFilter", None)
|
||||||
|
self._advertisement_filter = kwargs.get("AdvertisementFilter", None)
|
||||||
|
|
||||||
|
self._received_token: Optional[EventRegistrationToken] = None
|
||||||
|
self._stopped_token: Optional[EventRegistrationToken] = None
|
||||||
|
|
||||||
|
def _received_handler(
|
||||||
|
self,
|
||||||
|
sender: BluetoothLEAdvertisementWatcher,
|
||||||
|
event_args: BluetoothLEAdvertisementReceivedEventArgs,
|
||||||
|
):
|
||||||
|
"""Callback for AdvertisementWatcher.Received"""
|
||||||
|
# TODO: Cannot check for if sender == self.watcher in winrt?
|
||||||
|
logger.debug("Received %s.", _format_event_args(event_args))
|
||||||
|
|
||||||
|
# REVISIT: if scanning filters with BluetoothSignalStrengthFilter.OutOfRangeTimeout
|
||||||
|
# are in place, an RSSI of -127 means that the device has gone out of range and should
|
||||||
|
# be removed from the list of seen devices instead of processing the advertisement data.
|
||||||
|
# https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothsignalstrengthfilter.outofrangetimeout
|
||||||
|
|
||||||
|
bdaddr = _format_bdaddr(event_args.bluetooth_address)
|
||||||
|
|
||||||
|
# Unlike other platforms, Windows does not combine advertising data for
|
||||||
|
# us (regular advertisement + scan response) so we have to do it manually.
|
||||||
|
|
||||||
|
# get the previous advertising data/scan response pair or start a new one
|
||||||
|
raw_data = self._advertisement_pairs.get(bdaddr, RawAdvData(None, None))
|
||||||
|
|
||||||
|
# update the advertising data depending on the advertising data type
|
||||||
|
if event_args.advertisement_type == BluetoothLEAdvertisementType.SCAN_RESPONSE:
|
||||||
|
raw_data = RawAdvData(raw_data.adv, event_args)
|
||||||
|
else:
|
||||||
|
raw_data = RawAdvData(event_args, raw_data.scan)
|
||||||
|
|
||||||
|
self._advertisement_pairs[bdaddr] = raw_data
|
||||||
|
|
||||||
|
uuids: list[str] = []
|
||||||
|
mfg_data = {}
|
||||||
|
service_data = {}
|
||||||
|
local_name = None
|
||||||
|
tx_power = None
|
||||||
|
|
||||||
|
for args in filter(lambda d: d is not None, raw_data):
|
||||||
|
assert args
|
||||||
|
|
||||||
|
for u in args.advertisement.service_uuids:
|
||||||
|
uuids.append(str(u))
|
||||||
|
|
||||||
|
for m in args.advertisement.manufacturer_data:
|
||||||
|
mfg_data[m.company_id] = bytes(m.data)
|
||||||
|
|
||||||
|
# local name is empty string rather than None if not present
|
||||||
|
if args.advertisement.local_name:
|
||||||
|
local_name = args.advertisement.local_name
|
||||||
|
|
||||||
|
try:
|
||||||
|
if args.transmit_power_level_in_dbm is not None:
|
||||||
|
tx_power = args.transmit_power_level_in_dbm
|
||||||
|
except AttributeError:
|
||||||
|
# the transmit_power_level_in_d_bm property was introduce in
|
||||||
|
# Windows build 19041 so we have a fallback for older versions
|
||||||
|
for section in args.advertisement.get_sections_by_type(
|
||||||
|
AdvertisementDataType.TX_POWER_LEVEL
|
||||||
|
):
|
||||||
|
tx_power = bytes(section.data)[0]
|
||||||
|
|
||||||
|
# Decode service data
|
||||||
|
for section in args.advertisement.get_sections_by_type(
|
||||||
|
AdvertisementDataType.SERVICE_DATA_UUID16
|
||||||
|
):
|
||||||
|
data = bytes(section.data)
|
||||||
|
service_data[normalize_uuid_str(f"{data[1]:02x}{data[0]:02x}")] = data[
|
||||||
|
2:
|
||||||
|
]
|
||||||
|
for section in args.advertisement.get_sections_by_type(
|
||||||
|
AdvertisementDataType.SERVICE_DATA_UUID32
|
||||||
|
):
|
||||||
|
data = bytes(section.data)
|
||||||
|
service_data[
|
||||||
|
normalize_uuid_str(
|
||||||
|
f"{data[3]:02x}{data[2]:02x}{data[1]:02x}{data[0]:02x}"
|
||||||
|
)
|
||||||
|
] = data[4:]
|
||||||
|
for section in args.advertisement.get_sections_by_type(
|
||||||
|
AdvertisementDataType.SERVICE_DATA_UUID128
|
||||||
|
):
|
||||||
|
data = bytes(section.data)
|
||||||
|
service_data[str(UUID(bytes=bytes(data[15::-1])))] = data[16:]
|
||||||
|
|
||||||
|
if not self.is_allowed_uuid(uuids):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Use the BLEDevice to populate all the fields for the advertisement data to return
|
||||||
|
advertisement_data = AdvertisementData(
|
||||||
|
local_name=local_name,
|
||||||
|
manufacturer_data=mfg_data,
|
||||||
|
service_data=service_data,
|
||||||
|
service_uuids=uuids,
|
||||||
|
tx_power=tx_power,
|
||||||
|
rssi=event_args.raw_signal_strength_in_dbm,
|
||||||
|
platform_data=(sender, raw_data),
|
||||||
|
)
|
||||||
|
|
||||||
|
device = self.create_or_update_device(
|
||||||
|
bdaddr, bdaddr, local_name, raw_data, advertisement_data
|
||||||
|
)
|
||||||
|
|
||||||
|
self.call_detection_callbacks(device, advertisement_data)
|
||||||
|
|
||||||
|
def _stopped_handler(
|
||||||
|
self,
|
||||||
|
sender: BluetoothLEAdvertisementWatcher,
|
||||||
|
e: BluetoothLEAdvertisementWatcherStoppedEventArgs,
|
||||||
|
) -> None:
|
||||||
|
logger.debug(
|
||||||
|
"%s devices found. Watcher status: %r.",
|
||||||
|
len(self.seen_devices),
|
||||||
|
sender.status,
|
||||||
|
)
|
||||||
|
assert self._stopped_event
|
||||||
|
self._stopped_event.set()
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def start(self) -> None:
|
||||||
|
if self.watcher:
|
||||||
|
raise BleakError("Scanner already started")
|
||||||
|
|
||||||
|
# Callbacks for WinRT async methods will never happen in STA mode if
|
||||||
|
# there is nothing pumping a Windows message loop.
|
||||||
|
await assert_mta()
|
||||||
|
|
||||||
|
# TODO: need to fix return type of get_default_async() in PyWinRT
|
||||||
|
adapter = await BluetoothAdapter.get_default_async()
|
||||||
|
if adapter is None: # pyright: ignore[reportUnnecessaryComparison]
|
||||||
|
raise BleakBluetoothNotAvailableError(
|
||||||
|
"No Bluetooth adapter found",
|
||||||
|
BleakBluetoothNotAvailableReason.NO_BLUETOOTH,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not adapter.is_central_role_supported:
|
||||||
|
raise BleakBluetoothNotAvailableError(
|
||||||
|
"BLE 'central' role not supported on this adapter",
|
||||||
|
BleakBluetoothNotAvailableReason.NO_BLE_CENTRAL_ROLE,
|
||||||
|
)
|
||||||
|
|
||||||
|
radio = await adapter.get_radio_async()
|
||||||
|
if radio.state != RadioState.ON:
|
||||||
|
raise BleakBluetoothNotAvailableError(
|
||||||
|
"Bluetooth radio is not powered on. Turn on Bluetooth and try again.",
|
||||||
|
BleakBluetoothNotAvailableReason.POWERED_OFF,
|
||||||
|
)
|
||||||
|
|
||||||
|
# start with fresh list of discovered devices
|
||||||
|
self.seen_devices = {}
|
||||||
|
self._advertisement_pairs.clear()
|
||||||
|
|
||||||
|
self.watcher = BluetoothLEAdvertisementWatcher()
|
||||||
|
self.watcher.scanning_mode = self._scanning_mode
|
||||||
|
# BlueZ and CoreBluetooth don't allow controlling this and always enabled it, so do the same here
|
||||||
|
try:
|
||||||
|
self.watcher.allow_extended_advertisements = True
|
||||||
|
except AttributeError:
|
||||||
|
logger.warning(
|
||||||
|
"Extended advertisements are not available in this OS Version."
|
||||||
|
)
|
||||||
|
|
||||||
|
event_loop = asyncio.get_running_loop()
|
||||||
|
self._stopped_event = asyncio.Event()
|
||||||
|
|
||||||
|
def on_received(
|
||||||
|
sender: BluetoothLEAdvertisementWatcher,
|
||||||
|
args: BluetoothLEAdvertisementReceivedEventArgs,
|
||||||
|
) -> None:
|
||||||
|
event_loop.call_soon_threadsafe(self._received_handler, sender, args)
|
||||||
|
|
||||||
|
self._received_token = self.watcher.add_received(on_received)
|
||||||
|
|
||||||
|
def on_stopped(
|
||||||
|
sender: BluetoothLEAdvertisementWatcher,
|
||||||
|
args: BluetoothLEAdvertisementWatcherStoppedEventArgs,
|
||||||
|
) -> None:
|
||||||
|
event_loop.call_soon_threadsafe(self._stopped_handler, sender, args)
|
||||||
|
|
||||||
|
self._stopped_token = self.watcher.add_stopped(on_stopped)
|
||||||
|
|
||||||
|
if self._signal_strength_filter is not None:
|
||||||
|
self.watcher.signal_strength_filter = self._signal_strength_filter
|
||||||
|
if self._advertisement_filter is not None:
|
||||||
|
self.watcher.advertisement_filter = self._advertisement_filter
|
||||||
|
|
||||||
|
self.watcher.start()
|
||||||
|
|
||||||
|
# no events for status changes, so we have to poll :-(
|
||||||
|
while self.watcher.status == BluetoothLEAdvertisementWatcherStatus.CREATED:
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
|
|
||||||
|
if self.watcher.status == BluetoothLEAdvertisementWatcherStatus.ABORTED:
|
||||||
|
raise BleakError("Failed to start scanner. Is Bluetooth turned on?")
|
||||||
|
|
||||||
|
if self.watcher.status != BluetoothLEAdvertisementWatcherStatus.STARTED:
|
||||||
|
raise BleakError(f"Unexpected watcher status: {self.watcher.status.name}")
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def stop(self) -> None:
|
||||||
|
assert self.watcher
|
||||||
|
assert self._stopped_event
|
||||||
|
assert self._received_token
|
||||||
|
assert self._stopped_token
|
||||||
|
|
||||||
|
self.watcher.stop()
|
||||||
|
|
||||||
|
if self.watcher.status == BluetoothLEAdvertisementWatcherStatus.STOPPING:
|
||||||
|
await self._stopped_event.wait()
|
||||||
|
else:
|
||||||
|
logger.debug(
|
||||||
|
"skipping waiting for stop because status is %r",
|
||||||
|
self.watcher.status,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.watcher.remove_received(self._received_token)
|
||||||
|
self.watcher.remove_stopped(self._stopped_token)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug("Could not remove event handlers: %s", e)
|
||||||
|
|
||||||
|
self._stopped_token = None
|
||||||
|
self._received_token = None
|
||||||
|
|
||||||
|
self.watcher = None
|
||||||
@ -0,0 +1,224 @@
|
|||||||
|
import sys
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
if sys.platform != "win32":
|
||||||
|
assert False, "This backend is only available on Windows"
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import ctypes
|
||||||
|
from ctypes import wintypes
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
|
from bleak._compat import timeout as async_timeout
|
||||||
|
from bleak.exc import BleakError
|
||||||
|
|
||||||
|
|
||||||
|
def _check_result(result: int, func: Any, args: Any) -> Any:
|
||||||
|
if not result:
|
||||||
|
raise ctypes.WinError()
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def check_hresult(result: int, func: Any, args: Any) -> Any:
|
||||||
|
if result:
|
||||||
|
raise ctypes.WinError(result)
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
# not defined in wintypes
|
||||||
|
_UINT_PTR = wintypes.WPARAM
|
||||||
|
|
||||||
|
# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-timerproc
|
||||||
|
_TIMERPROC = ctypes.WINFUNCTYPE(
|
||||||
|
None, wintypes.HWND, _UINT_PTR, wintypes.UINT, wintypes.DWORD
|
||||||
|
)
|
||||||
|
|
||||||
|
# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-settimer
|
||||||
|
_SET_TIMER_PROTOTYPE = ctypes.WINFUNCTYPE(
|
||||||
|
_UINT_PTR, wintypes.HWND, _UINT_PTR, wintypes.UINT, _TIMERPROC
|
||||||
|
)
|
||||||
|
_SET_TIMER_PARAM_FLAGS = (
|
||||||
|
(1, "hwnd", None),
|
||||||
|
(1, "nidevent"),
|
||||||
|
(1, "uelapse"),
|
||||||
|
(1, "lptimerfunc", None),
|
||||||
|
)
|
||||||
|
_SetTimer = _SET_TIMER_PROTOTYPE(
|
||||||
|
("SetTimer", ctypes.windll.user32), _SET_TIMER_PARAM_FLAGS
|
||||||
|
)
|
||||||
|
_SetTimer.errcheck = _check_result # type: ignore[assignment]
|
||||||
|
|
||||||
|
# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-killtimer
|
||||||
|
_KILL_TIMER_PROTOTYPE = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, _UINT_PTR)
|
||||||
|
_KILL_TIMER_PARAM_FLAGS = (
|
||||||
|
(1, "hwnd", None),
|
||||||
|
(1, "uidevent"),
|
||||||
|
)
|
||||||
|
_KillTimer = _KILL_TIMER_PROTOTYPE(
|
||||||
|
("KillTimer", ctypes.windll.user32), _KILL_TIMER_PARAM_FLAGS
|
||||||
|
)
|
||||||
|
|
||||||
|
# https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-cogetapartmenttype
|
||||||
|
_CO_GET_APARTMENT_TYPE_PROTOTYPE = ctypes.WINFUNCTYPE(
|
||||||
|
ctypes.c_int,
|
||||||
|
ctypes.POINTER(ctypes.c_int),
|
||||||
|
ctypes.POINTER(ctypes.c_int),
|
||||||
|
)
|
||||||
|
_CO_GET_APARTMENT_TYPE_PARAM_FLAGS = (
|
||||||
|
(1, "papttype", None),
|
||||||
|
(1, "paptqualifier", None),
|
||||||
|
)
|
||||||
|
_CoGetApartmentType = _CO_GET_APARTMENT_TYPE_PROTOTYPE(
|
||||||
|
("CoGetApartmentType", ctypes.windll.ole32), _CO_GET_APARTMENT_TYPE_PARAM_FLAGS
|
||||||
|
)
|
||||||
|
_CoGetApartmentType.errcheck = check_hresult # type: ignore[assignment]
|
||||||
|
|
||||||
|
_CO_E_NOTINITIALIZED = -2147221008
|
||||||
|
|
||||||
|
|
||||||
|
# https://learn.microsoft.com/en-us/windows/win32/api/objidl/ne-objidl-apttype
|
||||||
|
class _AptType(IntEnum):
|
||||||
|
CURRENT = -1
|
||||||
|
STA = 0
|
||||||
|
MTA = 1
|
||||||
|
NA = 2
|
||||||
|
MAIN_STA = 3
|
||||||
|
|
||||||
|
|
||||||
|
# https://learn.microsoft.com/en-us/windows/win32/api/objidl/ne-objidl-apttypequalifier
|
||||||
|
class _AptQualifierType(IntEnum):
|
||||||
|
NONE = 0
|
||||||
|
IMPLICIT_MTA = 1
|
||||||
|
NA_ON_MTA = 2
|
||||||
|
NA_ON_STA = 3
|
||||||
|
NA_ON_IMPLICIT_STA = 4
|
||||||
|
NA_ON_MAIN_STA = 5
|
||||||
|
APPLICATION_STA = 6
|
||||||
|
RESERVED_1 = 7
|
||||||
|
|
||||||
|
|
||||||
|
def _get_apartment_type() -> tuple[_AptType, _AptQualifierType]:
|
||||||
|
"""
|
||||||
|
Calls CoGetApartmentType to get the current apartment type and qualifier.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The current apartment type and qualifier.
|
||||||
|
Raises:
|
||||||
|
OSError: If the call to CoGetApartmentType fails.
|
||||||
|
"""
|
||||||
|
api_type = ctypes.c_int()
|
||||||
|
api_type_qualifier = ctypes.c_int()
|
||||||
|
_CoGetApartmentType(ctypes.byref(api_type), ctypes.byref(api_type_qualifier))
|
||||||
|
return _AptType(api_type.value), _AptQualifierType(api_type_qualifier.value)
|
||||||
|
|
||||||
|
|
||||||
|
async def assert_mta() -> None:
|
||||||
|
"""
|
||||||
|
Asserts that the current apartment type is MTA.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
BleakError:
|
||||||
|
If the current apartment type is not MTA and there is no Windows
|
||||||
|
message loop running.
|
||||||
|
|
||||||
|
.. versionadded:: 0.22
|
||||||
|
|
||||||
|
.. versionchanged:: 0.22.2
|
||||||
|
|
||||||
|
Function is now async and will not raise if the current apartment type
|
||||||
|
is STA and the Windows message loop is running.
|
||||||
|
"""
|
||||||
|
if hasattr(allow_sta, "_allowed"):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
apt_type, _ = _get_apartment_type()
|
||||||
|
except OSError as e:
|
||||||
|
# All is OK if not initialized yet. WinRT will initialize it.
|
||||||
|
if e.winerror == _CO_E_NOTINITIALIZED:
|
||||||
|
return
|
||||||
|
|
||||||
|
raise
|
||||||
|
|
||||||
|
if apt_type == _AptType.MTA:
|
||||||
|
# if we get here, WinRT probably set the apartment type to MTA and all
|
||||||
|
# is well, we don't need to check again
|
||||||
|
setattr(allow_sta, "_allowed", True)
|
||||||
|
return
|
||||||
|
|
||||||
|
event = asyncio.Event()
|
||||||
|
|
||||||
|
def wait_event(hwnd: int, uidevent: int, uelapse: int, dwtime: int) -> None:
|
||||||
|
event.set()
|
||||||
|
|
||||||
|
# have to keep a reference to the callback or it will be garbage collected
|
||||||
|
# before it is called
|
||||||
|
callback = _TIMERPROC(wait_event)
|
||||||
|
|
||||||
|
# set a timer to see if we get a callback to ensure the windows event loop
|
||||||
|
# is running
|
||||||
|
timer = _SetTimer(None, 1, 0, callback)
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with async_timeout(0.5):
|
||||||
|
await event.wait()
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
raise BleakError(
|
||||||
|
"Thread is configured for Windows GUI but callbacks are not working."
|
||||||
|
+ (
|
||||||
|
" Suspect unwanted side effects from importing 'pythoncom'."
|
||||||
|
if "pythoncom" in sys.modules
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# if the windows event loop is running, we assume it is going to keep
|
||||||
|
# running and we don't need to check again
|
||||||
|
setattr(allow_sta, "_allowed", True)
|
||||||
|
finally:
|
||||||
|
_KillTimer(None, timer)
|
||||||
|
|
||||||
|
|
||||||
|
def allow_sta() -> None:
|
||||||
|
"""
|
||||||
|
Suppress check for MTA thread type and allow STA.
|
||||||
|
|
||||||
|
Bleak will hang forever if the current thread is not MTA - unless there is
|
||||||
|
a Windows event loop running that is properly integrated with asyncio in
|
||||||
|
Python.
|
||||||
|
|
||||||
|
If your program meets that condition, you must call this function do disable
|
||||||
|
the check for MTA. If your program doesn't have a graphical user interface
|
||||||
|
you probably shouldn't call this function. and use ``uninitialize_sta()``
|
||||||
|
instead.
|
||||||
|
|
||||||
|
.. versionadded:: 0.22.1
|
||||||
|
"""
|
||||||
|
setattr(allow_sta, "_allowed", True)
|
||||||
|
|
||||||
|
|
||||||
|
def uninitialize_sta():
|
||||||
|
"""
|
||||||
|
Uninitialize the COM library on the current thread if it was not initialized
|
||||||
|
as MTA.
|
||||||
|
|
||||||
|
This is intended to undo the implicit initialization of the COM library as STA
|
||||||
|
by packages like pywin32.
|
||||||
|
|
||||||
|
It should be called as early as possible in your application after the
|
||||||
|
offending package has been imported.
|
||||||
|
|
||||||
|
.. versionadded:: 0.22
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
_get_apartment_type()
|
||||||
|
except OSError as e:
|
||||||
|
# All is OK if not initialized yet. WinRT will initialize it.
|
||||||
|
if e.winerror == _CO_E_NOTINITIALIZED:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
ctypes.windll.ole32.CoUninitialize()
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user