diff --git a/.gitignore b/.gitignore index 5040cbf..6dbccac 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,6 @@ mcm-ddc-ble/MDK-ARM/build.log mcm-ddc-ble/MDK-ARM/build-app2.log mcm-ddc-ble/MDK-ARM/Objects-app2 -# portable production runtime (bundled, not committed) -tools/python-embed/ -tools/NSpyocd/ -tools/N32WB03x_DFP.1.4.0.pack +# release zips are regenerable via tools/make_release.bat tools/out/mothercup_tools_*.zip +tools/python-embed/Lib/site-packages/__pycache__/ diff --git a/tools/N32WB03x_DFP.1.4.0.pack b/tools/N32WB03x_DFP.1.4.0.pack new file mode 100644 index 0000000..ea4c3fa Binary files /dev/null and b/tools/N32WB03x_DFP.1.4.0.pack differ diff --git a/tools/NSpyocd/NSpyocd.exe b/tools/NSpyocd/NSpyocd.exe new file mode 100644 index 0000000..40678af Binary files /dev/null and b/tools/NSpyocd/NSpyocd.exe differ diff --git a/tools/python-embed/LICENSE.txt b/tools/python-embed/LICENSE.txt new file mode 100644 index 0000000..7a0193a --- /dev/null +++ b/tools/python-embed/LICENSE.txt @@ -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. + diff --git a/tools/python-embed/Lib/site-packages/bin/pyserial-miniterm.exe b/tools/python-embed/Lib/site-packages/bin/pyserial-miniterm.exe new file mode 100644 index 0000000..c3202ae Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bin/pyserial-miniterm.exe differ diff --git a/tools/python-embed/Lib/site-packages/bin/pyserial-ports.exe b/tools/python-embed/Lib/site-packages/bin/pyserial-ports.exe new file mode 100644 index 0000000..a68a901 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bin/pyserial-ports.exe differ diff --git a/tools/python-embed/Lib/site-packages/bleak-3.0.2.dist-info/INSTALLER b/tools/python-embed/Lib/site-packages/bleak-3.0.2.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak-3.0.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/tools/python-embed/Lib/site-packages/bleak-3.0.2.dist-info/METADATA b/tools/python-embed/Lib/site-packages/bleak-3.0.2.dist-info/METADATA new file mode 100644 index 0000000..ab1f1f3 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak-3.0.2.dist-info/METADATA @@ -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 +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 `_ diff --git a/tools/python-embed/Lib/site-packages/bleak-3.0.2.dist-info/RECORD b/tools/python-embed/Lib/site-packages/bleak-3.0.2.dist-info/RECORD new file mode 100644 index 0000000..da2ff31 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak-3.0.2.dist-info/RECORD @@ -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 diff --git a/tools/python-embed/Lib/site-packages/bleak-3.0.2.dist-info/REQUESTED b/tools/python-embed/Lib/site-packages/bleak-3.0.2.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/tools/python-embed/Lib/site-packages/bleak-3.0.2.dist-info/WHEEL b/tools/python-embed/Lib/site-packages/bleak-3.0.2.dist-info/WHEEL new file mode 100644 index 0000000..38653c9 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak-3.0.2.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: uv 0.11.8 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/tools/python-embed/Lib/site-packages/bleak-3.0.2.dist-info/licenses/LICENSE b/tools/python-embed/Lib/site-packages/bleak-3.0.2.dist-info/licenses/LICENSE new file mode 100644 index 0000000..3b7ffa1 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak-3.0.2.dist-info/licenses/LICENSE @@ -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. + diff --git a/tools/python-embed/Lib/site-packages/bleak/__init__.py b/tools/python-embed/Lib/site-packages/bleak/__init__.py new file mode 100644 index 0000000..2ebbea2 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/__init__.py @@ -0,0 +1,1008 @@ +"""Top-level package for bleak.""" + +from __future__ import annotations + +import asyncio +import functools +import inspect +import logging +import os +import sys +import uuid +from collections.abc import AsyncGenerator, Awaitable, Callable, Iterable +from types import TracebackType +from typing import Any, Literal, Optional, TypedDict, Union, cast, overload +from warnings import warn + +from bleak._compat import Never, Self, Unpack, assert_never +from bleak._compat import timeout as async_timeout +from bleak.args import SizedBuffer +from bleak.args.bluez import BlueZClientArgs, BlueZNotifyArgs, BlueZScannerArgs +from bleak.args.corebluetooth import CBScannerArgs, CBStartNotifyArgs +from bleak.args.winrt import WinRTClientArgs +from bleak.backends import BleakBackend +from bleak.backends.characteristic import BleakGATTCharacteristic +from bleak.backends.client import BaseBleakClient, get_platform_client_backend_type +from bleak.backends.descriptor import BleakGATTDescriptor +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import ( + AdvertisementData, + AdvertisementDataCallback, + AdvertisementDataFilter, + BaseBleakScanner, + get_platform_scanner_backend_type, +) +from bleak.backends.service import BleakGATTServiceCollection +from bleak.exc import BleakCharacteristicNotFoundError, BleakError +from bleak.uuids import normalize_uuid_16, normalize_uuid_str + +__author__ = """Henrik Blidh""" +__email__ = "henrik.blidh@gmail.com" + +_logger = logging.getLogger(__name__) +_logger.addHandler(logging.NullHandler()) +if bool(os.environ.get("BLEAK_LOGGING", False)): + FORMAT = "%(asctime)-15s %(name)-8s %(threadName)s %(levelname)s: %(message)s" + handler = logging.StreamHandler(sys.stderr) + handler.setLevel(logging.DEBUG) + handler.setFormatter(logging.Formatter(fmt=FORMAT)) + _logger.addHandler(handler) + _logger.setLevel(logging.DEBUG) + + +# prevent tasks from being garbage collected +_background_tasks: set[asyncio.Task[None]] = set() + + +class BleakScanner: + """ + Interface for Bleak Bluetooth LE Scanners. + + The scanner will listen for BLE advertisements, optionally filtering on advertised services or + other conditions, and collect a list of :class:`BLEDevice` objects. These can subsequently be used to + connect to the corresponding BLE server. + + A :class:`BleakScanner` can be used as an asynchronous context manager in which case it automatically + starts and stops scanning. + + 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. + Passive scanning is not supported on macOS! Will raise + :class:`BleakError` if set to ``"passive"`` on macOS. + bluez: + Dictionary of arguments specific to the BlueZ backend. + cb: + Dictionary of arguments specific to the CoreBluetooth backend. + backend: + Used to override the automatically selected backend (i.e. for a + custom backend). + **kwargs: + Additional args for backwards compatibility. + + .. tip:: The first received advertisement in ``detection_callback`` may or + may not include scan response data if the remote device supports it. + Be sure to take this into account when handing the callback. For example, + the scan response often contains the local name of the device so if you + are matching a device based on other data but want to display the local + name to the user, be sure to wait for ``adv_data.local_name is not None``. + + .. versionchanged:: 0.15 + ``detection_callback``, ``service_uuids`` and ``scanning_mode`` are no longer keyword-only. + Added ``bluez`` parameter. + + .. versionchanged:: 0.18 + No longer is alias for backend type and no longer inherits from :class:`BaseBleakScanner`. + Added ``backend`` parameter. + + .. versionchanged:: 3.0 + Deprecated ``adapter`` keyword argument. Use ``bluez`` argument instead + with ``{"adapter": ""}``. + """ + + def __init__( + self, + detection_callback: Optional[AdvertisementDataCallback] = None, + service_uuids: Optional[list[str]] = None, + scanning_mode: Literal["active", "passive"] = "active", + *, + bluez: BlueZScannerArgs = {}, + cb: CBScannerArgs = {}, + backend: Optional[type[BaseBleakScanner]] = None, + **kwargs: Any, + ) -> None: + PlatformBleakScanner, backend_id = ( + get_platform_scanner_backend_type() + if backend is None + else (backend, backend.__name__) + ) + + # TODO: upgrade this to FutureWarning in 2027 and remove in 2028 or so + adapter_kwarg = kwargs.get("adapter") + if adapter_kwarg is not None: + warn( + "the 'adapter' keyword argument is deprecated, use the 'bluez' kwarg instead with {'adapter': ''}", + DeprecationWarning, + stacklevel=2, + ) + + if "adapter" not in bluez: + bluez["adapter"] = adapter_kwarg + + self._backend = PlatformBleakScanner( + detection_callback, + service_uuids, + scanning_mode, # type: ignore + bluez=bluez, + cb=cb, + **kwargs, + ) # type: ignore + self._backend_id = backend_id + + @property + def backend_id(self) -> BleakBackend | str: + """ + Gets the identifier of the backend in use. + + The value is one of the :class:`BleakBackend` enum values in case of + built-in backends, or a string identifying a custom backend. + + .. versionadded:: 2.0 + """ + return self._backend_id + + async def __aenter__(self) -> Self: + await self.start() + return self + + async def __aexit__( + self, + exc_type: Optional[type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + await self.stop() + + async def start(self) -> None: + """ + Start scanning for devices. + + Raises: + BleakBluetoothNotAvailableError: + if Bluetooth is not currently available + + .. versionchanged:: 2.0 + Now raises :class:`BleakBluetoothNotAvailableError` instead of :class:`BleakError` + when Bluetooth is not currently available. + """ + await self._backend.start() + + async def stop(self) -> None: + """Stop scanning for devices""" + await self._backend.stop() + + async def advertisement_data( + self, + ) -> AsyncGenerator[tuple[BLEDevice, AdvertisementData], None]: + """ + Yields devices and associated advertising data packets as they are discovered. + + .. note:: + Ensure that scanning is started before calling this method. + + Returns: + An async iterator that yields tuples (:class:`BLEDevice`, :class:`AdvertisementData`). + + .. versionadded:: 0.21 + """ + devices: asyncio.Queue[tuple[BLEDevice, AdvertisementData]] = asyncio.Queue() + + unregister_callback = self._backend.register_detection_callback( + lambda bd, ad: devices.put_nowait((bd, ad)) + ) + try: + while True: + yield await devices.get() + finally: + unregister_callback() + + class ExtraArgs(TypedDict, total=False): + """ + Keyword args from :class:`~bleak.BleakScanner` that can be passed to + other convenience methods. + """ + + service_uuids: list[str] + """ + 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: Literal["active", "passive"] + """ + Set to ``"passive"`` to avoid the ``"active"`` scanning mode. + Passive scanning is not supported on macOS! Will raise + :class:`BleakError` if set to ``"passive"`` on macOS. + """ + bluez: BlueZScannerArgs + """ + Dictionary of arguments specific to the BlueZ backend. + """ + cb: CBScannerArgs + """ + Dictionary of arguments specific to the CoreBluetooth backend. + """ + backend: type[BaseBleakScanner] + """ + Used to override the automatically selected backend (i.e. for a + custom backend). + """ + adapter: str + """ + Name of adapter to use (BlueZ specific), e.g. hci0. + + .. versionchanged:: 3.0 + This argument is deprecated and will be removed in a future release. + Use the ``bluez`` argument with ``{"adapter": ""}`` + instead. + """ + + @overload + @classmethod + async def discover( + cls, + timeout: float = 5.0, + *, + return_adv: Literal[False] = False, + **kwargs: Unpack[ExtraArgs], + ) -> list[BLEDevice]: ... + + @overload + @classmethod + async def discover( + cls, + timeout: float = 5.0, + *, + return_adv: Literal[True], + **kwargs: Unpack[ExtraArgs], + ) -> dict[str, tuple[BLEDevice, AdvertisementData]]: ... + + @classmethod + async def discover( + cls, + timeout: float = 5.0, + *, + return_adv: bool = False, + **kwargs: Unpack[ExtraArgs], + ): + """ + Scan continuously for ``timeout`` seconds and return discovered devices. + + Args: + timeout: + Time, in seconds, to scan for. + return_adv: + If ``True``, the return value will include advertising data. + **kwargs: + Additional arguments will be passed to the :class:`BleakScanner` + constructor. + + Returns: + The value of :attr:`discovered_devices_and_advertisement_data` if + ``return_adv`` is ``True``, otherwise the value of :attr:`discovered_devices`. + + .. versionchanged:: 0.19 + Added ``return_adv`` parameter. + """ + async with cls(**kwargs) as scanner: + await asyncio.sleep(timeout) + + if return_adv: + return scanner.discovered_devices_and_advertisement_data + + return scanner.discovered_devices + + @property + def discovered_devices(self) -> list[BLEDevice]: + """ + Gets list of the devices that the scanner has discovered during the scanning. + + If you also need advertisement data, use :attr:`discovered_devices_and_advertisement_data` instead. + """ + return [d for d, _ in self._backend.seen_devices.values()] + + @property + def discovered_devices_and_advertisement_data( + self, + ) -> dict[str, tuple[BLEDevice, AdvertisementData]]: + """ + Gets a map of device address to tuples of devices and the most recently + received advertisement data for that device. + + The address keys are useful to compare the discovered devices to a set + of known devices. If you don't need to do that, consider using + ``discovered_devices_and_advertisement_data.values()`` to just get the + values instead. + + .. versionadded:: 0.19 + """ + return {d[0].address: d for d in self._backend.seen_devices.values()} + + @classmethod + async def find_device_by_address( + cls, device_identifier: str, timeout: float = 10.0, **kwargs: Unpack[ExtraArgs] + ) -> Optional[BLEDevice]: + """Obtain a ``BLEDevice`` for a BLE server specified by Bluetooth address or (macOS) UUID address. + + Args: + device_identifier: The Bluetooth/UUID address of the Bluetooth peripheral sought. + timeout: Optional timeout to wait for detection of specified peripheral before giving up. Defaults to 10.0 seconds. + **kwargs: additional args passed to the :class:`BleakScanner` constructor. + + Returns: + The ``BLEDevice`` sought or ``None`` if not detected. + + """ + device_identifier = device_identifier.lower() + return await cls.find_device_by_filter( + lambda d, ad: d.address.lower() == device_identifier, + timeout=timeout, + **kwargs, + ) + + @classmethod + async def find_device_by_name( + cls, name: str, timeout: float = 10.0, **kwargs: Unpack[ExtraArgs] + ) -> Optional[BLEDevice]: + """Obtain a ``BLEDevice`` for a BLE server specified by the local name in the advertising data. + + Args: + name: The name sought. + timeout: Optional timeout to wait for detection of specified peripheral before giving up. Defaults to 10.0 seconds. + **kwargs: additional args passed to the :class:`BleakScanner` constructor. + + Returns: + The ``BLEDevice`` sought or ``None`` if not detected. + + .. versionadded:: 0.20 + """ + return await cls.find_device_by_filter( + lambda d, ad: ad.local_name == name, + timeout=timeout, + **kwargs, + ) + + @classmethod + async def find_device_by_filter( + cls, + filterfunc: AdvertisementDataFilter, + timeout: float = 10.0, + **kwargs: Unpack[ExtraArgs], + ) -> Optional[BLEDevice]: + """Obtain a ``BLEDevice`` for a BLE server that matches a given filter function. + + This can be used to find a BLE server by other identifying information than its address, + for example its name. + + Args: + filterfunc: + A function that is called for every BLEDevice found. It should + return ``True`` only for the wanted device. + timeout: + Optional timeout to wait for detection of specified peripheral + before giving up. Defaults to 10.0 seconds. + **kwargs: + Additional arguments to be passed to the :class:`BleakScanner` + constructor. + + Returns: + The :class:`BLEDevice` sought or ``None`` if not detected before + the timeout. + + """ + async with cls(**kwargs) as scanner: + try: + async with async_timeout(timeout): + async for bd, ad in scanner.advertisement_data(): + if filterfunc(bd, ad): + return bd + assert_never(cast(Never, "advertisement_data() should never stop")) + except asyncio.TimeoutError: + return None + + +def _resolve_characteristic( + char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], + services: BleakGATTServiceCollection, +) -> BleakGATTCharacteristic: + + if isinstance(char_specifier, BleakGATTCharacteristic): + return char_specifier + + characteristic = services.get_characteristic(char_specifier) + + if not characteristic: + raise BleakCharacteristicNotFoundError(char_specifier) + + return characteristic + + +def _resolve_descriptor( + desc_specifier: Union[BleakGATTDescriptor, int], + services: BleakGATTServiceCollection, +) -> BleakGATTDescriptor: + + if isinstance(desc_specifier, BleakGATTDescriptor): + return desc_specifier + + characteristic = services.get_descriptor(desc_specifier) + + if not characteristic: + raise BleakError(f"Descriptor with handle {desc_specifier} was not found!") + + return characteristic + + +class BleakClient: + """The Client interface for connecting to a specific BLE GATT server and communicating with it. + + A BleakClient can be used as an asynchronous context manager in which case it automatically + connects and disconnects. + + How many BLE connections can be active simultaneously, and whether connections can be active while + scanning depends on the Bluetooth adapter hardware. + + Args: + address_or_ble_device: + A :class:`BLEDevice` received from a :class:`BleakScanner` or a + Bluetooth address (device UUID on macOS). + disconnected_callback: + 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. + services: + Optional list of services to filter. If provided, only these services + will be resolved. This may or may not reduce the time needed to + enumerate the services depending on if the OS supports such filtering + in the Bluetooth stack or not (should affect Windows and Mac). + These can be 16-bit or 128-bit UUIDs. + timeout: + Timeout in seconds passed to the implicit ``discover`` call when + ``address_or_ble_device`` is not a :class:`BLEDevice`. Defaults to 30. + pair: + Attempt to pair with the the device before connecting, if it is not + already paired. This has no effect on macOS since pairing is initiated + automatically when accessing a characteristic that requires authentication. + In rare cases, on other platforms, it might be necessary to pair the + device first in order to be able to even enumerate the services during + the connection process. + bluez: + Dictionary of BlueZ/Linux platform-specific options. + winrt: + Dictionary of WinRT/Windows platform-specific options. + backend: + Used to override the automatically selected backend (i.e. for a + custom backend). + **kwargs: + Additional keyword arguments for backwards compatibility. + + .. tip:: If you enable pairing with the ``pair`` argument, you will also + want to extend the timeout to allow enough time for the user to find + and enter the PIN code on the device, if required. + + .. warning:: Although example code frequently initializes :class:`BleakClient` + with a Bluetooth address for simplicity, it is not recommended to do so + for more complex use cases. There are several known issues with providing + a Bluetooth address as the ``address_or_ble_device`` argument. + + 1. macOS does not provide access to the Bluetooth address for privacy/ + security reasons. Instead it creates a UUID for each Bluetooth + device which is used in place of the address on this platform. + 2. Providing an address or UUID instead of a :class:`BLEDevice` causes + the :meth:`connect` method to implicitly call :meth:`BleakScanner.discover`. + This is known to cause problems when trying to connect to multiple + devices at the same time. + + .. versionchanged:: 0.15 + ``disconnected_callback`` is no longer keyword-only. Added ``winrt`` parameter. + + .. versionchanged:: 0.18 + No longer is alias for backend type and no longer inherits from :class:`BaseBleakClient`. + Added ``backend`` parameter. + + .. versionchanged:: 1.0 + Added ``pair`` parameter. + + .. versionchanged:: 2.1.1 + Changed default connect timeout from 10 to 30 seconds. + + .. versionchanged:: 3.0 + Added ``bluez`` parameter. + """ + + def __init__( + self, + address_or_ble_device: Union[BLEDevice, str], + disconnected_callback: Optional[Callable[[BleakClient], None]] = None, + services: Optional[Iterable[str]] = None, + *, + timeout: float = 30, + pair: bool = False, + bluez: BlueZClientArgs = {}, + winrt: WinRTClientArgs = {}, + backend: Optional[type[BaseBleakClient]] = None, + **kwargs: Any, + ) -> None: + PlatformBleakClient, backend_id = ( + get_platform_client_backend_type() + if backend is None + else (backend, backend.__name__) + ) + + # TODO: upgrade this to FutureWarning in 2027 and remove in 2028 or so + adapter_kwarg = kwargs.get("adapter") + if adapter_kwarg is not None: + warn( + "the 'adapter' keyword argument is deprecated, use the 'bluez' kwarg instead with {'adapter': ''}", + DeprecationWarning, + stacklevel=2, + ) + + if "adapter" not in bluez: + bluez["adapter"] = adapter_kwarg + + self._backend = PlatformBleakClient( + address_or_ble_device, + disconnected_callback=( + None + if disconnected_callback is None + else functools.partial(disconnected_callback, self) + ), + services=( + None if services is None else set(map(normalize_uuid_str, services)) + ), + timeout=timeout, + bluez=bluez, + winrt=winrt, + **kwargs, + ) + self._pair_before_connect = pair + self._backend_id = backend_id + + @property + def backend_id(self) -> BleakBackend | str: + """ + Gets the identifier of the backend in use. + + The value is one of the :class:`BleakBackend` enum values in case of + built-in backends, or a string identifying a custom backend. + + .. versionadded:: 2.0 + """ + return self._backend_id + + # device info + + @property + def name(self) -> str: + """ + Gets a human-readable name for the peripheral device. + + The name can be somewhat OS-dependent. It is usually the name provided + by the standard Device Name characteristic, if present or the name + provided by the advertising data. If neither is available, it will be + a Bluetooth address separated with dashes (``-``) instead of colons + (``:``) (or a UUID on Apple devices). It may also be possible to override + the device name using the OS's Bluetooth settings. + + .. versionadded:: 1.1 + """ + return self._backend.name + + @property + def address(self) -> str: + """ + Gets the Bluetooth address of this device (UUID on macOS). + """ + return self._backend.address + + @property + def mtu_size(self) -> int: + """ + Gets the negotiated MTU size in bytes for the active connection. + + Consider using :attr:`bleak.backends.characteristic.BleakGATTCharacteristic.max_write_without_response_size` instead. + + .. warning:: The BlueZ backend will always return 23 (the minimum MTU size). + See the ``mtu_size.py`` example for a way to hack around this. + + """ + return self._backend.mtu_size + + def __str__(self) -> str: + return f"{self.__class__.__name__}, {self.address}" + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}, {self.address}, {type(self._backend)}>" + + # Async Context managers + + async def __aenter__(self) -> Self: + await self.connect() + return self + + async def __aexit__( + self, + exc_type: Optional[type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + await self.disconnect() + + # Connectivity methods + + async def connect(self, **kwargs: Any) -> None: + """Connect to the specified GATT server. + + Args: + **kwargs: For backwards compatibility - should not be used. + + .. versionchanged:: 1.0 + No longer returns ``True``. Instead, the return type is ``None``. + """ + await self._backend.connect(self._pair_before_connect, **kwargs) + + async def disconnect(self) -> None: + """Disconnect from the specified GATT server. + + .. versionchanged:: 1.0 + No longer returns ``True``. Instead, the return type is ``None``. + """ + await self._backend.disconnect() + + async def pair(self, *args: Any, **kwargs: Any) -> None: + """ + Pair with the specified GATT server. + + This method is not available on macOS. Instead of manually initiating + paring, the user will be prompted to pair the device the first time + that a characteristic that requires authentication is read or written. + This method may have backend-specific additional keyword arguments. + + .. versionchanged:: 1.0 + No longer returns ``True``. Instead, the return type is ``None``. + """ + await self._backend.pair(*args, **kwargs) + + async def unpair(self) -> None: + """ + Unpair from the specified GATT server. + + Unpairing will also disconnect the device. + + This method is only available on Windows and Linux and will raise an + exception on other platforms. + + .. versionchanged:: 1.0 + No longer returns ``True``. Instead, the return type is ``None``. + """ + await self._backend.unpair() + + @property + def is_connected(self) -> bool: + """ + Check connection status between this client and the GATT server. + + Returns: + Boolean representing connection status. + + """ + return self._backend.is_connected + + # GATT services methods + + @property + def services(self) -> BleakGATTServiceCollection: + """ + Gets the collection of GATT services available on the device. + + The returned value is only valid as long as the device is connected. + + Raises: + BleakError: if service discovery has not been performed yet during this connection. + """ + if not self._backend.services: + raise BleakError("Service Discovery has not been performed yet") + + return self._backend.services + + # I/O methods + + async def read_gatt_char( + self, + char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], + *, + use_cached: bool = False, + **kwargs: Any, + ) -> bytearray: + """ + Perform read operation on the specified GATT characteristic. + + Args: + char_specifier: + The characteristic to read from, specified by either integer + handle, UUID or directly by the BleakGATTCharacteristic object + representing it. + use_cached: + If ``True``, the cached value will be returned instead of + performing a new read operation. May be ignored by some backends. + + Returns: + The read data. + + Raises: + BleakCharacteristicNotFoundError: if a characteristic with the + handle or UUID specified by ``char_specifier`` could not be found. + BleakGATTProtocolError: if the peripheral replied with ATT_ERROR_RSP. + backend-specific exceptions: in rare cases + + .. versionchanged:: 3.0 + Now raises ``BleakGATTProtocolError`` when possible instead of + backend-specific exceptions. + """ + characteristic = _resolve_characteristic(char_specifier, self.services) + return await self._backend.read_gatt_char( + characteristic, use_cached=use_cached, **kwargs + ) + + async def write_gatt_char( + self, + char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], + data: SizedBuffer, + response: Optional[bool] = None, + ) -> None: + r""" + Perform a write operation on the specified GATT characteristic. + + There are two possible kinds of writes. *Write with response* (sometimes + called a *Request*) will write the data then wait for a response from + the remote device. *Write without response* (sometimes called *Command*) + will queue data to be written and return immediately. + + Each characteristic may support one kind or the other or both or neither. + Consult the device's documentation or inspect the properties of the + characteristic to find out which kind of writes are supported. + + Args: + char_specifier: + The characteristic to write to, specified by either integer + handle, UUID or directly by the :class:`~bleak.backends.characteristic.BleakGATTCharacteristic` + object representing it. If a device has more than one characteristic + with the same UUID, then attempting to use the UUID wil fail and + a characteristic object must be used instead. + data: + The data to send. When a write-with-response operation is used, + the length of the data is limited to 512 bytes. When a + write-without-response operation is used, the length of the + data is limited to :attr:`~bleak.backends.characteristic.BleakGATTCharacteristic.max_write_without_response_size`. + Any type that supports the buffer protocol can be passed. + response: + If ``True``, a write-with-response operation will be used. If + ``False``, a write-without-response operation will be used. + Omitting the argument is deprecated and may raise a warning. + If this arg is omitted, the default behavior is to check the + characteristic properties to see if the "write" property is + present. If it is, a write-with-response operation will be + used. Note: some devices may incorrectly report or omit the + property, which is why an explicit argument is encouraged. + + Raises: + BleakCharacteristicNotFoundError: if a characteristic with the + handle or UUID specified by ``char_specifier`` could not be found. + BleakGATTProtocolError: if the peripheral replied with ATT_ERROR_RSP. + Only applies when ``response=True``. + backend-specific exceptions: in rare cases. + + .. versionchanged:: 0.21 + The default behavior when ``response=`` is omitted was changed. + + .. versionchanged:: 3.0 + Now raises ``BleakGATTProtocolError`` when possible instead of + backend-specific exceptions. + + Example:: + + MY_CHAR_UUID = "1234" + ... + await client.write_gatt_char(MY_CHAR_UUID, b"\x00\x01\x02\x03", response=True) + """ + characteristic = _resolve_characteristic(char_specifier, self.services) + + if response is None: + # If not specified, prefer write-with-response over write-without- + # response if it is available since it is the more reliable write. + # This assumes that the peripheral correctly reports the + # characteristic properties, so doesn't work in some cases. + response = "write" in characteristic.properties + + await self._backend.write_gatt_char(characteristic, data, response) + + async def start_notify( + self, + char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], + callback: Callable[ + [BleakGATTCharacteristic, bytearray], Union[None, Awaitable[None]] + ], + *, + bluez: BlueZNotifyArgs = {}, + cb: CBStartNotifyArgs = {}, + **kwargs: Any, + ) -> None: + """ + Activate notifications/indications on a characteristic. + + Callbacks must accept two inputs. The first will be the characteristic + and the second will be a ``bytearray`` containing the data received. + + .. code-block:: python + + def callback(sender: BleakGATTCharacteristic, data: bytearray): + print(f"{sender}: {data}") + + client.start_notify(char_uuid, callback) + + Args: + char_specifier: + The characteristic to activate notifications/indications on a + characteristic, specified by either integer handle, + UUID or directly by the BleakGATTCharacteristic object representing it. + callback: + The function to be called on notification. Can be regular + function or async function. + bluez: + BlueZ backend-specific arguments. + cb: + CoreBluetooth backend-specific arguments. + + Raises: + BleakCharacteristicNotFoundError: if a characteristic with the + handle or UUID specified by ``char_specifier`` could not be found. + backend-specific exceptions: if the start notification operation failed. + + .. versionchanged:: 0.18 + The first argument of the callback is now a :class:`BleakGATTCharacteristic` + instead of an ``int``. + + .. versionchanged:: 1.0 + Added the ``cb`` parameter. + + .. versionchanged:: 2.1 + Added the ``bluez`` parameter. + """ + if not self.is_connected: + raise BleakError("Not connected") + + characteristic = _resolve_characteristic(char_specifier, self.services) + + if inspect.iscoroutinefunction(callback): + + def wrapped_callback(data: bytearray) -> None: + task = asyncio.create_task(callback(characteristic, data)) + _background_tasks.add(task) + task.add_done_callback(_background_tasks.discard) + + else: + wrapped_callback = functools.partial(callback, characteristic) # type: ignore + + await self._backend.start_notify( + characteristic, wrapped_callback, bluez=bluez, cb=cb, **kwargs + ) + + async def stop_notify( + self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID] + ) -> None: + """ + Deactivate notification/indication on a specified characteristic. + + Args: + char_specifier: + The characteristic to deactivate notification/indication on, + specified by either integer handle, UUID or directly by the + BleakGATTCharacteristic object representing it. + + Raises: + BleakCharacteristicNotFoundError: if a characteristic with the + handle or UUID specified by ``char_specifier`` could not be found. + backend-specific exceptions: if the stop notification operation failed. + + .. tip:: Notifications are stopped automatically on disconnect, so this + method does not need to be called unless notifications need to be + stopped some time before the device disconnects. + """ + characteristic = _resolve_characteristic(char_specifier, self.services) + await self._backend.stop_notify(characteristic) + + async def read_gatt_descriptor( + self, + desc_specifier: Union[BleakGATTDescriptor, int], + *, + use_cached: bool = False, + **kwargs: Any, + ) -> bytearray: + """ + Perform read operation on the specified GATT descriptor. + + Args: + desc_specifier: + The descriptor to read from, specified by either integer handle + or directly by the BleakGATTDescriptor object representing it. + use_cached: + If ``True``, the cached value will be returned instead of + performing a new read operation. May be ignored by some backends. + + Returns: + The read data. + + Raises: + BleakError: if the descriptor could not be found. + BleakGATTProtocolError: if the peripheral replied with ATT_ERROR_RSP. + backend-specific exceptions: in rare cases. + + .. versionchanged:: 3.0 + Now raises ``BleakGATTProtocolError`` when possible instead of + backend-specific exceptions. + """ + descriptor = _resolve_descriptor(desc_specifier, self.services) + return await self._backend.read_gatt_descriptor( + descriptor, use_cached=use_cached, **kwargs + ) + + async def write_gatt_descriptor( + self, + desc_specifier: Union[BleakGATTDescriptor, int], + data: SizedBuffer, + ) -> None: + """ + Perform a write operation on the specified GATT descriptor. + + Args: + desc_specifier: + The descriptor to write to, specified by either integer handle + directly by the BleakGATTDescriptor object representing it. + data: + The data to send. + + Raises: + BleakError: if the descriptor could not be found. + ValueError: if attempting to write to the Client Characteristic + Configuration Descriptor (CCCD, UUID 0x2902). + BleakGATTProtocolError: if the peripheral replied with ATT_ERROR_RSP. + backend-specific exceptions: in rare cases. + + .. versionchanged:: 3.0 + Now raises ``ValueError`` when attempting to write to the CCCD + instead of backend-specific exceptions. + + .. versionchanged:: 3.0 + Now raises ``BleakGATTProtocolError`` when possible instead of + backend-specific exceptions. + """ + descriptor = _resolve_descriptor(desc_specifier, self.services) + + # Some backends do not allow allowing writing Client Characteristic + # Configuration Descriptor (CCCD) directly and will raise an OS-specific + # error. Instead we check for this here and raise a common exception for + # consistent cross-platform behavior. + if descriptor.uuid == normalize_uuid_16(0x2902): + raise ValueError( + "Cannot write to CCCD (0x2902) directly. Use start_notify() or stop_notify() instead." + ) + + await self._backend.write_gatt_descriptor(descriptor, data) diff --git a/tools/python-embed/Lib/site-packages/bleak/__pycache__/__init__.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..f89a98d Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/__pycache__/__init__.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/__pycache__/_compat.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/__pycache__/_compat.cpython-312.pyc new file mode 100644 index 0000000..51374c8 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/__pycache__/_compat.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/__pycache__/assigned_numbers.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/__pycache__/assigned_numbers.cpython-312.pyc new file mode 100644 index 0000000..9b0a223 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/__pycache__/assigned_numbers.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/__pycache__/exc.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/__pycache__/exc.cpython-312.pyc new file mode 100644 index 0000000..a614e34 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/__pycache__/exc.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/__pycache__/uuids.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/__pycache__/uuids.cpython-312.pyc new file mode 100644 index 0000000..2dedc14 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/__pycache__/uuids.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/_compat.py b/tools/python-embed/Lib/site-packages/bleak/_compat.py new file mode 100644 index 0000000..d28a6e1 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/_compat.py @@ -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 diff --git a/tools/python-embed/Lib/site-packages/bleak/args/__init__.py b/tools/python-embed/Lib/site-packages/bleak/args/__init__.py new file mode 100644 index 0000000..3162c8b --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/args/__init__.py @@ -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 + """ diff --git a/tools/python-embed/Lib/site-packages/bleak/args/__pycache__/__init__.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/args/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..94c7351 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/args/__pycache__/__init__.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/args/__pycache__/bluez.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/args/__pycache__/bluez.cpython-312.pyc new file mode 100644 index 0000000..1e21431 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/args/__pycache__/bluez.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/args/__pycache__/corebluetooth.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/args/__pycache__/corebluetooth.cpython-312.pyc new file mode 100644 index 0000000..87c6b27 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/args/__pycache__/corebluetooth.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/args/__pycache__/winrt.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/args/__pycache__/winrt.cpython-312.pyc new file mode 100644 index 0000000..7f76fef Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/args/__pycache__/winrt.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/args/bluez.py b/tools/python-embed/Lib/site-packages/bleak/args/bluez.py new file mode 100644 index 0000000..b3ccbc5 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/args/bluez.py @@ -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``. + """ diff --git a/tools/python-embed/Lib/site-packages/bleak/args/corebluetooth.py b/tools/python-embed/Lib/site-packages/bleak/args/corebluetooth.py new file mode 100644 index 0000000..4850a95 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/args/corebluetooth.py @@ -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 `_. + """ + + +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. + """ diff --git a/tools/python-embed/Lib/site-packages/bleak/args/winrt.py b/tools/python-embed/Lib/site-packages/bleak/args/winrt.py new file mode 100644 index 0000000..856d9f0 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/args/winrt.py @@ -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. + """ diff --git a/tools/python-embed/Lib/site-packages/bleak/assigned_numbers.py b/tools/python-embed/Lib/site-packages/bleak/assigned_numbers.py new file mode 100644 index 0000000..df1bf6b --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/assigned_numbers.py @@ -0,0 +1,93 @@ +""" +Bluetooth Assigned Numbers +-------------------------- + +This module contains useful assigned numbers from the Bluetooth spec. + +See . +""" + +from enum import IntEnum +from typing import Literal + + +class AdvertisementDataType(IntEnum): + """ + Generic Access Profile advertisement data types. + + `Source `. + + .. 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 + ) diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/__init__.py b/tools/python-embed/Lib/site-packages/bleak/backends/__init__.py new file mode 100644 index 0000000..7d3c053 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/__init__.py @@ -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()}") diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/__init__.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..cf6cf2d Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/__init__.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/_manufacturers.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/_manufacturers.cpython-312.pyc new file mode 100644 index 0000000..720c9a3 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/_manufacturers.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/_utils.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/_utils.cpython-312.pyc new file mode 100644 index 0000000..6f11fc2 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/_utils.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/characteristic.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/characteristic.cpython-312.pyc new file mode 100644 index 0000000..3ba771a Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/characteristic.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/client.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/client.cpython-312.pyc new file mode 100644 index 0000000..9679b22 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/client.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/descriptor.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/descriptor.cpython-312.pyc new file mode 100644 index 0000000..8a70e37 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/descriptor.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/device.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/device.cpython-312.pyc new file mode 100644 index 0000000..8894236 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/device.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/scanner.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/scanner.cpython-312.pyc new file mode 100644 index 0000000..80d998d Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/scanner.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/service.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/service.cpython-312.pyc new file mode 100644 index 0000000..c57e25c Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/__pycache__/service.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/_manufacturers.py b/tools/python-embed/Lib/site-packages/bleak/backends/_manufacturers.py new file mode 100644 index 0000000..4a16bee --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/_manufacturers.py @@ -0,0 +1,1936 @@ +""" +Manufacturer data retrieved from https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers +""" + +MANUFACTURERS = { + 0x0000: "Ericsson Technology Licensing", + 0x0001: "Nokia Mobile Phones", + 0x0002: "Intel Corp.", + 0x0003: "IBM Corp.", + 0x0004: "Toshiba Corp.", + 0x0005: "3Com", + 0x0006: "Microsoft", + 0x0007: "Lucent", + 0x0008: "Motorola", + 0x0009: "Infineon Technologies AG", + 0x000A: "Qualcomm Technologies International, Ltd. (QTIL)", + 0x000B: "Silicon Wave", + 0x000C: "Digianswer A/S", + 0x000D: "Texas Instruments Inc.", + 0x000E: "Parthus Technologies Inc.", + 0x000F: "Broadcom Corporation", + 0x0010: "Mitel Semiconductor", + 0x0011: "Widcomm, Inc.", + 0x0012: "Zeevo, Inc.", + 0x0013: "Atmel Corporation", + 0x0014: "Mitsubishi Electric Corporation", + 0x0015: "RTX Telecom A/S", + 0x0016: "KC Technology Inc.", + 0x0017: "Newlogic", + 0x0018: "Transilica, Inc.", + 0x0019: "Rohde & Schwarz GmbH & Co. KG", + 0x001A: "TTPCom Limited", + 0x001B: "Signia Technologies, Inc.", + 0x001C: "Conexant Systems Inc.", + 0x001D: "Qualcomm", + 0x001E: "Inventel", + 0x001F: "AVM Berlin", + 0x0020: "BandSpeed, Inc.", + 0x0021: "Mansella Ltd", + 0x0022: "NEC Corporation", + 0x0023: "WavePlus Technology Co., Ltd.", + 0x0024: "Alcatel", + 0x0025: "NXP Semiconductors (formerly Philips Semiconductors)", + 0x0026: "C Technologies", + 0x0027: "Open Interface", + 0x0028: "R F Micro Devices", + 0x0029: "Hitachi Ltd", + 0x002A: "Symbol Technologies, Inc.", + 0x002B: "Tenovis", + 0x002C: "Macronix International Co. Ltd.", + 0x002D: "GCT Semiconductor", + 0x002E: "Norwood Systems", + 0x002F: "MewTel Technology Inc.", + 0x0030: "ST Microelectronics", + 0x0031: "Synopsys, Inc.", + 0x0032: "Red-M (Communications) Ltd", + 0x0033: "Commil Ltd", + 0x0034: "Computer Access Technology Corporation (CATC)", + 0x0035: "Eclipse (HQ Espana) S.L.", + 0x0036: "Renesas Electronics Corporation", + 0x0037: "Mobilian Corporation", + 0x0038: "Syntronix Corporation", + 0x0039: "Integrated System Solution Corp.", + 0x003A: "Matsushita Electric Industrial Co., Ltd.", + 0x003B: "Gennum Corporation", + 0x003C: "BlackBerry Limited (formerly Research In Motion)", + 0x003D: "IPextreme, Inc.", + 0x003E: "Systems and Chips, Inc", + 0x003F: "Bluetooth SIG, Inc", + 0x0040: "Seiko Epson Corporation", + 0x0041: "Integrated Silicon Solution Taiwan, Inc.", + 0x0042: "CONWISE Technology Corporation Ltd", + 0x0043: "PARROT AUTOMOTIVE SAS", + 0x0044: "Socket Mobile", + 0x0045: "Atheros Communications, Inc.", + 0x0046: "MediaTek, Inc.", + 0x0047: "Bluegiga", + 0x0048: "Marvell Technology Group Ltd.", + 0x0049: "3DSP Corporation", + 0x004A: "Accel Semiconductor Ltd.", + 0x004B: "Continental Automotive Systems", + 0x004C: "Apple, Inc.", + 0x004D: "Staccato Communications, Inc.", + 0x004E: "Avago Technologies", + 0x004F: "APT Ltd.", + 0x0050: "SiRF Technology, Inc.", + 0x0051: "Tzero Technologies, Inc.", + 0x0052: "J&M Corporation", + 0x0053: "Free2move AB", + 0x0054: "3DiJoy Corporation", + 0x0055: "Plantronics, Inc.", + 0x0056: "Sony Ericsson Mobile Communications", + 0x0057: "Harman International Industries, Inc.", + 0x0058: "Vizio, Inc.", + 0x0059: "Nordic Semiconductor ASA", + 0x005A: "EM Microelectronic-Marin SA", + 0x005B: "Ralink Technology Corporation", + 0x005C: "Belkin International, Inc.", + 0x005D: "Realtek Semiconductor Corporation", + 0x005E: "Stonestreet One, LLC", + 0x005F: "Wicentric, Inc.", + 0x0060: "RivieraWaves S.A.S", + 0x0061: "RDA Microelectronics", + 0x0062: "Gibson Guitars", + 0x0063: "MiCommand Inc.", + 0x0064: "Band XI International, LLC", + 0x0065: "Hewlett-Packard Company", + 0x0066: "9Solutions Oy", + 0x0067: "GN Netcom A/S", + 0x0068: "General Motors", + 0x0069: "A&D Engineering, Inc.", + 0x006A: "MindTree Ltd.", + 0x006B: "Polar Electro OY", + 0x006C: "Beautiful Enterprise Co., Ltd.", + 0x006D: "BriarTek, Inc", + 0x006E: "Summit Data Communications, Inc.", + 0x006F: "Sound ID", + 0x0070: "Monster, LLC", + 0x0071: "connectBlue AB", + 0x0072: "ShangHai Super Smart Electronics Co. Ltd.", + 0x0073: "Group Sense Ltd.", + 0x0074: "Zomm, LLC", + 0x0075: "Samsung Electronics Co. Ltd.", + 0x0076: "Creative Technology Ltd.", + 0x0077: "Laird Technologies", + 0x0078: "Nike, Inc.", + 0x0079: "lesswire AG", + 0x007A: "MStar Semiconductor, Inc.", + 0x007B: "Hanlynn Technologies", + 0x007C: "A & R Cambridge", + 0x007D: "Seers Technology Co., Ltd.", + 0x007E: "Sports Tracking Technologies Ltd.", + 0x007F: "Autonet Mobile", + 0x0080: "DeLorme Publishing Company, Inc.", + 0x0081: "WuXi Vimicro", + 0x0082: "Sennheiser Communications A/S", + 0x0083: "TimeKeeping Systems, Inc.", + 0x0084: "Ludus Helsinki Ltd.", + 0x0085: "BlueRadios, Inc.", + 0x0086: "Equinux AG", + 0x0087: "Garmin International, Inc.", + 0x0088: "Ecotest", + 0x0089: "GN ReSound A/S", + 0x008A: "Jawbone", + 0x008B: "Topcon Positioning Systems, LLC", + 0x008C: "Gimbal Inc. (formerly Qualcomm Labs, Inc. and Qualcomm Retail Solutions, Inc.)", + 0x008D: "Zscan Software", + 0x008E: "Quintic Corp", + 0x008F: "Telit Wireless Solutions GmbH (formerly Stollmann E+V GmbH)", + 0x0090: "Funai Electric Co., Ltd.", + 0x0091: "Advanced PANMOBIL systems GmbH & Co. KG", + 0x0092: "ThinkOptics, Inc.", + 0x0093: "Universal Electronics, Inc.", + 0x0094: "Airoha Technology Corp.", + 0x0095: "NEC Lighting, Ltd.", + 0x0096: "ODM Technology, Inc.", + 0x0097: "ConnecteDevice Ltd.", + 0x0098: "zero1.tv GmbH", + 0x0099: "i.Tech Dynamic Global Distribution Ltd.", + 0x009A: "Alpwise", + 0x009B: "Jiangsu Toppower Automotive Electronics Co., Ltd.", + 0x009C: "Colorfy, Inc.", + 0x009D: "Geoforce Inc.", + 0x009E: "Bose Corporation", + 0x009F: "Suunto Oy", + 0x00A0: "Kensington Computer Products Group", + 0x00A1: "SR-Medizinelektronik", + 0x00A2: "Vertu Corporation Limited", + 0x00A3: "Meta Watch Ltd.", + 0x00A4: "LINAK A/S", + 0x00A5: "OTL Dynamics LLC", + 0x00A6: "Panda Ocean Inc.", + 0x00A7: "Visteon Corporation", + 0x00A8: "ARP Devices Limited", + 0x00A9: "Magneti Marelli S.p.A", + 0x00AA: "CAEN RFID srl", + 0x00AB: "Ingenieur-Systemgruppe Zahn GmbH", + 0x00AC: "Green Throttle Games", + 0x00AD: "Peter Systemtechnik GmbH", + 0x00AE: "Omegawave Oy", + 0x00AF: "Cinetix", + 0x00B0: "Passif Semiconductor Corp", + 0x00B1: "Saris Cycling Group, Inc", + 0x00B2: "Bekey A/S", + 0x00B3: "Clarinox Technologies Pty. Ltd.", + 0x00B4: "BDE Technology Co., Ltd.", + 0x00B5: "Swirl Networks", + 0x00B6: "Meso international", + 0x00B7: "TreLab Ltd", + 0x00B8: "Qualcomm Innovation Center, Inc. (QuIC)", + 0x00B9: "Johnson Controls, Inc.", + 0x00BA: "Starkey Laboratories Inc.", + 0x00BB: "S-Power Electronics Limited", + 0x00BC: "Ace Sensor Inc", + 0x00BD: "Aplix Corporation", + 0x00BE: "AAMP of America", + 0x00BF: "Stalmart Technology Limited", + 0x00C0: "AMICCOM Electronics Corporation", + 0x00C1: "Shenzhen Excelsecu Data Technology Co.,Ltd", + 0x00C2: "Geneq Inc.", + 0x00C3: "adidas AG", + 0x00C4: "LG Electronics", + 0x00C5: "Onset Computer Corporation", + 0x00C6: "Selfly BV", + 0x00C7: "Quuppa Oy.", + 0x00C8: "GeLo Inc", + 0x00C9: "Evluma", + 0x00CA: "MC10", + 0x00CB: "Binauric SE", + 0x00CC: "Beats Electronics", + 0x00CD: "Microchip Technology Inc.", + 0x00CE: "Elgato Systems GmbH", + 0x00CF: "ARCHOS SA", + 0x00D0: "Dexcom, Inc.", + 0x00D1: "Polar Electro Europe B.V.", + 0x00D2: "Dialog Semiconductor B.V.", + 0x00D3: "Taixingbang Technology (HK) Co,. LTD.", + 0x00D4: "Kawantech", + 0x00D5: "Austco Communication Systems", + 0x00D6: "Timex Group USA, Inc.", + 0x00D7: "Qualcomm Technologies, Inc.", + 0x00D8: "Qualcomm Connected Experiences, Inc.", + 0x00D9: "Voyetra Turtle Beach", + 0x00DA: "txtr GmbH", + 0x00DB: "Biosentronics", + 0x00DC: "Procter & Gamble", + 0x00DD: "Hosiden Corporation", + 0x00DE: "Muzik LLC", + 0x00DF: "Misfit Wearables Corp", + 0x00E0: "Google", + 0x00E1: "Danlers Ltd", + 0x00E2: "Semilink Inc", + 0x00E3: "inMusic Brands, Inc", + 0x00E4: "L.S. Research Inc.", + 0x00E5: "Eden Software Consultants Ltd.", + 0x00E6: "Freshtemp", + 0x00E7: "KS Technologies", + 0x00E8: "ACTS Technologies", + 0x00E9: "Vtrack Systems", + 0x00EA: "Nielsen-Kellerman Company", + 0x00EB: "Server Technology Inc.", + 0x00EC: "BioResearch Associates", + 0x00ED: "Jolly Logic, LLC", + 0x00EE: "Above Average Outcomes, Inc.", + 0x00EF: "Bitsplitters GmbH", + 0x00F0: "PayPal, Inc.", + 0x00F1: "Witron Technology Limited", + 0x00F2: "Morse Project Inc.", + 0x00F3: "Kent Displays Inc.", + 0x00F4: "Nautilus Inc.", + 0x00F5: "Smartifier Oy", + 0x00F6: "Elcometer Limited", + 0x00F7: "VSN Technologies, Inc.", + 0x00F8: "AceUni Corp., Ltd.", + 0x00F9: "StickNFind", + 0x00FA: "Crystal Code AB", + 0x00FB: "KOUKAAM a.s.", + 0x00FC: "Delphi Corporation", + 0x00FD: "ValenceTech Limited", + 0x00FE: "Stanley Black and Decker", + 0x00FF: "Typo Products, LLC", + 0x0100: "TomTom International BV", + 0x0101: "Fugoo, Inc.", + 0x0102: "Keiser Corporation", + 0x0103: "Bang & Olufsen A/S", + 0x0104: "PLUS Location Systems Pty Ltd", + 0x0105: "Ubiquitous Computing Technology Corporation", + 0x0106: "Innovative Yachtter Solutions", + 0x0107: "William Demant Holding A/S", + 0x0108: "Chicony Electronics Co., Ltd.", + 0x0109: "Atus BV", + 0x010A: "Codegate Ltd", + 0x010B: "ERi, Inc", + 0x010C: "Transducers Direct, LLC", + 0x010D: "Fujitsu Ten LImited", + 0x010E: "Audi AG", + 0x010F: "HiSilicon Technologies Col, Ltd.", + 0x0110: "Nippon Seiki Co., Ltd.", + 0x0111: "Steelseries ApS", + 0x0112: "Visybl Inc.", + 0x0113: "Openbrain Technologies, Co., Ltd.", + 0x0114: "Xensr", + 0x0115: "e.solutions", + 0x0116: "10AK Technologies", + 0x0117: "Wimoto Technologies Inc", + 0x0118: "Radius Networks, Inc.", + 0x0119: "Wize Technology Co., Ltd.", + 0x011A: "Qualcomm Labs, Inc.", + 0x011B: "Hewlett Packard Enterprise", + 0x011C: "Baidu", + 0x011D: "Arendi AG", + 0x011E: "Skoda Auto a.s.", + 0x011F: "Volkswagen AG", + 0x0120: "Porsche AG", + 0x0121: "Sino Wealth Electronic Ltd.", + 0x0122: "AirTurn, Inc.", + 0x0123: "Kinsa, Inc", + 0x0124: "HID Global", + 0x0125: "SEAT es", + 0x0126: "Promethean Ltd.", + 0x0127: "Salutica Allied Solutions", + 0x0128: "GPSI Group Pty Ltd", + 0x0129: "Nimble Devices Oy", + 0x012A: "Changzhou Yongse Infotech Co., Ltd.", + 0x012B: "SportIQ", + 0x012C: "TEMEC Instruments B.V.", + 0x012D: "Sony Corporation", + 0x012E: "ASSA ABLOY", + 0x012F: "Clarion Co. Inc.", + 0x0130: "Warehouse Innovations", + 0x0131: "Cypress Semiconductor", + 0x0132: "MADS Inc", + 0x0133: "Blue Maestro Limited", + 0x0134: "Resolution Products, Ltd.", + 0x0135: "Aireware LLC", + 0x0136: "Silvair, Inc.", + 0x0137: "Prestigio Plaza Ltd.", + 0x0138: "NTEO Inc.", + 0x0139: "Focus Systems Corporation", + 0x013A: "Tencent Holdings Ltd.", + 0x013B: "Allegion", + 0x013C: "Murata Manufacturing Co., Ltd.", + 0x013D: "WirelessWERX", + 0x013E: "Nod, Inc.", + 0x013F: "B&B Manufacturing Company", + 0x0140: "Alpine Electronics (China) Co., Ltd", + 0x0141: "FedEx Services", + 0x0142: "Grape Systems Inc.", + 0x0143: "Bkon Connect", + 0x0144: "Lintech GmbH", + 0x0145: "Novatel Wireless", + 0x0146: "Ciright", + 0x0147: "Mighty Cast, Inc.", + 0x0148: "Ambimat Electronics", + 0x0149: "Perytons Ltd.", + 0x014A: "Tivoli Audio, LLC", + 0x014B: "Master Lock", + 0x014C: "Mesh-Net Ltd", + 0x014D: "HUIZHOU DESAY SV AUTOMOTIVE CO., LTD.", + 0x014E: "Tangerine, Inc.", + 0x014F: "B&W Group Ltd.", + 0x0150: "Pioneer Corporation", + 0x0151: "OnBeep", + 0x0152: "Vernier Software & Technology", + 0x0153: "ROL Ergo", + 0x0154: "Pebble Technology", + 0x0155: "NETATMO", + 0x0156: "Accumulate AB", + 0x0157: "Anhui Huami Information Technology Co., Ltd.", + 0x0158: "Inmite s.r.o.", + 0x0159: "ChefSteps, Inc.", + 0x015A: "micas AG", + 0x015B: "Biomedical Research Ltd.", + 0x015C: "Pitius Tec S.L.", + 0x015D: "Estimote, Inc.", + 0x015E: "Unikey Technologies, Inc.", + 0x015F: "Timer Cap Co.", + 0x0160: "AwoX", + 0x0161: "yikes", + 0x0162: "MADSGlobalNZ Ltd.", + 0x0163: "PCH International", + 0x0164: "Qingdao Yeelink Information Technology Co., Ltd.", + 0x0165: "Milwaukee Tool (Formally Milwaukee Electric Tools)", + 0x0166: "MISHIK Pte Ltd", + 0x0167: "Ascensia Diabetes Care US Inc.", + 0x0168: "Spicebox LLC", + 0x0169: "emberlight", + 0x016A: "Cooper-Atkins Corporation", + 0x016B: "Qblinks", + 0x016C: "MYSPHERA", + 0x016D: "LifeScan Inc", + 0x016E: "Volantic AB", + 0x016F: "Podo Labs, Inc", + 0x0170: "Roche Diabetes Care AG", + 0x0171: "Amazon Fulfillment Service", + 0x0172: "Connovate Technology Private Limited", + 0x0173: "Kocomojo, LLC", + 0x0174: "Everykey Inc.", + 0x0175: "Dynamic Controls", + 0x0176: "SentriLock", + 0x0177: "I-SYST inc.", + 0x0178: "CASIO COMPUTER CO., LTD.", + 0x0179: "LAPIS Semiconductor Co., Ltd.", + 0x017A: "Telemonitor, Inc.", + 0x017B: "taskit GmbH", + 0x017C: "Daimler AG", + 0x017D: "BatAndCat", + 0x017E: "BluDotz Ltd", + 0x017F: "XTel Wireless ApS", + 0x0180: "Gigaset Communications GmbH", + 0x0181: "Gecko Health Innovations, Inc.", + 0x0182: "HOP Ubiquitous", + 0x0183: "Walt Disney", + 0x0184: "Nectar", + 0x0185: "bel'apps LLC", + 0x0186: "CORE Lighting Ltd", + 0x0187: "Seraphim Sense Ltd", + 0x0188: "Unico RBC", + 0x0189: "Physical Enterprises Inc.", + 0x018A: "Able Trend Technology Limited", + 0x018B: "Konica Minolta, Inc.", + 0x018C: "Wilo SE", + 0x018D: "Extron Design Services", + 0x018E: "Fitbit, Inc.", + 0x018F: "Fireflies Systems", + 0x0190: "Intelletto Technologies Inc.", + 0x0191: "FDK CORPORATION", + 0x0192: "Cloudleaf, Inc", + 0x0193: "Maveric Automation LLC", + 0x0194: "Acoustic Stream Corporation", + 0x0195: "Zuli", + 0x0196: "Paxton Access Ltd", + 0x0197: "WiSilica Inc.", + 0x0198: "VENGIT Korlatolt Felelossegu Tarsasag", + 0x0199: "SALTO SYSTEMS S.L.", + 0x019A: "TRON Forum (formerly T-Engine Forum)", + 0x019B: "CUBETECH s.r.o.", + 0x019C: "Cokiya Incorporated", + 0x019D: "CVS Health", + 0x019E: "Ceruus", + 0x019F: "Strainstall Ltd", + 0x01A0: "Channel Enterprises (HK) Ltd.", + 0x01A1: "FIAMM", + 0x01A2: "GIGALANE.CO.,LTD", + 0x01A3: "EROAD", + 0x01A4: "Mine Safety Appliances", + 0x01A5: "Icon Health and Fitness", + 0x01A6: "Wille Engineering (formely as Asandoo GmbH)", + 0x01A7: "ENERGOUS CORPORATION", + 0x01A8: "Taobao", + 0x01A9: "Canon Inc.", + 0x01AA: "Geophysical Technology Inc.", + 0x01AB: "Facebook, Inc.", + 0x01AC: "Trividia Health, Inc.", + 0x01AD: "FlightSafety International", + 0x01AE: "Earlens Corporation", + 0x01AF: "Sunrise Micro Devices, Inc.", + 0x01B0: "Star Micronics Co., Ltd.", + 0x01B1: "Netizens Sp. z o.o.", + 0x01B2: "Nymi Inc.", + 0x01B3: "Nytec, Inc.", + 0x01B4: "Trineo Sp. z o.o.", + 0x01B5: "Nest Labs Inc.", + 0x01B6: "LM Technologies Ltd", + 0x01B7: "General Electric Company", + 0x01B8: "i+D3 S.L.", + 0x01B9: "HANA Micron", + 0x01BA: "Stages Cycling LLC", + 0x01BB: "Cochlear Bone Anchored Solutions AB", + 0x01BC: "SenionLab AB", + 0x01BD: "Syszone Co., Ltd", + 0x01BE: "Pulsate Mobile Ltd.", + 0x01BF: "Hong Kong HunterSun Electronic Limited", + 0x01C0: "pironex GmbH", + 0x01C1: "BRADATECH Corp.", + 0x01C2: "Transenergooil AG", + 0x01C3: "Bunch", + 0x01C4: "DME Microelectronics", + 0x01C5: "Bitcraze AB", + 0x01C6: "HASWARE Inc.", + 0x01C7: "Abiogenix Inc.", + 0x01C8: "Poly-Control ApS", + 0x01C9: "Avi-on", + 0x01CA: "Laerdal Medical AS", + 0x01CB: "Fetch My Pet", + 0x01CC: "Sam Labs Ltd.", + 0x01CD: "Chengdu Synwing Technology Ltd", + 0x01CE: "HOUWA SYSTEM DESIGN, k.k.", + 0x01CF: "BSH", + 0x01D0: "Primus Inter Pares Ltd", + 0x01D1: "August Home, Inc", + 0x01D2: "Gill Electronics", + 0x01D3: "Sky Wave Design", + 0x01D4: "Newlab S.r.l.", + 0x01D5: "ELAD srl", + 0x01D6: "G-wearables inc.", + 0x01D7: "Squadrone Systems Inc.", + 0x01D8: "Code Corporation", + 0x01D9: "Savant Systems LLC", + 0x01DA: "Logitech International SA", + 0x01DB: "Innblue Consulting", + 0x01DC: "iParking Ltd.", + 0x01DD: "Koninklijke Philips Electronics N.V.", + 0x01DE: "Minelab Electronics Pty Limited", + 0x01DF: "Bison Group Ltd.", + 0x01E0: "Widex A/S", + 0x01E1: "Jolla Ltd", + 0x01E2: "Lectronix, Inc.", + 0x01E3: "Caterpillar Inc", + 0x01E4: "Freedom Innovations", + 0x01E5: "Dynamic Devices Ltd", + 0x01E6: "Technology Solutions (UK) Ltd", + 0x01E7: "IPS Group Inc.", + 0x01E8: "STIR", + 0x01E9: "Sano, Inc.", + 0x01EA: "Advanced Application Design, Inc.", + 0x01EB: "AutoMap LLC", + 0x01EC: "Spreadtrum Communications Shanghai Ltd", + 0x01ED: "CuteCircuit LTD", + 0x01EE: "Valeo Service", + 0x01EF: "Fullpower Technologies, Inc.", + 0x01F0: "KloudNation", + 0x01F1: "Zebra Technologies Corporation", + 0x01F2: "Itron, Inc.", + 0x01F3: "The University of Tokyo", + 0x01F4: "UTC Fire and Security", + 0x01F5: "Cool Webthings Limited", + 0x01F6: "DJO Global", + 0x01F7: "Gelliner Limited", + 0x01F8: "Anyka (Guangzhou) Microelectronics Technology Co, LTD", + 0x01F9: "Medtronic Inc.", + 0x01FA: "Gozio Inc.", + 0x01FB: "Form Lifting, LLC", + 0x01FC: "Wahoo Fitness, LLC", + 0x01FD: "Kontakt Micro-Location Sp. z o.o.", + 0x01FE: "Radio Systems Corporation", + 0x01FF: "Freescale Semiconductor, Inc.", + 0x0200: "Verifone Systems Pte Ltd. Taiwan Branch", + 0x0201: "AR Timing", + 0x0202: "Rigado LLC", + 0x0203: "Kemppi Oy", + 0x0204: "Tapcentive Inc.", + 0x0205: "Smartbotics Inc.", + 0x0206: "Otter Products, LLC", + 0x0207: "STEMP Inc.", + 0x0208: "LumiGeek LLC", + 0x0209: "InvisionHeart Inc.", + 0x020A: "Macnica Inc.", + 0x020B: "Jaguar Land Rover Limited", + 0x020C: "CoroWare Technologies, Inc", + 0x020D: "Simplo Technology Co., LTD", + 0x020E: "Omron Healthcare Co., LTD", + 0x020F: "Comodule GMBH", + 0x0210: "ikeGPS", + 0x0211: "Telink Semiconductor Co. Ltd", + 0x0212: "Interplan Co., Ltd", + 0x0213: "Wyler AG", + 0x0214: "IK Multimedia Production srl", + 0x0215: "Lukoton Experience Oy", + 0x0216: "MTI Ltd", + 0x0217: "Tech4home, Lda", + 0x0218: "Hiotech AB", + 0x0219: "DOTT Limited", + 0x021A: "Blue Speck Labs, LLC", + 0x021B: "Cisco Systems, Inc", + 0x021C: "Mobicomm Inc", + 0x021D: "Edamic", + 0x021E: "Goodnet, Ltd", + 0x021F: "Luster Leaf Products Inc", + 0x0220: "Manus Machina BV", + 0x0221: "Mobiquity Networks Inc", + 0x0222: "Praxis Dynamics", + 0x0223: "Philip Morris Products S.A.", + 0x0224: "Comarch SA", + 0x0225: "Nestl Nespresso S.A.", + 0x0226: "Merlinia A/S", + 0x0227: "LifeBEAM Technologies", + 0x0228: "Twocanoes Labs, LLC", + 0x0229: "Muoverti Limited", + 0x022A: "Stamer Musikanlagen GMBH", + 0x022B: "Tesla Motors", + 0x022C: "Pharynks Corporation", + 0x022D: "Lupine", + 0x022E: "Siemens AG", + 0x022F: "Huami (Shanghai) Culture Communication CO., LTD", + 0x0230: "Foster Electric Company, Ltd", + 0x0231: "ETA SA", + 0x0232: "x-Senso Solutions Kft", + 0x0233: "Shenzhen SuLong Communication Ltd", + 0x0234: "FengFan (BeiJing) Technology Co, Ltd", + 0x0235: "Qrio Inc", + 0x0236: "Pitpatpet Ltd", + 0x0237: "MSHeli s.r.l.", + 0x0238: "Trakm8 Ltd", + 0x0239: "JIN CO, Ltd", + 0x023A: "Alatech Tehnology", + 0x023B: "Beijing CarePulse Electronic Technology Co, Ltd", + 0x023C: "Awarepoint", + 0x023D: "ViCentra B.V.", + 0x023E: "Raven Industries", + 0x023F: "WaveWare Technologies Inc.", + 0x0240: "Argenox Technologies", + 0x0241: "Bragi GmbH", + 0x0242: "16Lab Inc", + 0x0243: "Masimo Corp", + 0x0244: "Iotera Inc", + 0x0245: "Endress+Hauser", + 0x0246: "ACKme Networks, Inc.", + 0x0247: "FiftyThree Inc.", + 0x0248: "Parker Hannifin Corp", + 0x0249: "Transcranial Ltd", + 0x024A: "Uwatec AG", + 0x024B: "Orlan LLC", + 0x024C: "Blue Clover Devices", + 0x024D: "M-Way Solutions GmbH", + 0x024E: "Microtronics Engineering GmbH", + 0x024F: "Schneider Schreibgerte GmbH", + 0x0250: "Sapphire Circuits LLC", + 0x0251: "Lumo Bodytech Inc.", + 0x0252: "UKC Technosolution", + 0x0253: "Xicato Inc.", + 0x0254: "Playbrush", + 0x0255: "Dai Nippon Printing Co., Ltd.", + 0x0256: "G24 Power Limited", + 0x0257: "AdBabble Local Commerce Inc.", + 0x0258: "Devialet SA", + 0x0259: "ALTYOR", + 0x025A: "University of Applied Sciences Valais/Haute Ecole Valaisanne", + 0x025B: "Five Interactive, LLC dba Zendo", + 0x025C: "NetEaseHangzhouNetwork co.Ltd.", + 0x025D: "Lexmark International Inc.", + 0x025E: "Fluke Corporation", + 0x025F: "Yardarm Technologies", + 0x0260: "SensaRx", + 0x0261: "SECVRE GmbH", + 0x0262: "Glacial Ridge Technologies", + 0x0263: "Identiv, Inc.", + 0x0264: "DDS, Inc.", + 0x0265: "SMK Corporation", + 0x0266: "Schawbel Technologies LLC", + 0x0267: "XMI Systems SA", + 0x0268: "Cerevo", + 0x0269: "Torrox GmbH & Co KG", + 0x026A: "Gemalto", + 0x026B: "DEKA Research & Development Corp.", + 0x026C: "Domster Tadeusz Szydlowski", + 0x026D: "Technogym SPA", + 0x026E: "FLEURBAEY BVBA", + 0x026F: "Aptcode Solutions", + 0x0270: "LSI ADL Technology", + 0x0271: "Animas Corp", + 0x0272: "Alps Electric Co., Ltd.", + 0x0273: "OCEASOFT", + 0x0274: "Motsai Research", + 0x0275: "Geotab", + 0x0276: "E.G.O. Elektro-Gertebau GmbH", + 0x0277: "bewhere inc", + 0x0278: "Johnson Outdoors Inc", + 0x0279: "steute Schaltgerate GmbH & Co. KG", + 0x027A: "Ekomini inc.", + 0x027B: "DEFA AS", + 0x027C: "Aseptika Ltd", + 0x027D: "HUAWEI Technologies Co., Ltd. ( )", + 0x027E: "HabitAware, LLC", + 0x027F: "ruwido austria gmbh", + 0x0280: "ITEC corporation", + 0x0281: "StoneL", + 0x0282: "Sonova AG", + 0x0283: "Maven Machines, Inc.", + 0x0284: "Synapse Electronics", + 0x0285: "Standard Innovation Inc.", + 0x0286: "RF Code, Inc.", + 0x0287: "Wally Ventures S.L.", + 0x0288: "Willowbank Electronics Ltd", + 0x0289: "SK Telecom", + 0x028A: "Jetro AS", + 0x028B: "Code Gears LTD", + 0x028C: "NANOLINK APS", + 0x028D: "IF, LLC", + 0x028E: "RF Digital Corp", + 0x028F: "Church & Dwight Co., Inc", + 0x0290: "Multibit Oy", + 0x0291: "CliniCloud Inc", + 0x0292: "SwiftSensors", + 0x0293: "Blue Bite", + 0x0294: "ELIAS GmbH", + 0x0295: "Sivantos GmbH", + 0x0296: "Petzl", + 0x0297: "storm power ltd", + 0x0298: "EISST Ltd", + 0x0299: "Inexess Technology Simma KG", + 0x029A: "Currant, Inc.", + 0x029B: "C2 Development, Inc.", + 0x029C: "Blue Sky Scientific, LLC", + 0x029D: "ALOTTAZS LABS, LLC", + 0x029E: "Kupson spol. s r.o.", + 0x029F: "Areus Engineering GmbH", + 0x02A0: "Impossible Camera GmbH", + 0x02A1: "InventureTrack Systems", + 0x02A2: "LockedUp", + 0x02A3: "Itude", + 0x02A4: "Pacific Lock Company", + 0x02A5: "Tendyron Corporation ( )", + 0x02A6: "Robert Bosch GmbH", + 0x02A7: "Illuxtron international B.V.", + 0x02A8: "miSport Ltd.", + 0x02A9: "Chargelib", + 0x02AA: "Doppler Lab", + 0x02AB: "BBPOS Limited", + 0x02AC: "RTB Elektronik GmbH & Co. KG", + 0x02AD: "Rx Networks, Inc.", + 0x02AE: "WeatherFlow, Inc.", + 0x02AF: "Technicolor USA Inc.", + 0x02B0: "Bestechnic(Shanghai),Ltd", + 0x02B1: "Raden Inc", + 0x02B2: "JouZen Oy", + 0x02B3: "CLABER S.P.A.", + 0x02B4: "Hyginex, Inc.", + 0x02B5: "HANSHIN ELECTRIC RAILWAY CO.,LTD.", + 0x02B6: "Schneider Electric", + 0x02B7: "Oort Technologies LLC", + 0x02B8: "Chrono Therapeutics", + 0x02B9: "Rinnai Corporation", + 0x02BA: "Swissprime Technologies AG", + 0x02BB: "Koha.,Co.Ltd", + 0x02BC: "Genevac Ltd", + 0x02BD: "Chemtronics", + 0x02BE: "Seguro Technology Sp. z o.o.", + 0x02BF: "Redbird Flight Simulations", + 0x02C0: "Dash Robotics", + 0x02C1: "LINE Corporation", + 0x02C2: "Guillemot Corporation", + 0x02C3: "Techtronic Power Tools Technology Limited", + 0x02C4: "Wilson Sporting Goods", + 0x02C5: "Lenovo (Singapore) Pte Ltd. ( )", + 0x02C6: "Ayatan Sensors", + 0x02C7: "Electronics Tomorrow Limited", + 0x02C8: "VASCO Data Security International, Inc.", + 0x02C9: "PayRange Inc.", + 0x02CA: "ABOV Semiconductor", + 0x02CB: "AINA-Wireless Inc.", + 0x02CC: "Eijkelkamp Soil & Water", + 0x02CD: "BMA ergonomics b.v.", + 0x02CE: "Teva Branded Pharmaceutical Products R&D, Inc.", + 0x02CF: "Anima", + 0x02D0: "3M", + 0x02D1: "Empatica Srl", + 0x02D2: "Afero, Inc.", + 0x02D3: "Powercast Corporation", + 0x02D4: "Secuyou ApS", + 0x02D5: "OMRON Corporation", + 0x02D6: "Send Solutions", + 0x02D7: "NIPPON SYSTEMWARE CO.,LTD.", + 0x02D8: "Neosfar", + 0x02D9: "Fliegl Agrartechnik GmbH", + 0x02DA: "Gilvader", + 0x02DB: "Digi International Inc (R)", + 0x02DC: "DeWalch Technologies, Inc.", + 0x02DD: "Flint Rehabilitation Devices, LLC", + 0x02DE: "Samsung SDS Co., Ltd.", + 0x02DF: "Blur Product Development", + 0x02E0: "University of Michigan", + 0x02E1: "Victron Energy BV", + 0x02E2: "NTT docomo", + 0x02E3: "Carmanah Technologies Corp.", + 0x02E4: "Bytestorm Ltd.", + 0x02E5: "Espressif Incorporated ( () )", + 0x02E6: "Unwire", + 0x02E7: "Connected Yard, Inc.", + 0x02E8: "American Music Environments", + 0x02E9: "Sensogram Technologies, Inc.", + 0x02EA: "Fujitsu Limited", + 0x02EB: "Ardic Technology", + 0x02EC: "Delta Systems, Inc", + 0x02ED: "HTC Corporation", + 0x02EE: "Citizen Holdings Co., Ltd.", + 0x02EF: "SMART-INNOVATION.inc", + 0x02F0: "Blackrat Software", + 0x02F1: "The Idea Cave, LLC", + 0x02F2: "GoPro, Inc.", + 0x02F3: "AuthAir, Inc", + 0x02F4: "Vensi, Inc.", + 0x02F5: "Indagem Tech LLC", + 0x02F6: "Intemo Technologies", + 0x02F7: "DreamVisions co., Ltd.", + 0x02F8: "Runteq Oy Ltd", + 0x02F9: "IMAGINATION TECHNOLOGIES LTD", + 0x02FA: "CoSTAR TEchnologies", + 0x02FB: "Clarius Mobile Health Corp.", + 0x02FC: "Shanghai Frequen Microelectronics Co., Ltd.", + 0x02FD: "Uwanna, Inc.", + 0x02FE: "Lierda Science & Technology Group Co., Ltd.", + 0x02FF: "Silicon Laboratories", + 0x0300: "World Moto Inc.", + 0x0301: "Giatec Scientific Inc.", + 0x0302: "Loop Devices, Inc", + 0x0303: "IACA electronique", + 0x0304: "Proxy Technologies, Inc.", + 0x0305: "Swipp ApS", + 0x0306: "Life Laboratory Inc.", + 0x0307: "FUJI INDUSTRIAL CO.,LTD.", + 0x0308: "Surefire, LLC", + 0x0309: "Dolby Labs", + 0x030A: "Ellisys", + 0x030B: "Magnitude Lighting Converters", + 0x030C: "Hilti AG", + 0x030D: "Devdata S.r.l.", + 0x030E: "Deviceworx", + 0x030F: "Shortcut Labs", + 0x0310: "SGL Italia S.r.l.", + 0x0311: "PEEQ DATA", + 0x0312: "Ducere Technologies Pvt Ltd", + 0x0313: "DiveNav, Inc.", + 0x0314: "RIIG AI Sp. z o.o.", + 0x0315: "Thermo Fisher Scientific", + 0x0316: "AG Measurematics Pvt. Ltd.", + 0x0317: "CHUO Electronics CO., LTD.", + 0x0318: "Aspenta International", + 0x0319: "Eugster Frismag AG", + 0x031A: "Amber wireless GmbH", + 0x031B: "HQ Inc", + 0x031C: "Lab Sensor Solutions", + 0x031D: "Enterlab ApS", + 0x031E: "Eyefi, Inc.", + 0x031F: "MetaSystem S.p.A.", + 0x0320: "SONO ELECTRONICS. CO., LTD", + 0x0321: "Jewelbots", + 0x0322: "Compumedics Limited", + 0x0323: "Rotor Bike Components", + 0x0324: "Astro, Inc.", + 0x0325: "Amotus Solutions", + 0x0326: "Healthwear Technologies (Changzhou)Ltd", + 0x0327: "Essex Electronics", + 0x0328: "Grundfos A/S", + 0x0329: "Eargo, Inc.", + 0x032A: "Electronic Design Lab", + 0x032B: "ESYLUX", + 0x032C: "NIPPON SMT.CO.,Ltd", + 0x032D: "BM innovations GmbH", + 0x032E: "indoormap", + 0x032F: "OttoQ Inc", + 0x0330: "North Pole Engineering", + 0x0331: "3flares Technologies Inc.", + 0x0332: "Electrocompaniet A.S.", + 0x0333: "Mul-T-Lock", + 0x0334: "Corentium AS", + 0x0335: "Enlighted Inc", + 0x0336: "GISTIC", + 0x0337: "AJP2 Holdings, LLC", + 0x0338: "COBI GmbH", + 0x0339: "Blue Sky Scientific, LLC", + 0x033A: "Appception, Inc.", + 0x033B: "Courtney Thorne Limited", + 0x033C: "Virtuosys", + 0x033D: "TPV Technology Limited", + 0x033E: "Monitra SA", + 0x033F: "Automation Components, Inc.", + 0x0340: "Letsense s.r.l.", + 0x0341: "Etesian Technologies LLC", + 0x0342: "GERTEC BRASIL LTDA.", + 0x0343: "Drekker Development Pty. Ltd.", + 0x0344: "Whirl Inc", + 0x0345: "Locus Positioning", + 0x0346: "Acuity Brands Lighting, Inc", + 0x0347: "Prevent Biometrics", + 0x0348: "Arioneo", + 0x0349: "VersaMe", + 0x034A: "Vaddio", + 0x034B: "Libratone A/S", + 0x034C: "HM Electronics, Inc.", + 0x034D: "TASER International, Inc.", + 0x034E: "SafeTrust Inc.", + 0x034F: "Heartland Payment Systems", + 0x0350: "Bitstrata Systems Inc.", + 0x0351: "Pieps GmbH", + 0x0352: "iRiding(Xiamen)Technology Co.,Ltd.", + 0x0353: "Alpha Audiotronics, Inc.", + 0x0354: "TOPPAN FORMS CO.,LTD.", + 0x0355: "Sigma Designs, Inc.", + 0x0356: "Spectrum Brands, Inc.", + 0x0357: "Polymap Wireless", + 0x0358: "MagniWare Ltd.", + 0x0359: "Novotec Medical GmbH", + 0x035A: "Medicom Innovation Partner a/s", + 0x035B: "Matrix Inc.", + 0x035C: "Eaton Corporation", + 0x035D: "KYS", + 0x035E: "Naya Health, Inc.", + 0x035F: "Acromag", + 0x0360: "Insulet Corporation", + 0x0361: "Wellinks Inc.", + 0x0362: "ON Semiconductor", + 0x0363: "FREELAP SA", + 0x0364: "Favero Electronics Srl", + 0x0365: "BioMech Sensor LLC", + 0x0366: "BOLTT Sports technologies Private limited", + 0x0367: "Saphe International", + 0x0368: "Metormote AB", + 0x0369: "littleBits", + 0x036A: "SetPoint Medical", + 0x036B: "BRControls Products BV", + 0x036C: "Zipcar", + 0x036D: "AirBolt Pty Ltd", + 0x036E: "KeepTruckin Inc", + 0x036F: "Motiv, Inc.", + 0x0370: "Wazombi Labs O", + 0x0371: "ORBCOMM", + 0x0372: "Nixie Labs, Inc.", + 0x0373: "AppNearMe Ltd", + 0x0374: "Holman Industries", + 0x0375: "Expain AS", + 0x0376: "Electronic Temperature Instruments Ltd", + 0x0377: "Plejd AB", + 0x0378: "Propeller Health", + 0x0379: "Shenzhen iMCO Electronic Technology Co.,Ltd", + 0x037A: "Algoria", + 0x037B: "Apption Labs Inc.", + 0x037C: "Cronologics Corporation", + 0x037D: "MICRODIA Ltd.", + 0x037E: "lulabytes S.L.", + 0x037F: "Nestec S.A.", + 0x0380: 'LLC "MEGA-F service"', + 0x0381: "Sharp Corporation", + 0x0382: "Precision Outcomes Ltd", + 0x0383: "Kronos Incorporated", + 0x0384: "OCOSMOS Co., Ltd.", + 0x0385: "Embedded Electronic Solutions Ltd. dba e2Solutions", + 0x0386: "Aterica Inc.", + 0x0387: "BluStor PMC, Inc.", + 0x0388: "Kapsch TrafficCom AB", + 0x0389: "ActiveBlu Corporation", + 0x038A: "Kohler Mira Limited", + 0x038B: "Noke", + 0x038C: "Appion Inc.", + 0x038D: "Resmed Ltd", + 0x038E: "Crownstone B.V.", + 0x038F: "Xiaomi Inc.", + 0x0390: "INFOTECH s.r.o.", + 0x0391: "Thingsquare AB", + 0x0392: "T&D", + 0x0393: "LAVAZZA S.p.A.", + 0x0394: "Netclearance Systems, Inc.", + 0x0395: "SDATAWAY", + 0x0396: "BLOKS GmbH", + 0x0397: "LEGO System A/S", + 0x0398: "Thetatronics Ltd", + 0x0399: "Nikon Corporation", + 0x039A: "NeST", + 0x039B: "South Silicon Valley Microelectronics", + 0x039C: "ALE International", + 0x039D: "CareView Communications, Inc.", + 0x039E: "SchoolBoard Limited", + 0x039F: "Molex Corporation", + 0x03A0: "BARROT TECHNOLOGY LIMITED (formerly IVT Wireless Limited)", + 0x03A1: "Alpine Labs LLC", + 0x03A2: "Candura Instruments", + 0x03A3: "SmartMovt Technology Co., Ltd", + 0x03A4: "Token Zero Ltd", + 0x03A5: "ACE CAD Enterprise Co., Ltd. (ACECAD)", + 0x03A6: "Medela, Inc", + 0x03A7: "AeroScout", + 0x03A8: "Esrille Inc.", + 0x03A9: "THINKERLY SRL", + 0x03AA: "Exon Sp. z o.o.", + 0x03AB: "Meizu Technology Co., Ltd.", + 0x03AC: "Smablo LTD", + 0x03AD: "XiQ", + 0x03AE: "Allswell Inc.", + 0x03AF: "Comm-N-Sense Corp DBA Verigo", + 0x03B0: "VIBRADORM GmbH", + 0x03B1: "Otodata Wireless Network Inc.", + 0x03B2: "Propagation Systems Limited", + 0x03B3: "Midwest Instruments & Controls", + 0x03B4: "Alpha Nodus, inc.", + 0x03B5: "petPOMM, Inc", + 0x03B6: "Mattel", + 0x03B7: "Airbly Inc.", + 0x03B8: "A-Safe Limited", + 0x03B9: "FREDERIQUE CONSTANT SA", + 0x03BA: "Maxscend Microelectronics Company Limited", + 0x03BB: "Abbott Diabetes Care", + 0x03BC: "ASB Bank Ltd", + 0x03BD: "amadas", + 0x03BE: "Applied Science, Inc.", + 0x03BF: "iLumi Solutions Inc.", + 0x03C0: "Arch Systems Inc.", + 0x03C1: "Ember Technologies, Inc.", + 0x03C2: "Snapchat Inc", + 0x03C3: "Casambi Technologies Oy", + 0x03C4: "Pico Technology Inc.", + 0x03C5: "St. Jude Medical, Inc.", + 0x03C6: "Intricon", + 0x03C7: "Structural Health Systems, Inc.", + 0x03C8: "Avvel International", + 0x03C9: "Gallagher Group", + 0x03CA: "In2things Automation Pvt. Ltd.", + 0x03CB: "SYSDEV Srl", + 0x03CC: "Vonkil Technologies Ltd", + 0x03CD: "Wynd Technologies, Inc.", + 0x03CE: "CONTRINEX S.A.", + 0x03CF: "MIRA, Inc.", + 0x03D0: "Watteam Ltd", + 0x03D1: "Density Inc.", + 0x03D2: "IOT Pot India Private Limited", + 0x03D3: "Sigma Connectivity AB", + 0x03D4: "PEG PEREGO SPA", + 0x03D5: "Wyzelink Systems Inc.", + 0x03D6: "Yota Devices LTD", + 0x03D7: "FINSECUR", + 0x03D8: "Zen-Me Labs Ltd", + 0x03D9: "3IWare Co., Ltd.", + 0x03DA: "EnOcean GmbH", + 0x03DB: "Instabeat, Inc", + 0x03DC: "Nima Labs", + 0x03DD: "Andreas Stihl AG & Co. KG", + 0x03DE: "Nathan Rhoades LLC", + 0x03DF: "Grob Technologies, LLC", + 0x03E0: "Actions (Zhuhai) Technology Co., Limited", + 0x03E1: "SPD Development Company Ltd", + 0x03E2: "Sensoan Oy", + 0x03E3: "Qualcomm Life Inc", + 0x03E4: "Chip-ing AG", + 0x03E5: "ffly4u", + 0x03E6: "IoT Instruments Oy", + 0x03E7: "TRUE Fitness Technology", + 0x03E8: "Reiner Kartengeraete GmbH & Co. KG.", + 0x03E9: "SHENZHEN LEMONJOY TECHNOLOGY CO., LTD.", + 0x03EA: "Hello Inc.", + 0x03EB: "Evollve Inc.", + 0x03EC: "Jigowatts Inc.", + 0x03ED: "BASIC MICRO.COM,INC.", + 0x03EE: "CUBE TECHNOLOGIES", + 0x03EF: "foolography GmbH", + 0x03F0: "CLINK", + 0x03F1: "Hestan Smart Cooking Inc.", + 0x03F2: "WindowMaster A/S", + 0x03F3: "Flowscape AB", + 0x03F4: "PAL Technologies Ltd", + 0x03F5: "WHERE, Inc.", + 0x03F6: "Iton Technology Corp.", + 0x03F7: "Owl Labs Inc.", + 0x03F8: "Rockford Corp.", + 0x03F9: "Becon Technologies Co.,Ltd.", + 0x03FA: "Vyassoft Technologies Inc", + 0x03FB: "Nox Medical", + 0x03FC: "Kimberly-Clark", + 0x03FD: "Trimble Navigation Ltd.", + 0x03FE: "Littelfuse", + 0x03FF: "Withings", + 0x0400: "i-developer IT Beratung UG", + 0x0401: "", + 0x0402: "Sears Holdings Corporation", + 0x0403: "Gantner Electronic GmbH", + 0x0404: "Authomate Inc", + 0x0405: "Vertex International, Inc.", + 0x0406: "Airtago", + 0x0407: "Swiss Audio SA", + 0x0408: "ToGetHome Inc.", + 0x0409: "AXIS", + 0x040A: "Openmatics", + 0x040B: "Jana Care Inc.", + 0x040C: "Senix Corporation", + 0x040D: "NorthStar Battery Company, LLC", + 0x040E: "SKF (U.K.) Limited", + 0x040F: "CO-AX Technology, Inc.", + 0x0410: "Fender Musical Instruments", + 0x0411: "Luidia Inc", + 0x0412: "SEFAM", + 0x0413: "Wireless Cables Inc", + 0x0414: "Lightning Protection International Pty Ltd", + 0x0415: "Uber Technologies Inc", + 0x0416: "SODA GmbH", + 0x0417: "Fatigue Science", + 0x0418: "Alpine Electronics Inc.", + 0x0419: "Novalogy LTD", + 0x041A: "Friday Labs Limited", + 0x041B: "OrthoAccel Technologies", + 0x041C: "WaterGuru, Inc.", + 0x041D: "Benning Elektrotechnik und Elektronik GmbH & Co. KG", + 0x041E: "Dell Computer Corporation", + 0x041F: "Kopin Corporation", + 0x0420: "TecBakery GmbH", + 0x0421: "Backbone Labs, Inc.", + 0x0422: "DELSEY SA", + 0x0423: "Chargifi Limited", + 0x0424: "Trainesense Ltd.", + 0x0425: "Unify Software and Solutions GmbH & Co. KG", + 0x0426: "Husqvarna AB", + 0x0427: "Focus fleet and fuel management inc", + 0x0428: "SmallLoop, LLC", + 0x0429: "Prolon Inc.", + 0x042A: "BD Medical", + 0x042B: "iMicroMed Incorporated", + 0x042C: "Ticto N.V.", + 0x042D: "Meshtech AS", + 0x042E: "MemCachier Inc.", + 0x042F: "Danfoss A/S", + 0x0430: "SnapStyk Inc.", + 0x0431: "Amway Corporation", + 0x0432: "Silk Labs, Inc.", + 0x0433: "Pillsy Inc.", + 0x0434: "Hatch Baby, Inc.", + 0x0435: "Blocks Wearables Ltd.", + 0x0436: "Drayson Technologies (Europe) Limited", + 0x0437: "eBest IOT Inc.", + 0x0438: "Helvar Ltd", + 0x0439: "Radiance Technologies", + 0x043A: "Nuheara Limited", + 0x043B: "Appside co., ltd.", + 0x043C: "DeLaval", + 0x043D: "Coiler Corporation", + 0x043E: "Thermomedics, Inc.", + 0x043F: "Tentacle Sync GmbH", + 0x0440: "Valencell, Inc.", + 0x0441: "iProtoXi Oy", + 0x0442: "SECOM CO., LTD.", + 0x0443: "Tucker International LLC", + 0x0444: "Metanate Limited", + 0x0445: "Kobian Canada Inc.", + 0x0446: "NETGEAR, Inc.", + 0x0447: "Fabtronics Australia Pty Ltd", + 0x0448: "Grand Centrix GmbH", + 0x0449: "1UP USA.com llc", + 0x044A: "SHIMANO INC.", + 0x044B: "Nain Inc.", + 0x044C: "LifeStyle Lock, LLC", + 0x044D: "VEGA Grieshaber KG", + 0x044E: "Xtrava Inc.", + 0x044F: "TTS Tooltechnic Systems AG & Co. KG", + 0x0450: "Teenage Engineering AB", + 0x0451: "Tunstall Nordic AB", + 0x0452: "Svep Design Center AB", + 0x0453: "GreenPeak Technologies BV", + 0x0454: "Sphinx Electronics GmbH & Co KG", + 0x0455: "Atomation", + 0x0456: "Nemik Consulting Inc", + 0x0457: "RF INNOVATION", + 0x0458: "Mini Solution Co., Ltd.", + 0x0459: "Lumenetix, Inc", + 0x045A: "2048450 Ontario Inc", + 0x045B: "SPACEEK LTD", + 0x045C: "Delta T Corporation", + 0x045D: "Boston Scientific Corporation", + 0x045E: "Nuviz, Inc.", + 0x045F: "Real Time Automation, Inc.", + 0x0460: "Kolibree", + 0x0461: "vhf elektronik GmbH", + 0x0462: "Bonsai Systems GmbH", + 0x0463: "Fathom Systems Inc.", + 0x0464: "Bellman & Symfon", + 0x0465: "International Forte Group LLC", + 0x0466: "CycleLabs Solutions inc.", + 0x0467: "Codenex Oy", + 0x0468: "Kynesim Ltd", + 0x0469: "Palago AB", + 0x046A: "INSIGMA INC.", + 0x046B: "PMD Solutions", + 0x046C: "Qingdao Realtime Technology Co., Ltd.", + 0x046D: "BEGA Gantenbrink-Leuchten KG", + 0x046E: "Pambor Ltd.", + 0x046F: "Develco Products A/S", + 0x0470: "iDesign s.r.l.", + 0x0471: "TiVo Corp", + 0x0472: "Control-J Pty Ltd", + 0x0473: "Steelcase, Inc.", + 0x0474: "iApartment co., ltd.", + 0x0475: "Icom inc.", + 0x0476: "Oxstren Wearable Technologies Private Limited", + 0x0477: "Blue Spark Technologies", + 0x0478: "FarSite Communications Limited", + 0x0479: "mywerk system GmbH", + 0x047A: "Sinosun Technology Co., Ltd.", + 0x047B: "MIYOSHI ELECTRONICS CORPORATION", + 0x047C: "POWERMAT LTD", + 0x047D: "Occly LLC", + 0x047E: "OurHub Dev IvS", + 0x047F: "Pro-Mark, Inc.", + 0x0480: "Dynometrics Inc.", + 0x0481: "Quintrax Limited", + 0x0482: "POS Tuning Udo Vosshenrich GmbH & Co. KG", + 0x0483: "Multi Care Systems B.V.", + 0x0484: "Revol Technologies Inc", + 0x0485: "SKIDATA AG", + 0x0486: "DEV TECNOLOGIA INDUSTRIA, COMERCIO E MANUTENCAO DE EQUIPAMENTOS LTDA. - ME", + 0x0487: "Centrica Connected Home", + 0x0488: "Automotive Data Solutions Inc", + 0x0489: "Igarashi Engineering", + 0x048A: "Taelek Oy", + 0x048B: "CP Electronics Limited", + 0x048C: "Vectronix AG", + 0x048D: "S-Labs Sp. z o.o.", + 0x048E: "Companion Medical, Inc.", + 0x048F: "BlueKitchen GmbH", + 0x0490: "Matting AB", + 0x0491: "SOREX - Wireless Solutions GmbH", + 0x0492: "ADC Technology, Inc.", + 0x0493: "Lynxemi Pte Ltd", + 0x0494: "SENNHEISER electronic GmbH & Co. KG", + 0x0495: "LMT Mercer Group, Inc", + 0x0496: "Polymorphic Labs LLC", + 0x0497: "Cochlear Limited", + 0x0498: "METER Group, Inc. USA", + 0x0499: "Ruuvi Innovations Ltd.", + 0x049A: "Situne AS", + 0x049B: "nVisti, LLC", + 0x049C: "DyOcean", + 0x049D: "Uhlmann & Zacher GmbH", + 0x049E: "AND!XOR LLC", + 0x049F: "tictote AB", + 0x04A0: "Vypin, LLC", + 0x04A1: "PNI Sensor Corporation", + 0x04A2: "ovrEngineered, LLC", + 0x04A3: "GT-tronics HK Ltd", + 0x04A4: "Herbert Waldmann GmbH & Co. KG", + 0x04A5: "Guangzhou FiiO Electronics Technology Co.,Ltd", + 0x04A6: "Vinetech Co., Ltd", + 0x04A7: "Dallas Logic Corporation", + 0x04A8: "BioTex, Inc.", + 0x04A9: "DISCOVERY SOUND TECHNOLOGY, LLC", + 0x04AA: "LINKIO SAS", + 0x04AB: "Harbortronics, Inc.", + 0x04AC: "Undagrid B.V.", + 0x04AD: "Shure Inc", + 0x04AE: "ERM Electronic Systems LTD", + 0x04AF: "BIOROWER Handelsagentur GmbH", + 0x04B0: "Weba Sport und Med. Artikel GmbH", + 0x04B1: "Kartographers Technologies Pvt. Ltd.", + 0x04B2: "The Shadow on the Moon", + 0x04B3: "mobike (Hong Kong) Limited", + 0x04B4: "Inuheat Group AB", + 0x04B5: "Swiftronix AB", + 0x04B6: "Diagnoptics Technologies", + 0x04B7: "Analog Devices, Inc.", + 0x04B8: "Soraa Inc.", + 0x04B9: "CSR Building Products Limited", + 0x04BA: "Crestron Electronics, Inc.", + 0x04BB: "Neatebox Ltd", + 0x04BC: "Draegerwerk AG & Co. KGaA", + 0x04BD: "AlbynMedical", + 0x04BE: "Averos FZCO", + 0x04BF: "VIT Initiative, LLC", + 0x04C0: "Statsports International", + 0x04C1: "Sospitas, s.r.o.", + 0x04C2: "Dmet Products Corp.", + 0x04C3: "Mantracourt Electronics Limited", + 0x04C4: "TeAM Hutchins AB", + 0x04C5: "Seibert Williams Glass, LLC", + 0x04C6: "Insta GmbH", + 0x04C7: "Svantek Sp. z o.o.", + 0x04C8: "Shanghai Flyco Electrical Appliance Co., Ltd.", + 0x04C9: "Thornwave Labs Inc", + 0x04CA: "Steiner-Optik GmbH", + 0x04CB: "Novo Nordisk A/S", + 0x04CC: "Enflux Inc.", + 0x04CD: "Safetech Products LLC", + 0x04CE: "GOOOLED S.R.L.", + 0x04CF: "DOM Sicherheitstechnik GmbH & Co. KG", + 0x04D0: "Olympus Corporation", + 0x04D1: "KTS GmbH", + 0x04D2: "Anloq Technologies Inc.", + 0x04D3: "Queercon, Inc", + 0x04D4: "5th Element Ltd", + 0x04D5: "Gooee Limited", + 0x04D6: "LUGLOC LLC", + 0x04D7: "Blincam, Inc.", + 0x04D8: "FUJIFILM Corporation", + 0x04D9: "RandMcNally", + 0x04DA: "Franceschi Marina snc", + 0x04DB: "Engineered Audio, LLC.", + 0x04DC: "IOTTIVE (OPC) PRIVATE LIMITED", + 0x04DD: "4MOD Technology", + 0x04DE: "Lutron Electronics Co., Inc.", + 0x04DF: "Emerson", + 0x04E0: "Guardtec, Inc.", + 0x04E1: "REACTEC LIMITED", + 0x04E2: "EllieGrid", + 0x04E3: "Under Armour", + 0x04E4: "Woodenshark", + 0x04E5: "Avack Oy", + 0x04E6: "Smart Solution Technology, Inc.", + 0x04E7: "REHABTRONICS INC.", + 0x04E8: "STABILO International", + 0x04E9: "Busch Jaeger Elektro GmbH", + 0x04EA: "Pacific Bioscience Laboratories, Inc", + 0x04EB: "Bird Home Automation GmbH", + 0x04EC: "Motorola Solutions", + 0x04ED: "R9 Technology, Inc.", + 0x04EE: "Auxivia", + 0x04EF: "DaisyWorks, Inc", + 0x04F0: "Kosi Limited", + 0x04F1: "Theben AG", + 0x04F2: "InDreamer Techsol Private Limited", + 0x04F3: "Cerevast Medical", + 0x04F4: "ZanCompute Inc.", + 0x04F5: "Pirelli Tyre S.P.A.", + 0x04F6: "McLear Limited", + 0x04F7: "Shenzhen Huiding Technology Co.,Ltd.", + 0x04F8: "Convergence Systems Limited", + 0x04F9: "Interactio", + 0x04FA: "Androtec GmbH", + 0x04FB: "Benchmark Drives GmbH & Co. KG", + 0x04FC: "SwingLync L. L. C.", + 0x04FD: "Tapkey GmbH", + 0x04FE: "Woosim Systems Inc.", + 0x04FF: "Microsemi Corporation", + 0x0500: "Wiliot LTD.", + 0x0501: "Polaris IND", + 0x0502: "Specifi-Kali LLC", + 0x0503: "Locoroll, Inc", + 0x0504: "PHYPLUS Inc", + 0x0505: "Inplay Technologies LLC", + 0x0506: "Hager", + 0x0507: "Yellowcog", + 0x0508: "Axes System sp. z o. o.", + 0x0509: "myLIFTER Inc.", + 0x050A: "Shake-on B.V.", + 0x050B: "Vibrissa Inc.", + 0x050C: "OSRAM GmbH", + 0x050D: "TRSystems GmbH", + 0x050E: "Yichip Microelectronics (Hangzhou) Co.,Ltd.", + 0x050F: "Foundation Engineering LLC", + 0x0510: "UNI-ELECTRONICS, INC.", + 0x0511: "Brookfield Equinox LLC", + 0x0512: "Soprod SA", + 0x0513: "9974091 Canada Inc.", + 0x0514: "FIBRO GmbH", + 0x0515: "RB Controls Co., Ltd.", + 0x0516: "Footmarks", + 0x0517: "Amtronic Sverige AB (formerly Amcore AB)", + 0x0518: "MAMORIO.inc", + 0x0519: "Tyto Life LLC", + 0x051A: "Leica Camera AG", + 0x051B: "Angee Technologies Ltd.", + 0x051C: "EDPS", + 0x051D: "OFF Line Co., Ltd.", + 0x051E: "Detect Blue Limited", + 0x051F: "Setec Pty Ltd", + 0x0520: "Target Corporation", + 0x0521: "IAI Corporation", + 0x0522: "NS Tech, Inc.", + 0x0523: "MTG Co., Ltd.", + 0x0524: "Hangzhou iMagic Technology Co., Ltd", + 0x0525: "HONGKONG NANO IC TECHNOLOGIES CO., LIMITED", + 0x0526: "Honeywell International Inc.", + 0x0527: "Albrecht JUNG", + 0x0528: "Lunera Lighting Inc.", + 0x0529: "Lumen UAB", + 0x052A: "Keynes Controls Ltd", + 0x052B: "Novartis AG", + 0x052C: "Geosatis SA", + 0x052D: "EXFO, Inc.", + 0x052E: "LEDVANCE GmbH", + 0x052F: "Center ID Corp.", + 0x0530: "Adolene, Inc.", + 0x0531: "D&M Holdings Inc.", + 0x0532: "CRESCO Wireless, Inc.", + 0x0533: "Nura Operations Pty Ltd", + 0x0534: "Frontiergadget, Inc.", + 0x0535: "Smart Component Technologies Limited", + 0x0536: "ZTR Control Systems LLC", + 0x0537: "MetaLogics Corporation", + 0x0538: "Medela AG", + 0x0539: "OPPLE Lighting Co., Ltd", + 0x053A: "Savitech Corp.,", + 0x053B: "prodigy", + 0x053C: "Screenovate Technologies Ltd", + 0x053D: "TESA SA", + 0x053E: "CLIM8 LIMITED", + 0x053F: "Silergy Corp", + 0x0540: "SilverPlus, Inc", + 0x0541: "Sharknet srl", + 0x0542: "Mist Systems, Inc.", + 0x0543: "MIWA LOCK CO.,Ltd", + 0x0544: "OrthoSensor, Inc.", + 0x0545: "Candy Hoover Group s.r.l", + 0x0546: "Apexar Technologies S.A.", + 0x0547: "LOGICDATA d.o.o.", + 0x0548: "Knick Elektronische Messgeraete GmbH & Co. KG", + 0x0549: "Smart Technologies and Investment Limited", + 0x054A: "Linough Inc.", + 0x054B: "Advanced Electronic Designs, Inc.", + 0x054C: "Carefree Scott Fetzer Co Inc", + 0x054D: "Sensome", + 0x054E: "FORTRONIK storitve d.o.o.", + 0x054F: "Sinnoz", + 0x0550: "Versa Networks, Inc.", + 0x0551: "Sylero", + 0x0552: "Avempace SARL", + 0x0553: "Nintendo Co., Ltd.", + 0x0554: "National Instruments", + 0x0555: "KROHNE Messtechnik GmbH", + 0x0556: "Otodynamics Ltd", + 0x0557: "Arwin Technology Limited", + 0x0558: "benegear, inc.", + 0x0559: "Newcon Optik", + 0x055A: "CANDY HOUSE, Inc.", + 0x055B: "FRANKLIN TECHNOLOGY INC", + 0x055C: "Lely", + 0x055D: "Valve Corporation", + 0x055E: "Hekatron Vertriebs GmbH", + 0x055F: "PROTECH S.A.S. DI GIRARDI ANDREA & C.", + 0x0560: "Sarita CareTech APS (formerly Sarita CareTech IVS)", + 0x0561: "Finder S.p.A.", + 0x0562: "Thalmic Labs Inc.", + 0x0563: "Steinel Vertrieb GmbH", + 0x0564: "Beghelli Spa", + 0x0565: "Beijing Smartspace Technologies Inc.", + 0x0566: "CORE TRANSPORT TECHNOLOGIES NZ LIMITED", + 0x0567: "Xiamen Everesports Goods Co., Ltd", + 0x0568: "Bodyport Inc.", + 0x0569: "Audionics System, INC.", + 0x056A: "Flipnavi Co.,Ltd.", + 0x056B: "Rion Co., Ltd.", + 0x056C: "Long Range Systems, LLC", + 0x056D: "Redmond Industrial Group LLC", + 0x056E: "VIZPIN INC.", + 0x056F: "BikeFinder AS", + 0x0570: "Consumer Sleep Solutions LLC", + 0x0571: "PSIKICK, INC.", + 0x0572: "AntTail.com", + 0x0573: "Lighting Science Group Corp.", + 0x0574: "AFFORDABLE ELECTRONICS INC", + 0x0575: "Integral Memroy Plc", + 0x0576: "Globalstar, Inc.", + 0x0577: "True Wearables, Inc.", + 0x0578: "Wellington Drive Technologies Ltd", + 0x0579: "Ensemble Tech Private Limited", + 0x057A: "OMNI Remotes", + 0x057B: "Duracell U.S. Operations Inc.", + 0x057C: "Toor Technologies LLC", + 0x057D: "Instinct Performance", + 0x057E: "Beco, Inc", + 0x057F: "Scuf Gaming International, LLC", + 0x0580: "ARANZ Medical Limited", + 0x0581: "LYS TECHNOLOGIES LTD", + 0x0582: "Breakwall Analytics, LLC", + 0x0583: "Code Blue Communications", + 0x0584: "Gira Giersiepen GmbH & Co. KG", + 0x0585: "Hearing Lab Technology", + 0x0586: "LEGRAND", + 0x0587: "Derichs GmbH", + 0x0588: "ALT-TEKNIK LLC", + 0x0589: "Star Technologies", + 0x058A: "START TODAY CO.,LTD.", + 0x058B: "Maxim Integrated Products", + 0x058C: "MERCK Kommanditgesellschaft auf Aktien", + 0x058D: "Jungheinrich Aktiengesellschaft", + 0x058E: "Oculus VR, LLC", + 0x058F: "HENDON SEMICONDUCTORS PTY LTD", + 0x0590: "Pur3 Ltd", + 0x0591: "Viasat Group S.p.A.", + 0x0592: "IZITHERM", + 0x0593: "Spaulding Clinical Research", + 0x0594: "Kohler Company", + 0x0595: "Inor Process AB", + 0x0596: "My Smart Blinds", + 0x0597: "RadioPulse Inc", + 0x0598: "rapitag GmbH", + 0x0599: "Lazlo326, LLC.", + 0x059A: "Teledyne Lecroy, Inc.", + 0x059B: "Dataflow Systems Limited", + 0x059C: "Macrogiga Electronics", + 0x059D: "Tandem Diabetes Care", + 0x059E: "Polycom, Inc.", + 0x059F: "Fisher & Paykel Healthcare", + 0x05A0: "RCP Software Oy", + 0x05A1: "Shanghai Xiaoyi Technology Co.,Ltd.", + 0x05A2: "ADHERIUM(NZ) LIMITED", + 0x05A3: "Axiomware Systems Incorporated", + 0x05A4: "O. E. M. Controls, Inc.", + 0x05A5: "Kiiroo BV", + 0x05A6: "Telecon Mobile Limited", + 0x05A7: "Sonos Inc", + 0x05A8: "Tom Allebrandi Consulting", + 0x05A9: "Monidor", + 0x05AA: "Tramex Limited", + 0x05AB: "Nofence AS", + 0x05AC: "GoerTek Dynaudio Co., Ltd.", + 0x05AD: "INIA", + 0x05AE: "CARMATE MFG.CO.,LTD", + 0x05AF: "ONvocal", + 0x05B0: "NewTec GmbH", + 0x05B1: "Medallion Instrumentation Systems", + 0x05B2: "CAREL INDUSTRIES S.P.A.", + 0x05B3: "Parabit Systems, Inc.", + 0x05B4: "White Horse Scientific ltd", + 0x05B5: "verisilicon", + 0x05B6: "Elecs Industry Co.,Ltd.", + 0x05B7: "Beijing Pinecone Electronics Co.,Ltd.", + 0x05B8: "Ambystoma Labs Inc.", + 0x05B9: "Suzhou Pairlink Network Technology", + 0x05BA: "igloohome", + 0x05BB: "Oxford Metrics plc", + 0x05BC: "Leviton Mfg. Co., Inc.", + 0x05BD: "ULC Robotics Inc.", + 0x05BE: "RFID Global by Softwork SrL", + 0x05BF: "Real-World-Systems Corporation", + 0x05C0: "Nalu Medical, Inc.", + 0x05C1: "P.I.Engineering", + 0x05C2: "Grote Industries", + 0x05C3: "Runtime, Inc.", + 0x05C4: "Codecoup sp. z o.o. sp. k.", + 0x05C5: "SELVE GmbH & Co. KG", + 0x05C6: "Smart Animal Training Systems, LLC", + 0x05C7: "Lippert Components, INC", + 0x05C8: "SOMFY SAS", + 0x05C9: "TBS Electronics B.V.", + 0x05CA: "MHL Custom Inc", + 0x05CB: "LucentWear LLC", + 0x05CC: "WATTS ELECTRONICS", + 0x05CD: "RJ Brands LLC", + 0x05CE: "V-ZUG Ltd", + 0x05CF: "Biowatch SA", + 0x05D0: "Anova Applied Electronics", + 0x05D1: "Lindab AB", + 0x05D2: "frogblue TECHNOLOGY GmbH", + 0x05D3: "Acurable Limited", + 0x05D4: "LAMPLIGHT Co., Ltd.", + 0x05D5: "TEGAM, Inc.", + 0x05D6: "Zhuhai Jieli technology Co.,Ltd", + 0x05D7: "modum.io AG", + 0x05D8: "Farm Jenny LLC", + 0x05D9: "Toyo Electronics Corporation", + 0x05DA: "Applied Neural Research Corp", + 0x05DB: "Avid Identification Systems, Inc.", + 0x05DC: "Petronics Inc.", + 0x05DD: "essentim GmbH", + 0x05DE: "QT Medical INC.", + 0x05DF: "VIRTUALCLINIC.DIRECT LIMITED", + 0x05E0: "Viper Design LLC", + 0x05E1: "Human, Incorporated", + 0x05E2: "stAPPtronics GmbH", + 0x05E3: "Elemental Machines, Inc.", + 0x05E4: "Taiyo Yuden Co., Ltd", + 0x05E5: "INEO ENERGY& SYSTEMS", + 0x05E6: "Motion Instruments Inc.", + 0x05E7: "PressurePro", + 0x05E8: "COWBOY", + 0x05E9: "iconmobile GmbH", + 0x05EA: "ACS-Control-System GmbH", + 0x05EB: "Bayerische Motoren Werke AG", + 0x05EC: "Gycom Svenska AB", + 0x05ED: "Fuji Xerox Co., Ltd", + 0x05EE: "Glide Inc.", + 0x05EF: "SIKOM AS", + 0x05F0: "beken", + 0x05F1: "The Linux Foundation", + 0x05F2: "Try and E CO.,LTD.", + 0x05F3: "SeeScan", + 0x05F4: "Clearity, LLC", + 0x05F5: "GS TAG", + 0x05F6: "DPTechnics", + 0x05F7: "TRACMO, INC.", + 0x05F8: "Anki Inc.", + 0x05F9: "Hagleitner Hygiene International GmbH", + 0x05FA: "Konami Sports Life Co., Ltd.", + 0x05FB: "Arblet Inc.", + 0x05FC: "Masbando GmbH", + 0x05FD: "Innoseis", + 0x05FE: "Niko", + 0x05FF: "Wellnomics Ltd", + 0x0600: "iRobot Corporation", + 0x0601: "Schrader Electronics", + 0x0602: "Geberit International AG", + 0x0603: "Fourth Evolution Inc", + 0x0604: "Cell2Jack LLC", + 0x0605: "FMW electronic Futterer u. Maier-Wolf OHG", + 0x0606: "John Deere", + 0x0607: "Rookery Technology Ltd", + 0x0608: "KeySafe-Cloud", + 0x0609: "BUCHI Labortechnik AG", + 0x060A: "IQAir AG", + 0x060B: "Triax Technologies Inc", + 0x060C: "Vuzix Corporation", + 0x060D: "TDK Corporation", + 0x060E: "Blueair AB", + 0x060F: "Signify Netherlands (formerlyPhilips Lighting B.V.)", + 0x0610: "ADH GUARDIAN USA LLC", + 0x0611: "Beurer GmbH", + 0x0612: "Playfinity AS", + 0x0613: "Hans Dinslage GmbH", + 0x0614: "OnAsset Intelligence, Inc.", + 0x0615: "INTER ACTION Corporation", + 0x0616: "OS42 UG (haftungsbeschraenkt)", + 0x0617: "WIZCONNECTED COMPANY LIMITED", + 0x0618: "Audio-Technica Corporation", + 0x0619: "Six Guys Labs, s.r.o.", + 0x061A: "R.W. Beckett Corporation", + 0x061B: "silex technology, inc.", + 0x061C: "Univations Limited", + 0x061D: "SENS Innovation ApS", + 0x061E: "Diamond Kinetics, Inc.", + 0x061F: "Phrame Inc.", + 0x0620: "Forciot Oy", + 0x0621: "Noordung d.o.o.", + 0x0622: "Beam Labs, LLC", + 0x0623: "Philadelphia Scientific (U.K.) Limited", + 0x0624: "Biovotion AG", + 0x0625: "Square Panda, Inc.", + 0x0626: "Amplifico", + 0x0627: "WEG S.A.", + 0x0628: "Ensto Oy", + 0x0629: "PHONEPE PVT LTD", + 0x062A: "Lunatico Astronomia SL", + 0x062B: "MinebeaMitsumi Inc.", + 0x062C: "ASPion GmbH", + 0x062D: "Vossloh-Schwabe Deutschland GmbH", + 0x062E: "Procept", + 0x062F: "ONKYO Corporation", + 0x0630: "Asthrea D.O.O.", + 0x0631: "Fortiori Design LLC", + 0x0632: "Hugo Muller GmbH & Co KG", + 0x0633: "Wangi Lai PLT", + 0x0634: "Fanstel Corp", + 0x0635: "Crookwood", + 0x0636: "ELECTRONICA INTEGRAL DE SONIDO S.A.", + 0x0637: "GiP Innovation Tools GmbH", + 0x0638: "LX SOLUTIONS PTY LIMITED", + 0x0639: "Shenzhen Minew Technologies Co., Ltd.", + 0x063A: "Prolojik Limited", + 0x063B: "Kromek Group Plc", + 0x063C: "Contec Medical Systems Co., Ltd.", + 0x063D: "Xradio Technology Co.,Ltd.", + 0x063E: "The Indoor Lab, LLC", + 0x063F: "LDL TECHNOLOGY", + 0x0640: "Parkifi", + 0x0641: "Revenue Collection Systems FRANCE SAS", + 0x0642: "Bluetrum Technology Co.,Ltd", + 0x0643: "makita corporation", + 0x0644: "Apogee Instruments", + 0x0645: "BM3", + 0x0646: "SGV Group Holding GmbH & Co. KG", + 0x0647: "MED-EL", + 0x0648: "Ultune Technologies", + 0x0649: "Ryeex Technology Co.,Ltd.", + 0x064A: "Open Research Institute, Inc.", + 0x064B: "Scale-Tec, Ltd", + 0x064C: "Zumtobel Group AG", + 0x064D: "iLOQ Oy", + 0x064E: "KRUXWorks Technologies Private Limited", + 0x064F: "Digital Matter Pty Ltd", + 0x0650: "Coravin, Inc.", + 0x0651: "Stasis Labs, Inc.", + 0x0652: "ITZ Innovations- und Technologiezentrum GmbH", + 0x0653: "Meggitt SA", + 0x0654: "Ledlenser GmbH & Co. KG", + 0x0655: "Renishaw PLC", + 0x0656: "ZhuHai AdvanPro Technology Company Limited", + 0x0657: "Meshtronix Limited", + 0x0658: "Payex Norge AS", + 0x0659: "UnSeen Technologies Oy", + 0x065A: "Zound Industries International AB", + 0x065B: "Sesam Solutions BV", + 0x065C: "PixArt Imaging Inc.", + 0x065D: "Panduit Corp.", + 0x065E: "Alo AB", + 0x065F: "Ricoh Company Ltd", + 0x0660: "RTC Industries, Inc.", + 0x0661: "Mode Lighting Limited", + 0x0662: "Particle Industries, Inc.", + 0x0663: "Advanced Telemetry Systems, Inc.", + 0x0664: "RHA TECHNOLOGIES LTD", + 0x0665: "Pure International Limited", + 0x0666: "WTO Werkzeug-Einrichtungen GmbH", + 0x0667: "Spark Technology Labs Inc.", + 0x0668: "Bleb Technology srl", + 0x0669: "Livanova USA, Inc.", + 0x066A: "Brady Worldwide Inc.", + 0x066B: "DewertOkin GmbH", + 0x066C: "Ztove ApS", + 0x066D: "Venso EcoSolutions AB", + 0x066E: "Eurotronik Kranj d.o.o.", + 0x066F: "Hug Technology Ltd", + 0x0670: "Gema Switzerland GmbH", + 0x0671: "Buzz Products Ltd.", + 0x0672: "Kopi", + 0x0673: "Innova Ideas Limited", + 0x0674: "BeSpoon", + 0x0675: "Deco Enterprises, Inc.", + 0x0676: "Expai Solutions Private Limited", + 0x0677: "Innovation First, Inc.", + 0x0678: "SABIK Offshore GmbH", + 0x0679: "4iiii Innovations Inc.", + 0x067A: "The Energy Conservatory, Inc.", + 0x067B: "I.FARM, INC.", + 0x067C: "Tile, Inc.", + 0x067D: "Form Athletica Inc.", + 0x067E: "MbientLab Inc", + 0x067F: "NETGRID S.N.C. DI BISSOLI MATTEO, CAMPOREALE SIMONE, TOGNETTI FEDERICO", + 0x0680: "Mannkind Corporation", + 0x0681: "Trade FIDES a.s.", + 0x0682: "Photron Limited", + 0x0683: "Eltako GmbH", + 0x0684: "Dermalapps, LLC", + 0x0685: "Greenwald Industries", + 0x0686: "inQs Co., Ltd.", + 0x0687: "Cherry GmbH", + 0x0688: "Amsted Digital Solutions Inc.", + 0x0689: "Tacx b.v.", + 0x068A: "Raytac Corporation", + 0x068B: "Jiangsu Teranovo Tech Co., Ltd.", + 0x068C: "Changzhou Sound Dragon Electronics and Acoustics Co., Ltd", + 0x068D: "JetBeep Inc.", + 0x068E: "Razer Inc.", + 0x068F: "JRM Group Limited", + 0x0690: "Eccrine Systems, Inc.", + 0x0691: "Curie Point AB", + 0x0692: "Georg Fischer AG", + 0x0693: "Hach - Danaher", + 0x0694: "T&A Laboratories LLC", + 0x0695: "Koki Holdings Co., Ltd.", + 0x0696: "Gunakar Private Limited", + 0x0697: "Stemco Products Inc", + 0x0698: "Wood IT Security, LLC", + 0x0699: "RandomLab SAS", + 0x069A: "Adero, Inc. (formerly as TrackR, Inc.)", + 0x069B: "Dragonchip Limited", + 0x069C: "Noomi AB", + 0x069D: "Vakaros LLC", + 0x069E: "Delta Electronics, Inc.", + 0x069F: "FlowMotion Technologies AS", + 0x06A0: "OBIQ Location Technology Inc.", + 0x06A1: "Cardo Systems, Ltd", + 0x06A2: "Globalworx GmbH", + 0x06A3: "Nymbus, LLC", + 0x06A4: "Sanyo Techno Solutions Tottori Co., Ltd.", + 0x06A5: "TEKZITEL PTY LTD", + 0x06A6: "Roambee Corporation", + 0x06A7: "Chipsea Technologies (ShenZhen) Corp.", + 0x06A8: "GD Midea Air-Conditioning Equipment Co., Ltd.", + 0x06A9: "Soundmax Electronics Limited", + 0x06AA: "Produal Oy", + 0x06AB: "HMS Industrial Networks AB", + 0x06AC: "Ingchips Technology Co., Ltd.", + 0x06AD: "InnovaSea Systems Inc.", + 0x06AE: "SenseQ Inc.", + 0x06AF: "Shoof Technologies", + 0x06B0: "BRK Brands, Inc.", + 0x06B1: "SimpliSafe, Inc.", + 0x06B2: "Tussock Innovation 2013 Limited", + 0x06B3: "The Hablab ApS", + 0x06B4: "Sencilion Oy", + 0x06B5: "Wabilogic Ltd.", + 0x06B6: "Sociometric Solutions, Inc.", + 0x06B7: "iCOGNIZE GmbH", + 0x06B8: "ShadeCraft, Inc", + 0x06B9: "Beflex Inc.", + 0x06BA: "Beaconzone Ltd", + 0x06BB: "Leaftronix Analogic Solutions Private Limited", + 0x06BC: "TWS Srl", + 0x06BD: "ABB Oy", + 0x06BE: "HitSeed Oy", + 0x06BF: "Delcom Products Inc.", + 0x06C0: "CAME S.p.A.", + 0x06C1: "Alarm.com Holdings, Inc", + 0x06C2: "Measurlogic Inc.", + 0x06C3: "King I Electronics.Co.,Ltd", + 0x06C4: "Dream Labs GmbH", + 0x06C5: "Urban Compass, Inc", + 0x06C6: "Simm Tronic Limited", + 0x06C7: "Somatix Inc", + 0x06C8: "Storz & Bickel GmbH & Co. KG", + 0x06C9: "MYLAPS B.V.", + 0x06CA: "Shenzhen Zhongguang Infotech Technology Development Co., Ltd", + 0x06CB: "Dyeware, LLC", + 0x06CC: "Dongguan SmartAction Technology Co.,Ltd.", + 0x06CD: "DIG Corporation", + 0x06CE: "FIOR & GENTZ", + 0x06CF: "Belparts N.V.", + 0x06D0: "Etekcity Corporation", + 0x06D1: "Meyer Sound Laboratories, Incorporated", + 0x06D2: "CeoTronics AG", + 0x06D3: "TriTeq Lock and Security, LLC", + 0x06D4: "DYNAKODE TECHNOLOGY PRIVATE LIMITED", + 0x06D5: "Sensirion AG", + 0x06D6: "JCT Healthcare Pty Ltd", + 0x06D7: "FUBA Automotive Electronics GmbH", + 0x06D8: "AW Company", + 0x06D9: "Shanghai Mountain View Silicon Co.,Ltd.", + 0x06DA: "Zliide Technologies ApS", + 0x06DB: "Automatic Labs, Inc.", + 0x06DC: "Industrial Network Controls, LLC", + 0x06DD: "Intellithings Ltd.", + 0x06DE: "Navcast, Inc.", + 0x06DF: "Hubbell Lighting, Inc.", + 0x06E0: "Avaya", + 0x06E1: "Milestone AV Technologies LLC", + 0x06E2: "Alango Technologies Ltd", + 0x06E3: "Spinlock Ltd", + 0x06E4: "Aluna", + 0x06E5: "OPTEX CO.,LTD.", + 0x06E6: "NIHON DENGYO KOUSAKU", + 0x06E7: "VELUX A/S", + 0x06E8: "Almendo Technologies GmbH", + 0x06E9: "Zmartfun Electronics, Inc.", + 0x06EA: "SafeLine Sweden AB", + 0x06EB: "Houston Radar LLC", + 0x06EC: "Sigur", + 0x06ED: "J Neades Ltd", + 0x06EE: "Avantis Systems Limited", + 0x06EF: "ALCARE Co., Ltd.", + 0x06F0: "Chargy Technologies, SL", + 0x06F1: "Shibutani Co., Ltd.", + 0x06F2: "Trapper Data AB", + 0x06F3: "Alfred International Inc.", + 0x06F4: "Near Field Solutions Ltd", + 0x06F5: "Vigil Technologies Inc.", + 0x06F6: "Vitulo Plus BV", + 0x06F7: "WILKA Schliesstechnik GmbH", + 0x06F8: "BodyPlus Technology Co.,Ltd", + 0x06F9: "happybrush GmbH", + 0x06FA: "Enequi AB", + 0x06FB: "Sartorius AG", + 0x06FC: "Tom Communication Industrial Co.,Ltd.", + 0x06FD: "ESS Embedded System Solutions Inc.", + 0x06FE: "Mahr GmbH", + 0x06FF: "Redpine Signals Inc", + 0x0700: "TraqFreq LLC", + 0x0701: "PAFERS TECH", + 0x0702: 'Akciju sabiedriba "SAF TEHNIKA"', + 0x0703: "Beijing Jingdong Century Trading Co., Ltd.", + 0x0704: "JBX Designs Inc.", + 0x0705: "AB Electrolux", + 0x0706: "Wernher von Braun Center for ASdvanced Research", + 0x0707: "Essity Hygiene and Health Aktiebolag", + 0x0708: "Be Interactive Co., Ltd", + 0x0709: "Carewear Corp.", + 0x070A: "Huf Hlsbeck & Frst GmbH & Co. KG", + 0x070B: "Element Products, Inc.", + 0x070C: "Beijing Winner Microelectronics Co.,Ltd", + 0x070D: "SmartSnugg Pty Ltd", + 0x070E: "FiveCo Sarl", + 0x070F: "California Things Inc.", + 0x0710: "Audiodo AB", + 0x0711: "ABAX AS", + 0x0712: "Bull Group Company Limited", + 0x0713: "Respiri Limited", + 0x0714: "MindPeace Safety LLC", + 0x0715: "MBARC LABS Inc (formerly Vgyan Solutions)", + 0x0716: "Altonics", + 0x0717: "iQsquare BV", + 0x0718: "IDIBAIX enginneering", + 0x0719: "ECSG", + 0x071A: "REVSMART WEARABLE HK CO LTD", + 0x071B: "Precor", + 0x071C: "F5 Sports, Inc", + 0x071D: "exoTIC Systems", + 0x071E: "DONGGUAN HELE ELECTRONICS CO., LTD", + 0x071F: "Dongguan Liesheng Electronic Co.Ltd", + 0x0720: "Oculeve, Inc.", + 0x0721: "Clover Network, Inc.", + 0x0722: "Xiamen Eholder Electronics Co.Ltd", + 0x0723: "Ford Motor Company", + 0x0724: "Guangzhou SuperSound Information Technology Co.,Ltd", + 0x0725: "Tedee Sp. z o.o.", + 0x0726: "PHC Corporation", + 0x0727: "STALKIT AS", + 0x0728: "Eli Lilly and Company", + 0x0729: "SwaraLink Technologies", + 0x072A: "JMR embedded systems GmbH", + 0x072B: "Bitkey Inc.", + 0x072C: "GWA Hygiene GmbH", + 0x072D: "Safera Oy", + 0x072E: "Open Platform Systems LLC", + 0x072F: "OnePlus Electronics (Shenzhen) Co., Ltd.", + 0x0730: "Wildlife Acoustics, Inc.", + 0x0731: "ABLIC Inc.", + 0x0732: "Dairy Tech, Inc.", + 0x0733: "Iguanavation, Inc.", + 0x0734: "DiUS Computing Pty Ltd", + 0x0735: "UpRight Technologies LTD", + 0x0736: "FrancisFund, LLC", + 0x0737: "LLC Navitek", + 0x0738: "Glass Security Pte Ltd", + 0x0739: "Jiangsu Qinheng Co., Ltd.", + 0x073A: "Chandler Systems Inc.", + 0x073B: "Fantini Cosmi s.p.a.", + 0x073C: "Acubit ApS", + 0x073D: "Beijing Hao Heng Tian Tech Co., Ltd.", + 0x073E: "Bluepack S.R.L.", + 0x073F: "Beijing Unisoc Technologies Co., Ltd.", + 0x0740: "HITIQ LIMITED", + 0x0741: "MAC SRL", + 0x0742: "DML LLC", + 0x0743: "Sanofi", + 0x0744: "SOCOMEC", + 0x0745: "WIZNOVA, Inc.", + 0x0746: "Seitec Elektronik GmbH", + 0x0747: "OR Technologies Pty Ltd", + 0x0748: "GuangZhou KuGou Computer Technology Co.Ltd", + 0x0749: "DIAODIAO (Beijing) Technology Co., Ltd.", + 0x074A: "Illusory Studios LLC", + 0x074B: "Sarvavid Software Solutions LLP", + 0x074C: "iopool s.a.", + 0x074D: "Amtech Systems, LLC", + 0x074E: "EAGLE DETECTION SA", + 0x074F: "MEDIATECH S.R.L.", + 0x0750: "Hamilton Professional Services of Canada Incorporated", + 0x0751: "Changsha JEMO IC Design Co.,Ltd", + 0x0752: "Elatec GmbH", + 0x0753: "JLG Industries, Inc.", + 0x0754: "Michael Parkin", + 0x0755: "Brother Industries, Ltd", + 0x0756: "Lumens For Less, Inc", + 0x0757: "ELA Innovation", + 0x0758: "umanSense AB", + 0x0759: "Shanghai InGeek Cyber Security Co., Ltd.", + 0x075A: "HARMAN CO.,LTD.", + 0x075B: "Smart Sensor Devices AB", + 0x075C: "Antitronics Inc.", + 0x075D: "RHOMBUS SYSTEMS, INC.", + 0x075E: "Katerra Inc.", + 0x075F: "Remote Solution Co., LTD.", + 0x0760: "Vimar SpA", + 0x0761: "Mantis Tech LLC", + 0x0762: "TerOpta Ltd", + 0x0763: "PIKOLIN S.L.", + 0x0764: "WWZN Information Technology Company Limited", + 0x0765: "Voxx International", + 0x0766: "ART AND PROGRAM, INC.", + 0x0767: "NITTO DENKO ASIA TECHNICAL CENTRE PTE. LTD.", + 0x0768: "Peloton Interactive Inc.", + 0x0769: "Force Impact Technologies", + 0x076A: "Dmac Mobile Developments, LLC", + 0x076B: "Engineered Medical Technologies", + 0x076C: "Noodle Technology inc", + 0x076D: "Graesslin GmbH", + 0x076E: "WuQi technologies, Inc.", + 0x076F: "Successful Endeavours Pty Ltd", + 0x0770: "InnoCon Medical ApS", + 0x0771: "Corvex Connected Safety", + 0x0772: "Thirdwayv Inc.", + 0x0773: "Echoflex Solutions Inc.", + 0x0774: "C-MAX Asia Limited", + 0x0775: "4eBusiness GmbH", + 0x0776: "Cyber Transport Control GmbH", + 0x0777: "Cue", + 0x0778: "KOAMTAC INC.", + 0x0779: "Loopshore Oy", + 0x077A: "Niruha Systems Private Limited", + 0x077B: "AmaterZ, Inc.", + 0x077C: "radius co., ltd.", + 0x077D: "Sensority, s.r.o.", + 0x077E: "Sparkage Inc.", + 0x077F: "Glenview Software Corporation", + 0x0780: "Finch Technologies Ltd.", + 0x0781: "Qingping Technology (Beijing) Co., Ltd.", + 0x0782: "DeviceDrive AS", + 0x0783: "ESEMBER LIMITED LIABILITY COMPANY", + 0x0784: "audifon GmbH & Co. KG", + 0x0785: "O2 Micro, Inc.", + 0x0786: "HLP Controls Pty Limited", + 0x0787: "Pangaea Solution", + 0x0788: "BubblyNet, LLC", + 0xFFFF: "This value has special meaning depending on the context in which it used. Link Manager Protocol (LMP): This value may be used in the internal and interoperability tests before a Company ID has been assigned. This value shall not be used in shipping end products. Device ID Profile: This value is reserved as the default vendor ID when no Device ID service record is present in a remote device.", +} diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/_utils.py b/tools/python-embed/Lib/site-packages/bleak/backends/_utils.py new file mode 100644 index 0000000..daec885 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/_utils.py @@ -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 diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__init__.py b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__init__.py new file mode 100644 index 0000000..688b9df --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__init__.py @@ -0,0 +1 @@ +"""BlueZ backend.""" diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/__init__.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..9a64ce9 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/__init__.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/advertisement_monitor.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/advertisement_monitor.cpython-312.pyc new file mode 100644 index 0000000..c300d2a Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/advertisement_monitor.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/client.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/client.cpython-312.pyc new file mode 100644 index 0000000..2a4ec12 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/client.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/defs.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/defs.cpython-312.pyc new file mode 100644 index 0000000..0b2bb9e Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/defs.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/manager.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/manager.cpython-312.pyc new file mode 100644 index 0000000..4b70dab Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/manager.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/scanner.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/scanner.cpython-312.pyc new file mode 100644 index 0000000..d577225 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/scanner.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/signals.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/signals.cpython-312.pyc new file mode 100644 index 0000000..626a3c4 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/signals.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/utils.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000..d1b60c6 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/utils.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/version.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/version.cpython-312.pyc new file mode 100644 index 0000000..c7e2f43 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/__pycache__/version.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/advertisement_monitor.py b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/advertisement_monitor.py new file mode 100644 index 0000000..a2f8472 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/advertisement_monitor.py @@ -0,0 +1,131 @@ +""" +Advertisement Monitor +--------------------- + +This module contains types associated with the BlueZ D-Bus `advertisement +monitor api `. +""" + +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 diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/client.py b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/client.py new file mode 100644 index 0000000..5e0af10 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/client.py @@ -0,0 +1,1039 @@ +""" +BLE Client for BlueZ on Linux +""" + +import sys +from typing import TYPE_CHECKING + +from bleak.args.bluez import BlueZClientArgs, BlueZNotifyArgs + +if TYPE_CHECKING: + if sys.platform != "linux": + assert False, "This backend is only available on Linux" + +import asyncio +import logging +import os +import warnings +from collections.abc import Callable +from contextlib import AsyncExitStack +from typing import Any, Optional, Union + +from dbus_fast.aio import MessageBus +from dbus_fast.constants import BusType, ErrorType, MessageType +from dbus_fast.message import Message +from dbus_fast.signature import Variant + +from bleak import BleakScanner +from bleak._compat import override +from bleak._compat import timeout as async_timeout +from bleak.args import SizedBuffer +from bleak.backends.bluezdbus import defs +from bleak.backends.bluezdbus.manager import get_global_bluez_manager +from bleak.backends.bluezdbus.scanner import BleakScannerBlueZDBus +from bleak.backends.bluezdbus.utils import ( + assert_gatt_reply, + assert_reply, + get_dbus_authenticator, +) +from bleak.backends.bluezdbus.version import BlueZFeatures +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.service import BleakGATTServiceCollection +from bleak.exc import BleakDBusError, BleakDeviceNotFoundError, BleakError + +logger = logging.getLogger(__name__) + +# prevent tasks from being garbage collected +_background_tasks: set[asyncio.Task[None]] = set() + + +class BleakClientBlueZDBus(BaseBleakClient): + """A native Linux Bleak Client + + Implemented by using the `BlueZ DBUS API `_. + + 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 list of service UUIDs that will be used. + """ + + def __init__( + self, + address_or_ble_device: Union[BLEDevice, str], + services: Optional[set[str]] = None, + *, + bluez: BlueZClientArgs, + **kwargs: Any, + ): + super().__init__(address_or_ble_device, **kwargs) + + self._adapter = bluez.get("adapter") + self._device_path: Optional[str] + self._device_info: Optional[dict[str, Any]] + + # Backend specific, D-Bus objects and data + if isinstance(address_or_ble_device, BLEDevice): + self._device_path = address_or_ble_device.details["path"] + self._device_info = address_or_ble_device.details.get("props") + else: + self._device_path = None + self._device_info = None + + self._requested_services = services + + # D-Bus message bus + self._bus: Optional[MessageBus] = None + # tracks device watcher subscription + self._remove_device_watcher: Optional[Callable[[], None]] = None + # private backing for is_connected property + self._is_connected = False + # indicates disconnect request in progress when not None + self._disconnecting_event: Optional[asyncio.Event] = None + # used to ensure device gets disconnected if event loop crashes + self._disconnect_monitor_event: Optional[asyncio.Event] = None + # map of characteristic D-Bus object path to notification callback + self._notification_callbacks: dict[str, NotifyCallback] = {} + # map of characteristic D-Bus path to AcquireNotify file descriptor + self._notification_fds: dict[str, int] = {} + + # used to override mtu_size property + self._mtu_size: Optional[int] = None + + # Connectivity methods + + @override + async def connect( + self, pair: bool, dangerous_use_bleak_cache: bool = False, **kwargs: Any + ) -> None: + """Connect to the specified GATT server. + + Keyword Args: + timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. + + Raises: + BleakError: If the device is already connected or if the device could not be found. + BleakDBusError: If there was a D-Bus error + asyncio.TimeoutError: If the connection timed out + """ + logger.debug("Connecting to device @ %s", self.address) + + if self.is_connected: + raise BleakError("Client is already connected") + + if not BlueZFeatures.checked_bluez_version: + await BlueZFeatures.check_bluez_version() + if not BlueZFeatures.supported_version: + raise BleakError("Bleak requires BlueZ >= 5.55.") + # A Discover must have been run before connecting to any devices. + # Find the desired device before trying to connect. + timeout = kwargs.get("timeout", self._timeout) + if self._device_path is None: + device = await BleakScanner.find_device_by_address( + self.address, + timeout=timeout, + bluez={} if self._adapter is None else {"adapter": self._adapter}, + backend=BleakScannerBlueZDBus, + ) + + if device: + self._device_info = device.details.get("props") + self._device_path = device.details["path"] + else: + raise BleakDeviceNotFoundError( + self.address, f"Device with address {self.address} was not found." + ) + + assert self._device_path is not None + + manager = await get_global_bluez_manager() + + async with async_timeout(timeout): + while True: + async with AsyncExitStack() as stack: + # Each BLE connection session needs a new D-Bus connection to avoid a + # BlueZ quirk where notifications are automatically enabled on reconnect. + self._bus = MessageBus( + bus_type=BusType.SYSTEM, + negotiate_unix_fd=True, + auth=get_dbus_authenticator(), + ) + # dbus-fast is weird and requires disconnect to be called + # even if connect fails. + stack.callback(self._bus.disconnect) + await self._bus.connect() + + stack.callback(self._cleanup_all) + + def on_connected_changed(connected: bool) -> None: + if not connected: + logger.debug("Device disconnected (%s)", self._device_path) + + self._is_connected = False + + if self._disconnect_monitor_event: + self._disconnect_monitor_event.set() + self._disconnect_monitor_event = None + + self._cleanup_all() + if self._disconnected_callback is not None: + self._disconnected_callback() + disconnecting_event = self._disconnecting_event + if disconnecting_event: + disconnecting_event.set() + + def on_value_changed(char_path: str, value: bytes) -> None: + callback = self._notification_callbacks.get(char_path) + + if callback: + callback(bytearray(value)) + + watcher = manager.add_device_watcher( + self._device_path, on_connected_changed, on_value_changed + ) + self._remove_device_watcher = lambda: manager.remove_device_watcher( + watcher + ) + + self._disconnect_monitor_event = local_disconnect_monitor_event = ( + asyncio.Event() + ) + + # this effectively cancels the disconnect monitor in case the event + # was not triggered by a D-Bus callback + stack.callback(local_disconnect_monitor_event.set) + + async def disconnect_device() -> None: + # Calling Disconnect cancels any pending connect request. Also, + # if connection was successful but _get_services() raises (e.g. + # because task was cancelled), then we still need to disconnect + # before passing on the exception. + if self._bus: + # If disconnected callback already fired, this will be a no-op + # since self._bus will be None and the _cleanup_all call will + # have already disconnected. + try: + disconnect_reply = await self._bus.call( + Message( + destination=defs.BLUEZ_SERVICE, + interface=defs.DEVICE_INTERFACE, + path=self._device_path, + member="Disconnect", + ) + ) + assert disconnect_reply + try: + assert_reply(disconnect_reply) + except BleakDBusError as e: + # if the object no longer exists, then we know we + # are disconnected for sure, so don't need to log a + # warning about it + if e.dbus_error != ErrorType.UNKNOWN_OBJECT.value: + raise + except Exception as e: + logger.warning( + f"Failed to cancel connection ({self._device_path}): {e}" + ) + + stack.push_async_callback(disconnect_device) + + # The BlueZ backend does not disconnect devices when the + # application closes or crashes. This can cause problems + # when trying to reconnect to the same device. To work + # around this, we check if the device is already connected. + # + # For additional details see https://github.com/bluez/bluez/issues/89 + if manager.is_connected(self._device_path): + logger.debug( + 'skipping calling "Connect" since %s is already connected', + self._device_path, + ) + else: + logger.debug("Connecting to BlueZ path %s", self._device_path) + + # Calling pair will fail if we are already paired, so + # in that case we just call Connect. + if pair and not manager.is_paired(self._device_path): + reply = await self._pair() + + # For resolvable private addresses, the address will + # change after pairing, so we need to update that. + # Hopefully there is no race condition here. D-Bus + # traffic capture shows that Address change happens + # at the same time as Paired property change and + # that PropertiesChanged signal is sent before the + # "Pair" reply is sent. + self.address = manager.get_device_address(self._device_path) + else: + reply = await self._bus.call( + Message( + destination=defs.BLUEZ_SERVICE, + interface=defs.DEVICE_INTERFACE, + path=self._device_path, + member="Connect", + ) + ) + + if reply.message_type == MessageType.ERROR: + # This error is often caused by RF interference + # from other Bluetooth or Wi-Fi devices. In many + # cases, retrying will connect successfully. + # Note: this error was added in BlueZ 6.62. + if ( + reply.error_name == defs.BLUEZ_ERROR_FAILED + and reply.body + and reply.body[0] == "le-connection-abort-by-local" + ): + logger.debug( + "retry due to le-connection-abort-by-local" + ) + + # When this error occurs, BlueZ actually + # connected so we get "Connected" property changes + # that we need to wait for before attempting + # to connect again. + await local_disconnect_monitor_event.wait() + + # Jump way back to the `while True:`` to retry. + continue + + if reply.error_name == ErrorType.UNKNOWN_OBJECT.value: + raise BleakDeviceNotFoundError( + self.address, + f"Device with address {self.address} was not found. It may have been removed from BlueZ when scanning stopped.", + ) + + assert_reply(reply) + + self._is_connected = True + + # Create a task that runs until the device is disconnected. + task = asyncio.create_task( + self._disconnect_monitor( + self._bus, + self._device_path, + local_disconnect_monitor_event, + ) + ) + _background_tasks.add(task) + task.add_done_callback(_background_tasks.discard) + + # We will try to use the cache if it exists and `dangerous_use_bleak_cache` + # is True. + await self._get_services( + dangerous_use_bleak_cache=dangerous_use_bleak_cache + ) + + stack.pop_all() + return + + @staticmethod + async def _disconnect_monitor( + bus: MessageBus, device_path: str, disconnect_monitor_event: asyncio.Event + ) -> None: + # This task runs until the device is disconnected. If the task is + # cancelled, it probably means that the event loop crashed so we + # try to disconnected the device. Otherwise BlueZ will keep the device + # connected even after Python exits. This will only work if the event + # loop is called with asyncio.run() or otherwise runs pending tasks + # after the original event loop stops. This will also cause an exception + # if a run loop is stopped before the device is disconnected since this + # task will still be running and asyncio complains if a loop with running + # tasks is stopped. + try: + await disconnect_monitor_event.wait() + except asyncio.CancelledError: + try: + # by using send() instead of call(), we ensure that the message + # gets sent, but we don't wait for a reply, which could take + # over one second while the device disconnects. + # TODO: fix send() return type in dbus-fast so we can remove the pyright ignore + await bus.send( # pyright: ignore[reportUnknownMemberType] + Message( + destination=defs.BLUEZ_SERVICE, + path=device_path, + interface=defs.DEVICE_INTERFACE, + member="Disconnect", + ) + ) + except Exception: + pass + + def _cleanup_all(self) -> None: + """ + Free all the allocated resource in DBus. Use this method to + eventually cleanup all otherwise leaked resources. + """ + logger.debug("_cleanup_all(%s)", self._device_path) + + if self._remove_device_watcher: + self._remove_device_watcher() + self._remove_device_watcher = None + + if not self._bus: + logger.debug("already disconnected (%s)", self._device_path) + return + + # Reset all stored services. + self.services = None + + @override + async def disconnect(self) -> None: + """Disconnect from the specified GATT server. + + Raises: + BleakDBusError: If there was a D-Bus error + asyncio.TimeoutError if the device was not disconnected within 10 seconds + """ + logger.debug("Disconnecting (%s)", self._device_path) + + if self._bus is None: + # No connection exists. Either one hasn't been created or + # we have already called disconnect and closed the D-Bus + # connection. + logger.debug("already disconnected (%s)", self._device_path) + return + + if self._disconnecting_event: + # another call to disconnect() is already in progress + logger.debug("already in progress (%s)", self._device_path) + async with async_timeout(10): + await self._disconnecting_event.wait() + else: + self._disconnecting_event = asyncio.Event() + try: + if self.is_connected: + # Try to disconnect the actual device/peripheral + reply = await self._bus.call( + Message( + destination=defs.BLUEZ_SERVICE, + path=self._device_path, + interface=defs.DEVICE_INTERFACE, + member="Disconnect", + ) + ) + assert_reply(reply) + + async with async_timeout(10): + await self._disconnecting_event.wait() + finally: + self._disconnecting_event = None + + self._bus.disconnect() + await self._bus.wait_for_disconnect() + self._bus = None + + # sanity check to make sure _cleanup_all() was triggered by the + # "PropertiesChanged" signal handler and that it completed successfully + assert self.services is None + + async def _pair(self) -> Message: + """ + Pair with the peripheral and return the D-Bus reply. + + Caller is responsible for checking reply for errors. + """ + + assert self._bus is not None + assert self._device_path is not None + + # REVIST: This leaves "Trusted" property set if we + # fail later. Probably not a big deal since we were + # going to trust it anyway. + # Trusted means device is authorized + reply = await self._bus.call( + Message( + destination=defs.BLUEZ_SERVICE, + path=self._device_path, + interface=defs.PROPERTIES_INTERFACE, + member="Set", + signature="ssv", + body=[defs.DEVICE_INTERFACE, "Trusted", Variant("b", True)], + ) + ) + assert_reply(reply) + + logger.debug("Pairing to BLE device @ %s", self.address) + # Pairing means device is authenticated + reply = await self._bus.call( + Message( + destination=defs.BLUEZ_SERVICE, + path=self._device_path, + interface=defs.DEVICE_INTERFACE, + member="Pair", + ) + ) + + return reply + + @override + async def pair(self, *args: Any, **kwargs: Any) -> 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. + """ + assert self._device_path is not None + manager = await get_global_bluez_manager() + if manager.is_paired(self._device_path): + logger.debug("BLE device @ %s is already paired", self.address) + return + + reply = await self._pair() + assert_reply(reply) + + # For resolvable private addresses, the address will + # change after pairing, so we need to update that. + # Hopefully there is no race condition here. D-Bus + # traffic capture shows that Address change happens + # at the same time as Paired property change and + # that PropertiesChanged signal is sent before the + # "Pair" reply is sent. + self.address = manager.get_device_address(self._device_path) + + @override + async def unpair(self) -> None: + """Unpair with the peripheral.""" + adapter_path = await self._get_adapter_path() + device_path = await self._get_device_path() + manager = await get_global_bluez_manager() + + logger.debug( + "Removing BlueZ device path %s from adapter path %s", + device_path, + adapter_path, + ) + + # If this client object wants to connect again, BlueZ needs the device + # to follow Discovery process again - so reset the local connection + # state. + # + # (This is true even if the request to RemoveDevice fails, + # so clear it before.) + self._device_path = None + self._device_info = None + self._is_connected = False + + assert manager._bus # pyright: ignore[reportPrivateUsage] + + # TODO: should move this to manager so that we don't access its private members + try: + reply = await manager._bus.call( # pyright: ignore[reportPrivateUsage] + Message( + destination=defs.BLUEZ_SERVICE, + path=adapter_path, + interface=defs.ADAPTER_INTERFACE, + member="RemoveDevice", + signature="o", + body=[device_path], + ) + ) + assert_reply(reply) + except BleakDBusError as e: + if e.dbus_error == defs.BLUEZ_ERROR_DOES_NOT_EXIST: + raise BleakDeviceNotFoundError( + self.address, f"Device with address {self.address} was not found." + ) from e + raise + + @property + @override + def is_connected(self) -> bool: + """Check connection status between this client and the server. + + Returns: + Boolean representing connection status. + + """ + return False if self._bus is None else self._is_connected + + async def _acquire_mtu(self) -> None: + """Acquires the MTU for this device by calling the "AcquireWrite" or + "AcquireNotify" method of the first characteristic that has such a method. + + This method only needs to be called once, after connecting to the device + but before accessing the ``mtu_size`` property. + + If a device uses encryption on characteristics, it will need to be bonded + first before calling this method. + """ + + assert ( + self.services is not None + ), "Services must be acquired before acquiring MTU" + + # This will try to get the "best" characteristic for getting the MTU. + # We would rather not start notifications if we don't have to. + try: + method = "AcquireWrite" + char = next( + c + for c in self.services.characteristics.values() + if "write-without-response" in c.properties + ) + except StopIteration: + method = "AcquireNotify" + char = next( + c + for c in self.services.characteristics.values() + if "notify" in c.properties + ) + + assert self._bus + + reply = await self._bus.call( + Message( + destination=defs.BLUEZ_SERVICE, + path=char.obj[0], + interface=defs.GATT_CHARACTERISTIC_INTERFACE, + member=method, + signature="a{sv}", + body=[{}], + ) + ) + assert_reply(reply) + + # we aren't actually using the write or notify, we just want the MTU + os.close(reply.unix_fds[0]) + self._mtu_size = reply.body[1] + + async def _get_adapter_path(self) -> str: + """Private coroutine to return the BlueZ path to the adapter this client is assigned to. + + Can be called even if no connection has been established yet. + """ + if self._device_info: + # If we have a BlueZ DBus object with _device_info, use what it tell us + return self._device_info["Adapter"] + if self._adapter: + # If the adapter name was set in the constructor, convert to a BlueZ path + return f"/org/bluez/{self._adapter}" + + # Fall back to the system's default Bluetooth adapter + manager = await get_global_bluez_manager() + return manager.get_default_adapter() + + async def _get_device_path(self) -> str: + """Private coroutine to return the BlueZ path to the device address this client is assigned to. + + Unlike the _device_path property, this function can be called even if discovery process has not + started and/or connection has not been established yet. + """ + if self._device_path: + # If we have a BlueZ DBus object, return its device path + return self._device_path + + # Otherwise, build a new path using the adapter path and the BLE address + adapter_path = await self._get_adapter_path() + bluez_address = self.address.upper().replace(":", "_") + return f"{adapter_path}/dev_{bluez_address}" + + @property + @override + def name(self) -> str: + """See :meth:`bleak.BleakClient.name`.""" + if self._device_info is None: + raise BleakError("Not connected") + return self._device_info["Alias"] + + @property + @override + def mtu_size(self) -> int: + """Get ATT MTU size for active connection""" + if self._mtu_size is None: + warnings.warn( + "Using default MTU value. Call _acquire_mtu() or set _mtu_size first to avoid this warning." + ) + return 23 + + return self._mtu_size + + # GATT services methods + + async def _get_services( + self, dangerous_use_bleak_cache: bool = False, **kwargs: Any + ) -> BleakGATTServiceCollection: + """Get all services registered for this GATT server. + + Args: + dangerous_use_bleak_cache (bool): Use cached services if available. + + Returns: + A :py:class:`bleak.backends.service.BleakGATTServiceCollection` with this device's services tree. + + """ + if not self.is_connected: + raise BleakError("Not connected") + + if self.services is not None: + return self.services + + assert self._device_path is not None + + manager = await get_global_bluez_manager() + + self.services = await manager.get_services( + self._device_path, dangerous_use_bleak_cache, self._requested_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 not self.is_connected: + raise BleakError("Not connected") + + char_path = characteristic.obj[0] + + if use_cached: + manager = await get_global_bluez_manager() + return bytearray(manager.get_char_value(char_path)) + + while True: + assert self._bus + + reply = await self._bus.call( + Message( + destination=defs.BLUEZ_SERVICE, + path=char_path, + interface=defs.GATT_CHARACTERISTIC_INTERFACE, + member="ReadValue", + signature="a{sv}", + body=[{}], + ) + ) + + if reply.error_name == defs.BLUEZ_ERROR_IN_PROGRESS: + logger.debug("retrying characteristic ReadValue due to InProgress") + # Avoid calling in a tight loop. There is no dbus signal to + # indicate ready, so unfortunately, we have to poll. + await asyncio.sleep(0.01) + continue + + assert_gatt_reply(reply) + break + + value = bytearray(reply.body[0]) + + logger.debug( + "Read Characteristic %s | %s: %r", + characteristic.uuid, + char_path, + 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 not self.is_connected: + raise BleakError("Not connected") + + if use_cached: + manager = await get_global_bluez_manager() + return bytearray(manager.get_desc_value(descriptor.obj[0])) + + while True: + assert self._bus + + reply = await self._bus.call( + Message( + destination=defs.BLUEZ_SERVICE, + path=descriptor.obj[0], + interface=defs.GATT_DESCRIPTOR_INTERFACE, + member="ReadValue", + signature="a{sv}", + body=[{}], + ) + ) + + if reply.error_name == defs.BLUEZ_ERROR_IN_PROGRESS: + logger.debug("retrying descriptor ReadValue due to InProgress") + # Avoid calling in a tight loop. There is no dbus signal to + # indicate ready, so unfortunately, we have to poll. + await asyncio.sleep(0.01) + continue + + assert_gatt_reply(reply) + break + + value = bytearray(reply.body[0]) + + logger.debug( + "Read Descriptor %s | %s: %s", descriptor.handle, descriptor.obj[0], value + ) + return value + + @override + async def write_gatt_char( + self, characteristic: BleakGATTCharacteristic, data: SizedBuffer, response: bool + ) -> None: + if not self.is_connected: + raise BleakError("Not connected") + + char_path = characteristic.obj[0] + + while True: + assert self._bus + + reply = await self._bus.call( + Message( + destination=defs.BLUEZ_SERVICE, + path=char_path, + interface=defs.GATT_CHARACTERISTIC_INTERFACE, + member="WriteValue", + signature="aya{sv}", + body=[ + bytes(data), + {"type": Variant("s", "request" if response else "command")}, + ], + ) + ) + + if reply.error_name == defs.BLUEZ_ERROR_IN_PROGRESS: + logger.debug("retrying characteristic WriteValue due to InProgress") + # Avoid calling in a tight loop. There is no dbus signal to + # indicate ready, so unfortunately, we have to poll. + await asyncio.sleep(0.01) + continue + + assert_gatt_reply(reply) + break + + logger.debug( + "Write Characteristic %s | %s: %s", + characteristic.uuid, + char_path, + data, + ) + + @override + async def write_gatt_descriptor( + self, descriptor: BleakGATTDescriptor, data: SizedBuffer + ) -> None: + """Perform a write operation on the specified GATT descriptor. + + Args: + handle: The handle of the descriptor to read from. + data: The data to send (any bytes-like object). + + """ + if not self.is_connected: + raise BleakError("Not connected") + + while True: + assert self._bus + + reply = await self._bus.call( + Message( + destination=defs.BLUEZ_SERVICE, + path=descriptor.obj[0], + interface=defs.GATT_DESCRIPTOR_INTERFACE, + member="WriteValue", + signature="aya{sv}", + body=[bytes(data), {"type": Variant("s", "command")}], + ) + ) + + if reply.error_name == defs.BLUEZ_ERROR_IN_PROGRESS: + logger.debug("retrying descriptor WriteValue due to InProgress") + # Avoid calling in a tight loop. There is no dbus signal to + # indicate ready, so unfortunately, we have to poll. + await asyncio.sleep(0.01) + continue + + assert_gatt_reply(reply) + break + + logger.debug( + "Write Descriptor %s | %s: %s", descriptor.handle, descriptor.obj[0], data + ) + + def _register_notify_fd_reader( + self, char_path: str, fd: int, callback: NotifyCallback + ) -> None: + loop = asyncio.get_running_loop() + + def on_data(): + try: + data = os.read(fd, 1024) + if not data: + raise RuntimeError("Unexpected EOF on notification file handle") + except Exception as e: + logger.debug( + "AcquireNotify: Read error on fd %d: %s. Notifications have been stopped.", + fd, + e, + ) + try: + loop.remove_reader(fd) + except RuntimeError: + # Run loop is closed + pass + try: + os.close(fd) + except OSError: + # Bad file descriptor + pass + self._notification_fds.pop(char_path, None) + return + + callback(bytearray(data)) + + loop.add_reader(fd, on_data) + + @override + async def start_notify( + self, + characteristic: BleakGATTCharacteristic, + callback: NotifyCallback, + **kwargs: Any, + ) -> None: + """ + Activate notifications/indications on a characteristic. + + Args: + characteristic: The characteristic to activate notification/indication on. + callback: The callback to call when a notification/indication is received. + + Keyword Args: + bluez (BlueZNotifyArgs): dictionary of additional parameters. + """ + + bluez: BlueZNotifyArgs = kwargs["bluez"] + # A number of devices have issues with AcquireNotify, so we use StartNotify + # by default. For cases where AcquireNotify does work, users can set + # "use_start_notify" to False. + force_use_start_notify = bluez.get("use_start_notify", True) + + assert self._bus is not None + + char_path: str = characteristic.obj[0] + + # If using StartNotify and calling a read on the same characteristic, + # BlueZ will return the response as both a notification and read, + # duplicating the message. Using AcquireNotify on supported + # characteristics avoids this. However, using the preferred + # AcquireNotify requires that devices correctly indicate "notify" + # and/or "indicate" properties. If they don't, we fall back to + # StartNotify anyway. + use_notify_acquire = ( + not force_use_start_notify and "NotifyAcquired" in characteristic.obj[1] + ) + logger.debug( + 'using "%s" for notifications on characteristic %d', + "AcquireNotify" if use_notify_acquire else "StartNotify", + characteristic.handle, + ) + if use_notify_acquire: + reply = await self._bus.call( + Message( + destination=defs.BLUEZ_SERVICE, + path=char_path, + interface=defs.GATT_CHARACTERISTIC_INTERFACE, + member="AcquireNotify", + body=[{}], + signature="a{sv}", + ) + ) + assert_gatt_reply(reply) + + unix_fd = reply.unix_fds[0] + self._notification_fds[char_path] = unix_fd + self._register_notify_fd_reader(char_path, unix_fd, callback) + else: + self._notification_callbacks[char_path] = callback + reply = await self._bus.call( + Message( + destination=defs.BLUEZ_SERVICE, + path=char_path, + interface=defs.GATT_CHARACTERISTIC_INTERFACE, + member="StartNotify", + ) + ) + assert_gatt_reply(reply, start_notify=True) + + @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. + """ + if not self.is_connected: + raise BleakError("Not connected") + + assert self._bus is not None + + char_path: str = characteristic.obj[0] + + # If we have a notification fd for this characteristic, then we know we + # used AcquireNotify, otherwise we used StartNotify. + + if (fd := self._notification_fds.get(char_path)) is not None: + logger.debug( + "Closing notification fd for characteristic %d", characteristic.handle + ) + + loop = asyncio.get_running_loop() + try: + loop.remove_reader(fd) + except RuntimeError: + # Run loop is closed + pass + try: + os.close(fd) + except OSError as e: + logger.debug("Failed to remove file descriptor %d: %s", fd, e) + + self._notification_fds.pop(char_path, None) + + else: + logger.debug( + "Calling StopNotify for characteristic %d", characteristic.handle + ) + reply = await self._bus.call( + Message( + destination=defs.BLUEZ_SERVICE, + path=char_path, + interface=defs.GATT_CHARACTERISTIC_INTERFACE, + member="StopNotify", + ) + ) + assert_reply(reply) + self._notification_callbacks.pop(char_path, None) diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/defs.py b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/defs.py new file mode 100644 index 0000000..ab5669a --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/defs.py @@ -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 diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/manager.py b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/manager.py new file mode 100644 index 0000000..e98cb8a --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/manager.py @@ -0,0 +1,1221 @@ +""" +BlueZ D-Bus manager module +-------------------------- + +This module contains code for the global BlueZ D-Bus object manager that is +used internally by Bleak. +""" + +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 os +from collections import defaultdict +from collections.abc import Callable, Coroutine +from functools import partial +from typing import Any, NamedTuple, Optional, cast + +from dbus_fast import AuthError, BusType, Message, MessageType, Variant, unpack_variants +from dbus_fast.aio.message_bus import MessageBus + +from bleak.args.bluez import OrPatternLike +from bleak.backends.bluezdbus import defs +from bleak.backends.bluezdbus.advertisement_monitor import AdvertisementMonitor +from bleak.backends.bluezdbus.defs import ( + Device1, + GattCharacteristic1, + GattDescriptor1, + GattService1, +) +from bleak.backends.bluezdbus.signals import MatchRules, add_match +from bleak.backends.bluezdbus.utils import ( + assert_reply, + device_path_from_characteristic_path, + extract_service_handle_from_path, + get_dbus_authenticator, +) +from bleak.backends.characteristic import BleakGATTCharacteristic +from bleak.backends.descriptor import BleakGATTDescriptor +from bleak.backends.service import BleakGATTService, BleakGATTServiceCollection +from bleak.exc import ( + BleakBluetoothNotAvailableError, + BleakBluetoothNotAvailableReason, + BleakDBusError, + BleakError, +) + +logger = logging.getLogger(__name__) + +AdvertisementCallback = Callable[[str, Device1], None] +""" +A callback that is called when advertisement data is received. + +Args: + arg0: The D-Bus object path of the device. + arg1: The D-Bus properties of the device object. +""" + + +DevicePropertiesChangedCallback = Callable[[Optional[Any]], None] +""" +A callback that is called when the properties of a device change in BlueZ. + +Args: + arg0: The new property value. +""" + + +class DeviceConditionCallback(NamedTuple): + """ + Encapsulates a :data:`DevicePropertiesChangedCallback` and the property name being watched. + """ + + callback: DevicePropertiesChangedCallback + """ + The callback. + """ + + property_name: str + """ + The name of the property to watch. + """ + + +DeviceRemovedCallback = Callable[[str], None] +""" +A callback that is called when a device is removed from BlueZ. + +Args: + arg0: The D-Bus object path of the device. +""" + + +class DeviceRemovedCallbackAndState(NamedTuple): + """ + Encapsulates an :data:`DeviceRemovedCallback` and some state. + """ + + callback: DeviceRemovedCallback + """ + The callback. + """ + + adapter_path: str + """ + The D-Bus object path of the adapter associated with the callback. + """ + + +DeviceConnectedChangedCallback = Callable[[bool], None] +""" +A callback that is called when a device's "Connected" property changes. + +Args: + arg0: The current value of the "Connected" property. +""" + +CharacteristicValueChangedCallback = Callable[[str, bytes], None] +""" +A callback that is called when a characteristics's "Value" property changes. + +Args: + arg0: The D-Bus object path of the characteristic. + arg1: The current value of the "Value" property. +""" + + +class DeviceWatcher(NamedTuple): + device_path: str + """ + The D-Bus object path of the device. + """ + + on_connected_changed: DeviceConnectedChangedCallback + """ + A callback that is called when a device's "Connected" property changes. + """ + + on_characteristic_value_changed: CharacteristicValueChangedCallback + """ + A callback that is called when a characteristics's "Value" property changes. + """ + + +# set of org.bluez.Device1 property names that come from advertising data +_ADVERTISING_DATA_PROPERTIES = { + "AdvertisingData", + "AdvertisingFlags", + "ManufacturerData", + "Name", + "ServiceData", + "UUIDs", +} + + +def get_max_write_without_response_size(char_props: GattCharacteristic1) -> int: + # "MTU" property was added in BlueZ 5.62, otherwise fall + # back to minimum MTU according to Bluetooth spec. + return char_props.get("MTU", 23) - 3 + + +class BlueZManager: + """ + BlueZ D-Bus object manager. + + Use :func:`bleak.backends.bluezdbus.get_global_bluez_manager` to get the global instance. + """ + + def __init__(self) -> None: + self._bus: Optional[MessageBus] = None + self._bus_lock = asyncio.Lock() + + # dict of object path: dict of interface name: dict of property name: property value + self._properties: dict[str, dict[str, dict[str, Any]]] = {} + + # set of available adapters for quick lookup + self._adapters: set[str] = set() + + # The BlueZ APIs only maps children to parents, so we need to keep maps + # to quickly find the children of a parent D-Bus object. + + # map of device d-bus object paths to set of service d-bus object paths + self._service_map: dict[str, set[str]] = {} + # map of service d-bus object paths to set of characteristic d-bus object paths + self._characteristic_map: dict[str, set[str]] = {} + # map of characteristic d-bus object paths to set of descriptor d-bus object paths + self._descriptor_map: dict[str, set[str]] = {} + + self._advertisement_callbacks: defaultdict[str, list[AdvertisementCallback]] = ( + defaultdict(list) + ) + self._device_removed_callbacks: list[DeviceRemovedCallbackAndState] = [] + self._device_watchers: dict[str, set[DeviceWatcher]] = {} + self._condition_callbacks: dict[str, set[DeviceConditionCallback]] = {} + self._services_cache: dict[str, BleakGATTServiceCollection] = {} + + def _check_adapter(self, adapter_path: str) -> None: + """ + Raises: + BleakError: if adapter is not present in BlueZ + """ + if adapter_path not in self._properties: + raise BleakError(f"adapter '{adapter_path.split('/')[-1]}' not found") + + def _check_device(self, device_path: str) -> None: + """ + Raises: + BleakError: if device is not present in BlueZ + """ + if device_path not in self._properties: + raise BleakError(f"device '{device_path.split('/')[-1]}' not found") + + def _get_device_property( + self, device_path: str, interface: str, property_name: str + ) -> Any: + self._check_device(device_path) + device_properties = self._properties[device_path] + + try: + interface_properties = device_properties[interface] + except KeyError: + raise BleakError( + f"Interface {interface} not found for device '{device_path}'" + ) + + try: + value = interface_properties[property_name] + except KeyError: + raise BleakError( + f"Property '{property_name}' not found for '{interface}' in '{device_path}'" + ) + + return value + + async def async_init(self) -> None: + """ + Connects to the D-Bus message bus and begins monitoring signals. + + It is safe to call this method multiple times. If the bus is already + connected, no action is performed. + """ + async with self._bus_lock: + if self._bus and self._bus.connected: + return + + self._services_cache = {} + + # We need to create a new MessageBus each time as + # dbus-next will destroy the underlying file descriptors + # when the previous one is closed in its finalizer. + bus = MessageBus(bus_type=BusType.SYSTEM, auth=get_dbus_authenticator()) + + try: + # We need to call bus.disconnect() even when bus.connect() fails in + # order to release the file handles created in the constructor. + try: + await bus.connect() + except AuthError as e: + raise BleakBluetoothNotAvailableError( + e.args[0], + BleakBluetoothNotAvailableReason.DENIED_BY_SYSTEM, + ) from e + + # Add signal listeners + + bus.add_message_handler(self._parse_msg) + reply: Optional[Message] + + rules = MatchRules( + interface=defs.OBJECT_MANAGER_INTERFACE, + member="InterfacesAdded", + arg0path="/org/bluez/", + ) + reply = await add_match(bus, rules) + assert_reply(reply) + + rules = MatchRules( + interface=defs.OBJECT_MANAGER_INTERFACE, + member="InterfacesRemoved", + arg0path="/org/bluez/", + ) + reply = await add_match(bus, rules) + assert_reply(reply) + + rules = MatchRules( + interface=defs.PROPERTIES_INTERFACE, + member="PropertiesChanged", + path_namespace="/org/bluez", + ) + reply = await add_match(bus, rules) + assert_reply(reply) + + # get existing objects after adding signal handlers to avoid + # race condition + + reply = await bus.call( + Message( + destination=defs.BLUEZ_SERVICE, + path="/", + member="GetManagedObjects", + interface=defs.OBJECT_MANAGER_INTERFACE, + ) + ) + assert_reply(reply) + + # dictionaries are cleared in case AddInterfaces was received first + # or there was a bus reset and we are reconnecting + self._properties.clear() + self._service_map.clear() + self._characteristic_map.clear() + self._descriptor_map.clear() + + for path, interfaces in reply.body[0].items(): + props = unpack_variants(interfaces) + self._properties[path] = props + + if defs.ADAPTER_INTERFACE in props: + self._adapters.add(path) + + service_props = cast( + GattService1, props.get(defs.GATT_SERVICE_INTERFACE) + ) + + if service_props: + self._service_map.setdefault( + service_props["Device"], set() + ).add(path) + + char_props = cast( + GattCharacteristic1, + props.get(defs.GATT_CHARACTERISTIC_INTERFACE), + ) + + if char_props: + self._characteristic_map.setdefault( + char_props["Service"], set() + ).add(path) + + desc_props = cast( + GattDescriptor1, props.get(defs.GATT_DESCRIPTOR_INTERFACE) + ) + + if desc_props: + self._descriptor_map.setdefault( + desc_props["Characteristic"], set() + ).add(path) + + if logger.isEnabledFor(logging.DEBUG): + logger.debug("initial properties: %s", self._properties) + + except BaseException: + # if setup failed, disconnect + bus.disconnect() + raise + + if self._bus: + # Even if we are disconnected, still need to call this to + # release file handles. + self._bus.disconnect() + + # Everything is setup, so save the bus + self._bus = bus + + def get_default_adapter(self) -> str: + """ + Gets the D-Bus object path of of the first powered Bluetooth adapter. + + Returns: + Name of the first found powered adapter on the system, i.e. "/org/bluez/hciX". + + Raises: + BleakBluetoothNotAvailableError: + if there are no Bluetooth Low Energy adapters or if none of the adapters are powered + + .. versionchanged:: 2.0 + Now raises :class:`BleakBluetoothNotAvailableError` instead of :class:`BleakError`. + """ + if not any(self._adapters): + raise BleakBluetoothNotAvailableError( + "No Bluetooth adapters found.", + BleakBluetoothNotAvailableReason.NO_BLUETOOTH, + ) + + ble_central_adapters = list( + filter( + lambda a: "central" + in self._properties[a][defs.ADAPTER_INTERFACE]["Roles"], + self._adapters, + ) + ) + + if not ble_central_adapters: + raise BleakBluetoothNotAvailableError( + "No Bluetooth adapters with BLE 'central' role found.", + BleakBluetoothNotAvailableReason.NO_BLE_CENTRAL_ROLE, + ) + + for adapter_path in ble_central_adapters: + if cast( + defs.Adapter1, self._properties[adapter_path][defs.ADAPTER_INTERFACE] + )["Powered"]: + return adapter_path + + raise BleakBluetoothNotAvailableError( + "No powered Bluetooth adapters found. Turn on Bluetooth and try again.", + BleakBluetoothNotAvailableReason.POWERED_OFF, + ) + + async def active_scan( + self, + adapter_path: str, + filters: dict[str, Variant], + advertisement_callback: AdvertisementCallback, + device_removed_callback: DeviceRemovedCallback, + ) -> Callable[[], Coroutine[Any, Any, None]]: + """ + Configures the advertisement data filters and starts scanning. + + Args: + adapter_path: The D-Bus object path of the adapter to use for scanning. + filters: A dictionary of filters to pass to ``SetDiscoveryFilter``. + advertisement_callback: + A callable that will be called when new advertisement data is received. + device_removed_callback: + A callable that will be called when a device is removed from BlueZ. + + Returns: + An async function that is used to stop scanning and remove the filters. + + Raises: + BleakError: if the adapter is not present in BlueZ + """ + async with self._bus_lock: + assert self._bus + + # If the adapter doesn't exist, then the message calls below would + # fail with "method not found". This provides a more informative + # error message. + self._check_adapter(adapter_path) + + self._advertisement_callbacks[adapter_path].append(advertisement_callback) + + device_removed_callback_and_state = DeviceRemovedCallbackAndState( + device_removed_callback, adapter_path + ) + self._device_removed_callbacks.append(device_removed_callback_and_state) + + try: + # Apply the filters + reply = await self._bus.call( + Message( + destination=defs.BLUEZ_SERVICE, + path=adapter_path, + interface=defs.ADAPTER_INTERFACE, + member="SetDiscoveryFilter", + signature="a{sv}", + body=[filters], + ) + ) + assert_reply(reply) + + # Start scanning + reply = await self._bus.call( + Message( + destination=defs.BLUEZ_SERVICE, + path=adapter_path, + interface=defs.ADAPTER_INTERFACE, + member="StartDiscovery", + ) + ) + assert_reply(reply) + + async def stop() -> None: + # need to remove callbacks first, otherwise we get TxPower + # and RSSI properties removed during stop which causes + # incorrect advertisement data callbacks + self._advertisement_callbacks[adapter_path].remove( + advertisement_callback + ) + self._device_removed_callbacks.remove( + device_removed_callback_and_state + ) + + async with self._bus_lock: + assert self._bus + + reply = await self._bus.call( + Message( + destination=defs.BLUEZ_SERVICE, + path=adapter_path, + interface=defs.ADAPTER_INTERFACE, + member="StopDiscovery", + ) + ) + + try: + assert_reply(reply) + except BleakDBusError as ex: + if ex.dbus_error != defs.BLUEZ_ERROR_NOT_READY: + raise + else: + # remove the filters + reply = await self._bus.call( + Message( + destination=defs.BLUEZ_SERVICE, + path=adapter_path, + interface=defs.ADAPTER_INTERFACE, + member="SetDiscoveryFilter", + signature="a{sv}", + body=[{}], + ) + ) + assert_reply(reply) + + return stop + except BaseException: + # if starting scanning failed, don't leak the callbacks + self._advertisement_callbacks[adapter_path].remove( + advertisement_callback + ) + self._device_removed_callbacks.remove(device_removed_callback_and_state) + raise + + async def passive_scan( + self, + adapter_path: str, + filters: list[OrPatternLike], + advertisement_callback: AdvertisementCallback, + device_removed_callback: DeviceRemovedCallback, + ) -> Callable[[], Coroutine[Any, Any, None]]: + """ + Configures the advertisement data filters and starts scanning. + + Args: + adapter_path: The D-Bus object path of the adapter to use for scanning. + filters: A list of "or patterns" to pass to ``org.bluez.AdvertisementMonitor1``. + advertisement_callback: + A callable that will be called when new advertisement data is received. + device_removed_callback: + A callable that will be called when a device is removed from BlueZ. + + Returns: + An async function that is used to stop scanning and remove the filters. + + Raises: + BleakError: if the adapter is not present in BlueZ + """ + async with self._bus_lock: + assert self._bus + + # If the adapter doesn't exist, then the message calls below would + # fail with "method not found". This provides a more informative + # error message. + self._check_adapter(adapter_path) + + self._advertisement_callbacks[adapter_path].append(advertisement_callback) + + device_removed_callback_and_state = DeviceRemovedCallbackAndState( + device_removed_callback, adapter_path + ) + self._device_removed_callbacks.append(device_removed_callback_and_state) + + try: + monitor = AdvertisementMonitor(filters) + + # this should be a unique path to allow multiple python interpreters + # running bleak and multiple scanners within a single interpreter + monitor_path = f"/org/bleak/{os.getpid()}/{id(monitor)}" + + reply = await self._bus.call( + Message( + destination=defs.BLUEZ_SERVICE, + path=adapter_path, + interface=defs.ADVERTISEMENT_MONITOR_MANAGER_INTERFACE, + member="RegisterMonitor", + signature="o", + body=[monitor_path], + ) + ) + + if ( + reply.message_type == MessageType.ERROR + and reply.error_name == "org.freedesktop.DBus.Error.UnknownMethod" + ): + raise BleakError( + "passive scanning on Linux requires BlueZ >= 5.56 with --experimental enabled and Linux kernel >= 5.10" + ) + + assert_reply(reply) + + # It is important to export after registering, otherwise BlueZ + # won't use the monitor + self._bus.export(monitor_path, monitor) + + async def stop() -> None: + # need to remove callbacks first, otherwise we get TxPower + # and RSSI properties removed during stop which causes + # incorrect advertisement data callbacks + self._advertisement_callbacks[adapter_path].remove( + advertisement_callback + ) + self._device_removed_callbacks.remove( + device_removed_callback_and_state + ) + + async with self._bus_lock: + assert self._bus + + self._bus.unexport(monitor_path, monitor) + + reply = await self._bus.call( + Message( + destination=defs.BLUEZ_SERVICE, + path=adapter_path, + interface=defs.ADVERTISEMENT_MONITOR_MANAGER_INTERFACE, + member="UnregisterMonitor", + signature="o", + body=[monitor_path], + ) + ) + assert_reply(reply) + + return stop + + except BaseException: + # if starting scanning failed, don't leak the callbacks + self._advertisement_callbacks[adapter_path].remove( + advertisement_callback + ) + self._device_removed_callbacks.remove(device_removed_callback_and_state) + raise + + def add_device_watcher( + self, + device_path: str, + on_connected_changed: DeviceConnectedChangedCallback, + on_characteristic_value_changed: CharacteristicValueChangedCallback, + ) -> DeviceWatcher: + """ + Registers a device watcher to receive callbacks when device state + changes or events are received. + + Args: + device_path: + The D-Bus object path of the device. + on_connected_changed: + A callback that is called when the device's "Connected" + state changes. + on_characteristic_value_changed: + A callback that is called whenever a characteristic receives + a notification/indication. + + Returns: + A device watcher object that acts a token to unregister the watcher. + + Raises: + BleakError: if the device is not present in BlueZ + """ + self._check_device(device_path) + + watcher = DeviceWatcher( + device_path, on_connected_changed, on_characteristic_value_changed + ) + + self._device_watchers.setdefault(device_path, set()).add(watcher) + return watcher + + def remove_device_watcher(self, watcher: DeviceWatcher) -> None: + """ + Unregisters a device watcher. + + Args: + The device watcher token that was returned by + :meth:`add_device_watcher`. + """ + device_path = watcher.device_path + self._device_watchers[device_path].remove(watcher) + if not self._device_watchers[device_path]: + del self._device_watchers[device_path] + + async def get_services( + self, device_path: str, use_cached: bool, requested_services: Optional[set[str]] + ) -> BleakGATTServiceCollection: + """ + Builds a new :class:`BleakGATTServiceCollection` from the current state. + + Args: + device_path: + The D-Bus object path of the Bluetooth device. + use_cached: + When ``True`` if there is a cached :class:`BleakGATTServiceCollection`, + the method will not wait for ``"ServicesResolved"`` to become true + and instead return the cached service collection immediately. + requested_services: + When given, only return services with UUID that is in the list + of requested services. + + Returns: + A new :class:`BleakGATTServiceCollection`. + + Raises: + BleakError: if the device is not present in BlueZ + """ + self._check_device(device_path) + + if use_cached: + services = self._services_cache.get(device_path) + if services is not None: + logger.debug("Using cached services for %s", device_path) + return services + + await self._wait_for_services_discovery(device_path) + + services = BleakGATTServiceCollection() + + for service_path in self._service_map.get(device_path, set()): + service_props = cast( + GattService1, + self._properties[service_path][defs.GATT_SERVICE_INTERFACE], + ) + + service = BleakGATTService( + (service_path, service_props), + extract_service_handle_from_path(service_path), + service_props["UUID"], + ) + + if ( + requested_services is not None + and service.uuid not in requested_services + ): + continue + + services.add_service(service) + + for char_path in self._characteristic_map.get(service_path, set()): + char_props = cast( + GattCharacteristic1, + self._properties[char_path][defs.GATT_CHARACTERISTIC_INTERFACE], + ) + + char = BleakGATTCharacteristic( + (char_path, char_props), + extract_service_handle_from_path(char_path), + char_props["UUID"], + char_props["Flags"], + # Because `char_props` is a loop varialbe, we cannot + # directly bind a closure (i.e. lambda) to it; + # instead, we let `functools.partial` create a new + # function frame to close over at each iteration. + partial(get_max_write_without_response_size, char_props), + service, + ) + + services.add_characteristic(char) + + for desc_path in self._descriptor_map.get(char_path, set()): + desc_props = cast( + GattDescriptor1, + self._properties[desc_path][defs.GATT_DESCRIPTOR_INTERFACE], + ) + + desc = BleakGATTDescriptor( + (desc_path, desc_props), + int(desc_path[-4:], 16), + desc_props["UUID"], + char, + ) + + services.add_descriptor(desc) + + self._services_cache[device_path] = services + + return services + + def get_device_name(self, device_path: str) -> str: + """ + Gets the value of the "Name" property for a device. + + Args: + device_path: The D-Bus object path of the device. + + Returns: + The current property value. + + Raises: + BleakError: if the device is not present in BlueZ + """ + return self._get_device_property(device_path, defs.DEVICE_INTERFACE, "Name") + + def get_device_address(self, device_path: str) -> str: + """ + Gets the value of the "Address" property for a device. + + Args: + device_path: The D-Bus object path of the device. + + Returns: + The current property value. + + Raises: + BleakError: if the device is not present in BlueZ + """ + return self._get_device_property(device_path, defs.DEVICE_INTERFACE, "Address") + + def is_connected(self, device_path: str) -> bool: + """ + Gets the value of the "Connected" property for a device. + + Args: + device_path: The D-Bus object path of the device. + + Returns: + The current property value or ``False`` if the device does not exist in BlueZ. + """ + try: + return self._properties[device_path][defs.DEVICE_INTERFACE]["Connected"] + except KeyError: + return False + + def is_paired(self, device_path: str) -> bool: + """ + Gets the value of the "Paired" property for a device. + + Args: + device_path: The D-Bus object path of the device. + + Returns: + The current property value or ``False`` if the device does not exist in BlueZ. + """ + try: + return self._properties[device_path][defs.DEVICE_INTERFACE]["Paired"] + except KeyError: + return False + + async def _wait_for_services_discovery(self, device_path: str) -> None: + """ + Waits for the device services to be discovered. + + If a disconnect happens before the completion a BleakError exception is raised. + + Raises: + BleakError: if the device is not present in BlueZ + """ + self._check_device(device_path) + + with contextlib.ExitStack() as stack: + services_discovered_wait_task = asyncio.create_task( + self._wait_condition(device_path, "ServicesResolved", True) + ) + stack.callback(services_discovered_wait_task.cancel) + + device_disconnected_wait_task = asyncio.create_task( + self._wait_condition(device_path, "Connected", False) + ) + stack.callback(device_disconnected_wait_task.cancel) + + # in some cases, we can get "InterfaceRemoved" without the + # "Connected" property changing, so we need to race against both + # conditions + device_removed_wait_task = asyncio.create_task( + self._wait_removed(device_path) + ) + stack.callback(device_removed_wait_task.cancel) + + done, _ = await asyncio.wait( + { + services_discovered_wait_task, + device_disconnected_wait_task, + device_removed_wait_task, + }, + return_when=asyncio.FIRST_COMPLETED, + ) + + # check for exceptions + for task in done: + task.result() + + if not done.isdisjoint( + {device_disconnected_wait_task, device_removed_wait_task} + ): + raise BleakError("failed to discover services, device disconnected") + + async def _wait_removed(self, device_path: str) -> None: + """ + Waits for the device interface to be removed. + + If the device is not present in BlueZ, this returns immediately. + + Args: + device_path: The D-Bus object path of a Bluetooth device. + """ + if device_path not in self._properties: + return + + event = asyncio.Event() + + def callback(o: str) -> None: + if o == device_path: + event.set() + + device_removed_callback_and_state = DeviceRemovedCallbackAndState( + callback, self._properties[device_path][defs.DEVICE_INTERFACE]["Adapter"] + ) + + with contextlib.ExitStack() as stack: + self._device_removed_callbacks.append(device_removed_callback_and_state) + stack.callback( + self._device_removed_callbacks.remove, device_removed_callback_and_state + ) + await event.wait() + + async def _wait_condition( + self, device_path: str, property_name: str, property_value: Any + ) -> None: + """ + Waits for a condition to become true. + + Args: + device_path: The D-Bus object path of a Bluetooth device. + property_name: The name of the property to test. + property_value: A value to compare the current property value to. + + Raises: + BleakError: if the device is not present in BlueZ + """ + value = self._get_device_property( + device_path, defs.DEVICE_INTERFACE, property_name + ) + + if value == property_value: + return + + event = asyncio.Event() + + def _wait_condition_callback(new_value: Optional[Any]) -> None: + """Callback for when a property changes.""" + if new_value == property_value: + event.set() + + condition_callbacks = self._condition_callbacks + device_callbacks = condition_callbacks.setdefault(device_path, set()) + callback = DeviceConditionCallback(_wait_condition_callback, property_name) + device_callbacks.add(callback) + + try: + # can be canceled + await event.wait() + finally: + device_callbacks.remove(callback) + if not device_callbacks: + del condition_callbacks[device_path] + + def get_char_value(self, char_path: str) -> bytes: + """ + Gets the value of the "Value" property for a characteristic. + + Args: + char_path: The D-Bus object path of the characteristic. + Returns: + The current property value. + """ + try: + char_props = cast( + GattCharacteristic1, + self._properties[char_path][defs.GATT_CHARACTERISTIC_INTERFACE], + ) + return char_props["Value"] + except KeyError as ex: + raise BleakError( + f"characteristic at {char_path} not found, device may be disconnected" + ) from ex + + def get_desc_value(self, desc_path: str) -> bytes: + """ + Gets the value of the "Value" property for a descriptor. + + Args: + desc_path: The D-Bus object path of the descriptor. + Returns: + The current property value. + """ + try: + desc_props = cast( + GattDescriptor1, + self._properties[desc_path][defs.GATT_DESCRIPTOR_INTERFACE], + ) + return desc_props["Value"] + except KeyError as ex: + raise BleakError( + f"descriptor at {desc_path} not found, device may be disconnected" + ) from ex + + def _parse_msg(self, message: Message) -> None: + """ + Handles callbacks from dbus_fast. + """ + + if message.message_type != MessageType.SIGNAL: + return + + if logger.isEnabledFor(logging.DEBUG): + logger.debug( + "received D-Bus signal: %s.%s (%s): %s", + message.interface, + message.member, + message.path, + message.body, + ) + + # type hints + obj_path: str + interfaces_and_props: dict[str, dict[str, Variant]] + interfaces: list[str] + interface: str + changed: dict[str, Variant] + invalidated: list[str] + + if message.member == "InterfacesAdded": + obj_path, interfaces_and_props = message.body + + for interface, props in interfaces_and_props.items(): + unpacked_props = unpack_variants(props) + self._properties.setdefault(obj_path, {})[interface] = unpacked_props + + if interface == defs.GATT_SERVICE_INTERFACE: + service_props = cast(GattService1, unpacked_props) + self._service_map.setdefault(service_props["Device"], set()).add( + obj_path + ) + elif interface == defs.GATT_CHARACTERISTIC_INTERFACE: + char_props = cast(GattCharacteristic1, unpacked_props) + self._characteristic_map.setdefault( + char_props["Service"], set() + ).add(obj_path) + elif interface == defs.GATT_DESCRIPTOR_INTERFACE: + desc_props = cast(GattDescriptor1, unpacked_props) + self._descriptor_map.setdefault( + desc_props["Characteristic"], set() + ).add(obj_path) + + elif interface == defs.ADAPTER_INTERFACE: + self._adapters.add(obj_path) + + # If this is a device and it has advertising data properties, + # then it should mean that this device just started advertising. + # Previously, we just relied on RSSI updates to determine if + # a device was actually advertising, but we were missing "slow" + # devices that only advertise once and then go to sleep for a while. + elif interface == defs.DEVICE_INTERFACE: + self._run_advertisement_callbacks( + obj_path, cast(Device1, unpacked_props) + ) + elif message.member == "InterfacesRemoved": + obj_path, interfaces = message.body + + for interface in interfaces: + try: + del self._properties[obj_path][interface] + except KeyError: + pass + + if interface == defs.ADAPTER_INTERFACE: + try: + self._adapters.remove(obj_path) + except KeyError: + pass + elif interface == defs.DEVICE_INTERFACE: + self._services_cache.pop(obj_path, None) + try: + del self._service_map[obj_path] + except KeyError: + pass + + for callback, adapter_path in self._device_removed_callbacks: + if obj_path.startswith(adapter_path): + callback(obj_path) + elif interface == defs.GATT_SERVICE_INTERFACE: + device_path = obj_path[: obj_path.rfind("/")] + + try: + self._service_map[device_path].remove(obj_path) + except KeyError: + pass + + try: + del self._characteristic_map[obj_path] + except KeyError: + pass + elif interface == defs.GATT_CHARACTERISTIC_INTERFACE: + try: + del self._descriptor_map[obj_path] + except KeyError: + pass + + # Remove empty properties when all interfaces have been removed. + # This avoids wasting memory for people who have noisy devices + # with private addresses that change frequently. + if obj_path in self._properties and not self._properties[obj_path]: + del self._properties[obj_path] + elif message.member == "PropertiesChanged": + interface, changed, invalidated = message.body + message_path = message.path + assert message_path is not None + + try: + self_interface = self._properties[message_path][interface] + except KeyError: + # This can happen during initialization. The "PropertiesChanged" + # handler is attached before "GetManagedObjects" is called + # and so self._properties may not yet be populated. + # This is not a problem. We just discard the property value + # since "GetManagedObjects" will return a newer value. + pass + else: + # update self._properties first + + self_interface.update(unpack_variants(changed)) + + for name in invalidated: + try: + del self_interface[name] + except KeyError: + # sometimes there BlueZ tries to remove properties + # that were never added + pass + + # then call any callbacks so they will be called with the + # updated state + + if interface == defs.DEVICE_INTERFACE: + # handle advertisement watchers + device_path = message_path + + self._run_advertisement_callbacks( + device_path, cast(Device1, self_interface) + ) + + # handle device condition watchers + callbacks = self._condition_callbacks.get(device_path) + if callbacks: + for item in callbacks: + name = item.property_name + if name in changed: + item.callback(self_interface.get(name)) + + # handle device connection change watchers + if "Connected" in changed: + new_connected = self_interface["Connected"] + watchers = self._device_watchers.get(device_path) + if watchers: + # callbacks may remove the watcher, hence the copy + for watcher in watchers.copy(): + watcher.on_connected_changed(new_connected) + + elif interface == defs.GATT_CHARACTERISTIC_INTERFACE: + # handle characteristic value change watchers + if "Value" in changed: + new_value = self_interface["Value"] + device_path = device_path_from_characteristic_path(message_path) + watchers = self._device_watchers.get(device_path) + if watchers: + for watcher in watchers: + watcher.on_characteristic_value_changed( + message_path, new_value + ) + + def _run_advertisement_callbacks(self, device_path: str, device: Device1) -> None: + """ + Runs any registered advertisement callbacks. + + Args: + device_path: The D-Bus object path of the remote device. + device: The current D-Bus properties of the device. + """ + adapter_path = device["Adapter"] + for callback in self._advertisement_callbacks[adapter_path]: + callback(device_path, device.copy()) + + +# Bleak is designed to run in a single event loop for the duration of an +# application. Starting a new manager is a very expensive operation because it +# has to read all of the properties of all known Bluetooth devices over the bus. +# So even though this technically supports more than one run loop, we don't +# recommend doing that. This dict maps each event loop to its associated +# BlueZManager instance so that multiple callers in the same loop share one +# manager. Entries for closed loops are removed lazily when a new manager is +# requested. +_global_instances: dict[asyncio.AbstractEventLoop, BlueZManager] = {} + + +async def get_global_bluez_manager() -> BlueZManager: + """ + Gets an existing initialized global BlueZ manager instance associated with the current event loop, + or initializes a new instance. + """ + + loop = asyncio.get_running_loop() + try: + instance = _global_instances[loop] + except KeyError: + # Clean up any entries whose event loop has been closed. + closed_loops = [ + event_loop for event_loop in _global_instances if event_loop.is_closed() + ] + for closed_loop in closed_loops: + manager = _global_instances.pop(closed_loop) + if manager._bus is not None: # pyright: ignore[reportPrivateUsage] + manager._bus._finalize(None) # pyright: ignore[reportPrivateUsage] + + instance = _global_instances[loop] = BlueZManager() + + await instance.async_init() + + return instance diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/scanner.py b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/scanner.py new file mode 100644 index 0000000..914f4ca --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/scanner.py @@ -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 + `_ + + 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 + `_ + + See variant types here: + + 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 diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/signals.py b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/signals.py new file mode 100644 index 0000000..c7d28f2 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/signals.py @@ -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 diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/utils.py b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/utils.py new file mode 100644 index 0000000..d6e603b --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/utils.py @@ -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 diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/version.py b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/version.py new file mode 100644 index 0000000..8f9ca5f --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/bluezdbus/version.py @@ -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 diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/characteristic.py b/tools/python-embed/Lib/site-packages/bleak/backends/characteristic.py new file mode 100644 index 0000000..40974e2 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/characteristic.py @@ -0,0 +1,157 @@ +# Created on 2019-03-19 by hbldh +""" +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 diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/client.py b/tools/python-embed/Lib/site-packages/bleak/backends/client.py new file mode 100644 index 0000000..d430472 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/client.py @@ -0,0 +1,258 @@ +# Created on 2018-04-23 by hbldh +""" +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}") diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/CentralManagerDelegate.py b/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/CentralManagerDelegate.py new file mode 100644 index 0000000..369335d --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/CentralManagerDelegate.py @@ -0,0 +1,402 @@ +# Created on June, 25 2019 by kevincar +""" +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() diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/PeripheralDelegate.py b/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/PeripheralDelegate.py new file mode 100644 index 0000000..1c5b372 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/PeripheralDelegate.py @@ -0,0 +1,727 @@ +""" + +PeripheralDelegate + +Created by kevincar + +""" + +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) diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/__init__.py b/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/__init__.py new file mode 100644 index 0000000..a66daf3 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/__init__.py @@ -0,0 +1,14 @@ +# Created on 2017-11-19 by hbldh +""" +__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 diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/__pycache__/CentralManagerDelegate.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/__pycache__/CentralManagerDelegate.cpython-312.pyc new file mode 100644 index 0000000..525ba93 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/__pycache__/CentralManagerDelegate.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/__pycache__/PeripheralDelegate.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/__pycache__/PeripheralDelegate.cpython-312.pyc new file mode 100644 index 0000000..2c79e94 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/__pycache__/PeripheralDelegate.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/__pycache__/__init__.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..7ae9ce2 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/__pycache__/__init__.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/__pycache__/client.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/__pycache__/client.cpython-312.pyc new file mode 100644 index 0000000..277b6a4 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/__pycache__/client.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/__pycache__/scanner.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/__pycache__/scanner.cpython-312.pyc new file mode 100644 index 0000000..ebdad23 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/__pycache__/scanner.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/__pycache__/utils.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000..df7ed9f Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/__pycache__/utils.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/client.py b/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/client.py new file mode 100644 index 0000000..e3ebd0c --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/client.py @@ -0,0 +1,402 @@ +# Created on 2019-06-26 by kevincar +""" +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 `_ + - `Stack Overflow post #1 `_ + - `Stack Overflow post #2 `_ + """ + 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()) diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/scanner.py b/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/scanner.py new file mode 100644 index 0000000..820cd11 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/scanner.py @@ -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) diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/utils.py b/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/utils.py new file mode 100644 index 0000000..26ca155 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/corebluetooth/utils.py @@ -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}" + ) diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/descriptor.py b/tools/python-embed/Lib/site-packages/bleak/backends/descriptor.py new file mode 100644 index 0000000..03b97e6 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/descriptor.py @@ -0,0 +1,154 @@ +# Created on 2019-03-19 by hbldh +""" +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] diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/device.py b/tools/python-embed/Lib/site-packages/bleak/backends/device.py new file mode 100644 index 0000000..53ce667 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/device.py @@ -0,0 +1,39 @@ +# Created on 2018-04-23 by hbldh +""" +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})" diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/p4android/__init__.py b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/p4android/__pycache__/__init__.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..d448421 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/__pycache__/__init__.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/p4android/__pycache__/client.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/__pycache__/client.cpython-312.pyc new file mode 100644 index 0000000..d2733b0 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/__pycache__/client.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/p4android/__pycache__/defs.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/__pycache__/defs.cpython-312.pyc new file mode 100644 index 0000000..81fb03a Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/__pycache__/defs.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/p4android/__pycache__/scanner.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/__pycache__/scanner.cpython-312.pyc new file mode 100644 index 0000000..8d18bac Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/__pycache__/scanner.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/p4android/__pycache__/utils.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000..3ea2cf4 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/__pycache__/utils.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/p4android/client.py b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/client.py new file mode 100644 index 0000000..8924b10 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/client.py @@ -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)) diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/p4android/defs.py b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/defs.py new file mode 100644 index 0000000..c5a4082 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/defs.py @@ -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) diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/p4android/java/com/github/hbldh/bleak/PythonBluetoothGattCallback.java b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/java/com/github/hbldh/bleak/PythonBluetoothGattCallback.java new file mode 100644 index 0000000..76434ea --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/java/com/github/hbldh/bleak/PythonBluetoothGattCallback.java @@ -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); + } +} diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/p4android/java/com/github/hbldh/bleak/PythonScanCallback.java b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/java/com/github/hbldh/bleak/PythonScanCallback.java new file mode 100644 index 0000000..8b873b2 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/java/com/github/hbldh/bleak/PythonScanCallback.java @@ -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 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); + } +} diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/p4android/recipes/bleak/__init__.py b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/recipes/bleak/__init__.py new file mode 100644 index 0000000..5c10397 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/recipes/bleak/__init__.py @@ -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() diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/p4android/recipes/bleak/__pycache__/__init__.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/recipes/bleak/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..6a54eee Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/recipes/bleak/__pycache__/__init__.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/p4android/recipes/bleak/__pycache__/fix_setup.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/recipes/bleak/__pycache__/fix_setup.cpython-312.pyc new file mode 100644 index 0000000..9494b13 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/recipes/bleak/__pycache__/fix_setup.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/p4android/recipes/bleak/fix_setup.py b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/recipes/bleak/fix_setup.py new file mode 100644 index 0000000..b43d2c1 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/recipes/bleak/fix_setup.py @@ -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")), +) diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/p4android/scanner.py b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/scanner.py new file mode 100644 index 0000000..e414414 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/scanner.py @@ -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) diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/p4android/utils.py b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/utils.py new file mode 100644 index 0000000..0850acd --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/p4android/utils.py @@ -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 diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/scanner.py b/tools/python-embed/Lib/site-packages/bleak/backends/scanner.py new file mode 100644 index 0000000..bc5c081 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/scanner.py @@ -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}") diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/service.py b/tools/python-embed/Lib/site-packages/bleak/backends/service.py new file mode 100644 index 0000000..ba230ce --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/service.py @@ -0,0 +1,218 @@ +# Created on 2019-03-19 by hbldh +""" +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) diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/winrt/__init__.py b/tools/python-embed/Lib/site-packages/bleak/backends/winrt/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/winrt/__pycache__/__init__.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/winrt/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..b283225 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/winrt/__pycache__/__init__.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/winrt/__pycache__/client.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/winrt/__pycache__/client.cpython-312.pyc new file mode 100644 index 0000000..86bbca1 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/winrt/__pycache__/client.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/winrt/__pycache__/scanner.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/winrt/__pycache__/scanner.cpython-312.pyc new file mode 100644 index 0000000..1eb97d9 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/winrt/__pycache__/scanner.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/winrt/__pycache__/util.cpython-312.pyc b/tools/python-embed/Lib/site-packages/bleak/backends/winrt/__pycache__/util.cpython-312.pyc new file mode 100644 index 0000000..a303772 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/bleak/backends/winrt/__pycache__/util.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/winrt/client.py b/tools/python-embed/Lib/site-packages/bleak/backends/winrt/client.py new file mode 100644 index 0000000..9b59096 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/winrt/client.py @@ -0,0 +1,1198 @@ +# Created on 2020-08-19 by hbldh +""" +BLE Client for Windows 10 systems, implemented with WinRT. +""" + +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + if sys.platform != "win32": + assert False, "This backend is only available on Windows" + +import asyncio +import logging +import uuid +from collections.abc import Callable +from contextvars import Context +from ctypes import WinError # type: ignore[attr-defined] +from typing import Any, Generic, Optional, Protocol, Sequence, TypeVar, Union, cast +from warnings import warn + +from winrt.system import Object +from winrt.windows.devices.bluetooth import ( + BluetoothAddressType, + BluetoothCacheMode, + BluetoothError, + BluetoothLEDevice, +) +from winrt.windows.devices.bluetooth.genericattributeprofile import ( + GattCharacteristic, + GattCharacteristicProperties, + GattClientCharacteristicConfigurationDescriptorValue, + GattCommunicationStatus, + GattDescriptor, + GattDeviceService, + GattSession, + GattSessionStatus, + GattSessionStatusChangedEventArgs, + GattValueChangedEventArgs, + GattWriteOption, +) +from winrt.windows.devices.enumeration import ( + DeviceInformation, + DeviceInformationCustomPairing, + DevicePairingKinds, + DevicePairingProtectionLevel, + DevicePairingRequestedEventArgs, + DevicePairingResultStatus, + DeviceUnpairingResultStatus, +) +from winrt.windows.foundation import ( + AsyncStatus, + EventRegistrationToken, + IAsyncOperation, +) +from winrt.windows.storage.streams import Buffer + +from bleak import BleakScanner +from bleak._compat import Self, assert_never, override +from bleak._compat import timeout as async_timeout +from bleak.args import SizedBuffer +from bleak.args.winrt import WinRTClientArgs as _WinRTClientArgs +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.service import BleakGATTService, BleakGATTServiceCollection +from bleak.backends.winrt.scanner import BleakScannerWinRT, RawAdvData +from bleak.exc import BleakDeviceNotFoundError, BleakError, BleakGATTProtocolError + +logger = logging.getLogger(__name__) + + +def __getattr__(name: str): + if name == "WinRTClientArgs": + warn( + "importing WinRTClientArgs from bleak.backends.winrt.client is deprecated, use bleak.args.winrt instead", + DeprecationWarning, + stacklevel=2, + ) + return _WinRTClientArgs + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +class _Result(Protocol): + @property + def status(self) -> GattCommunicationStatus: ... + @property + def protocol_error(self) -> Optional[int]: ... + + +def _address_to_int(address: str) -> int: + """Converts the Bluetooth device address string to its representing integer + + Args: + address (str): Bluetooth device address to convert + + Returns: + int: integer representation of the given Bluetooth device address + """ + _address_separators = [":", "-"] + for char in _address_separators: + address = address.replace(char, "") + + return int(address, base=16) + + +def _ensure_success(result: _Result, attr: Optional[str], fail_msg: str) -> Any: + """ + Ensures that *status* is ``GattCommunicationStatus.SUCCESS``, otherwise + raises ``BleakError``. + + Args: + result: The result returned by a WinRT API method. + attr: The name of the attribute containing the result. + fail_msg: A message to include in the exception. + """ + status = result.status if hasattr(result, "status") else result + + if status == GattCommunicationStatus.SUCCESS: + return None if attr is None else getattr(result, attr) + + if status == GattCommunicationStatus.PROTOCOL_ERROR: + assert result.protocol_error is not None + raise BleakGATTProtocolError(result.protocol_error) + + if status == GattCommunicationStatus.ACCESS_DENIED: + raise BleakError(f"{fail_msg}: Access Denied") + + if status == GattCommunicationStatus.UNREACHABLE: + raise BleakError(f"{fail_msg}: Unreachable") + + raise BleakError(f"{fail_msg}: Unexpected status code 0x{status:02X}") + + +class BleakClientWinRT(BaseBleakClient): + """Native Windows Bleak Client. + + Args: + address_or_ble_device (str or BLEDevice): 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. + winrt (dict): A dictionary of Windows-specific configuration values. + """ + + def __init__( + self, + address_or_ble_device: Union[BLEDevice, str], + services: Optional[set[str]] = None, + *, + winrt: _WinRTClientArgs, + **kwargs: Any, + ): + super().__init__(address_or_ble_device, **kwargs) + + self._device_info: int | None + + # Backend specific. WinRT objects. + if isinstance(address_or_ble_device, BLEDevice): + self._device_info = _address_to_int(address_or_ble_device.address) + else: + self._device_info = None + + self._requested_services = ( + [uuid.UUID(s) for s in services] if services else None + ) + self._requester: Optional[BluetoothLEDevice] = None + self._services_changed_events: list[asyncio.Event] = [] + self._session_active_events: list[asyncio.Event] = [] + self._session: Optional[GattSession] = None + self._notification_callbacks: dict[int, EventRegistrationToken] = {} + + # os-specific options + self._use_cached_services = winrt.get("use_cached_services") + self._address_type = winrt.get("address_type") + self._retry_on_services_changed = False + + self._services_changed_token: Optional[EventRegistrationToken] = None + self._session_status_changed_token: Optional[EventRegistrationToken] = None + self._max_pdu_size_changed_token: Optional[EventRegistrationToken] = None + + def __str__(self) -> str: + return f"{type(self).__name__} ({self.address})" + + # Connectivity methods + + async def _create_requester(self, bluetooth_address: int) -> BluetoothLEDevice: + if self._address_type is not None: + requester = await BluetoothLEDevice.from_bluetooth_address_with_bluetooth_address_type_async( + bluetooth_address, + ( + BluetoothAddressType.PUBLIC + if self._address_type == "public" + else BluetoothAddressType.RANDOM + ), + ) + else: + requester = await BluetoothLEDevice.from_bluetooth_address_async( + bluetooth_address + ) + + # https://github.com/microsoft/Windows-universal-samples/issues/1089#issuecomment-487586755 + if requester is None: + raise BleakDeviceNotFoundError( + self.address, f"Device with address {self.address} was not found." + ) + return requester + + @override + async def connect(self, pair: bool, **kwargs: Any) -> None: + """Connect to the specified GATT server. + + Keyword Args: + timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. + """ + # Try to find the desired device. + timeout = kwargs.get("timeout", self._timeout) + if self._device_info is None: + device = await BleakScanner.find_device_by_address( + self.address, timeout=timeout, backend=BleakScannerWinRT + ) + + if device is None: + raise BleakDeviceNotFoundError( + self.address, f"Device with address {self.address} was not found." + ) + + data: RawAdvData = device.details + args = data.adv or data.scan + assert args + self._device_info = args.bluetooth_address + + logger.debug("Connecting to BLE device @ %s", self.address) + + loop = asyncio.get_running_loop() + + self._requester = await self._create_requester(self._device_info) + + if pair: + await self.pair(**kwargs) + + def handle_services_changed() -> None: + if not self._services_changed_events: + logger.warning("%s: unhandled services changed event", self.address) + else: + for event in self._services_changed_events: + event.set() + + def services_changed_handler(sender: BluetoothLEDevice, args: Object) -> None: + logger.debug("%s: services changed", self.address) + loop.call_soon_threadsafe(handle_services_changed) + + self._services_changed_token = self._requester.add_gatt_services_changed( + services_changed_handler + ) + + # Called on disconnect event or on failure to connect. + def handle_disconnect() -> None: + if self._requester: + if self._services_changed_token: + self._requester.remove_gatt_services_changed( + self._services_changed_token + ) + self._services_changed_token = None + + logger.debug("closing requester") + self._requester.close() + self._requester = None + + if self._session: + if self._session_status_changed_token: + self._session.remove_session_status_changed( + self._session_status_changed_token + ) + self._session_status_changed_token = None + + if self._max_pdu_size_changed_token: + self._session.remove_max_pdu_size_changed( + self._max_pdu_size_changed_token + ) + self._max_pdu_size_changed_token = None + + logger.debug("closing session") + self._session.close() + self._session = None + + is_connect_complete = False + + def handle_session_status_changed( + args: GattSessionStatusChangedEventArgs, + ) -> None: + if args.error != BluetoothError.SUCCESS: + logger.error("Unhandled GATT error %r", args.error) + + if args.status == GattSessionStatus.ACTIVE: + for e in self._session_active_events: + e.set() + + # Don't run this if we have not exited from the connect method yet. + # Cleanup is handled by the connect method in that case. + elif args.status == GattSessionStatus.CLOSED and is_connect_complete: + if self._disconnected_callback: + self._disconnected_callback() + + handle_disconnect() + + # this is the WinRT event handler will be called on another thread + def session_status_changed_event_handler( + sender: GattSession, args: GattSessionStatusChangedEventArgs + ): + logger.debug( + "session_status_changed_event_handler: id: %s, error: %r, status: %r", + sender.device_id.id, + args.error, + args.status, + ) + loop.call_soon_threadsafe(handle_session_status_changed, args) + + def max_pdu_size_changed_handler(sender: GattSession, args: Object) -> None: + try: + max_pdu_size = sender.max_pdu_size + except OSError: + # There is a race condition where this event was already + # queued when the GattSession object was closed. In that + # case, we get a Windows error which we can just ignore. + return + + logger.debug("max_pdu_size_changed_handler: %d", max_pdu_size) + + # Start a GATT Session to connect + event = asyncio.Event() + self._session_active_events.append(event) + try: + self._session = await GattSession.from_device_id_async( + self._requester.bluetooth_device_id + ) + + if not self._session.can_maintain_connection: + raise BleakError("device does not support GATT sessions") + + self._session_status_changed_token = ( + self._session.add_session_status_changed( + session_status_changed_event_handler + ) + ) + + self._max_pdu_size_changed_token = self._session.add_max_pdu_size_changed( + max_pdu_size_changed_handler + ) + + services_changed_event = asyncio.Event() + self._services_changed_events.append(services_changed_event) + + # If the session is already active, we need to set the event since + # the session_status_changed event won't fire. This happens, e.g., + # when pairing before connecting which causes the device to already + # be connected. + if self._session.session_status == GattSessionStatus.ACTIVE: + event.set() + + try: + # Windows does not support explicitly connecting to a device. + # Instead it has the concept of a GATT session that is owned + # by the calling program. + self._session.maintain_connection = True + # This keeps the device connected until we set maintain_connection = False. + + cache_mode = None + + if self._use_cached_services is not None: + cache_mode = ( + BluetoothCacheMode.CACHED + if self._use_cached_services + else BluetoothCacheMode.UNCACHED + ) + + # if we receive a services changed event before get_gatt_services_async() + # finishes, we need to call it again with BluetoothCacheMode.CACHED + # to ensure we have the correct services as described in + # https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothledevice.gattserviceschanged + service_cache_mode = cache_mode + + async with async_timeout(timeout): + if self._retry_on_services_changed: + while True: + services_changed_event.clear() + services_changed_event_task = asyncio.create_task( + services_changed_event.wait() + ) + + get_services_task = asyncio.create_task( + self._get_services( + service_cache_mode=service_cache_mode, + cache_mode=cache_mode, + ) + ) + + _, pending = await asyncio.wait( + [services_changed_event_task, get_services_task], + return_when=asyncio.FIRST_COMPLETED, + ) + + for p in pending: + p.cancel() + + if not services_changed_event.is_set(): + # services did not change while getting services, + # so this is the final result + self.services = get_services_task.result() + break + + logger.debug( + "%s: restarting get services due to services changed event", + self.address, + ) + service_cache_mode = BluetoothCacheMode.CACHED + + # ensure the task ran to completion to avoid OSError + # on next call to get_services() + try: + await get_services_task + except OSError: + pass + except asyncio.CancelledError: + pass + else: + self.services = await self._get_services( + service_cache_mode=service_cache_mode, + cache_mode=cache_mode, + ) + + # a connection may not be made until we request info from the + # device, so we have to get services before the GATT session + # is set to active + await event.wait() + + is_connect_complete = True + finally: + self._services_changed_events.remove(services_changed_event) + + except BaseException: + handle_disconnect() + raise + finally: + self._session_active_events.remove(event) + + @override + async def disconnect(self) -> None: + """Disconnect from the specified GATT server.""" + logger.debug("Disconnecting from BLE device...") + + if self.services 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 + + # Remove notifications. + for handle, event_handler_token in list(self._notification_callbacks.items()): + char = self.services.get_characteristic(handle) + assert char + gatt_char = cast(GattCharacteristic, char.obj) + gatt_char.remove_value_changed(event_handler_token) + self._notification_callbacks.clear() + + # Since we can't wait for the session close event (it may never come) + # we need to synthesize the disconnect event + if self._session and self._session_status_changed_token: + self._session.remove_session_status_changed( + self._session_status_changed_token + ) + self._session_status_changed_token = None + + if self._disconnected_callback: + self._disconnected_callback() + + # Dispose all service components that we have requested and created. + if self.services: + # HACK: sometimes GattDeviceService.Close() hangs forever, so we + # add a delay to give the Windows Bluetooth stack some time to + # "settle" before closing the services + await asyncio.sleep(0.1) + + for service in self.services: + service.obj.close() + self.services = None + + if self._session: + self._session.close() + self._session = None + + if self._requester: + self._requester.close() + self._requester = None + + @property + @override + def is_connected(self) -> bool: + """Check connection status between this client and the server. + + Returns: + Boolean representing connection status. + + """ + return ( + False + if self._session is None + else self._session.session_status == GattSessionStatus.ACTIVE + ) + + @property + @override + def name(self) -> str: + """See :meth:`bleak.BleakClient.name`.""" + if self._requester is None: + raise BleakError("Not connected") + return self._requester.name + + @property + @override + def mtu_size(self) -> int: + """Get ATT MTU size for active connection""" + assert self._session is not None + return self._session.max_pdu_size + + @override + async def pair( + self, + **kwargs: Any, + ) -> None: + """Attempts to pair with the device. + + Keyword Args: + protection_level (int): A ``DevicePairingProtectionLevel`` enum value: + + 1. None - Pair the device using no levels of protection. + 2. Encryption - Pair the device using encryption. + 3. EncryptionAndAuthentication - Pair the device using + encryption and authentication. + + .. versionchanged:: 1.0 + Issues :class:`DeprecationWarning` if used. The default + behavior has changed and this argument should no longer + be needed. + """ + assert self._requester + + # New local device information object created since the object from the requester isn't updated + device_information = await DeviceInformation.create_from_id_async( + self._requester.device_information.id + ) + + if device_information.pairing.is_paired: + logger.debug("Device is already paired. Skipping pairing.") + return + + if not device_information.pairing.can_pair: + raise BleakError("Device does not support pairing") + + protection_level = kwargs.get("protection_level") + + # Currently only supporting Just Works solutions... + ceremony = DevicePairingKinds.CONFIRM_ONLY + custom_pairing = device_information.pairing.custom + + def handler( + sender: DeviceInformationCustomPairing, + args: DevicePairingRequestedEventArgs, + ): + args.accept() + + pairing_requested_token = custom_pairing.add_pairing_requested(handler) + + try: + if protection_level is not None: + warn( + "protection_level is deprecated and will be removed in a future version. The default protection level has changed, so it should be safe to omit this argument.", + DeprecationWarning, + 2, + ) + pairing_result = await custom_pairing.pair_with_protection_level_async( + ceremony, protection_level + ) + else: + for level in ( + DevicePairingProtectionLevel.ENCRYPTION_AND_AUTHENTICATION, + DevicePairingProtectionLevel.ENCRYPTION, + ): + pairing_result = ( + await custom_pairing.pair_with_protection_level_async( + ceremony, level + ) + ) + if ( + pairing_result.status + != DevicePairingResultStatus.PROTECTION_LEVEL_COULD_NOT_BE_MET + ): + break + + logger.debug("Protection level %r not met. Retrying.", level) + else: + pairing_result = await custom_pairing.pair_async(ceremony) + + except Exception as e: + raise BleakError("Failure trying to pair with device!") from e + finally: + custom_pairing.remove_pairing_requested(pairing_requested_token) + + if pairing_result.status not in ( + DevicePairingResultStatus.PAIRED, + DevicePairingResultStatus.ALREADY_PAIRED, + ): + raise BleakError( + f"Could not pair with device: {pairing_result.status.name}" + ) + + if logger.isEnabledFor(logging.DEBUG): + # pairing_result.protection_level_used doesn't seem to return + # accurate information if we don't update the DeviceInformation + # first. + device_information = await DeviceInformation.create_from_id_async( + self._requester.device_information.id + ) + + logger.debug( + "Paired to device with protection level %s.", + pairing_result.protection_level_used.name, + ) + + @override + async def unpair(self) -> None: + """Attempts to unpair from the device. + + N.B. unpairing also leads to disconnection in the Windows backend. + """ + device = await self._create_requester( + self._device_info + if self._device_info is not None + else _address_to_int(self.address) + ) + + try: + unpairing_result = await device.device_information.pairing.unpair_async() + if unpairing_result.status not in ( + DeviceUnpairingResultStatus.UNPAIRED, + DeviceUnpairingResultStatus.ALREADY_UNPAIRED, + ): + raise BleakError( + f"Could not unpair with device: {unpairing_result.status}" + ) + logger.info("Unpaired with device.") + finally: + device.close() + + # GATT services methods + + async def _get_services( + self, + *, + service_cache_mode: Optional[BluetoothCacheMode] = None, + cache_mode: Optional[BluetoothCacheMode] = None, + **kwargs: Any, + ) -> BleakGATTServiceCollection: + """Get all services registered for this GATT server. + + Returns: + A :py:class:`bleak.backends.service.BleakGATTServiceCollection` with this device's services tree. + + """ + + # Return the Service Collection. + if self.services is not None: + return self.services + + logger.debug( + "getting services (service_cache_mode=%r, cache_mode=%r)...", + service_cache_mode, + cache_mode, + ) + + new_services = BleakGATTServiceCollection() + services: Sequence[GattDeviceService] + + retries = 10 + assert self._requester + + if self._requested_services is None: + while True: + if service_cache_mode is not None: + result = await FutureLike( + self._requester.get_gatt_services_with_cache_mode_async( + service_cache_mode + ) + ) + else: + result = await FutureLike(self._requester.get_gatt_services_async()) + + # Windows Bluetooth can be quite flakey and often we get the + # UNREACHABLE error here. Retrying a few times seems to eventually + # work around this. + if result.status == GattCommunicationStatus.UNREACHABLE: + if retries > 0: + retries -= 1 + logger.debug( + "%s: device unreachable when getting services, retrying in 1 second...", + self.address, + ) + await asyncio.sleep(1) + continue + + services = _ensure_success( + result, + "services", + "Could not get GATT services", + ) + + break + else: + services = [] + # REVISIT: should properly dispose services on cancel or protect from cancellation + + for s in self._requested_services: + while True: + if service_cache_mode is not None: + result = await FutureLike( + self._requester.get_gatt_services_for_uuid_with_cache_mode_async( + s, service_cache_mode + ) + ) + else: + result = await FutureLike( + self._requester.get_gatt_services_for_uuid_async(s) + ) + + if result.status == GattCommunicationStatus.UNREACHABLE: + if retries > 0: + retries -= 1 + logger.debug( + "%s: device unreachable when getting service %s, retrying in 1 second...", + self.address, + s, + ) + await asyncio.sleep(1) + continue + + services.extend( + _ensure_success( + result, + "services", + "Could not get GATT services", + ) + ) + + break + + try: + for service in services: + if cache_mode is not None: + result = await FutureLike( + service.get_characteristics_with_cache_mode_async(cache_mode) + ) + else: + result = await FutureLike(service.get_characteristics_async()) + + if result.status == GattCommunicationStatus.ACCESS_DENIED: + # Windows does not allow access to services "owned" by the + # OS. This includes services like HID and Bond Manager. + logger.debug( + "skipping service %s due to access denied", service.uuid + ) + continue + + characteristics: Sequence[GattCharacteristic] = _ensure_success( + result, + "characteristics", + f"Could not get GATT characteristics for service {service.uuid} ({service.attribute_handle})", + ) + + serv = BleakGATTService( + service, service.attribute_handle, str(service.uuid) + ) + new_services.add_service(serv) + + for characteristic in characteristics: + if cache_mode is not None: + result = await FutureLike( + characteristic.get_descriptors_with_cache_mode_async( + cache_mode + ) + ) + else: + result = await FutureLike( + characteristic.get_descriptors_async() + ) + + descriptors: Sequence[GattDescriptor] = _ensure_success( + result, + "descriptors", + f"Could not get GATT descriptors for characteristic {characteristic.uuid} ({characteristic.attribute_handle})", + ) + + char = BleakGATTCharacteristic( + characteristic, + characteristic.attribute_handle, + str(characteristic.uuid), + list( + gatt_char_props_to_strs( + characteristic.characteristic_properties + ) + ), + lambda: self.mtu_size - 3, + serv, + ) + + new_services.add_characteristic(char) + + for descriptor in descriptors: + desc = BleakGATTDescriptor( + descriptor, + descriptor.attribute_handle, + str(descriptor.uuid), + char, + ) + new_services.add_descriptor(desc) + + return new_services + except BaseException: + # Don't leak services. WinRT is quite particular about services + # being closed. + logger.debug("disposing service objects") + + # HACK: sometimes GattDeviceService.Close() hangs forever, so we + # add a delay to give the Windows Bluetooth stack some time to + # "settle" before closing the services + await asyncio.sleep(0.1) + + for service in services: + service.close() + raise + + # I/O 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. + + Keyword Args: + 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. + + """ + if not self.is_connected: + raise BleakError("Not connected") + + assert self.services + + gatt_char = cast(GattCharacteristic, characteristic.obj) + + value = bytearray( + _ensure_success( + await gatt_char.read_value_with_cache_mode_async( + BluetoothCacheMode.CACHED + if use_cached + else BluetoothCacheMode.UNCACHED + ), + "value", + f"Could not read characteristic handle {characteristic.handle}", + ) + ) + + logger.debug("Read Characteristic %04X : %s", 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 (bool): `False` forces Windows to read the value from the + device again and not use its own cached value. Defaults to `False`. + + Returns: + The read data. + + """ + if not self.is_connected: + raise BleakError("Not connected") + + assert self.services + + gatt_desc = cast(GattDescriptor, descriptor.obj) + + value = bytearray( + _ensure_success( + await gatt_desc.read_value_with_cache_mode_async( + BluetoothCacheMode.CACHED + if use_cached + else BluetoothCacheMode.UNCACHED + ), + "value", + f"Could not read Descriptor value for {descriptor.handle:04X}", + ) + ) + + logger.debug("Read Descriptor %04X : %s", descriptor.handle, value) + + return value + + @override + async def write_gatt_char( + self, characteristic: BleakGATTCharacteristic, data: SizedBuffer, response: bool + ) -> None: + if not self.is_connected: + raise BleakError("Not connected") + + buf = Buffer(len(data)) + buf.length = buf.capacity + + with memoryview(buf) as mv: + mv[:] = data + + gatt_char = cast(GattCharacteristic, characteristic.obj) + + _ensure_success( + await gatt_char.write_value_with_result_and_option_async( + buf, + ( + GattWriteOption.WRITE_WITH_RESPONSE + if response + else GattWriteOption.WRITE_WITHOUT_RESPONSE + ), + ), + None, + f"Could not write value {data} to characteristic {characteristic.handle:04X}", + ) + + @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). + + """ + if not self.is_connected: + raise BleakError("Not connected") + + assert self.services + + buf = Buffer(len(data)) + buf.length = buf.capacity + + with memoryview(buf) as mv: + mv[:] = data + + gatt_desc = cast(GattDescriptor, descriptor.obj) + + _ensure_success( + await gatt_desc.write_value_with_result_async(buf), + None, + f"Could not write value {data!r} to descriptor {descriptor.handle:04X}", + ) + + logger.debug("Write Descriptor %04X : %s", descriptor.handle, data) + + @override + async def start_notify( + self, + characteristic: BleakGATTCharacteristic, + callback: NotifyCallback, + **kwargs: Any, + ) -> None: + """ + Activate notifications/indications on a characteristic. + + Keyword Args: + force_indicate (bool): If this is set to True, then Bleak will set up a indication request instead of a + notification request, given that the characteristic supports notifications as well as indications. + """ + winrt_char = cast(GattCharacteristic, characteristic.obj) + + # If we want to force indicate even when notify is available, also check if the device + # actually supports indicate as well. + if not kwargs.get("force_indicate", False) and ( + winrt_char.characteristic_properties & GattCharacteristicProperties.NOTIFY + ): + cccd = GattClientCharacteristicConfigurationDescriptorValue.NOTIFY + elif ( + winrt_char.characteristic_properties & GattCharacteristicProperties.INDICATE + ): + cccd = GattClientCharacteristicConfigurationDescriptorValue.INDICATE + else: + raise BleakError( + "characteristic does not support notifications or indications" + ) + + loop = asyncio.get_running_loop() + + def handle_value_changed( + sender: GattCharacteristic, args: GattValueChangedEventArgs + ) -> None: + value = bytearray(args.characteristic_value) + loop.call_soon_threadsafe(callback, value) + + event_handler_token = winrt_char.add_value_changed(handle_value_changed) + self._notification_callbacks[characteristic.handle] = event_handler_token + + try: + _ensure_success( + await winrt_char.write_client_characteristic_configuration_descriptor_with_result_async( + cccd + ), + None, + f"Could not start notify on {characteristic.handle:04X}", + ) + except BaseException: + # This usually happens when a device reports that it supports indicate, + # but it actually doesn't. + if characteristic.handle in self._notification_callbacks: + event_handler_token = self._notification_callbacks.pop( + characteristic.handle + ) + winrt_char.remove_value_changed(event_handler_token) + + raise + + @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. + """ + if not self.is_connected: + raise BleakError("Not connected") + + assert self.services + + gatt_char = cast(GattCharacteristic, characteristic.obj) + + _ensure_success( + await gatt_char.write_client_characteristic_configuration_descriptor_with_result_async( + GattClientCharacteristicConfigurationDescriptorValue.NONE + ), + None, + f"Could not stop notify on {characteristic.handle:04X}", + ) + + event_handler_token = self._notification_callbacks.pop(characteristic.handle) + gatt_char.remove_value_changed(event_handler_token) + + +T = TypeVar("T") + + +class FutureLike(Generic[T]): + """ + Wraps a WinRT IAsyncOperation in a "future-like" object so that it can + be passed to Python APIs. + + Needed until https://github.com/pywinrt/pywinrt/issues/14 + """ + + _asyncio_future_blocking = False + + def __init__(self: Self, op: IAsyncOperation[T]) -> None: + self._op = op + self._callbacks: list[Callable[[Self], None]] = [] + self._loop = asyncio.get_running_loop() + self._cancel_requested = False + self._result = None + + def call_callbacks() -> None: + for c in self._callbacks: + c(self) + + def call_callbacks_threadsafe( + op: IAsyncOperation[T], status: AsyncStatus + ) -> None: + if status == AsyncStatus.COMPLETED: + # have to get result on this thread, otherwise it may not return correct value + self._result = op.get_results() + + self._loop.call_soon_threadsafe(call_callbacks) + + op.completed = call_callbacks_threadsafe + + def result(self) -> T: + if self._op.status == AsyncStatus.STARTED: + raise asyncio.InvalidStateError + + if self._op.status == AsyncStatus.COMPLETED: + if self._cancel_requested: + raise asyncio.CancelledError + + assert self._result + + return self._result + + if self._op.status == AsyncStatus.CANCELED: + raise asyncio.CancelledError + + if self._op.status == AsyncStatus.ERROR: + if self._cancel_requested: + raise asyncio.CancelledError + + error_code = self._op.error_code.value + raise WinError(error_code) + + assert_never(self._op.status) + + def done(self) -> bool: + return self._op.status != AsyncStatus.STARTED + + def cancelled(self) -> bool: + return self._cancel_requested or self._op.status == AsyncStatus.CANCELED + + def add_done_callback( + self, + callback: Callable[[Self], None], + *, + context: Optional[Context] = None, + ) -> None: + self._callbacks.append(callback) + + def remove_done_callback(self, callback: Callable[[Self], None]) -> None: + self._callbacks.remove(callback) + + def cancel(self, msg: Optional[str] = None) -> bool: + if self._cancel_requested or self._op.status != AsyncStatus.STARTED: + return False + + self._cancel_requested = True + self._op.cancel() + + return True + + def exception(self) -> Optional[Exception]: + if self._op.status == AsyncStatus.STARTED: + raise asyncio.InvalidStateError + + if self._op.status == AsyncStatus.COMPLETED: + if self._cancel_requested: + raise asyncio.CancelledError + + return None + + if self._op.status == AsyncStatus.CANCELED: + raise asyncio.CancelledError + + if self._op.status == AsyncStatus.ERROR: + if self._cancel_requested: + raise asyncio.CancelledError + + error_code = self._op.error_code.value + + return WinError(error_code) + + assert_never(self._op.status) + + def get_loop(self) -> asyncio.AbstractEventLoop: + return self._loop + + def __await__(self): + if not self.done(): + self._asyncio_future_blocking = True + yield self # This tells Task to wait for completion. + + if not self.done(): + raise RuntimeError("await wasn't used with future") + + return self.result() # May raise too. diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/winrt/scanner.py b/tools/python-embed/Lib/site-packages/bleak/backends/winrt/scanner.py new file mode 100644 index 0000000..6d16f7d --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/winrt/scanner.py @@ -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 `_. + + 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 diff --git a/tools/python-embed/Lib/site-packages/bleak/backends/winrt/util.py b/tools/python-embed/Lib/site-packages/bleak/backends/winrt/util.py new file mode 100644 index 0000000..8b03331 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/backends/winrt/util.py @@ -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() diff --git a/tools/python-embed/Lib/site-packages/bleak/exc.py b/tools/python-embed/Lib/site-packages/bleak/exc.py new file mode 100644 index 0000000..34c0f3e --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/exc.py @@ -0,0 +1,344 @@ +from __future__ import annotations + +import enum +import uuid +from typing import Any, Optional, Union + + +class BleakError(Exception): + """Base Exception for bleak.""" + + pass + + +class BleakBluetoothNotAvailableReason(enum.Enum): + """ + Reasons for Bluetooth not being available. + + .. versionadded:: 2.0 + """ + + NO_BLUETOOTH = enum.auto() + """ + The system does not support Bluetooth. I.e. there is no Bluetooth radio. + """ + + NO_BLE_CENTRAL_ROLE = enum.auto() + """ + The Bluetooth radio does not support the Central role. (E.g. classic-only adapters.) + """ + + POWERED_OFF = enum.auto() + """ + Bluetooth is not currently available because the radio is turned off. + """ + + DENIED_BY_USER = enum.auto() + """ + The user denied permission for the app to use Bluetooth when prompted. + """ + + DENIED_BY_SYSTEM = enum.auto() + """ + Using Bluetooth was denied by the system. E.g. because of a system administrator policy. + """ + + DENIED_BY_UNKNOWN = enum.auto() + """ + Permission to use Bluetooth was denied for an unknown reason. + """ + + UNKNOWN = enum.auto() + """ + Bluetooth is not available for an unknown reason. + """ + + +class BleakBluetoothNotAvailableError(BleakError): + """ + Exception which is raised if the Bluetooth access is not available for some reason. + + .. versionadded:: 2.0 + """ + + def __init__(self, msg: str, reason: BleakBluetoothNotAvailableReason) -> None: + super().__init__(msg, reason) + + @property + def reason(self) -> BleakBluetoothNotAvailableReason: + """ + Gets the reason why Bluetooth is not available. + """ + return self.args[1] + + +class BleakCharacteristicNotFoundError(BleakError): + """ + Exception which is raised if a device does not support a characteristic. + + .. versionadded:: 0.22 + """ + + char_specifier: Union[int, str, uuid.UUID] + + def __init__(self, char_specifier: Union[int, str, uuid.UUID]) -> None: + """ + Args: + characteristic (str): handle or UUID of the characteristic which was not found + """ + super().__init__(f"Characteristic {char_specifier} was not found!") + self.char_specifier = char_specifier + + +class BleakDeviceNotFoundError(BleakError): + """ + Exception which is raised if a device can not be found by ``connect``, ``pair`` and ``unpair``. + This is the case if the OS Bluetooth stack has never seen this device or it was removed and forgotten. + + .. versionadded:: 0.19 + """ + + identifier: str + + def __init__(self, identifier: str, *args: object) -> None: + """ + Args: + identifier (str): device identifier (Bluetooth address or UUID) of the device which was not found + """ + super().__init__(*args) + self.identifier = identifier + + +class BleakDBusError(BleakError): + """Specialized exception type for D-Bus errors.""" + + def __init__(self, dbus_error: str, error_body: list[Any]): + """ + Args: + dbus_error (str): The D-Bus error, e.g. ``org.freedesktop.DBus.Error.UnknownObject``. + error_body (list): Body of the D-Bus error, sometimes containing error description or details. + """ + super().__init__(dbus_error, *error_body) + + @property + def dbus_error(self) -> str: + """Gets the D-Bus error name, e.g. ``org.freedesktop.DBus.Error.UnknownObject``.""" + return self.args[0] + + @property + def dbus_error_details(self) -> Optional[str]: + """Gets the optional D-Bus error details, e.g. 'Invalid UUID'.""" + if len(self.args) > 1: + details = self.args[1] + # Some error descriptions can be further parsed to be even more helpful + if "ATT error: 0x" in details: + more_detail = PROTOCOL_ERROR_CODES.get( + int(details.rsplit("x")[1], 16), "Unknown code" + ) + details += f" ({more_detail})" + return details + return None + + def __str__(self) -> str: + name = f"[{self.dbus_error}]" + details = self.dbus_error_details + return (name + " " + details) if details else name + + +CONTROLLER_ERROR_CODES = { + 0x00: "Success", + 0x01: "Unknown HCI Command", + 0x02: "Unknown Connection Identifier", + 0x03: "Hardware Failure", + 0x04: "Page Timeout", + 0x05: "Authentication Failure", + 0x06: "PIN or Key Missing", + 0x07: "Memory Capacity Exceeded", + 0x08: "Connection Timeout", + 0x09: "Connection Limit Exceeded", + 0x0A: "Synchronous Connection Limit To A Device Exceeded", + 0x0B: "Connection Already Exists", + 0x0C: "Command Disallowed", + 0x0D: "Connection Rejected due to Limited Resources", + 0x0E: "Connection Rejected Due To Security Reasons", + 0x0F: "Connection Rejected due to Unacceptable BD_ADDR", + 0x10: "Connection Accept Timeout Exceeded", + 0x11: "Unsupported Feature or Parameter Value", + 0x12: "Invalid HCI Command Parameters", + 0x13: "Remote User Terminated Connection", + 0x14: "Remote Device Terminated Connection due to Low Resources", + 0x15: "Remote Device Terminated Connection due to Power Off", + 0x16: "Connection Terminated By Local Host", + 0x17: "Repeated Attempts", + 0x18: "Pairing Not Allowed", + 0x19: "Unknown LMP PDU", + 0x1A: "Unsupported Remote Feature / Unsupported LMP Feature", + 0x1B: "SCO Offset Rejected", + 0x1C: "SCO Interval Rejected", + 0x1D: "SCO Air Mode Rejected", + 0x1E: "Invalid LMP Parameters / Invalid LL Parameters", + 0x1F: "Unspecified Error", + 0x20: "Unsupported LMP Parameter Value / Unsupported LL Parameter Value", + 0x21: "Role Change Not Allowed", + 0x22: "LMP Response Timeout / LL Response Timeout", + 0x23: "LMP Error Transaction Collision / LL Procedure Collision", + 0x24: "LMP PDU Not Allowed", + 0x25: "Encryption Mode Not Acceptable", + 0x26: "Link Key cannot be Changed", + 0x27: "Requested QoS Not Supported", + 0x28: "Instant Passed", + 0x29: "Pairing With Unit Key Not Supported", + 0x2A: "Different Transaction Collision", + 0x2B: "Reserved for future use", + 0x2C: "QoS Unacceptable Parameter", + 0x2D: "QoS Rejected", + 0x2E: "Channel Classification Not Supported", + 0x2F: "Insufficient Security", + 0x30: "Parameter Out Of Mandatory Range", + 0x31: "Reserved for future use", + 0x32: "Role Switch Pending", + 0x33: "Reserved for future use", + 0x34: "Reserved Slot Violation", + 0x35: "Role Switch Failed", + 0x36: "Extended Inquiry Response Too Large", + 0x37: "Secure Simple Pairing Not Supported By Host", + 0x38: "Host Busy - Pairing", + 0x39: "Connection Rejected due to No Suitable Channel Found", + 0x3A: "Controller Busy", + 0x3B: "Unacceptable Connection Parameters", + 0x3C: "Advertising Timeout", + 0x3D: "Connection Terminated due to MIC Failure", + 0x3E: "Connection Failed to be Established / Synchronization Timeout", + 0x3F: "MAC Connection Failed", + 0x40: "Coarse Clock Adjustment Rejected but Will Try to Adjust Using Clock", + 0x41: "Type0 Submap Not Defined", + 0x42: "Unknown Advertising Identifier", + 0x43: "Limit Reached", + 0x44: "Operation Cancelled by Host", + 0x45: "Packet Too Long", +} + +# as defined in Bluetooth Core Specification v5.2, volume 3, part F, section 3.4.1.1, table 3.4. +PROTOCOL_ERROR_CODES = { + 0x01: "Invalid Handle", + 0x02: "Read Not Permitted", + 0x03: "Write Not Permitted", + 0x04: "Invalid PDU", + 0x05: "Insufficient Authentication", + 0x06: "Request Not Supported", + 0x07: "Invalid Offset", + 0x08: "Insufficient Authorization", + 0x09: "Prepare Queue Full", + 0x0A: "Attribute Not Found", + 0x0B: "Attribute Not Long", + 0x0C: "Insufficient Encryption Key Size", + 0x0D: "Invalid Attribute Value Length", + 0x0E: "Unlikely Error", + 0x0F: "Insufficient Encryption", + 0x10: "Unsupported Group Type", + 0x11: "Insufficient Resource", + 0x12: "Database Out Of Sync", + 0x13: "Value Not Allowed", + 0x80: "Application-specific Error 0x80", + 0x81: "Application-specific Error 0x81", + 0x82: "Application-specific Error 0x82", + 0x83: "Application-specific Error 0x83", + 0x84: "Application-specific Error 0x84", + 0x85: "Application-specific Error 0x85", + 0x86: "Application-specific Error 0x86", + 0x87: "Application-specific Error 0x87", + 0x88: "Application-specific Error 0x88", + 0x89: "Application-specific Error 0x89", + 0x8A: "Application-specific Error 0x8A", + 0x8B: "Application-specific Error 0x8B", + 0x8C: "Application-specific Error 0x8C", + 0x8D: "Application-specific Error 0x8D", + 0x8E: "Application-specific Error 0x8E", + 0x8F: "Application-specific Error 0x8F", + 0x90: "Application-specific Error 0x90", + 0x91: "Application-specific Error 0x91", + 0x92: "Application-specific Error 0x92", + 0x93: "Application-specific Error 0x93", + 0x94: "Application-specific Error 0x94", + 0x95: "Application-specific Error 0x95", + 0x96: "Application-specific Error 0x96", + 0x97: "Application-specific Error 0x97", + 0x98: "Application-specific Error 0x98", + 0x99: "Application-specific Error 0x99", + 0x9A: "Application-specific Error 0x9A", + 0x9B: "Application-specific Error 0x9B", + 0x9C: "Application-specific Error 0x9C", + 0x9D: "Application-specific Error 0x9D", + 0x9E: "Application-specific Error 0x9E", + 0x9F: "Application-specific Error 0x9F", + 0xFC: "Write Request Rejected", + 0xFD: "Client Characteristic Configuration Descriptor Improperly Configured", + 0xFE: "Procedure Already in Progress", + 0xFF: "Out of Range", +} + + +class BleakGATTProtocolErrorCode(enum.IntEnum): + """ + Enumeration of GATT protocol error codes. + + .. versionadded:: 3.0 + """ + + INVALID_HANDLE = 0x01 + READ_NOT_PERMITTED = 0x02 + WRITE_NOT_PERMITTED = 0x03 + INVALID_PDU = 0x04 + INSUFFICIENT_AUTHENTICATION = 0x05 + REQUEST_NOT_SUPPORTED = 0x06 + INVALID_OFFSET = 0x07 + INSUFFICIENT_AUTHORIZATION = 0x08 + PREPARE_QUEUE_FULL = 0x09 + ATTRIBUTE_NOT_FOUND = 0x0A + ATTRIBUTE_NOT_LONG = 0x0B + INSUFFICIENT_ENCRYPTION_KEY_SIZE = 0x0C + INVALID_ATTRIBUTE_VALUE_LENGTH = 0x0D + UNLIKELY_ERROR = 0x0E + INSUFFICIENT_ENCRYPTION = 0x0F + UNSUPPORTED_GROUP_TYPE = 0x10 + INSUFFICIENT_RESOURCE = 0x11 + DATABASE_OUT_OF_SYNC = 0x12 + VALUE_NOT_ALLOWED = 0x13 + WRITE_REQUEST_REJECTED = 0xFC + CCCD_IMPROPERLY_CONFIGURED = 0xFD + PROCEDURE_ALREADY_IN_PROGRESS = 0xFE + OUT_OF_RANGE = 0xFF + + @classmethod + def _missing_(cls, value: Any) -> BleakGATTProtocolErrorCode | None: + try: + obj = int.__new__(cls, value) + except TypeError: # pragma: no cover + return None + + obj._value_ = value + obj._name_ = f"{cls.__name__}[{value}]" + return obj + + +class BleakGATTProtocolError(BleakError): + """ + Exception which is raised if a GATT protocol error occurs. + + .. versionadded:: 3.0 + """ + + def __init__(self, error_code: int) -> None: + """ + Args: + error_code (int): The GATT protocol error code. + """ + error_message = PROTOCOL_ERROR_CODES.get(error_code, "Unknown code") + super().__init__(error_code, f"GATT Protocol Error: {error_message}") + + @property + def code(self) -> BleakGATTProtocolErrorCode: + """ + Gets the GATT protocol error code. + """ + return BleakGATTProtocolErrorCode(self.args[0]) diff --git a/tools/python-embed/Lib/site-packages/bleak/py.typed b/tools/python-embed/Lib/site-packages/bleak/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tools/python-embed/Lib/site-packages/bleak/uuids.py b/tools/python-embed/Lib/site-packages/bleak/uuids.py new file mode 100644 index 0000000..b55df53 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/bleak/uuids.py @@ -0,0 +1,1271 @@ +from uuid import UUID + +uuid16_dict: dict[int, str] = { + 0x0001: "SDP", + 0x0003: "RFCOMM", + 0x0005: "TCS-BIN", + 0x0007: "ATT", + 0x0008: "OBEX", + 0x000F: "BNEP", + 0x0010: "UPNP", + 0x0011: "HIDP", + 0x0012: "Hardcopy Control Channel", + 0x0014: "Hardcopy Data Channel", + 0x0016: "Hardcopy Notification", + 0x0017: "AVCTP", + 0x0019: "AVDTP", + 0x001B: "CMTP", + 0x001E: "MCAP Control Channel", + 0x001F: "MCAP Data Channel", + 0x0100: "L2CAP", + # 0x0101 to 0x0fff undefined */ + 0x1000: "Service Discovery Server Service Class", + 0x1001: "Browse Group Descriptor Service Class", + 0x1002: "Public Browse Root", + # 0x1003 to 0x1100 undefined */ + 0x1101: "Serial Port", + 0x1102: "LAN Access Using PPP", + 0x1103: "Dialup Networking", + 0x1104: "IrMC Sync", + 0x1105: "OBEX Object Push", + 0x1106: "OBEX File Transfer", + 0x1107: "IrMC Sync Command", + 0x1108: "Headset", + 0x1109: "Cordless Telephony", + 0x110A: "Audio Source", + 0x110B: "Audio Sink", + 0x110C: "A/V Remote Control Target", + 0x110D: "Advanced Audio Distribution", + 0x110E: "A/V Remote Control", + 0x110F: "A/V Remote Control Controller", + 0x1110: "Intercom", + 0x1111: "Fax", + 0x1112: "Headset AG", + 0x1113: "WAP", + 0x1114: "WAP Client", + 0x1115: "PANU", + 0x1116: "NAP", + 0x1117: "GN", + 0x1118: "Direct Printing", + 0x1119: "Reference Printing", + 0x111A: "Basic Imaging Profile", + 0x111B: "Imaging Responder", + 0x111C: "Imaging Automatic Archive", + 0x111D: "Imaging Referenced Objects", + 0x111E: "Handsfree", + 0x111F: "Handsfree Audio Gateway", + 0x1120: "Direct Printing Refrence Objects Service", + 0x1121: "Reflected UI", + 0x1122: "Basic Printing", + 0x1123: "Printing Status", + 0x1124: "Human Interface Device Service", + 0x1125: "Hardcopy Cable Replacement", + 0x1126: "HCR Print", + 0x1127: "HCR Scan", + 0x1128: "Common ISDN Access", + # 0x1129 and 0x112a undefined */ + 0x112D: "SIM Access", + 0x112E: "Phonebook Access Client", + 0x112F: "Phonebook Access Server", + 0x1130: "Phonebook Access", + 0x1131: "Headset HS", + 0x1132: "Message Access Server", + 0x1133: "Message Notification Server", + 0x1134: "Message Access Profile", + 0x1135: "GNSS", + 0x1136: "GNSS Server", + 0x1137: "3D Display", + 0x1138: "3D Glasses", + 0x1139: "3D Synchronization", + 0x113A: "MPS Profile", + 0x113B: "MPS Service", + 0x113C: "CTN Access Service", + 0x113D: "CTN Notification Service", + 0x113E: "CTN Profile", + # 0x113f to 0x11ff undefined */ + 0x1200: "PnP Information", + 0x1201: "Generic Networking", + 0x1202: "Generic File Transfer", + 0x1203: "Generic Audio", + 0x1204: "Generic Telephony", + 0x1205: "UPNP Service", + 0x1206: "UPNP IP Service", + 0x1300: "UPNP IP PAN", + 0x1301: "UPNP IP LAP", + 0x1302: "UPNP IP L2CAP", + 0x1303: "Video Source", + 0x1304: "Video Sink", + 0x1305: "Video Distribution", + # 0x1306 to 0x13ff undefined */ + 0x1400: "HDP", + 0x1401: "HDP Source", + 0x1402: "HDP Sink", + # 0x1403 to 0x17ff undefined */ + 0x1800: "Generic Access Profile", + 0x1801: "Generic Attribute Profile", + 0x1802: "Immediate Alert", + 0x1803: "Link Loss", + 0x1804: "Tx Power", + 0x1805: "Current Time Service", + 0x1806: "Reference Time Update Service", + 0x1807: "Next DST Change Service", + 0x1808: "Glucose", + 0x1809: "Health Thermometer", + 0x180A: "Device Information", + # 0x180b and 0x180c undefined */ + 0x180D: "Heart Rate", + 0x180E: "Phone Alert Status Service", + 0x180F: "Battery Service", + 0x1810: "Blood Pressure", + 0x1811: "Alert Notification Service", + 0x1812: "Human Interface Device", + 0x1813: "Scan Parameters", + 0x1814: "Running Speed and Cadence", + 0x1815: "Automation IO", + 0x1816: "Cycling Speed and Cadence", + # 0x1817 undefined */ + 0x1818: "Cycling Power", + 0x1819: "Location and Navigation", + 0x181A: "Environmental Sensing", + 0x181B: "Body Composition", + 0x181C: "User Data", + 0x181D: "Weight Scale", + 0x181E: "Bond Management", + 0x181F: "Continuous Glucose Monitoring", + 0x1820: "Internet Protocol Support", + 0x1821: "Indoor Positioning", + 0x1822: "Pulse Oximeter", + 0x1823: "HTTP Proxy", + 0x1824: "Transport Discovery", + 0x1825: "Object Transfer", + 0x1826: "Fitness Machine", + 0x1827: "Mesh Provisioning", + 0x1828: "Mesh Proxy", + 0x1829: "Reconnection Configuration", + # 0x182a-0x1839 undefined + 0x183A: "Insulin Delivery", + 0x183B: "Binary Sensor", + 0x183C: "Emergency Configuration", + 0x183D: "Authorization Control", + 0x183E: "Physical Activity Monitor", + 0x183F: "Elapsed Time", + 0x1840: "Generic Health Sensor", + 0x1843: "Audio Input Control", + 0x1844: "Volume Control", + 0x1845: "Volume Offset Control", + 0x1846: "Coordinated Set Identification Service", + 0x1847: "Device Time", + 0x1848: "Media Control Service", + 0x1849: "Generic Media Control Service", + 0x184A: "Constant Tone Extension", + 0x184B: "Telephone Bearer Service", + 0x184C: "Generic Telephone Bearer Service", + 0x184D: "Microphone Control", + 0x184E: "Audio Stream Control Service", + 0x184F: "Broadcast Audio Scan Service", + 0x1850: "Published Audio Capabilities Service", + 0x1851: "Basic Audio Announcement Service", + 0x1852: "Broadcast Audio Announcement Service", + 0x1853: "Common Audio", + 0x1854: "Hearing Access", + 0x1855: "Telephony and Media Audio", + 0x1856: "Public Broadcast Announcement", + 0x1857: "Electronic Shelf Label", + 0x1859: "Mesh Proxy Solicitation", + # 0x185A to 0x26ff undefined */ + # 0x2700.. GATT Units + 0x2700: "unitless", + 0x2701: "length (metre)", + 0x2702: "mass (kilogram)", + 0x2703: "time (second)", + 0x2704: "electric current (ampere)", + 0x2705: "thermodynamic temperature (kelvin)", + 0x2706: "amount of substance (mole)", + 0x2707: "luminous intensity (candela)", + 0x2710: "area (square metres)", + 0x2711: "volume (cubic metres)", + 0x2712: "velocity (metres per second)", + 0x2713: "acceleration (metres per second squared)", + 0x2714: "wavenumber (reciprocal metre)", + 0x2715: "density (kilogram per cubic metre)", + 0x2716: "surface density (kilogram per square metre)", + 0x2717: "specific volume (cubic metre per kilogram)", + 0x2718: "current density (ampere per square metre)", + 0x2719: "magnetic field strength (ampere per metre)", + 0x271A: "amount concentration (mole per cubic metre)", + 0x271B: "mass concentration (kilogram per cubic metre)", + 0x271C: "luminance (candela per square metre)", + 0x271D: "refractive index", + 0x271E: "relative permeability", + 0x2720: "plane angle (radian)", + 0x2721: "solid angle (steradian)", + 0x2722: "frequency (hertz)", + 0x2723: "force (newton)", + 0x2724: "pressure (pascal)", + 0x2725: "energy (joule)", + 0x2726: "power (watt)", + 0x2727: "electric charge (coulomb)", + 0x2728: "electric potential difference (volt)", + 0x2729: "capacitance (farad)", + 0x272A: "electric resistance (ohm)", + 0x272B: "electric conductance (siemens)", + 0x272C: "magnetic flux (weber)", + 0x272D: "magnetic flux density (tesla)", + 0x272E: "inductance (henry)", + 0x272F: "Celsius temperature (degree Celsius)", + 0x2730: "luminous flux (lumen)", + 0x2731: "illuminance (lux)", + 0x2732: "activity referred to a radionuclide (becquerel)", + 0x2733: "absorbed dose (gray)", + 0x2734: "dose equivalent (sievert)", + 0x2735: "catalytic activity (katal)", + 0x2740: "dynamic viscosity (pascal second)", + 0x2741: "moment of force (newton metre)", + 0x2742: "surface tension (newton per metre)", + 0x2743: "angular velocity (radian per second)", + 0x2744: "angular acceleration (radian per second squared)", + 0x2745: "heat flux density (watt per square metre)", + 0x2746: "heat capacity (joule per kelvin)", + 0x2747: "specific heat capacity (joule per kilogram kelvin)", + 0x2748: "specific energy (joule per kilogram)", + 0x2749: "thermal conductivity (watt per metre kelvin)", + 0x274A: "energy density (joule per cubic metre)", + 0x274B: "electric field strength (volt per metre)", + 0x274C: "electric charge density (coulomb per cubic metre)", + 0x274D: "surface charge density (coulomb per square metre)", + 0x274E: "electric flux density (coulomb per square metre)", + 0x274F: "permittivity (farad per metre)", + 0x2750: "permeability (henry per metre)", + 0x2751: "molar energy (joule per mole)", + 0x2752: "molar entropy (joule per mole kelvin)", + 0x2753: "exposure (coulomb per kilogram)", + 0x2754: "absorbed dose rate (gray per second)", + 0x2755: "radiant intensity (watt per steradian)", + 0x2756: "radiance (watt per square metre steradian)", + 0x2757: "catalytic activity concentration (katal per cubic metre)", + 0x2760: "time (minute)", + 0x2761: "time (hour)", + 0x2762: "time (day)", + 0x2763: "plane angle (degree)", + 0x2764: "plane angle (minute)", + 0x2765: "plane angle (second)", + 0x2766: "area (hectare)", + 0x2767: "volume (litre)", + 0x2768: "mass (tonne)", + 0x2780: "pressure (bar)", + 0x2781: "pressure (millimetre of mercury)", + 0x2782: "length (ångström)", + 0x2783: "length (nautical mile)", + 0x2784: "area (barn)", + 0x2785: "velocity (knot)", + 0x2786: "logarithmic radio quantity (neper)", + 0x2787: "logarithmic radio quantity (bel)", + 0x27A0: "length (yard)", + 0x27A1: "length (parsec)", + 0x27A2: "length (inch)", + 0x27A3: "length (foot)", + 0x27A4: "length (mile)", + 0x27A5: "pressure (pound-force per square inch)", + 0x27A6: "velocity (kilometre per hour)", + 0x27A7: "velocity (mile per hour)", + 0x27A8: "angular velocity (revolution per minute)", + 0x27A9: "energy (gram calorie)", + 0x27AA: "energy (kilogram calorie)", + 0x27AB: "energy (kilowatt hour)", + 0x27AC: "thermodynamic temperature (degree Fahrenheit)", + 0x27AD: "percentage", + 0x27AE: "per mille", + 0x27AF: "period (beats per minute)", + 0x27B0: "electric charge (ampere hours)", + 0x27B1: "mass density (milligram per decilitre)", + 0x27B2: "mass density (millimole per litre)", + 0x27B3: "time (year)", + 0x27B4: "time (month)", + 0x27B5: "concentration (count per cubic metre)", + 0x27B6: "irradiance (watt per square metre)", + 0x27B7: "milliliter (per kilogram per minute)", + 0x27B8: "mass (pound)", + 0x27B9: "metabolic equivalent", + 0x27BA: "step (per minute)", + 0x27BC: "stroke (per minute)", + 0x27BD: "pace (kilometre per minute)", + 0x27BE: "luminous efficacy (lumen per watt)", + 0x27BF: "luminous energy (lumen hour)", + 0x27C0: "luminous exposure (lux hour)", + 0x27C1: "mass flow (gram per second)", + 0x27C2: "volume flow (litre per second)", + 0x27C3: "sound pressure (decible)", + 0x27C4: "parts per million", + 0x27C5: "parts per billion", + 0x2800: "Primary Service", + 0x2801: "Secondary Service", + 0x2802: "Include", + 0x2803: "Characteristic", + # 0x2804 to 0x28ff undefined */ + # Descriptors (SIG) + 0x2900: "Characteristic Extended Properties", + 0x2901: "Characteristic User Description", + 0x2902: "Client Characteristic Configuration", + 0x2903: "Server Characteristic Configuration", + 0x2904: "Characteristic Presentation Format", + 0x2905: "Characteristic Aggregate Format", + 0x2906: "Valid Range", + 0x2907: "External Report Reference", + 0x2908: "Report Reference", + 0x2909: "Number of Digitals", + 0x290A: "Value Trigger Setting", + 0x290B: "Environmental Sensing Configuration", + 0x290C: "Environmental Sensing Measurement", + 0x290D: "Environmental Sensing Trigger Setting", + 0x290E: "Time Trigger Setting", + 0x290F: "Complete BR-EDR Transport Block Data", + # 0x2910 to 0x29ff undefined */ + # 0x2a00.. GATT characteristic and Object Types + 0x2A00: "Device Name", + 0x2A01: "Appearance", + 0x2A02: "Peripheral Privacy Flag", + 0x2A03: "Reconnection Address", + 0x2A04: "Peripheral Preferred Connection Parameters", + 0x2A05: "Service Changed", + 0x2A06: "Alert Level", + 0x2A07: "Tx Power Level", + 0x2A08: "Date Time", + 0x2A09: "Day of Week", + 0x2A0A: "Day Date Time", + 0x2A0B: "Exact Time 100", + 0x2A0C: "Exact Time 256", + 0x2A0D: "DST Offset", + 0x2A0E: "Time Zone", + 0x2A0F: "Local Time Information", + 0x2A10: "Secondary Time Zone", + 0x2A11: "Time with DST", + 0x2A12: "Time Accuracy", + 0x2A13: "Time Source", + 0x2A14: "Reference Time Information", + 0x2A15: "Time Broadcast", + 0x2A16: "Time Update Control Point", + 0x2A17: "Time Update State", + 0x2A18: "Glucose Measurement", + 0x2A19: "Battery Level", + 0x2A1A: "Battery Power State", + 0x2A1B: "Battery Level State", + 0x2A1C: "Temperature Measurement", + 0x2A1D: "Temperature Type", + 0x2A1E: "Intermediate Temperature", + 0x2A1F: "Temperature Celsius", + 0x2A20: "Temperature Fahrenheit", + 0x2A21: "Measurement Interval", + 0x2A22: "Boot Keyboard Input Report", + 0x2A23: "System ID", + 0x2A24: "Model Number String", + 0x2A25: "Serial Number String", + 0x2A26: "Firmware Revision String", + 0x2A27: "Hardware Revision String", + 0x2A28: "Software Revision String", + 0x2A29: "Manufacturer Name String", + 0x2A2A: "IEEE 11073-20601 Regulatory Cert. Data List", + 0x2A2B: "Current Time", + 0x2A2C: "Magnetic Declination", + # 0x2a2d to 0x2a2e undefined */ + 0x2A2F: "Position 2D", + 0x2A30: "Position 3D", + 0x2A31: "Scan Refresh", + 0x2A32: "Boot Keyboard Output Report", + 0x2A33: "Boot Mouse Input Report", + 0x2A34: "Glucose Measurement Context", + 0x2A35: "Blood Pressure Measurement", + 0x2A36: "Intermediate Cuff Pressure", + 0x2A37: "Heart Rate Measurement", + 0x2A38: "Body Sensor Location", + 0x2A39: "Heart Rate Control Point", + 0x2A3A: "Removable", + 0x2A3B: "Service Required", + 0x2A3C: "Scientific Temperature Celsius", + 0x2A3D: "String", + 0x2A3E: "Network Availability", + 0x2A3F: "Alert Status", + 0x2A40: "Ringer Control Point", + 0x2A41: "Ringer Setting", + 0x2A42: "Alert Category ID Bit Mask", + 0x2A43: "Alert Category ID", + 0x2A44: "Alert Notification Control Point", + 0x2A45: "Unread Alert Status", + 0x2A46: "New Alert", + 0x2A47: "Supported New Alert Category", + 0x2A48: "Supported Unread Alert Category", + 0x2A49: "Blood Pressure Feature", + 0x2A4A: "HID Information", + 0x2A4B: "Report Map", + 0x2A4C: "HID Control Point", + 0x2A4D: "Report", + 0x2A4E: "Protocol Mode", + 0x2A4F: "Scan Interval Window", + 0x2A50: "PnP ID", + 0x2A51: "Glucose Feature", + 0x2A52: "Record Access Control Point", + 0x2A53: "RSC Measurement", + 0x2A54: "RSC Feature", + 0x2A55: "SC Control Point", + 0x2A56: "Digital", + 0x2A57: "Digital Output", + 0x2A58: "Analog", + 0x2A59: "Analog Output", + 0x2A5A: "Aggregate", + 0x2A5B: "CSC Measurement", + 0x2A5C: "CSC Feature", + 0x2A5D: "Sensor Location", + 0x2A5E: "PLX Spot-Check Measurement", + 0x2A5F: "PLX Continuous Measurement Characteristic", + 0x2A60: "PLX Features", + 0x2A62: "Pulse Oximetry Control Point", + 0x2A63: "Cycling Power Measurement", + 0x2A64: "Cycling Power Vector", + 0x2A65: "Cycling Power Feature", + 0x2A66: "Cycling Power Control Point", + 0x2A67: "Location and Speed", + 0x2A68: "Navigation", + 0x2A69: "Position Quality", + 0x2A6A: "LN Feature", + 0x2A6B: "LN Control Point", + 0x2A6C: "Elevation", + 0x2A6D: "Pressure", + 0x2A6E: "Temperature", + 0x2A6F: "Humidity", + 0x2A70: "True Wind Speed", + 0x2A71: "True Wind Direction", + 0x2A72: "Apparent Wind Speed", + 0x2A73: "Apparent Wind Direction", + 0x2A74: "Gust Factor", + 0x2A75: "Pollen Concentration", + 0x2A76: "UV Index", + 0x2A77: "Irradiance", + 0x2A78: "Rainfall", + 0x2A79: "Wind Chill", + 0x2A7A: "Heat Index", + 0x2A7B: "Dew Point", + 0x2A7C: "Trend", + 0x2A7D: "Descriptor Value Changed", + 0x2A7E: "Aerobic Heart Rate Lower Limit", + 0x2A7F: "Aerobic Threshold", + 0x2A80: "Age", + 0x2A81: "Anaerobic Heart Rate Lower Limit", + 0x2A82: "Anaerobic Heart Rate Upper Limit", + 0x2A83: "Anaerobic Threshold", + 0x2A84: "Aerobic Heart Rate Upper Limit", + 0x2A85: "Date of Birth", + 0x2A86: "Date of Threshold Assessment", + 0x2A87: "Email Address", + 0x2A88: "Fat Burn Heart Rate Lower Limit", + 0x2A89: "Fat Burn Heart Rate Upper Limit", + 0x2A8A: "First Name", + 0x2A8B: "Five Zone Heart Rate Limits", + 0x2A8C: "Gender", + 0x2A8D: "Heart Rate Max", + 0x2A8E: "Height", + 0x2A8F: "Hip Circumference", + 0x2A90: "Last Name", + 0x2A91: "Maximum Recommended Heart Rate", + 0x2A92: "Resting Heart Rate", + 0x2A93: "Sport Type for Aerobic/Anaerobic Thresholds", + 0x2A94: "Three Zone Heart Rate Limits", + 0x2A95: "Two Zone Heart Rate Limit", + 0x2A96: "VO2 Max", + 0x2A97: "Waist Circumference", + 0x2A98: "Weight", + 0x2A99: "Database Change Increment", + 0x2A9A: "User Index", + 0x2A9B: "Body Composition Feature", + 0x2A9C: "Body Composition Measurement", + 0x2A9D: "Weight Measurement", + 0x2A9E: "Weight Scale Feature", + 0x2A9F: "User Control Point", + 0x2AA0: "Magnetic Flux Density - 2D", + 0x2AA1: "Magnetic Flux Density - 3D", + 0x2AA2: "Language", + 0x2AA3: "Barometric Pressure Trend", + 0x2AA4: "Bond Management Control Point", + 0x2AA5: "Bond Management Feature", + 0x2AA6: "Central Address Resolution", + 0x2AA7: "CGM Measurement", + 0x2AA8: "CGM Feature", + 0x2AA9: "CGM Status", + 0x2AAA: "CGM Session Start Time", + 0x2AAB: "CGM Session Run Time", + 0x2AAC: "CGM Specific Ops Control Point", + 0x2AAD: "Indoor Positioning Configuration", + 0x2AAE: "Latitude", + 0x2AAF: "Longitude", + 0x2AB0: "Local North Coordinate", + 0x2AB1: "Local East Coordinate", + 0x2AB2: "Floor Number", + 0x2AB3: "Altitude", + 0x2AB4: "Uncertainty", + 0x2AB5: "Location Name", + 0x2AB6: "URI", + 0x2AB7: "HTTP Headers", + 0x2AB8: "HTTP Status Code", + 0x2AB9: "HTTP Entity Body", + 0x2ABA: "HTTP Control Point", + 0x2ABB: "HTTPS Security", + 0x2ABC: "TDS Control Point", + 0x2ABD: "OTS Feature", + 0x2ABE: "Object Name", + 0x2ABF: "Object Type", + 0x2AC0: "Object Size", + 0x2AC1: "Object First-Created", + 0x2AC2: "Object Last-Modified", + 0x2AC3: "Object ID", + 0x2AC4: "Object Properties", + 0x2AC5: "Object Action Control Point", + 0x2AC6: "Object List Control Point", + 0x2AC7: "Object List Filter", + 0x2AC8: "Object Changed", + 0x2AC9: "Resolvable Private Address Only", + # 0x2aca and 0x2acb undefined */ + 0x2ACC: "Fitness Machine Feature", + 0x2ACD: "Treadmill Data", + 0x2ACE: "Cross Trainer Data", + 0x2ACF: "Step Climber Data", + 0x2AD0: "Stair Climber Data", + 0x2AD1: "Rower Data", + 0x2AD2: "Indoor Bike Data", + 0x2AD3: "Training Status", + 0x2AD4: "Supported Speed Range", + 0x2AD5: "Supported Inclination Range", + 0x2AD6: "Supported Resistance Level Range", + 0x2AD7: "Supported Heart Rate Range", + 0x2AD8: "Supported Power Range", + 0x2AD9: "Fitness Machine Control Point", + 0x2ADA: "Fitness Machine Status", + 0x2ADB: "Mesh Provisioning Data In", + 0x2ADC: "Mesh Provisioning Data Out", + 0x2ADD: "Mesh Proxy Data In", + 0x2ADE: "Mesh Proxy Data Out", + 0x2AE0: "Average Current", + 0x2AE1: "Average Voltage", + 0x2AE2: "Boolean", + 0x2AE3: "Chromatic Distance From Planckian", + 0x2AE4: "Chromaticity Coordinates", + 0x2AE5: "Chromaticity In CCT And Duv Values", + 0x2AE6: "Chromaticity Tolerance", + 0x2AE7: "CIE 13.3-1995 Color Rendering Index", + 0x2AE8: "Coefficient", + 0x2AE9: "Correlated Color Temperature", + 0x2AEA: "Count 16", + 0x2AEB: "Count 24", + 0x2AEC: "Country Code", + 0x2AED: "Date UTC", + 0x2AEE: "Electric Current", + 0x2AEF: "Electric Current Range", + 0x2AF0: "Electric Current Specification", + 0x2AF1: "Electric Current Statistics", + 0x2AF2: "Energy", + 0x2AF3: "Energy In A Period Of Day", + 0x2AF4: "Event Statistics", + 0x2AF5: "Fixed String 16", + 0x2AF6: "Fixed String 24", + 0x2AF7: "Fixed String 36", + 0x2AF8: "Fixed String 8", + 0x2AF9: "Generic Level", + 0x2AFA: "Global Trade Item Number", + 0x2AFB: "Illuminance", + 0x2AFC: "Luminous Efficacy", + 0x2AFD: "Luminous Energy", + 0x2AFE: "Luminous Exposure", + 0x2AFF: "Luminous Flux", + 0x2B00: "Luminous Flux Range", + 0x2B01: "Luminous Intensity", + 0x2B02: "Mass Flow", + 0x2B03: "Perceived Lightness", + 0x2B04: "Percentage 8", + 0x2B05: "Power", + 0x2B06: "Power Specification", + 0x2B07: "Relative Runtime In A Current Range", + 0x2B08: "Relative Runtime In A Generic Level Range", + 0x2B09: "Relative Value In A Voltage Range", + 0x2B0A: "Relative Value In An Illuminance Range", + 0x2B0B: "Relative Value In A Period of Day", + 0x2B0C: "Relative Value In A Temperature Range", + 0x2B0D: "Temperature 8", + 0x2B0E: "Temperature 8 In A Period Of Day", + 0x2B0F: "Temperature 8 Statistics", + 0x2B10: "Temperature Range", + 0x2B11: "Temperature Statistics", + 0x2B12: "Time Decihour 8", + 0x2B13: "Time Exponential 8", + 0x2B14: "Time Hour 24", + 0x2B15: "Time Millisecond 24", + 0x2B16: "Time Second 16", + 0x2B17: "Time Second 8", + 0x2B18: "Voltage", + 0x2B19: "Voltage Specification", + 0x2B1A: "Voltage Statistics", + 0x2B1B: "Volume Flow", + 0x2B1C: "Chromaticity Coordinate", + 0x2B1D: "RC Feature", + 0x2B1E: "RC Settings", + 0x2B1F: "Reconnection Configuration Control Point", + 0x2B20: "IDD Status Changed", + 0x2B21: "IDD Status", + 0x2B22: "IDD Annunciation Status", + 0x2B23: "IDD Features", + 0x2B24: "IDD Status Reader Control Point", + 0x2B25: "IDD Command Control Point", + 0x2B26: "IDD Command Data", + 0x2B27: "IDD Record Access Control Point", + 0x2B28: "IDD History Data", + 0x2B29: "Client Supported Features", + 0x2B2A: "Database Hash", + 0x2B2B: "BSS Control Point", + 0x2B2C: "BSS Response", + 0x2B2D: "Emergency ID", + 0x2B2E: "Emergency Text", + 0x2B2F: "ACS Status", + 0x2B30: "ACS Data In", + 0x2B31: "ACS Data Out Notify", + 0x2B32: "ACS Data Out Indicate", + 0x2B33: "ACS Control Point", + 0x2B34: "Enhanced Blood Pressure Measurement", + 0x2B35: "Enhanced Intermediate Cuff Pressure", + 0x2B36: "Blood Pressure Record", + 0x2B37: "Registered User", + 0x2B38: "BR-EDR Handover Data", + 0x2B39: "Bluetooth SIG Data", + 0x2B3A: "Server Supported Features", + 0x2B3B: "Physical Activity Monitor Features", + 0x2B3C: "General Activity Instantaneous Data", + 0x2B3D: "General Activity Summary Data", + 0x2B3E: "CardioRespiratory Activity Instantaneous Data", + 0x2B3F: "CardioRespiratory Activity Summary Data", + 0x2B40: "Step Counter Activity Summary Data", + 0x2B41: "Sleep Activity Instantaneous Data", + 0x2B42: "Sleep Activity Summary Data", + 0x2B43: "Physical Activity Monitor Control Point", + 0x2B44: "Current Session", + 0x2B45: "Session", + 0x2B46: "Preferred Units", + 0x2B47: "High Resolution Height", + 0x2B48: "Middle Name", + 0x2B49: "Stride Length", + 0x2B4A: "Handedness", + 0x2B4B: "Device Wearing Position", + 0x2B4C: "Four Zone Heart Rate Limits", + 0x2B4D: "High Intensity Exercise Threshold", + 0x2B4E: "Activity Goal", + 0x2B4F: "Sedentary Interval Notification", + 0x2B50: "Caloric Intake", + 0x2B51: "TMAP Role", + 0x2B77: "Audio Input State", + 0x2B78: "Gain Settings Attribute", + 0x2B79: "Audio Input Type", + 0x2B7A: "Audio Input Status", + 0x2B7B: "Audio Input Control Point", + 0x2B7C: "Audio Input Description", + 0x2B7D: "Volume State", + 0x2B7E: "Volume Control Point", + 0x2B7F: "Volume Flags", + 0x2B80: "Offset State", + 0x2B81: "Audio Location", + 0x2B82: "Volume Offset Control Point", + 0x2B83: "Audio Output Description", + 0x2B84: "Set Identity Resolving Key Characteristic", + 0x2B85: "Size Characteristic", + 0x2B86: "Lock Characteristic", + 0x2B87: "Rank Characteristic", + 0x2B88: "Encrypted Data Key Material", + 0x2B89: "Apparent Energy 32", + 0x2B8A: "Apparent Power", + 0x2B8B: "Live Health Observations", + 0x2B8C: "CO\textsubscript{2} Concentration", + 0x2B8D: "Cosine of the Angle", + 0x2B8E: "Device Time Feature", + 0x2B8F: "Device Time Parameters", + 0x2B90: "Device Time", + 0x2B91: "Device Time Control Point", + 0x2B92: "Time Change Log Data", + 0x2B93: "Media Player Name", + 0x2B94: "Media Player Icon Object ID", + 0x2B95: "Media Player Icon URL", + 0x2B96: "Track Changed", + 0x2B97: "Track Title", + 0x2B98: "Track Duration", + 0x2B99: "Track Position", + 0x2B9A: "Playback Speed", + 0x2B9B: "Seeking Speed", + 0x2B9C: "Current Track Segments Object ID", + 0x2B9D: "Current Track Object ID", + 0x2B9E: "Next Track Object ID", + 0x2B9F: "Parent Group Object ID", + 0x2BA0: "Current Group Object ID", + 0x2BA1: "Playing Order", + 0x2BA2: "Playing Orders Supported", + 0x2BA3: "Media State", + 0x2BA4: "Media Control Point", + 0x2BA5: "Media Control Point Opcodes Supported", + 0x2BA6: "Search Results Object ID", + 0x2BA7: "Search Control Point", + 0x2BA8: "Energy 32", + 0x2BA9: "Media Player Icon Object Type", + 0x2BAA: "Track Segments Object Type", + 0x2BAB: "Track Object Type", + 0x2BAC: "Group Object Type", + 0x2BAD: "Constant Tone Extension Enable", + 0x2BAE: "Advertising Constant Tone Extension Minimum Length", + 0x2BAF: "Advertising Constant Tone Extension Minimum Transmit Count", + 0x2BB0: "Advertising Constant Tone Extension Transmit Duration", + 0x2BB1: "Advertising Constant Tone Extension Interval", + 0x2BB2: "Advertising Constant Tone Extension PHY", + 0x2BB3: "Bearer Provider Name", + 0x2BB4: "Bearer UCI", + 0x2BB5: "Bearer Technology", + 0x2BB6: "Bearer URI Schemes Supported List", + 0x2BB7: "Bearer Signal Strength", + 0x2BB8: "Bearer Signal Strength Reporting Interval", + 0x2BB9: "Bearer List Current Calls", + 0x2BBA: "Content Control ID", + 0x2BBB: "Status Flags", + 0x2BBC: "Incoming Call Target Bearer URI", + 0x2BBD: "Call State", + 0x2BBE: "Call Control Point", + 0x2BBF: "Call Control Point Optional Opcodes", + 0x2BC0: "Termination Reason", + 0x2BC1: "Incoming Call", + 0x2BC2: "Call Friendly Name", + 0x2BC3: "Mute", + 0x2BC4: "Sink ASE", + 0x2BC5: "Source ASE", + 0x2BC6: "ASE Control Point", + 0x2BC7: "Broadcast Audio Scan Control Point", + 0x2BC8: "Broadcast Receive State", + 0x2BC9: "Sink PAC", + 0x2BCA: "Sink Audio Locations", + 0x2BCB: "Source PAC", + 0x2BCC: "Source Audio Locations", + 0x2BCD: "Available Audio Contexts", + 0x2BCE: "Supported Audio Contexts", + 0x2BCF: "Ammonia Concentration", + 0x2BD0: "Carbon Monoxide Concentration", + 0x2BD1: "Methane Concentration", + 0x2BD2: "Nitrogen Dioxide Concentration", + 0x2BD3: "Non-Methane Volatile Organic Compounds Concentration", + 0x2BD4: "Ozone Concentration", + 0x2BD5: "Particulate Matter - PM1 Concentration", + 0x2BD6: "Particulate Matter - PM2.5 Concentration", + 0x2BD7: "Particulate Matter - PM10 Concentration", + 0x2BD8: "Sulfur Dioxide Concentration", + 0x2BD9: "Sulfur Hexafluoride Concentration", + 0x2BDA: "Hearing Aid Features", + 0x2BDB: "Hearing Aid Preset Control Point", + 0x2BDC: "Active Preset Index", + 0x2BDD: "Stored Health Observations", + 0x2BDE: "Fixed String 64", + 0x2BDF: "High Temperature", + 0x2BE0: "High Voltage", + 0x2BE1: "Light Distribution", + 0x2BE2: "Light Output", + 0x2BE3: "Light Source Type", + 0x2BE4: "Noise", + 0x2BE5: "Relative Runtime in a Correlated Color Temperature Range", + 0x2BE6: "Time Second 32", + 0x2BE7: "VOC Concentration", + 0x2BE8: "Voltage Frequency", + 0x2BE9: "Battery Critical Status", + 0x2BEA: "Battery Health Status", + 0x2BEB: "Battery Health Information", + 0x2BEC: "Battery Information", + 0x2BED: "Battery Level Status", + 0x2BEE: "Battery Time Status", + 0x2BEF: "Estimated Service Date", + 0x2BF0: "Battery Energy Status", + 0x2BF1: "Observation Schedule Changed", + 0x2BF2: "Current Elapsed Time", + 0x2BF3: "Health Sensor Features", + 0x2BF4: "GHS Control Point", + 0x2BF5: "LE GATT Security Levels", + 0x2BF6: "ESL Address", + 0x2BF7: "AP Sync Key Material", + 0x2BF8: "ESL Response Key Material", + 0x2BF9: "ESL Current Absolute Time", + 0x2BFA: "ESL Display Information", + 0x2BFB: "ESL Image Information", + 0x2BFC: "ESL Sensor Information", + 0x2BFD: "ESL LED Information", + 0x2BFE: "ESL Control Point", + 0x2BFF: "UDI for Medical Devices", + 0xFE1C: "NetMedia: Inc.", + 0xFE1D: "Illuminati Instrument Corporation", + 0xFE1E: "Smart Innovations Co.: Ltd", + 0xFE1F: "Garmin International: Inc.", + 0xFE20: "Emerson", + 0xFE21: "Bose Corporation", + 0xFE22: "Zoll Medical Corporation", + 0xFE23: "Zoll Medical Corporation", + 0xFE24: "August Home Inc", + 0xFE25: "Apple: Inc.", + 0xFE26: "Google Inc.", + 0xFE27: "Google Inc.", + 0xFE28: "Ayla Network", + 0xFE29: "Gibson Innovations", + 0xFE2A: "DaisyWorks: Inc.", + 0xFE2B: "ITT Industries", + 0xFE2C: "Google Inc.", + 0xFE2D: "SMART INNOVATION Co.,Ltd", + 0xFE2E: "ERi,Inc.", + 0xFE2F: "CRESCO Wireless: Inc", + 0xFE30: "Volkswagen AG", + 0xFE31: "Volkswagen AG", + 0xFE32: "Pro-Mark: Inc.", + 0xFE33: "CHIPOLO d.o.o.", + 0xFE34: "SmallLoop LLC", + 0xFE35: "HUAWEI Technologies Co.: Ltd", + 0xFE36: "HUAWEI Technologies Co.: Ltd", + 0xFE37: "Spaceek LTD", + 0xFE38: "Spaceek LTD", + 0xFE39: "TTS Tooltechnic Systems AG & Co. KG", + 0xFE3A: "TTS Tooltechnic Systems AG & Co. KG", + 0xFE3B: "Dolby Laboratories", + 0xFE3C: "Alibaba", + 0xFE3D: "BD Medical", + 0xFE3E: "BD Medical", + 0xFE3F: "Friday Labs Limited", + 0xFE40: "Inugo Systems Limited", + 0xFE41: "Inugo Systems Limited", + 0xFE42: "Nets A/S", + 0xFE43: "Andreas Stihl AG & Co. KG", + 0xFE44: "SK Telecom", + 0xFE45: "Snapchat Inc", + 0xFE46: "B&O Play A/S", + 0xFE47: "General Motors", + 0xFE48: "General Motors", + 0xFE49: "SenionLab AB", + 0xFE4A: "OMRON HEALTHCARE Co.: Ltd.", + 0xFE4B: "Koninklijke Philips N.V.", + 0xFE4C: "Volkswagen AG", + 0xFE4D: "Casambi Technologies Oy", + 0xFE4E: "NTT docomo", + 0xFE4F: "Molekule: Inc.", + 0xFE50: "Google Inc.", + 0xFE51: "SRAM", + 0xFE52: "SetPoint Medical", + 0xFE53: "3M", + 0xFE54: "Motiv: Inc.", + 0xFE55: "Google Inc.", + 0xFE56: "Google Inc.", + 0xFE57: "Dotted Labs", + 0xFE58: "Nordic Semiconductor ASA", + 0xFE59: "Nordic Semiconductor ASA", + 0xFE5A: "Chronologics Corporation", + 0xFE5B: "GT-tronics HK Ltd", + 0xFE5C: "million hunters GmbH", + 0xFE5D: "Grundfos A/S", + 0xFE5E: "Plastc Corporation", + 0xFE5F: "Eyefi: Inc.", + 0xFE60: "Lierda Science & Technology Group Co.: Ltd.", + 0xFE61: "Logitech International SA", + 0xFE62: "Indagem Tech LLC", + 0xFE63: "Connected Yard: Inc.", + 0xFE64: "Siemens AG", + 0xFE65: "CHIPOLO d.o.o.", + 0xFE66: "Intel Corporation", + 0xFE67: "Lab Sensor Solutions", + 0xFE68: "Qualcomm Life Inc", + 0xFE69: "Qualcomm Life Inc", + 0xFE6A: "Kontakt Micro-Location Sp. z o.o.", + 0xFE6B: "TASER International: Inc.", + 0xFE6C: "TASER International: Inc.", + 0xFE6D: "The University of Tokyo", + 0xFE6E: "The University of Tokyo", + 0xFE6F: "LINE Corporation", + 0xFE70: "Beijing Jingdong Century Trading Co.: Ltd.", + 0xFE71: "Plume Design Inc", + 0xFE72: "St. Jude Medical: Inc.", + 0xFE73: "St. Jude Medical: Inc.", + 0xFE74: "unwire", + 0xFE75: "TangoMe", + 0xFE76: "TangoMe", + 0xFE77: "Hewlett-Packard Company", + 0xFE78: "Hewlett-Packard Company", + 0xFE79: "Zebra Technologies", + 0xFE7A: "Bragi GmbH", + 0xFE7B: "Orion Labs: Inc.", + 0xFE7C: "Stollmann E+V GmbH", + 0xFE7D: "Aterica Health Inc.", + 0xFE7E: "Awear Solutions Ltd", + 0xFE7F: "Doppler Lab", + 0xFE80: "Doppler Lab", + 0xFE81: "Medtronic Inc.", + 0xFE82: "Medtronic Inc.", + 0xFE83: "Blue Bite", + 0xFE84: "RF Digital Corp", + 0xFE85: "RF Digital Corp", + 0xFE86: "HUAWEI Technologies Co.: Ltd.", + 0xFE87: "Qingdao Yeelink Information Technology Co.: Ltd.", + 0xFE88: "SALTO SYSTEMS S.L.", + 0xFE89: "B&O Play A/S", + 0xFE8A: "Apple: Inc.", + 0xFE8B: "Apple: Inc.", + 0xFE8C: "TRON Forum", + 0xFE8D: "Interaxon Inc.", + 0xFE8E: "ARM Ltd", + 0xFE8F: "CSR", + 0xFE90: "JUMA", + 0xFE91: "Shanghai Imilab Technology Co.,Ltd", + 0xFE92: "Jarden Safety & Security", + 0xFE93: "OttoQ Inc.", + 0xFE94: "OttoQ Inc.", + 0xFE95: "Xiaomi Inc.", + 0xFE96: "Tesla Motor Inc.", + 0xFE97: "Tesla Motor Inc.", + 0xFE98: "Currant: Inc.", + 0xFE99: "Currant: Inc.", + 0xFE9A: "Estimote", + 0xFE9B: "Samsara Networks: Inc", + 0xFE9C: "GSI Laboratories: Inc.", + 0xFE9D: "Mobiquity Networks Inc", + 0xFE9E: "Dialog Semiconductor B.V.", + 0xFE9F: "Google", + 0xFEA0: "Google", + 0xFEA1: "Intrepid Control Systems: Inc.", + 0xFEA2: "Intrepid Control Systems: Inc.", + 0xFEA3: "ITT Industries", + 0xFEA4: "Paxton Access Ltd", + 0xFEA5: "GoPro: Inc.", + 0xFEA6: "GoPro: Inc.", + 0xFEA7: "UTC Fire and Security", + 0xFEA8: "Savant Systems LLC", + 0xFEA9: "Savant Systems LLC", + 0xFEAA: "Google", + 0xFEAB: "Nokia Corporation", + 0xFEAC: "Nokia Corporation", + 0xFEAD: "Nokia Corporation", + 0xFEAE: "Nokia Corporation", + 0xFEAF: "Nest Labs Inc.", + 0xFEB0: "Nest Labs Inc.", + 0xFEB1: "Electronics Tomorrow Limited", + 0xFEB2: "Microsoft Corporation", + 0xFEB3: "Taobao", + 0xFEB4: "WiSilica Inc.", + 0xFEB5: "WiSilica Inc.", + 0xFEB6: "Vencer Co: Ltd", + 0xFEB7: "Facebook: Inc.", + 0xFEB8: "Facebook: Inc.", + 0xFEB9: "LG Electronics", + 0xFEBA: "Tencent Holdings Limited", + 0xFEBB: "adafruit industries", + 0xFEBC: "Dexcom: Inc.", + 0xFEBD: "Clover Network: Inc.", + 0xFEBE: "Bose Corporation", + 0xFEBF: "Nod: Inc.", + 0xFEC0: "KDDI Corporation", + 0xFEC1: "KDDI Corporation", + 0xFEC2: "Blue Spark Technologies: Inc.", + 0xFEC3: "360fly: Inc.", + 0xFEC4: "PLUS Location Systems", + 0xFEC5: "Realtek Semiconductor Corp.", + 0xFEC6: "Kocomojo: LLC", + 0xFEC7: "Apple: Inc.", + 0xFEC8: "Apple: Inc.", + 0xFEC9: "Apple: Inc.", + 0xFECA: "Apple: Inc.", + 0xFECB: "Apple: Inc.", + 0xFECC: "Apple: Inc.", + 0xFECD: "Apple: Inc.", + 0xFECE: "Apple: Inc.", + 0xFECF: "Apple: Inc.", + 0xFED0: "Apple: Inc.", + 0xFED1: "Apple: Inc.", + 0xFED2: "Apple: Inc.", + 0xFED3: "Apple: Inc.", + 0xFED4: "Apple: Inc.", + 0xFED5: "Plantronics Inc.", + 0xFED6: "Broadcom Corporation", + 0xFED7: "Broadcom Corporation", + 0xFED8: "Google", + 0xFED9: "Pebble Technology Corporation", + 0xFEDA: "ISSC Technologies Corporation", + 0xFEDB: "Perka: Inc.", + 0xFEDC: "Jawbone", + 0xFEDD: "Jawbone", + 0xFEDE: "Coin: Inc.", + 0xFEDF: "Design SHIFT", + 0xFEE0: "Anhui Huami Information Technology Co.", + 0xFEE1: "Anhui Huami Information Technology Co.", + 0xFEE2: "Anki: Inc.", + 0xFEE3: "Anki: Inc.", + 0xFEE4: "Nordic Semiconductor ASA", + 0xFEE5: "Nordic Semiconductor ASA", + 0xFEE6: "Seed Labs: Inc.", + 0xFEE7: "Tencent Holdings Limited", + 0xFEE8: "Quintic Corp.", + 0xFEE9: "Quintic Corp.", + 0xFEEA: "Swirl Networks: Inc.", + 0xFEEB: "Swirl Networks: Inc.", + 0xFEEC: "Tile: Inc.", + 0xFEED: "Tile: Inc.", + 0xFEEE: "Polar Electro Oy", + 0xFEEF: "Polar Electro Oy", + 0xFEF0: "Intel", + 0xFEF1: "CSR", + 0xFEF2: "CSR", + 0xFEF3: "Google", + 0xFEF4: "Google", + 0xFEF5: "Dialog Semiconductor GmbH", + 0xFEF6: "Wicentric: Inc.", + 0xFEF7: "Aplix Corporation", + 0xFEF8: "Aplix Corporation", + 0xFEF9: "PayPal: Inc.", + 0xFEFA: "PayPal: Inc.", + 0xFEFB: "Stollmann E+V GmbH", + 0xFEFC: "Gimbal: Inc.", + 0xFEFD: "Gimbal: Inc.", + 0xFEFE: "GN ReSound A/S", + 0xFEFF: "GN Netcom", + 0xFFFC: "AirFuel Alliance", + 0xFFFD: "Fast IDentity Online Alliance (FIDO)", + 0xFFFE: "Alliance for Wireless Power (A4WP)", +} + +uuid128_dict: dict[str, str] = { + "a3c87500-8ed3-4bdf-8a39-a01bebede295": "Eddystone Configuration Service", + "a3c87501-8ed3-4bdf-8a39-a01bebede295": "Capabilities", + "a3c87502-8ed3-4bdf-8a39-a01bebede295": "Active Slot", + "a3c87503-8ed3-4bdf-8a39-a01bebede295": "Advertising Interval", + "a3c87504-8ed3-4bdf-8a39-a01bebede295": "Radio Tx Power", + "a3c87505-8ed3-4bdf-8a39-a01bebede295": "(Advanced) Advertised Tx Power", + "a3c87506-8ed3-4bdf-8a39-a01bebede295": "Lock State", + "a3c87507-8ed3-4bdf-8a39-a01bebede295": "Unlock", + "a3c87508-8ed3-4bdf-8a39-a01bebede295": "Public ECDH Key", + "a3c87509-8ed3-4bdf-8a39-a01bebede295": "EID Identity Key", + "a3c8750a-8ed3-4bdf-8a39-a01bebede295": "ADV Slot Data", + "a3c8750b-8ed3-4bdf-8a39-a01bebede295": "(Advanced) Factory reset", + "a3c8750c-8ed3-4bdf-8a39-a01bebede295": "(Advanced) Remain Connectable", + # BBC micro:bit Bluetooth Profiles */ + "e95d0753-251d-470a-a062-fa1922dfa9a8": "MicroBit Accelerometer Service", + "e95dca4b-251d-470a-a062-fa1922dfa9a8": "MicroBit Accelerometer Data", + "e95dfb24-251d-470a-a062-fa1922dfa9a8": "MicroBit Accelerometer Period", + "e95df2d8-251d-470a-a062-fa1922dfa9a8": "MicroBit Magnetometer Service", + "e95dfb11-251d-470a-a062-fa1922dfa9a8": "MicroBit Magnetometer Data", + "e95d386c-251d-470a-a062-fa1922dfa9a8": "MicroBit Magnetometer Period", + "e95d9715-251d-470a-a062-fa1922dfa9a8": "MicroBit Magnetometer Bearing", + "e95d9882-251d-470a-a062-fa1922dfa9a8": "MicroBit Button Service", + "e95dda90-251d-470a-a062-fa1922dfa9a8": "MicroBit Button A State", + "e95dda91-251d-470a-a062-fa1922dfa9a8": "MicroBit Button B State", + "e95d127b-251d-470a-a062-fa1922dfa9a8": "MicroBit IO PIN Service", + "e95d8d00-251d-470a-a062-fa1922dfa9a8": "MicroBit PIN Data", + "e95d5899-251d-470a-a062-fa1922dfa9a8": "MicroBit PIN AD Configuration", + "e95dd822-251d-470a-a062-fa1922dfa9a8": "MicroBit PWM Control", + "e95dd91d-251d-470a-a062-fa1922dfa9a8": "MicroBit LED Service", + "e95d7b77-251d-470a-a062-fa1922dfa9a8": "MicroBit LED Matrix state", + "e95d93ee-251d-470a-a062-fa1922dfa9a8": "MicroBit LED Text", + "e95d0d2d-251d-470a-a062-fa1922dfa9a8": "MicroBit Scrolling Delay", + "e95d93af-251d-470a-a062-fa1922dfa9a8": "MicroBit Event Service", + "e95db84c-251d-470a-a062-fa1922dfa9a8": "MicroBit Requirements", + "e95d9775-251d-470a-a062-fa1922dfa9a8": "MicroBit Event Data", + "e95d23c4-251d-470a-a062-fa1922dfa9a8": "MicroBit Client Requirements", + "e95d5404-251d-470a-a062-fa1922dfa9a8": "MicroBit Client Events", + "e95d93b0-251d-470a-a062-fa1922dfa9a8": "MicroBit DFU Control Service", + "e95d93b1-251d-470a-a062-fa1922dfa9a8": "MicroBit DFU Control", + "e95d6100-251d-470a-a062-fa1922dfa9a8": "MicroBit Temperature Service", + "e95d1b25-251d-470a-a062-fa1922dfa9a8": "MicroBit Temperature Period", + # Nordic UART Port Emulation */ + "6e400001-b5a3-f393-e0a9-e50e24dcca9e": "Nordic UART Service", + "6e400003-b5a3-f393-e0a9-e50e24dcca9e": "Nordic UART TX", + "6e400002-b5a3-f393-e0a9-e50e24dcca9e": "Nordic UART RX", + # LEGO + "00001623-1212-efde-1623-785feabcd123": "LEGO Wireless Protocol v3 Hub Service", + "00001624-1212-efde-1623-785feabcd123": "LEGO Wireless Protocol v3 Hub Characteristic", + "00001625-1212-efde-1623-785feabcd123": "LEGO Wireless Protocol v3 Bootloader Service", + "00001626-1212-efde-1623-785feabcd123": "LEGO Wireless Protocol v3 Bootloader Characteristic", + "c5f50001-8280-46da-89f4-6d8051e4aeef": "Pybricks Service", + "c5f50002-8280-46da-89f4-6d8051e4aeef": "Pybricks Characteristic", + # from nRF connect + "be15bee0-6186-407e-8381-0bd89c4d8df4": "Anki Drive Vehicle Service READ", + "be15bee1-6186-407e-8381-0bd89c4d8df4": "Anki Drive Vehicle Service WRITE", + "955a1524-0fe2-f5aa-a094-84b8d4f3e8ad": "Beacon UUID", + "00001524-1212-efde-1523-785feabcd123": "Button", + "8ec90003-f315-4f60-9fb8-838830daea50": "Buttonless DFU", + "955a1525-0fe2-f5aa-a094-84b8d4f3e8ad": "Calibration", + "a6c31338-6c07-453e-961a-d8a8a41bf368": "Candy Control Point", + "955a1528-0fe2-f5aa-a094-84b8d4f3e8ad": "Connection Interval", + "00001531-1212-efde-1523-785feabcd123": "DFU Control Point", + "8ec90001-f315-4f60-9fb8-838830daea50": "DFU Control Point", + "00001532-1212-efde-1523-785feabcd123": "DFU Packet", + "8ec90002-f315-4f60-9fb8-838830daea50": "DFU Packet", + "00001534-1212-efde-1523-785feabcd123": "DFU Version", + "ee0c2084-8786-40ba-ab96-99b91ac981d8": "Data", + "b35d7da9-eed4-4d59-8f89-f6573edea967": "Data Length", + "b35d7da7-eed4-4d59-8f89-f6573edea967": "Data One", + "22eac6e9-24d6-4bb5-be44-b36ace7c7bfb": "Data Source", + "b35d7da8-eed4-4d59-8f89-f6573edea967": "Data Two", + "c6b2f38c-23ab-46d8-a6ab-a3a870bbd5d7": "Entity Attribute", + "2f7cabce-808d-411f-9a0c-bb92ba96c102": "Entity Update", + "ee0c2085-8786-40ba-ab96-99b91ac981d8": "Flags", + "88400002-e95a-844e-c53f-fbec32ed5e54": "Fly Button Characteristic", + "00001525-1212-efde-1523-785feabcd123": "LED", + "955a1529-0fe2-f5aa-a094-84b8d4f3e8ad": "LED Config", + "ee0c2082-8786-40ba-ab96-99b91ac981d8": "Lock", + "ee0c2081-8786-40ba-ab96-99b91ac981d8": "Lock State", + "955a1526-0fe2-f5aa-a094-84b8d4f3e8ad": "Major & Minor", + "955a1527-0fe2-f5aa-a094-84b8d4f3e8ad": "Manufacturer ID", + "9fbf120d-6301-42d9-8c58-25e699a21dbd": "Notification Source", + "ee0c2088-8786-40ba-ab96-99b91ac981d8": "Period", + "ee0c2086-8786-40ba-ab96-99b91ac981d8": "Power Levels", + "ee0c2087-8786-40ba-ab96-99b91ac981d8": "Power Mode", + "9b3c81d8-57b1-4a8a-b8df-0e56f7ca51c2": "Remote Command", + "ee0c2089-8786-40ba-ab96-99b91ac981d8": "Reset", + "da2e7828-fbce-4e01-ae9e-261174997c48": "SMP Characteristic", + "8ec90004-f315-4f60-9fb8-838830daea50": "Secure Buttonless DFU", + "ef680102-9b35-4933-9b10-52ffa9740042": "Thingy Advertising Parameters Characteristic", + "ef680204-9b35-4933-9b10-52ffa9740042": "Thingy Air Quality Characteristic", + "ef680302-9b35-4933-9b10-52ffa9740042": "Thingy Button Characteristic", + "ef680106-9b35-4933-9b10-52ffa9740042": "Thingy Cloud Token Characteristic", + "ef680104-9b35-4933-9b10-52ffa9740042": "Thingy Connection Parameters Characteristic", + "ef680105-9b35-4933-9b10-52ffa9740042": "Thingy Eddystone URL Characteristic", + "ef680206-9b35-4933-9b10-52ffa9740042": "Thingy Environment Configuration Characteristic", + "ef680407-9b35-4933-9b10-52ffa9740042": "Thingy Euler Characteristic", + "ef680303-9b35-4933-9b10-52ffa9740042": "Thingy External Pin Characteristic", + "ef680107-9b35-4933-9b10-52ffa9740042": "Thingy FW Version Characteristic", + "ef68040a-9b35-4933-9b10-52ffa9740042": "Thingy Gravity Vector Characteristic", + "ef680409-9b35-4933-9b10-52ffa9740042": "Thingy Heading Characteristic", + "ef680203-9b35-4933-9b10-52ffa9740042": "Thingy Humidity Characteristic", + "ef680301-9b35-4933-9b10-52ffa9740042": "Thingy LED Characteristic", + "ef680205-9b35-4933-9b10-52ffa9740042": "Thingy Light Intensity Characteristic", + "ef680108-9b35-4933-9b10-52ffa9740042": "Thingy MTU Request Characteristic", + "ef680504-9b35-4933-9b10-52ffa9740042": "Thingy Microphone Characteristic", + "ef680401-9b35-4933-9b10-52ffa9740042": "Thingy Motion Configuration Characteristic", + "ef680101-9b35-4933-9b10-52ffa9740042": "Thingy Name Characteristic", + "ef680403-9b35-4933-9b10-52ffa9740042": "Thingy Orientation Characteristic", + "ef680405-9b35-4933-9b10-52ffa9740042": "Thingy Pedometer Characteristic", + "ef680202-9b35-4933-9b10-52ffa9740042": "Thingy Pressure Characteristic", + "ef680404-9b35-4933-9b10-52ffa9740042": "Thingy Quaternion Characteristic", + "ef680406-9b35-4933-9b10-52ffa9740042": "Thingy Raw Data Characteristic", + "ef680408-9b35-4933-9b10-52ffa9740042": "Thingy Rotation Characteristic", + "ef680501-9b35-4933-9b10-52ffa9740042": "Thingy Sound Configuration Characteristic", + "ef680502-9b35-4933-9b10-52ffa9740042": "Thingy Speaker Data Characteristic", + "ef680503-9b35-4933-9b10-52ffa9740042": "Thingy Speaker Status Characteristic", + "ef680402-9b35-4933-9b10-52ffa9740042": "Thingy Tap Characteristic", + "ef680201-9b35-4933-9b10-52ffa9740042": "Thingy Temperature Characteristic", + "ee0c2083-8786-40ba-ab96-99b91ac981d8": "Unlock", + "e95db9fe-251d-470a-a062-fa1922dfa9a8": "micro:bit Pin IO Configuration", + "e95d9250-251d-470a-a062-fa1922dfa9a8": "micro:bit Temperature", + "be15beef-6186-407e-8381-0bd89c4d8df4": "Anki Drive Vehicle Service", + "7905f431-b5ce-4e99-a40f-4b1e122d00d0": "Apple Notification Center Service", + "d0611e78-bbb4-4591-a5f8-487910ae4366": "Apple Continuity Service", + "8667556c-9a37-4c91-84ed-54ee27d90049": "Apple Continuity Characteristic", + "9fa480e0-4967-4542-9390-d343dc5d04ae": "Apple Nearby Service", + "af0badb1-5b99-43cd-917a-a77bc549e3cc": "Nearby Characteristic", + "69d1d8f3-45e1-49a8-9821-9bbdfdaad9d9": "Control Point", + "89d3502b-0f36-433a-8ef4-c502ad55f8dc": "Apple Media Service", + "955a1523-0fe2-f5aa-a094-84b8d4f3e8ad": "Beacon Config", + "a6c31337-6c07-453e-961a-d8a8a41bf368": "Candy Dispenser Service", + "00001530-1212-efde-1523-785feabcd123": "Device Firmware Update Service", + "88400001-e95a-844e-c53f-fbec32ed5e54": "Digital Bird Service", + "ee0c2080-8786-40ba-ab96-99b91ac981d8": "Eddystone-URL Configuration Service", + "8e400001-f315-4f60-9fb8-838830daea50": "Experimental Buttonless DFU Service", + "00001523-1212-efde-1523-785feabcd123": "Nordic LED Button Service", + "8d53dc1d-1db7-4cd3-868b-8a527460aa84": "SMP Service", + "ef680100-9b35-4933-9b10-52ffa9740042": "Thingy Configuration Service", + "ef680200-9b35-4933-9b10-52ffa9740042": "Thingy Environment Service", + "ef680400-9b35-4933-9b10-52ffa9740042": "Thingy Motion Service", + "ef680500-9b35-4933-9b10-52ffa9740042": "Thingy Sound Service", + "ef680300-9b35-4933-9b10-52ffa9740042": "Thingy User Interface Service", + "b35d7da6-eed4-4d59-8f89-f6573edea967": "URI Beacon Config (V1)", +} + + +def uuidstr_to_str(uuid_: str) -> str: + uuid_ = uuid_.lower() + s = uuid128_dict.get(uuid_) + if s: + return s + + if not s and uuid_.endswith("-0000-1000-8000-00805f9b34fb"): + s = "Vendor specific" + v = int(uuid_[:8], 16) + if (v & 0xFFFF0000) == 0x0000: + s = uuid16_dict.get(v & 0x0000FFFF, s) + if not s: + return "Unknown" + + return s + + +def register_uuids(uuids_to_descriptions: dict[str, str]) -> None: + """Add or modify the mapping of 128-bit UUIDs for services and characteristics to descriptions. + + Args: + uuids_to_descriptions: A dictionary of new mappings + + """ + uuid128_dict.update(uuids_to_descriptions) + + +def normalize_uuid_str(uuid: str) -> str: + """ + Normaizes a UUID to the format used by Bleak. + + - Converted to lower case. + - 16-bit and 32-bit UUIDs are expanded to 128-bit. + + Example:: + + # 16-bit + uuid1 = normalize_uuid_str("1234") + # uuid1 == "00001234-0000-1000-8000-00805f9b34fb" + + # 32-bit + uuid2 = normalize_uuid_str("12345678") + # uuid2 == "12345678-0000-1000-8000-00805f9b34fb" + + # 128-bit + uuid3 = normalize_uuid_str("12345678-0000-1234-1234-1234567890ABC") + # uuid3 == "12345678-0000-1234-1234-1234567890abc" + + .. versionadded:: 0.20 + .. versionchanged:: 0.21 + Added support for 32-bit UUIDs. + """ + # See: BLUETOOTH CORE SPECIFICATION Version 5.4 | Vol 3, Part B - Section 2.5.1 + if len(uuid) == 4: + # Bluetooth SIG registered 16-bit UUIDs + uuid = f"0000{uuid}-0000-1000-8000-00805f9b34fb" + elif len(uuid) == 8: + # Bluetooth SIG registered 32-bit UUIDs + uuid = f"{uuid}-0000-1000-8000-00805f9b34fb" + + # let UUID class do the validation and conversion to lower case + return str(UUID(uuid)) + + +def normalize_uuid_16(uuid: int) -> str: + """ + Normaizes a 16-bit integer UUID to the format used by Bleak. + + Returns: + 128-bit UUID as string with the format ``"0000xxxx-0000-1000-8000-00805f9b34fb"``. + + Example:: + + uuid = normalize_uuid_16(0x1234) + # uuid == "00001234-0000-1000-8000-00805f9b34fb" + + .. versionadded:: 0.21 + """ + return normalize_uuid_str(f"{uuid:04X}") + + +def normalize_uuid_32(uuid: int) -> str: + """ + Normaizes a 32-bit integer UUID to the format used by Bleak. + + Returns: + 128-bit UUID as string with the format ``"xxxxxxxx-0000-1000-8000-00805f9b34fb"``. + + Example:: + + uuid = normalize_uuid_32(0x12345678) + # uuid == "12345678-0000-1000-8000-00805f9b34fb" + + .. versionadded:: 0.21 + """ + return normalize_uuid_str(f"{uuid:08X}") diff --git a/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/DESCRIPTION.rst b/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/DESCRIPTION.rst new file mode 100644 index 0000000..606dcaa --- /dev/null +++ b/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/DESCRIPTION.rst @@ -0,0 +1,13 @@ +Python Serial Port Extension for Win32, OSX, Linux, BSD, Jython, IronPython + +Stable: + +- Documentation: http://pythonhosted.org/pyserial/ +- Download Page: https://pypi.python.org/pypi/pyserial + +Latest: + +- Documentation: http://pyserial.readthedocs.io/en/latest/ +- Project Homepage: https://github.com/pyserial/pyserial + + diff --git a/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/INSTALLER b/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/METADATA b/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/METADATA new file mode 100644 index 0000000..363d8e1 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/METADATA @@ -0,0 +1,47 @@ +Metadata-Version: 2.0 +Name: pyserial +Version: 3.5 +Summary: Python Serial Port Extension +Home-page: https://github.com/pyserial/pyserial +Author: Chris Liechti +Author-email: cliechti@gmx.net +License: BSD +Platform: any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: End Users/Desktop +Classifier: License :: OSI Approved :: BSD License +Classifier: Natural Language :: English +Classifier: Operating System :: POSIX +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Topic :: Communications +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: Terminals :: Serial +Provides-Extra: cp2110 +Provides-Extra: cp2110 +Requires-Dist: hidapi; extra == 'cp2110' + +Python Serial Port Extension for Win32, OSX, Linux, BSD, Jython, IronPython + +Stable: + +- Documentation: http://pythonhosted.org/pyserial/ +- Download Page: https://pypi.python.org/pypi/pyserial + +Latest: + +- Documentation: http://pyserial.readthedocs.io/en/latest/ +- Project Homepage: https://github.com/pyserial/pyserial + + diff --git a/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/RECORD b/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/RECORD new file mode 100644 index 0000000..a3e60ad --- /dev/null +++ b/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/RECORD @@ -0,0 +1,67 @@ +../../bin/pyserial-miniterm.exe,sha256=zhcxp6QVlvLFzKGSJ7WWylR5Z4lFDWq7mFExDSsR4-Q,108429 +../../bin/pyserial-ports.exe,sha256=Xg3I50FqtVja2fv7HktG12gYWthpHs28HcrXgsJRgg0,108431 +pyserial-3.5.dist-info/DESCRIPTION.rst,sha256=rXXIUFeAsfXq2YS7DGkztGmXez-G7gAwbwdBL8t9KME,320 +pyserial-3.5.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +pyserial-3.5.dist-info/METADATA,sha256=QqirfpTvC3uqfpTNrGXWuSVMYIR29jASDJkAB79HKUM,1650 +pyserial-3.5.dist-info/RECORD,, +pyserial-3.5.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pyserial-3.5.dist-info/WHEEL,sha256=kdsN-5OJAZIiHN-iO4Rhl82KyS0bDWf4uBwMbkNafr8,110 +pyserial-3.5.dist-info/entry_points.txt,sha256=-AQ3oVmIn7rtW5Dh0Oup90Hq0qkIlMj79qGmdDIXk9U,112 +pyserial-3.5.dist-info/metadata.json,sha256=s5rFXxQKL9QXO3UMXRmYoMXGRQt2lol67rf_64S1v10,1647 +pyserial-3.5.dist-info/top_level.txt,sha256=FSjfWHWw-VjPiEqOhttbiP-F8OHn-liixq1wKL2fWOA,7 +serial/__init__.py,sha256=XeyJf970Wg6vY-rNoeAdYuHnVJAwJYSuAjj3U3ZZI0Q,3212 +serial/__main__.py,sha256=oSpVknDS2Yqn2JdXlDs5Fk0E8ccdiLIJaXvPWUizQj0,45 +serial/__pycache__/__init__.cpython-312.pyc,, +serial/__pycache__/__main__.cpython-312.pyc,, +serial/__pycache__/rfc2217.cpython-312.pyc,, +serial/__pycache__/rs485.cpython-312.pyc,, +serial/__pycache__/serialcli.cpython-312.pyc,, +serial/__pycache__/serialjava.cpython-312.pyc,, +serial/__pycache__/serialposix.cpython-312.pyc,, +serial/__pycache__/serialutil.cpython-312.pyc,, +serial/__pycache__/serialwin32.cpython-312.pyc,, +serial/__pycache__/win32.cpython-312.pyc,, +serial/rfc2217.py,sha256=ncG_5Ts42M_Tm_7XN3Q7iE24y-lGcwu2jC3MFSEv6Bc,59700 +serial/rs485.py,sha256=9t6yuGcte36gk8G1U6NgboKVGtJUFqtbpAOXj7vYxM0,3305 +serial/serialcli.py,sha256=u5QnG90UxttqsGG9nYgkj0GUyb0wIOxzlUgxJ4gCczg,9190 +serial/serialjava.py,sha256=AcHLp2D_sAihu7L_wCcg8mtk7etf6zAyB4L_tuthVo8,8480 +serial/serialposix.py,sha256=XVb5hRM5HhdmoYR6BOLhICztQXKhUoA7ocgjoUmptvk,35127 +serial/serialutil.py,sha256=PIT4x8MZ8WGoXW-Ntb7cT2UlVxRYm9y-7m8tr2WhfAo,21797 +serial/serialwin32.py,sha256=F2geqaZQEgxx2xqum4iBMBpA04xHs0uw2gUOP1v7vgA,20284 +serial/threaded/__init__.py,sha256=ikXlKYejRlzzCze9kwxR_uABKa1YfuTyqcPjw3VRV1I,9319 +serial/threaded/__pycache__/__init__.cpython-312.pyc,, +serial/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +serial/tools/__pycache__/__init__.cpython-312.pyc,, +serial/tools/__pycache__/hexlify_codec.cpython-312.pyc,, +serial/tools/__pycache__/list_ports.cpython-312.pyc,, +serial/tools/__pycache__/list_ports_common.cpython-312.pyc,, +serial/tools/__pycache__/list_ports_linux.cpython-312.pyc,, +serial/tools/__pycache__/list_ports_osx.cpython-312.pyc,, +serial/tools/__pycache__/list_ports_posix.cpython-312.pyc,, +serial/tools/__pycache__/list_ports_windows.cpython-312.pyc,, +serial/tools/__pycache__/miniterm.cpython-312.pyc,, +serial/tools/hexlify_codec.py,sha256=FRJSO8pfjM6AR9_SBqL34e50LVkvlzfFKdmCScGn408,3677 +serial/tools/list_ports.py,sha256=eDDoyIhoS3f9D3CVpthqlQUqiR2l-X0VTGGOBjuM4ew,3389 +serial/tools/list_ports_common.py,sha256=x5HIghG4NIz-Xf5iX6Gk7xZfdeads2tqCsfyJhh3Ifs,3736 +serial/tools/list_ports_linux.py,sha256=UnU1VYP1NJI7J8Zn7gY-A2mbi1lugbFZSVztfX8P1pU,4503 +serial/tools/list_ports_osx.py,sha256=eoefMGiuJqC-OCu9aAWqqJX75wGlBzoqZ6kdmMA82LM,11178 +serial/tools/list_ports_posix.py,sha256=EYqD5kRbk0f2a5scaRS4tgWGBynkpVH77ja_G6S3UhE,4535 +serial/tools/list_ports_windows.py,sha256=U4EzcOAiU66LWoPdMXI2oOM4LCd5vKwO3DgeTT6M3qc,16021 +serial/tools/miniterm.py,sha256=fXvkEU9FEyU7HPSNE8bdX2OCgiGYgCee066yF58nots,37840 +serial/urlhandler/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +serial/urlhandler/__pycache__/__init__.cpython-312.pyc,, +serial/urlhandler/__pycache__/protocol_alt.cpython-312.pyc,, +serial/urlhandler/__pycache__/protocol_cp2110.cpython-312.pyc,, +serial/urlhandler/__pycache__/protocol_hwgrep.cpython-312.pyc,, +serial/urlhandler/__pycache__/protocol_loop.cpython-312.pyc,, +serial/urlhandler/__pycache__/protocol_rfc2217.cpython-312.pyc,, +serial/urlhandler/__pycache__/protocol_socket.cpython-312.pyc,, +serial/urlhandler/__pycache__/protocol_spy.cpython-312.pyc,, +serial/urlhandler/protocol_alt.py,sha256=-kYoCgy9GyMWN6wC8Oew8FL04LjL4Ntx3HHVqSTKGcQ,2033 +serial/urlhandler/protocol_cp2110.py,sha256=iULOT4Vdw20P_w2jfSWdt0roUY1Ku8xJVHHYc6d3ImY,8540 +serial/urlhandler/protocol_hwgrep.py,sha256=GdKdQ9tExKRHJzsiPcQ9ExmaLa6-A71q52i4jQxmoBk,3159 +serial/urlhandler/protocol_loop.py,sha256=5barru_hfwNkayjjBz4w3snBJn0G7C-fG7-QmHaeTWo,10623 +serial/urlhandler/protocol_rfc2217.py,sha256=IPO8r3pFN6yEDl1Zv2jgUnfIa0tQ0iY0ZsD8_xhAUeQ,317 +serial/urlhandler/protocol_socket.py,sha256=QotaHCPd6t903W_9fa2Lv_5uElq0noir2Ci10a987XM,14299 +serial/urlhandler/protocol_spy.py,sha256=FdUaU43-bl1KXTy_S4HhzyGV5hpUsopm8IqTrX2VX-4,9130 +serial/win32.py,sha256=lk6rod9mHkqzgchmaqB3ygiTkmAWTNQ00IJ985ZjvTI,11138 diff --git a/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/REQUESTED b/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/WHEEL b/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/WHEEL new file mode 100644 index 0000000..7332a41 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.30.0) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/entry_points.txt b/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/entry_points.txt new file mode 100644 index 0000000..b69a613 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/entry_points.txt @@ -0,0 +1,4 @@ +[console_scripts] +pyserial-miniterm = serial.tools.miniterm:main +pyserial-ports = serial.tools.list_ports:main + diff --git a/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/metadata.json b/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/metadata.json new file mode 100644 index 0000000..5064763 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/metadata.json @@ -0,0 +1 @@ +{"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: BSD License", "Natural Language :: English", "Operating System :: POSIX", "Operating System :: Microsoft :: Windows", "Operating System :: MacOS :: MacOS X", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Topic :: Communications", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Terminals :: Serial"], "extensions": {"python.commands": {"wrap_console": {"pyserial-miniterm": "serial.tools.miniterm:main", "pyserial-ports": "serial.tools.list_ports:main"}}, "python.details": {"contacts": [{"email": "cliechti@gmx.net", "name": "Chris Liechti", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/pyserial/pyserial"}}, "python.exports": {"console_scripts": {"pyserial-miniterm": "serial.tools.miniterm:main", "pyserial-ports": "serial.tools.list_ports:main"}}}, "extras": ["cp2110"], "generator": "bdist_wheel (0.30.0)", "license": "BSD", "metadata_version": "2.0", "name": "pyserial", "platform": "any", "run_requires": [{"extra": "cp2110", "requires": ["hidapi"]}], "summary": "Python Serial Port Extension", "version": "3.5"} \ No newline at end of file diff --git a/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/top_level.txt b/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/top_level.txt new file mode 100644 index 0000000..b6be06a --- /dev/null +++ b/tools/python-embed/Lib/site-packages/pyserial-3.5.dist-info/top_level.txt @@ -0,0 +1 @@ +serial diff --git a/tools/python-embed/Lib/site-packages/serial/__init__.py b/tools/python-embed/Lib/site-packages/serial/__init__.py new file mode 100644 index 0000000..caa4de1 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/serial/__init__.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# +# This is a wrapper module for different platform implementations +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2001-2020 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import absolute_import + +import sys +import importlib + +from serial.serialutil import * +#~ SerialBase, SerialException, to_bytes, iterbytes + +__version__ = '3.5' + +VERSION = __version__ + +# pylint: disable=wrong-import-position +if sys.platform == 'cli': + from serial.serialcli import Serial +else: + import os + # chose an implementation, depending on os + if os.name == 'nt': # sys.platform == 'win32': + from serial.serialwin32 import Serial + elif os.name == 'posix': + from serial.serialposix import Serial, PosixPollSerial, VTIMESerial # noqa + elif os.name == 'java': + from serial.serialjava import Serial + else: + raise ImportError("Sorry: no implementation for your platform ('{}') available".format(os.name)) + + +protocol_handler_packages = [ + 'serial.urlhandler', +] + + +def serial_for_url(url, *args, **kwargs): + """\ + Get an instance of the Serial class, depending on port/url. The port is not + opened when the keyword parameter 'do_not_open' is true, by default it + is. All other parameters are directly passed to the __init__ method when + the port is instantiated. + + The list of package names that is searched for protocol handlers is kept in + ``protocol_handler_packages``. + + e.g. we want to support a URL ``foobar://``. A module + ``my_handlers.protocol_foobar`` is provided by the user. Then + ``protocol_handler_packages.append("my_handlers")`` would extend the search + path so that ``serial_for_url("foobar://"))`` would work. + """ + # check and remove extra parameter to not confuse the Serial class + do_open = not kwargs.pop('do_not_open', False) + # the default is to use the native implementation + klass = Serial + try: + url_lowercase = url.lower() + except AttributeError: + # it's not a string, use default + pass + else: + # if it is an URL, try to import the handler module from the list of possible packages + if '://' in url_lowercase: + protocol = url_lowercase.split('://', 1)[0] + module_name = '.protocol_{}'.format(protocol) + for package_name in protocol_handler_packages: + try: + importlib.import_module(package_name) + handler_module = importlib.import_module(module_name, package_name) + except ImportError: + continue + else: + if hasattr(handler_module, 'serial_class_for_url'): + url, klass = handler_module.serial_class_for_url(url) + else: + klass = handler_module.Serial + break + else: + raise ValueError('invalid URL, protocol {!r} not known'.format(protocol)) + # instantiate and open when desired + instance = klass(None, *args, **kwargs) + instance.port = url + if do_open: + instance.open() + return instance diff --git a/tools/python-embed/Lib/site-packages/serial/__main__.py b/tools/python-embed/Lib/site-packages/serial/__main__.py new file mode 100644 index 0000000..bd0a2e6 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/serial/__main__.py @@ -0,0 +1,3 @@ +from .tools import miniterm + +miniterm.main() diff --git a/tools/python-embed/Lib/site-packages/serial/__pycache__/__init__.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..0b09e25 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/__pycache__/__init__.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/__pycache__/__main__.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/__pycache__/__main__.cpython-312.pyc new file mode 100644 index 0000000..31f0d2c Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/__pycache__/__main__.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/__pycache__/rfc2217.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/__pycache__/rfc2217.cpython-312.pyc new file mode 100644 index 0000000..917c95f Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/__pycache__/rfc2217.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/__pycache__/rs485.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/__pycache__/rs485.cpython-312.pyc new file mode 100644 index 0000000..ce49879 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/__pycache__/rs485.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/__pycache__/serialcli.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/__pycache__/serialcli.cpython-312.pyc new file mode 100644 index 0000000..41b9c74 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/__pycache__/serialcli.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/__pycache__/serialjava.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/__pycache__/serialjava.cpython-312.pyc new file mode 100644 index 0000000..8691e42 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/__pycache__/serialjava.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/__pycache__/serialposix.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/__pycache__/serialposix.cpython-312.pyc new file mode 100644 index 0000000..14096db Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/__pycache__/serialposix.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/__pycache__/serialutil.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/__pycache__/serialutil.cpython-312.pyc new file mode 100644 index 0000000..e29b927 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/__pycache__/serialutil.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/__pycache__/serialwin32.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/__pycache__/serialwin32.cpython-312.pyc new file mode 100644 index 0000000..581b46e Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/__pycache__/serialwin32.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/__pycache__/win32.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/__pycache__/win32.cpython-312.pyc new file mode 100644 index 0000000..ada2873 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/__pycache__/win32.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/rfc2217.py b/tools/python-embed/Lib/site-packages/serial/rfc2217.py new file mode 100644 index 0000000..2ae188e --- /dev/null +++ b/tools/python-embed/Lib/site-packages/serial/rfc2217.py @@ -0,0 +1,1351 @@ +#! python +# +# This module implements a RFC2217 compatible client. RF2217 descibes a +# protocol to access serial ports over TCP/IP and allows setting the baud rate, +# modem control lines etc. +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2001-2015 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause + +# TODO: +# - setting control line -> answer is not checked (had problems with one of the +# severs). consider implementing a compatibility mode flag to make check +# conditional +# - write timeout not implemented at all + +# ########################################################################### +# observations and issues with servers +# =========================================================================== +# sredird V2.2.1 +# - http://www.ibiblio.org/pub/Linux/system/serial/ sredird-2.2.2.tar.gz +# - does not acknowledge SET_CONTROL (RTS/DTR) correctly, always responding +# [105 1] instead of the actual value. +# - SET_BAUDRATE answer contains 4 extra null bytes -> probably for larger +# numbers than 2**32? +# - To get the signature [COM_PORT_OPTION 0] has to be sent. +# - run a server: while true; do nc -l -p 7000 -c "sredird debug /dev/ttyUSB0 /var/lock/sredir"; done +# =========================================================================== +# telnetcpcd (untested) +# - http://ftp.wayne.edu/kermit/sredird/telnetcpcd-1.09.tar.gz +# - To get the signature [COM_PORT_OPTION] w/o data has to be sent. +# =========================================================================== +# ser2net +# - does not negotiate BINARY or COM_PORT_OPTION for his side but at least +# acknowledges that the client activates these options +# - The configuration may be that the server prints a banner. As this client +# implementation does a flushInput on connect, this banner is hidden from +# the user application. +# - NOTIFY_MODEMSTATE: the poll interval of the server seems to be one +# second. +# - To get the signature [COM_PORT_OPTION 0] has to be sent. +# - run a server: run ser2net daemon, in /etc/ser2net.conf: +# 2000:telnet:0:/dev/ttyS0:9600 remctl banner +# ########################################################################### + +# How to identify ports? pySerial might want to support other protocols in the +# future, so lets use an URL scheme. +# for RFC2217 compliant servers we will use this: +# rfc2217://:[?option[&option...]] +# +# options: +# - "logging" set log level print diagnostic messages (e.g. "logging=debug") +# - "ign_set_control": do not look at the answers to SET_CONTROL +# - "poll_modem": issue NOTIFY_MODEMSTATE requests when CTS/DTR/RI/CD is read. +# Without this option it expects that the server sends notifications +# automatically on change (which most servers do and is according to the +# RFC). +# the order of the options is not relevant + +from __future__ import absolute_import + +import logging +import socket +import struct +import threading +import time +try: + import urlparse +except ImportError: + import urllib.parse as urlparse +try: + import Queue +except ImportError: + import queue as Queue + +import serial +from serial.serialutil import SerialBase, SerialException, to_bytes, \ + iterbytes, PortNotOpenError, Timeout + +# port string is expected to be something like this: +# rfc2217://host:port +# host may be an IP or including domain, whatever. +# port is 0...65535 + +# map log level names to constants. used in from_url() +LOGGER_LEVELS = { + 'debug': logging.DEBUG, + 'info': logging.INFO, + 'warning': logging.WARNING, + 'error': logging.ERROR, +} + + +# telnet protocol characters +SE = b'\xf0' # Subnegotiation End +NOP = b'\xf1' # No Operation +DM = b'\xf2' # Data Mark +BRK = b'\xf3' # Break +IP = b'\xf4' # Interrupt process +AO = b'\xf5' # Abort output +AYT = b'\xf6' # Are You There +EC = b'\xf7' # Erase Character +EL = b'\xf8' # Erase Line +GA = b'\xf9' # Go Ahead +SB = b'\xfa' # Subnegotiation Begin +WILL = b'\xfb' +WONT = b'\xfc' +DO = b'\xfd' +DONT = b'\xfe' +IAC = b'\xff' # Interpret As Command +IAC_DOUBLED = b'\xff\xff' + +# selected telnet options +BINARY = b'\x00' # 8-bit data path +ECHO = b'\x01' # echo +SGA = b'\x03' # suppress go ahead + +# RFC2217 +COM_PORT_OPTION = b'\x2c' + +# Client to Access Server +SET_BAUDRATE = b'\x01' +SET_DATASIZE = b'\x02' +SET_PARITY = b'\x03' +SET_STOPSIZE = b'\x04' +SET_CONTROL = b'\x05' +NOTIFY_LINESTATE = b'\x06' +NOTIFY_MODEMSTATE = b'\x07' +FLOWCONTROL_SUSPEND = b'\x08' +FLOWCONTROL_RESUME = b'\x09' +SET_LINESTATE_MASK = b'\x0a' +SET_MODEMSTATE_MASK = b'\x0b' +PURGE_DATA = b'\x0c' + +SERVER_SET_BAUDRATE = b'\x65' +SERVER_SET_DATASIZE = b'\x66' +SERVER_SET_PARITY = b'\x67' +SERVER_SET_STOPSIZE = b'\x68' +SERVER_SET_CONTROL = b'\x69' +SERVER_NOTIFY_LINESTATE = b'\x6a' +SERVER_NOTIFY_MODEMSTATE = b'\x6b' +SERVER_FLOWCONTROL_SUSPEND = b'\x6c' +SERVER_FLOWCONTROL_RESUME = b'\x6d' +SERVER_SET_LINESTATE_MASK = b'\x6e' +SERVER_SET_MODEMSTATE_MASK = b'\x6f' +SERVER_PURGE_DATA = b'\x70' + +RFC2217_ANSWER_MAP = { + SET_BAUDRATE: SERVER_SET_BAUDRATE, + SET_DATASIZE: SERVER_SET_DATASIZE, + SET_PARITY: SERVER_SET_PARITY, + SET_STOPSIZE: SERVER_SET_STOPSIZE, + SET_CONTROL: SERVER_SET_CONTROL, + NOTIFY_LINESTATE: SERVER_NOTIFY_LINESTATE, + NOTIFY_MODEMSTATE: SERVER_NOTIFY_MODEMSTATE, + FLOWCONTROL_SUSPEND: SERVER_FLOWCONTROL_SUSPEND, + FLOWCONTROL_RESUME: SERVER_FLOWCONTROL_RESUME, + SET_LINESTATE_MASK: SERVER_SET_LINESTATE_MASK, + SET_MODEMSTATE_MASK: SERVER_SET_MODEMSTATE_MASK, + PURGE_DATA: SERVER_PURGE_DATA, +} + +SET_CONTROL_REQ_FLOW_SETTING = b'\x00' # Request Com Port Flow Control Setting (outbound/both) +SET_CONTROL_USE_NO_FLOW_CONTROL = b'\x01' # Use No Flow Control (outbound/both) +SET_CONTROL_USE_SW_FLOW_CONTROL = b'\x02' # Use XON/XOFF Flow Control (outbound/both) +SET_CONTROL_USE_HW_FLOW_CONTROL = b'\x03' # Use HARDWARE Flow Control (outbound/both) +SET_CONTROL_REQ_BREAK_STATE = b'\x04' # Request BREAK State +SET_CONTROL_BREAK_ON = b'\x05' # Set BREAK State ON +SET_CONTROL_BREAK_OFF = b'\x06' # Set BREAK State OFF +SET_CONTROL_REQ_DTR = b'\x07' # Request DTR Signal State +SET_CONTROL_DTR_ON = b'\x08' # Set DTR Signal State ON +SET_CONTROL_DTR_OFF = b'\x09' # Set DTR Signal State OFF +SET_CONTROL_REQ_RTS = b'\x0a' # Request RTS Signal State +SET_CONTROL_RTS_ON = b'\x0b' # Set RTS Signal State ON +SET_CONTROL_RTS_OFF = b'\x0c' # Set RTS Signal State OFF +SET_CONTROL_REQ_FLOW_SETTING_IN = b'\x0d' # Request Com Port Flow Control Setting (inbound) +SET_CONTROL_USE_NO_FLOW_CONTROL_IN = b'\x0e' # Use No Flow Control (inbound) +SET_CONTROL_USE_SW_FLOW_CONTOL_IN = b'\x0f' # Use XON/XOFF Flow Control (inbound) +SET_CONTROL_USE_HW_FLOW_CONTOL_IN = b'\x10' # Use HARDWARE Flow Control (inbound) +SET_CONTROL_USE_DCD_FLOW_CONTROL = b'\x11' # Use DCD Flow Control (outbound/both) +SET_CONTROL_USE_DTR_FLOW_CONTROL = b'\x12' # Use DTR Flow Control (inbound) +SET_CONTROL_USE_DSR_FLOW_CONTROL = b'\x13' # Use DSR Flow Control (outbound/both) + +LINESTATE_MASK_TIMEOUT = 128 # Time-out Error +LINESTATE_MASK_SHIFTREG_EMPTY = 64 # Transfer Shift Register Empty +LINESTATE_MASK_TRANSREG_EMPTY = 32 # Transfer Holding Register Empty +LINESTATE_MASK_BREAK_DETECT = 16 # Break-detect Error +LINESTATE_MASK_FRAMING_ERROR = 8 # Framing Error +LINESTATE_MASK_PARTIY_ERROR = 4 # Parity Error +LINESTATE_MASK_OVERRUN_ERROR = 2 # Overrun Error +LINESTATE_MASK_DATA_READY = 1 # Data Ready + +MODEMSTATE_MASK_CD = 128 # Receive Line Signal Detect (also known as Carrier Detect) +MODEMSTATE_MASK_RI = 64 # Ring Indicator +MODEMSTATE_MASK_DSR = 32 # Data-Set-Ready Signal State +MODEMSTATE_MASK_CTS = 16 # Clear-To-Send Signal State +MODEMSTATE_MASK_CD_CHANGE = 8 # Delta Receive Line Signal Detect +MODEMSTATE_MASK_RI_CHANGE = 4 # Trailing-edge Ring Detector +MODEMSTATE_MASK_DSR_CHANGE = 2 # Delta Data-Set-Ready +MODEMSTATE_MASK_CTS_CHANGE = 1 # Delta Clear-To-Send + +PURGE_RECEIVE_BUFFER = b'\x01' # Purge access server receive data buffer +PURGE_TRANSMIT_BUFFER = b'\x02' # Purge access server transmit data buffer +PURGE_BOTH_BUFFERS = b'\x03' # Purge both the access server receive data + # buffer and the access server transmit data buffer + + +RFC2217_PARITY_MAP = { + serial.PARITY_NONE: 1, + serial.PARITY_ODD: 2, + serial.PARITY_EVEN: 3, + serial.PARITY_MARK: 4, + serial.PARITY_SPACE: 5, +} +RFC2217_REVERSE_PARITY_MAP = dict((v, k) for k, v in RFC2217_PARITY_MAP.items()) + +RFC2217_STOPBIT_MAP = { + serial.STOPBITS_ONE: 1, + serial.STOPBITS_ONE_POINT_FIVE: 3, + serial.STOPBITS_TWO: 2, +} +RFC2217_REVERSE_STOPBIT_MAP = dict((v, k) for k, v in RFC2217_STOPBIT_MAP.items()) + +# Telnet filter states +M_NORMAL = 0 +M_IAC_SEEN = 1 +M_NEGOTIATE = 2 + +# TelnetOption and TelnetSubnegotiation states +REQUESTED = 'REQUESTED' +ACTIVE = 'ACTIVE' +INACTIVE = 'INACTIVE' +REALLY_INACTIVE = 'REALLY_INACTIVE' + + +class TelnetOption(object): + """Manage a single telnet option, keeps track of DO/DONT WILL/WONT.""" + + def __init__(self, connection, name, option, send_yes, send_no, ack_yes, + ack_no, initial_state, activation_callback=None): + """\ + Initialize option. + :param connection: connection used to transmit answers + :param name: a readable name for debug outputs + :param send_yes: what to send when option is to be enabled. + :param send_no: what to send when option is to be disabled. + :param ack_yes: what to expect when remote agrees on option. + :param ack_no: what to expect when remote disagrees on option. + :param initial_state: options initialized with REQUESTED are tried to + be enabled on startup. use INACTIVE for all others. + """ + self.connection = connection + self.name = name + self.option = option + self.send_yes = send_yes + self.send_no = send_no + self.ack_yes = ack_yes + self.ack_no = ack_no + self.state = initial_state + self.active = False + self.activation_callback = activation_callback + + def __repr__(self): + """String for debug outputs""" + return "{o.name}:{o.active}({o.state})".format(o=self) + + def process_incoming(self, command): + """\ + A DO/DONT/WILL/WONT was received for this option, update state and + answer when needed. + """ + if command == self.ack_yes: + if self.state is REQUESTED: + self.state = ACTIVE + self.active = True + if self.activation_callback is not None: + self.activation_callback() + elif self.state is ACTIVE: + pass + elif self.state is INACTIVE: + self.state = ACTIVE + self.connection.telnet_send_option(self.send_yes, self.option) + self.active = True + if self.activation_callback is not None: + self.activation_callback() + elif self.state is REALLY_INACTIVE: + self.connection.telnet_send_option(self.send_no, self.option) + else: + raise ValueError('option in illegal state {!r}'.format(self)) + elif command == self.ack_no: + if self.state is REQUESTED: + self.state = INACTIVE + self.active = False + elif self.state is ACTIVE: + self.state = INACTIVE + self.connection.telnet_send_option(self.send_no, self.option) + self.active = False + elif self.state is INACTIVE: + pass + elif self.state is REALLY_INACTIVE: + pass + else: + raise ValueError('option in illegal state {!r}'.format(self)) + + +class TelnetSubnegotiation(object): + """\ + A object to handle subnegotiation of options. In this case actually + sub-sub options for RFC 2217. It is used to track com port options. + """ + + def __init__(self, connection, name, option, ack_option=None): + if ack_option is None: + ack_option = option + self.connection = connection + self.name = name + self.option = option + self.value = None + self.ack_option = ack_option + self.state = INACTIVE + + def __repr__(self): + """String for debug outputs.""" + return "{sn.name}:{sn.state}".format(sn=self) + + def set(self, value): + """\ + Request a change of the value. a request is sent to the server. if + the client needs to know if the change is performed he has to check the + state of this object. + """ + self.value = value + self.state = REQUESTED + self.connection.rfc2217_send_subnegotiation(self.option, self.value) + if self.connection.logger: + self.connection.logger.debug("SB Requesting {} -> {!r}".format(self.name, self.value)) + + def is_ready(self): + """\ + Check if answer from server has been received. when server rejects + the change, raise a ValueError. + """ + if self.state == REALLY_INACTIVE: + raise ValueError("remote rejected value for option {!r}".format(self.name)) + return self.state == ACTIVE + # add property to have a similar interface as TelnetOption + active = property(is_ready) + + def wait(self, timeout=3): + """\ + Wait until the subnegotiation has been acknowledged or timeout. It + can also throw a value error when the answer from the server does not + match the value sent. + """ + timeout_timer = Timeout(timeout) + while not timeout_timer.expired(): + time.sleep(0.05) # prevent 100% CPU load + if self.is_ready(): + break + else: + raise SerialException("timeout while waiting for option {!r}".format(self.name)) + + def check_answer(self, suboption): + """\ + Check an incoming subnegotiation block. The parameter already has + cut off the header like sub option number and com port option value. + """ + if self.value == suboption[:len(self.value)]: + self.state = ACTIVE + else: + # error propagation done in is_ready + self.state = REALLY_INACTIVE + if self.connection.logger: + self.connection.logger.debug("SB Answer {} -> {!r} -> {}".format(self.name, suboption, self.state)) + + +class Serial(SerialBase): + """Serial port implementation for RFC 2217 remote serial ports.""" + + BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800, + 9600, 19200, 38400, 57600, 115200) + + def __init__(self, *args, **kwargs): + self._thread = None + self._socket = None + self._linestate = 0 + self._modemstate = None + self._modemstate_timeout = Timeout(-1) + self._remote_suspend_flow = False + self._write_lock = None + self.logger = None + self._ignore_set_control_answer = False + self._poll_modem_state = False + self._network_timeout = 3 + self._telnet_options = None + self._rfc2217_port_settings = None + self._rfc2217_options = None + self._read_buffer = None + super(Serial, self).__init__(*args, **kwargs) # must be last call in case of auto-open + + def open(self): + """\ + Open port with current settings. This may throw a SerialException + if the port cannot be opened. + """ + self.logger = None + self._ignore_set_control_answer = False + self._poll_modem_state = False + self._network_timeout = 3 + if self._port is None: + raise SerialException("Port must be configured before it can be used.") + if self.is_open: + raise SerialException("Port is already open.") + try: + self._socket = socket.create_connection(self.from_url(self.portstr), timeout=5) # XXX good value? + self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + except Exception as msg: + self._socket = None + raise SerialException("Could not open port {}: {}".format(self.portstr, msg)) + + # use a thread save queue as buffer. it also simplifies implementing + # the read timeout + self._read_buffer = Queue.Queue() + # to ensure that user writes does not interfere with internal + # telnet/rfc2217 options establish a lock + self._write_lock = threading.Lock() + # name the following separately so that, below, a check can be easily done + mandadory_options = [ + TelnetOption(self, 'we-BINARY', BINARY, WILL, WONT, DO, DONT, INACTIVE), + TelnetOption(self, 'we-RFC2217', COM_PORT_OPTION, WILL, WONT, DO, DONT, REQUESTED), + ] + # all supported telnet options + self._telnet_options = [ + TelnetOption(self, 'ECHO', ECHO, DO, DONT, WILL, WONT, REQUESTED), + TelnetOption(self, 'we-SGA', SGA, WILL, WONT, DO, DONT, REQUESTED), + TelnetOption(self, 'they-SGA', SGA, DO, DONT, WILL, WONT, REQUESTED), + TelnetOption(self, 'they-BINARY', BINARY, DO, DONT, WILL, WONT, INACTIVE), + TelnetOption(self, 'they-RFC2217', COM_PORT_OPTION, DO, DONT, WILL, WONT, REQUESTED), + ] + mandadory_options + # RFC 2217 specific states + # COM port settings + self._rfc2217_port_settings = { + 'baudrate': TelnetSubnegotiation(self, 'baudrate', SET_BAUDRATE, SERVER_SET_BAUDRATE), + 'datasize': TelnetSubnegotiation(self, 'datasize', SET_DATASIZE, SERVER_SET_DATASIZE), + 'parity': TelnetSubnegotiation(self, 'parity', SET_PARITY, SERVER_SET_PARITY), + 'stopsize': TelnetSubnegotiation(self, 'stopsize', SET_STOPSIZE, SERVER_SET_STOPSIZE), + } + # There are more subnegotiation objects, combine all in one dictionary + # for easy access + self._rfc2217_options = { + 'purge': TelnetSubnegotiation(self, 'purge', PURGE_DATA, SERVER_PURGE_DATA), + 'control': TelnetSubnegotiation(self, 'control', SET_CONTROL, SERVER_SET_CONTROL), + } + self._rfc2217_options.update(self._rfc2217_port_settings) + # cache for line and modem states that the server sends to us + self._linestate = 0 + self._modemstate = None + self._modemstate_timeout = Timeout(-1) + # RFC 2217 flow control between server and client + self._remote_suspend_flow = False + + self.is_open = True + self._thread = threading.Thread(target=self._telnet_read_loop) + self._thread.setDaemon(True) + self._thread.setName('pySerial RFC 2217 reader thread for {}'.format(self._port)) + self._thread.start() + + try: # must clean-up if open fails + # negotiate Telnet/RFC 2217 -> send initial requests + for option in self._telnet_options: + if option.state is REQUESTED: + self.telnet_send_option(option.send_yes, option.option) + # now wait until important options are negotiated + timeout = Timeout(self._network_timeout) + while not timeout.expired(): + time.sleep(0.05) # prevent 100% CPU load + if sum(o.active for o in mandadory_options) == sum(o.state != INACTIVE for o in mandadory_options): + break + else: + raise SerialException( + "Remote does not seem to support RFC2217 or BINARY mode {!r}".format(mandadory_options)) + if self.logger: + self.logger.info("Negotiated options: {}".format(self._telnet_options)) + + # fine, go on, set RFC 2217 specific things + self._reconfigure_port() + # all things set up get, now a clean start + if not self._dsrdtr: + self._update_dtr_state() + if not self._rtscts: + self._update_rts_state() + self.reset_input_buffer() + self.reset_output_buffer() + except: + self.close() + raise + + def _reconfigure_port(self): + """Set communication parameters on opened port.""" + if self._socket is None: + raise SerialException("Can only operate on open ports") + + # if self._timeout != 0 and self._interCharTimeout is not None: + # XXX + + if self._write_timeout is not None: + raise NotImplementedError('write_timeout is currently not supported') + # XXX + + # Setup the connection + # to get good performance, all parameter changes are sent first... + if not 0 < self._baudrate < 2 ** 32: + raise ValueError("invalid baudrate: {!r}".format(self._baudrate)) + self._rfc2217_port_settings['baudrate'].set(struct.pack(b'!I', self._baudrate)) + self._rfc2217_port_settings['datasize'].set(struct.pack(b'!B', self._bytesize)) + self._rfc2217_port_settings['parity'].set(struct.pack(b'!B', RFC2217_PARITY_MAP[self._parity])) + self._rfc2217_port_settings['stopsize'].set(struct.pack(b'!B', RFC2217_STOPBIT_MAP[self._stopbits])) + + # and now wait until parameters are active + items = self._rfc2217_port_settings.values() + if self.logger: + self.logger.debug("Negotiating settings: {}".format(items)) + timeout = Timeout(self._network_timeout) + while not timeout.expired(): + time.sleep(0.05) # prevent 100% CPU load + if sum(o.active for o in items) == len(items): + break + else: + raise SerialException("Remote does not accept parameter change (RFC2217): {!r}".format(items)) + if self.logger: + self.logger.info("Negotiated settings: {}".format(items)) + + if self._rtscts and self._xonxoff: + raise ValueError('xonxoff and rtscts together are not supported') + elif self._rtscts: + self.rfc2217_set_control(SET_CONTROL_USE_HW_FLOW_CONTROL) + elif self._xonxoff: + self.rfc2217_set_control(SET_CONTROL_USE_SW_FLOW_CONTROL) + else: + self.rfc2217_set_control(SET_CONTROL_USE_NO_FLOW_CONTROL) + + def close(self): + """Close port""" + self.is_open = False + if self._socket: + try: + self._socket.shutdown(socket.SHUT_RDWR) + self._socket.close() + except: + # ignore errors. + pass + if self._thread: + self._thread.join(7) # XXX more than socket timeout + self._thread = None + # in case of quick reconnects, give the server some time + time.sleep(0.3) + self._socket = None + + def from_url(self, url): + """\ + extract host and port from an URL string, other settings are extracted + an stored in instance + """ + parts = urlparse.urlsplit(url) + if parts.scheme != "rfc2217": + raise SerialException( + 'expected a string in the form ' + '"rfc2217://:[?option[&option...]]": ' + 'not starting with rfc2217:// ({!r})'.format(parts.scheme)) + try: + # process options now, directly altering self + for option, values in urlparse.parse_qs(parts.query, True).items(): + if option == 'logging': + logging.basicConfig() # XXX is that good to call it here? + self.logger = logging.getLogger('pySerial.rfc2217') + self.logger.setLevel(LOGGER_LEVELS[values[0]]) + self.logger.debug('enabled logging') + elif option == 'ign_set_control': + self._ignore_set_control_answer = True + elif option == 'poll_modem': + self._poll_modem_state = True + elif option == 'timeout': + self._network_timeout = float(values[0]) + else: + raise ValueError('unknown option: {!r}'.format(option)) + if not 0 <= parts.port < 65536: + raise ValueError("port not in range 0...65535") + except ValueError as e: + raise SerialException( + 'expected a string in the form ' + '"rfc2217://:[?option[&option...]]": {}'.format(e)) + return (parts.hostname, parts.port) + + # - - - - - - - - - - - - - - - - - - - - - - - - + + @property + def in_waiting(self): + """Return the number of bytes currently in the input buffer.""" + if not self.is_open: + raise PortNotOpenError() + return self._read_buffer.qsize() + + def read(self, size=1): + """\ + Read size bytes from the serial port. If a timeout is set it may + return less characters as requested. With no timeout it will block + until the requested number of bytes is read. + """ + if not self.is_open: + raise PortNotOpenError() + data = bytearray() + try: + timeout = Timeout(self._timeout) + while len(data) < size: + if self._thread is None or not self._thread.is_alive(): + raise SerialException('connection failed (reader thread died)') + buf = self._read_buffer.get(True, timeout.time_left()) + if buf is None: + return bytes(data) + data += buf + if timeout.expired(): + break + except Queue.Empty: # -> timeout + pass + return bytes(data) + + def write(self, data): + """\ + Output the given byte string over the serial port. Can block if the + connection is blocked. May raise SerialException if the connection is + closed. + """ + if not self.is_open: + raise PortNotOpenError() + with self._write_lock: + try: + self._socket.sendall(to_bytes(data).replace(IAC, IAC_DOUBLED)) + except socket.error as e: + raise SerialException("connection failed (socket error): {}".format(e)) + return len(data) + + def reset_input_buffer(self): + """Clear input buffer, discarding all that is in the buffer.""" + if not self.is_open: + raise PortNotOpenError() + self.rfc2217_send_purge(PURGE_RECEIVE_BUFFER) + # empty read buffer + while self._read_buffer.qsize(): + self._read_buffer.get(False) + + def reset_output_buffer(self): + """\ + Clear output buffer, aborting the current output and + discarding all that is in the buffer. + """ + if not self.is_open: + raise PortNotOpenError() + self.rfc2217_send_purge(PURGE_TRANSMIT_BUFFER) + + def _update_break_state(self): + """\ + Set break: Controls TXD. When active, to transmitting is + possible. + """ + if not self.is_open: + raise PortNotOpenError() + if self.logger: + self.logger.info('set BREAK to {}'.format('active' if self._break_state else 'inactive')) + if self._break_state: + self.rfc2217_set_control(SET_CONTROL_BREAK_ON) + else: + self.rfc2217_set_control(SET_CONTROL_BREAK_OFF) + + def _update_rts_state(self): + """Set terminal status line: Request To Send.""" + if not self.is_open: + raise PortNotOpenError() + if self.logger: + self.logger.info('set RTS to {}'.format('active' if self._rts_state else 'inactive')) + if self._rts_state: + self.rfc2217_set_control(SET_CONTROL_RTS_ON) + else: + self.rfc2217_set_control(SET_CONTROL_RTS_OFF) + + def _update_dtr_state(self): + """Set terminal status line: Data Terminal Ready.""" + if not self.is_open: + raise PortNotOpenError() + if self.logger: + self.logger.info('set DTR to {}'.format('active' if self._dtr_state else 'inactive')) + if self._dtr_state: + self.rfc2217_set_control(SET_CONTROL_DTR_ON) + else: + self.rfc2217_set_control(SET_CONTROL_DTR_OFF) + + @property + def cts(self): + """Read terminal status line: Clear To Send.""" + if not self.is_open: + raise PortNotOpenError() + return bool(self.get_modem_state() & MODEMSTATE_MASK_CTS) + + @property + def dsr(self): + """Read terminal status line: Data Set Ready.""" + if not self.is_open: + raise PortNotOpenError() + return bool(self.get_modem_state() & MODEMSTATE_MASK_DSR) + + @property + def ri(self): + """Read terminal status line: Ring Indicator.""" + if not self.is_open: + raise PortNotOpenError() + return bool(self.get_modem_state() & MODEMSTATE_MASK_RI) + + @property + def cd(self): + """Read terminal status line: Carrier Detect.""" + if not self.is_open: + raise PortNotOpenError() + return bool(self.get_modem_state() & MODEMSTATE_MASK_CD) + + # - - - platform specific - - - + # None so far + + # - - - RFC2217 specific - - - + + def _telnet_read_loop(self): + """Read loop for the socket.""" + mode = M_NORMAL + suboption = None + try: + while self.is_open: + try: + data = self._socket.recv(1024) + except socket.timeout: + # just need to get out of recv form time to time to check if + # still alive + continue + except socket.error as e: + # connection fails -> terminate loop + if self.logger: + self.logger.debug("socket error in reader thread: {}".format(e)) + self._read_buffer.put(None) + break + if not data: + self._read_buffer.put(None) + break # lost connection + for byte in iterbytes(data): + if mode == M_NORMAL: + # interpret as command or as data + if byte == IAC: + mode = M_IAC_SEEN + else: + # store data in read buffer or sub option buffer + # depending on state + if suboption is not None: + suboption += byte + else: + self._read_buffer.put(byte) + elif mode == M_IAC_SEEN: + if byte == IAC: + # interpret as command doubled -> insert character + # itself + if suboption is not None: + suboption += IAC + else: + self._read_buffer.put(IAC) + mode = M_NORMAL + elif byte == SB: + # sub option start + suboption = bytearray() + mode = M_NORMAL + elif byte == SE: + # sub option end -> process it now + self._telnet_process_subnegotiation(bytes(suboption)) + suboption = None + mode = M_NORMAL + elif byte in (DO, DONT, WILL, WONT): + # negotiation + telnet_command = byte + mode = M_NEGOTIATE + else: + # other telnet commands + self._telnet_process_command(byte) + mode = M_NORMAL + elif mode == M_NEGOTIATE: # DO, DONT, WILL, WONT was received, option now following + self._telnet_negotiate_option(telnet_command, byte) + mode = M_NORMAL + finally: + if self.logger: + self.logger.debug("read thread terminated") + + # - incoming telnet commands and options + + def _telnet_process_command(self, command): + """Process commands other than DO, DONT, WILL, WONT.""" + # Currently none. RFC2217 only uses negotiation and subnegotiation. + if self.logger: + self.logger.warning("ignoring Telnet command: {!r}".format(command)) + + def _telnet_negotiate_option(self, command, option): + """Process incoming DO, DONT, WILL, WONT.""" + # check our registered telnet options and forward command to them + # they know themselves if they have to answer or not + known = False + for item in self._telnet_options: + # can have more than one match! as some options are duplicated for + # 'us' and 'them' + if item.option == option: + item.process_incoming(command) + known = True + if not known: + # handle unknown options + # only answer to positive requests and deny them + if command == WILL or command == DO: + self.telnet_send_option((DONT if command == WILL else WONT), option) + if self.logger: + self.logger.warning("rejected Telnet option: {!r}".format(option)) + + def _telnet_process_subnegotiation(self, suboption): + """Process subnegotiation, the data between IAC SB and IAC SE.""" + if suboption[0:1] == COM_PORT_OPTION: + if suboption[1:2] == SERVER_NOTIFY_LINESTATE and len(suboption) >= 3: + self._linestate = ord(suboption[2:3]) # ensure it is a number + if self.logger: + self.logger.info("NOTIFY_LINESTATE: {}".format(self._linestate)) + elif suboption[1:2] == SERVER_NOTIFY_MODEMSTATE and len(suboption) >= 3: + self._modemstate = ord(suboption[2:3]) # ensure it is a number + if self.logger: + self.logger.info("NOTIFY_MODEMSTATE: {}".format(self._modemstate)) + # update time when we think that a poll would make sense + self._modemstate_timeout.restart(0.3) + elif suboption[1:2] == FLOWCONTROL_SUSPEND: + self._remote_suspend_flow = True + elif suboption[1:2] == FLOWCONTROL_RESUME: + self._remote_suspend_flow = False + else: + for item in self._rfc2217_options.values(): + if item.ack_option == suboption[1:2]: + #~ print "processing COM_PORT_OPTION: %r" % list(suboption[1:]) + item.check_answer(bytes(suboption[2:])) + break + else: + if self.logger: + self.logger.warning("ignoring COM_PORT_OPTION: {!r}".format(suboption)) + else: + if self.logger: + self.logger.warning("ignoring subnegotiation: {!r}".format(suboption)) + + # - outgoing telnet commands and options + + def _internal_raw_write(self, data): + """internal socket write with no data escaping. used to send telnet stuff.""" + with self._write_lock: + self._socket.sendall(data) + + def telnet_send_option(self, action, option): + """Send DO, DONT, WILL, WONT.""" + self._internal_raw_write(IAC + action + option) + + def rfc2217_send_subnegotiation(self, option, value=b''): + """Subnegotiation of RFC2217 parameters.""" + value = value.replace(IAC, IAC_DOUBLED) + self._internal_raw_write(IAC + SB + COM_PORT_OPTION + option + value + IAC + SE) + + def rfc2217_send_purge(self, value): + """\ + Send purge request to the remote. + (PURGE_RECEIVE_BUFFER / PURGE_TRANSMIT_BUFFER / PURGE_BOTH_BUFFERS) + """ + item = self._rfc2217_options['purge'] + item.set(value) # transmit desired purge type + item.wait(self._network_timeout) # wait for acknowledge from the server + + def rfc2217_set_control(self, value): + """transmit change of control line to remote""" + item = self._rfc2217_options['control'] + item.set(value) # transmit desired control type + if self._ignore_set_control_answer: + # answers are ignored when option is set. compatibility mode for + # servers that answer, but not the expected one... (or no answer + # at all) i.e. sredird + time.sleep(0.1) # this helps getting the unit tests passed + else: + item.wait(self._network_timeout) # wait for acknowledge from the server + + def rfc2217_flow_server_ready(self): + """\ + check if server is ready to receive data. block for some time when + not. + """ + #~ if self._remote_suspend_flow: + #~ wait--- + + def get_modem_state(self): + """\ + get last modem state (cached value. If value is "old", request a new + one. This cache helps that we don't issue to many requests when e.g. all + status lines, one after the other is queried by the user (CTS, DSR + etc.) + """ + # active modem state polling enabled? is the value fresh enough? + if self._poll_modem_state and self._modemstate_timeout.expired(): + if self.logger: + self.logger.debug('polling modem state') + # when it is older, request an update + self.rfc2217_send_subnegotiation(NOTIFY_MODEMSTATE) + timeout = Timeout(self._network_timeout) + while not timeout.expired(): + time.sleep(0.05) # prevent 100% CPU load + # when expiration time is updated, it means that there is a new + # value + if not self._modemstate_timeout.expired(): + break + else: + if self.logger: + self.logger.warning('poll for modem state failed') + # even when there is a timeout, do not generate an error just + # return the last known value. this way we can support buggy + # servers that do not respond to polls, but send automatic + # updates. + if self._modemstate is not None: + if self.logger: + self.logger.debug('using cached modem state') + return self._modemstate + else: + # never received a notification from the server + raise SerialException("remote sends no NOTIFY_MODEMSTATE") + + +############################################################################# +# The following is code that helps implementing an RFC 2217 server. + +class PortManager(object): + """\ + This class manages the state of Telnet and RFC 2217. It needs a serial + instance and a connection to work with. Connection is expected to implement + a (thread safe) write function, that writes the string to the network. + """ + + def __init__(self, serial_port, connection, logger=None): + self.serial = serial_port + self.connection = connection + self.logger = logger + self._client_is_rfc2217 = False + + # filter state machine + self.mode = M_NORMAL + self.suboption = None + self.telnet_command = None + + # states for modem/line control events + self.modemstate_mask = 255 + self.last_modemstate = None + self.linstate_mask = 0 + + # all supported telnet options + self._telnet_options = [ + TelnetOption(self, 'ECHO', ECHO, WILL, WONT, DO, DONT, REQUESTED), + TelnetOption(self, 'we-SGA', SGA, WILL, WONT, DO, DONT, REQUESTED), + TelnetOption(self, 'they-SGA', SGA, DO, DONT, WILL, WONT, INACTIVE), + TelnetOption(self, 'we-BINARY', BINARY, WILL, WONT, DO, DONT, INACTIVE), + TelnetOption(self, 'they-BINARY', BINARY, DO, DONT, WILL, WONT, REQUESTED), + TelnetOption(self, 'we-RFC2217', COM_PORT_OPTION, WILL, WONT, DO, DONT, REQUESTED, self._client_ok), + TelnetOption(self, 'they-RFC2217', COM_PORT_OPTION, DO, DONT, WILL, WONT, INACTIVE, self._client_ok), + ] + + # negotiate Telnet/RFC2217 -> send initial requests + if self.logger: + self.logger.debug("requesting initial Telnet/RFC 2217 options") + for option in self._telnet_options: + if option.state is REQUESTED: + self.telnet_send_option(option.send_yes, option.option) + # issue 1st modem state notification + + def _client_ok(self): + """\ + callback of telnet option. It gets called when option is activated. + This one here is used to detect when the client agrees on RFC 2217. A + flag is set so that other functions like check_modem_lines know if the + client is OK. + """ + # The callback is used for we and they so if one party agrees, we're + # already happy. it seems not all servers do the negotiation correctly + # and i guess there are incorrect clients too.. so be happy if client + # answers one or the other positively. + self._client_is_rfc2217 = True + if self.logger: + self.logger.info("client accepts RFC 2217") + # this is to ensure that the client gets a notification, even if there + # was no change + self.check_modem_lines(force_notification=True) + + # - outgoing telnet commands and options + + def telnet_send_option(self, action, option): + """Send DO, DONT, WILL, WONT.""" + self.connection.write(IAC + action + option) + + def rfc2217_send_subnegotiation(self, option, value=b''): + """Subnegotiation of RFC 2217 parameters.""" + value = value.replace(IAC, IAC_DOUBLED) + self.connection.write(IAC + SB + COM_PORT_OPTION + option + value + IAC + SE) + + # - check modem lines, needs to be called periodically from user to + # establish polling + + def check_modem_lines(self, force_notification=False): + """\ + read control lines from serial port and compare the last value sent to remote. + send updates on changes. + """ + modemstate = ( + (self.serial.cts and MODEMSTATE_MASK_CTS) | + (self.serial.dsr and MODEMSTATE_MASK_DSR) | + (self.serial.ri and MODEMSTATE_MASK_RI) | + (self.serial.cd and MODEMSTATE_MASK_CD)) + # check what has changed + deltas = modemstate ^ (self.last_modemstate or 0) # when last is None -> 0 + if deltas & MODEMSTATE_MASK_CTS: + modemstate |= MODEMSTATE_MASK_CTS_CHANGE + if deltas & MODEMSTATE_MASK_DSR: + modemstate |= MODEMSTATE_MASK_DSR_CHANGE + if deltas & MODEMSTATE_MASK_RI: + modemstate |= MODEMSTATE_MASK_RI_CHANGE + if deltas & MODEMSTATE_MASK_CD: + modemstate |= MODEMSTATE_MASK_CD_CHANGE + # if new state is different and the mask allows this change, send + # notification. suppress notifications when client is not rfc2217 + if modemstate != self.last_modemstate or force_notification: + if (self._client_is_rfc2217 and (modemstate & self.modemstate_mask)) or force_notification: + self.rfc2217_send_subnegotiation( + SERVER_NOTIFY_MODEMSTATE, + to_bytes([modemstate & self.modemstate_mask])) + if self.logger: + self.logger.info("NOTIFY_MODEMSTATE: {}".format(modemstate)) + # save last state, but forget about deltas. + # otherwise it would also notify about changing deltas which is + # probably not very useful + self.last_modemstate = modemstate & 0xf0 + + # - outgoing data escaping + + def escape(self, data): + """\ + This generator function is for the user. All outgoing data has to be + properly escaped, so that no IAC character in the data stream messes up + the Telnet state machine in the server. + + socket.sendall(escape(data)) + """ + for byte in iterbytes(data): + if byte == IAC: + yield IAC + yield IAC + else: + yield byte + + # - incoming data filter + + def filter(self, data): + """\ + Handle a bunch of incoming bytes. This is a generator. It will yield + all characters not of interest for Telnet/RFC 2217. + + The idea is that the reader thread pushes data from the socket through + this filter: + + for byte in filter(socket.recv(1024)): + # do things like CR/LF conversion/whatever + # and write data to the serial port + serial.write(byte) + + (socket error handling code left as exercise for the reader) + """ + for byte in iterbytes(data): + if self.mode == M_NORMAL: + # interpret as command or as data + if byte == IAC: + self.mode = M_IAC_SEEN + else: + # store data in sub option buffer or pass it to our + # consumer depending on state + if self.suboption is not None: + self.suboption += byte + else: + yield byte + elif self.mode == M_IAC_SEEN: + if byte == IAC: + # interpret as command doubled -> insert character + # itself + if self.suboption is not None: + self.suboption += byte + else: + yield byte + self.mode = M_NORMAL + elif byte == SB: + # sub option start + self.suboption = bytearray() + self.mode = M_NORMAL + elif byte == SE: + # sub option end -> process it now + self._telnet_process_subnegotiation(bytes(self.suboption)) + self.suboption = None + self.mode = M_NORMAL + elif byte in (DO, DONT, WILL, WONT): + # negotiation + self.telnet_command = byte + self.mode = M_NEGOTIATE + else: + # other telnet commands + self._telnet_process_command(byte) + self.mode = M_NORMAL + elif self.mode == M_NEGOTIATE: # DO, DONT, WILL, WONT was received, option now following + self._telnet_negotiate_option(self.telnet_command, byte) + self.mode = M_NORMAL + + # - incoming telnet commands and options + + def _telnet_process_command(self, command): + """Process commands other than DO, DONT, WILL, WONT.""" + # Currently none. RFC2217 only uses negotiation and subnegotiation. + if self.logger: + self.logger.warning("ignoring Telnet command: {!r}".format(command)) + + def _telnet_negotiate_option(self, command, option): + """Process incoming DO, DONT, WILL, WONT.""" + # check our registered telnet options and forward command to them + # they know themselves if they have to answer or not + known = False + for item in self._telnet_options: + # can have more than one match! as some options are duplicated for + # 'us' and 'them' + if item.option == option: + item.process_incoming(command) + known = True + if not known: + # handle unknown options + # only answer to positive requests and deny them + if command == WILL or command == DO: + self.telnet_send_option((DONT if command == WILL else WONT), option) + if self.logger: + self.logger.warning("rejected Telnet option: {!r}".format(option)) + + def _telnet_process_subnegotiation(self, suboption): + """Process subnegotiation, the data between IAC SB and IAC SE.""" + if suboption[0:1] == COM_PORT_OPTION: + if self.logger: + self.logger.debug('received COM_PORT_OPTION: {!r}'.format(suboption)) + if suboption[1:2] == SET_BAUDRATE: + backup = self.serial.baudrate + try: + (baudrate,) = struct.unpack(b"!I", suboption[2:6]) + if baudrate != 0: + self.serial.baudrate = baudrate + except ValueError as e: + if self.logger: + self.logger.error("failed to set baud rate: {}".format(e)) + self.serial.baudrate = backup + else: + if self.logger: + self.logger.info("{} baud rate: {}".format('set' if baudrate else 'get', self.serial.baudrate)) + self.rfc2217_send_subnegotiation(SERVER_SET_BAUDRATE, struct.pack(b"!I", self.serial.baudrate)) + elif suboption[1:2] == SET_DATASIZE: + backup = self.serial.bytesize + try: + (datasize,) = struct.unpack(b"!B", suboption[2:3]) + if datasize != 0: + self.serial.bytesize = datasize + except ValueError as e: + if self.logger: + self.logger.error("failed to set data size: {}".format(e)) + self.serial.bytesize = backup + else: + if self.logger: + self.logger.info("{} data size: {}".format('set' if datasize else 'get', self.serial.bytesize)) + self.rfc2217_send_subnegotiation(SERVER_SET_DATASIZE, struct.pack(b"!B", self.serial.bytesize)) + elif suboption[1:2] == SET_PARITY: + backup = self.serial.parity + try: + parity = struct.unpack(b"!B", suboption[2:3])[0] + if parity != 0: + self.serial.parity = RFC2217_REVERSE_PARITY_MAP[parity] + except ValueError as e: + if self.logger: + self.logger.error("failed to set parity: {}".format(e)) + self.serial.parity = backup + else: + if self.logger: + self.logger.info("{} parity: {}".format('set' if parity else 'get', self.serial.parity)) + self.rfc2217_send_subnegotiation( + SERVER_SET_PARITY, + struct.pack(b"!B", RFC2217_PARITY_MAP[self.serial.parity])) + elif suboption[1:2] == SET_STOPSIZE: + backup = self.serial.stopbits + try: + stopbits = struct.unpack(b"!B", suboption[2:3])[0] + if stopbits != 0: + self.serial.stopbits = RFC2217_REVERSE_STOPBIT_MAP[stopbits] + except ValueError as e: + if self.logger: + self.logger.error("failed to set stop bits: {}".format(e)) + self.serial.stopbits = backup + else: + if self.logger: + self.logger.info("{} stop bits: {}".format('set' if stopbits else 'get', self.serial.stopbits)) + self.rfc2217_send_subnegotiation( + SERVER_SET_STOPSIZE, + struct.pack(b"!B", RFC2217_STOPBIT_MAP[self.serial.stopbits])) + elif suboption[1:2] == SET_CONTROL: + if suboption[2:3] == SET_CONTROL_REQ_FLOW_SETTING: + if self.serial.xonxoff: + self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_SW_FLOW_CONTROL) + elif self.serial.rtscts: + self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_HW_FLOW_CONTROL) + else: + self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_NO_FLOW_CONTROL) + elif suboption[2:3] == SET_CONTROL_USE_NO_FLOW_CONTROL: + self.serial.xonxoff = False + self.serial.rtscts = False + if self.logger: + self.logger.info("changed flow control to None") + self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_NO_FLOW_CONTROL) + elif suboption[2:3] == SET_CONTROL_USE_SW_FLOW_CONTROL: + self.serial.xonxoff = True + if self.logger: + self.logger.info("changed flow control to XON/XOFF") + self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_SW_FLOW_CONTROL) + elif suboption[2:3] == SET_CONTROL_USE_HW_FLOW_CONTROL: + self.serial.rtscts = True + if self.logger: + self.logger.info("changed flow control to RTS/CTS") + self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_HW_FLOW_CONTROL) + elif suboption[2:3] == SET_CONTROL_REQ_BREAK_STATE: + if self.logger: + self.logger.warning("requested break state - not implemented") + pass # XXX needs cached value + elif suboption[2:3] == SET_CONTROL_BREAK_ON: + self.serial.break_condition = True + if self.logger: + self.logger.info("changed BREAK to active") + self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_BREAK_ON) + elif suboption[2:3] == SET_CONTROL_BREAK_OFF: + self.serial.break_condition = False + if self.logger: + self.logger.info("changed BREAK to inactive") + self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_BREAK_OFF) + elif suboption[2:3] == SET_CONTROL_REQ_DTR: + if self.logger: + self.logger.warning("requested DTR state - not implemented") + pass # XXX needs cached value + elif suboption[2:3] == SET_CONTROL_DTR_ON: + self.serial.dtr = True + if self.logger: + self.logger.info("changed DTR to active") + self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_DTR_ON) + elif suboption[2:3] == SET_CONTROL_DTR_OFF: + self.serial.dtr = False + if self.logger: + self.logger.info("changed DTR to inactive") + self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_DTR_OFF) + elif suboption[2:3] == SET_CONTROL_REQ_RTS: + if self.logger: + self.logger.warning("requested RTS state - not implemented") + pass # XXX needs cached value + #~ self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_RTS_ON) + elif suboption[2:3] == SET_CONTROL_RTS_ON: + self.serial.rts = True + if self.logger: + self.logger.info("changed RTS to active") + self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_RTS_ON) + elif suboption[2:3] == SET_CONTROL_RTS_OFF: + self.serial.rts = False + if self.logger: + self.logger.info("changed RTS to inactive") + self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_RTS_OFF) + #~ elif suboption[2:3] == SET_CONTROL_REQ_FLOW_SETTING_IN: + #~ elif suboption[2:3] == SET_CONTROL_USE_NO_FLOW_CONTROL_IN: + #~ elif suboption[2:3] == SET_CONTROL_USE_SW_FLOW_CONTOL_IN: + #~ elif suboption[2:3] == SET_CONTROL_USE_HW_FLOW_CONTOL_IN: + #~ elif suboption[2:3] == SET_CONTROL_USE_DCD_FLOW_CONTROL: + #~ elif suboption[2:3] == SET_CONTROL_USE_DTR_FLOW_CONTROL: + #~ elif suboption[2:3] == SET_CONTROL_USE_DSR_FLOW_CONTROL: + elif suboption[1:2] == NOTIFY_LINESTATE: + # client polls for current state + self.rfc2217_send_subnegotiation( + SERVER_NOTIFY_LINESTATE, + to_bytes([0])) # sorry, nothing like that implemented + elif suboption[1:2] == NOTIFY_MODEMSTATE: + if self.logger: + self.logger.info("request for modem state") + # client polls for current state + self.check_modem_lines(force_notification=True) + elif suboption[1:2] == FLOWCONTROL_SUSPEND: + if self.logger: + self.logger.info("suspend") + self._remote_suspend_flow = True + elif suboption[1:2] == FLOWCONTROL_RESUME: + if self.logger: + self.logger.info("resume") + self._remote_suspend_flow = False + elif suboption[1:2] == SET_LINESTATE_MASK: + self.linstate_mask = ord(suboption[2:3]) # ensure it is a number + if self.logger: + self.logger.info("line state mask: 0x{:02x}".format(self.linstate_mask)) + elif suboption[1:2] == SET_MODEMSTATE_MASK: + self.modemstate_mask = ord(suboption[2:3]) # ensure it is a number + if self.logger: + self.logger.info("modem state mask: 0x{:02x}".format(self.modemstate_mask)) + elif suboption[1:2] == PURGE_DATA: + if suboption[2:3] == PURGE_RECEIVE_BUFFER: + self.serial.reset_input_buffer() + if self.logger: + self.logger.info("purge in") + self.rfc2217_send_subnegotiation(SERVER_PURGE_DATA, PURGE_RECEIVE_BUFFER) + elif suboption[2:3] == PURGE_TRANSMIT_BUFFER: + self.serial.reset_output_buffer() + if self.logger: + self.logger.info("purge out") + self.rfc2217_send_subnegotiation(SERVER_PURGE_DATA, PURGE_TRANSMIT_BUFFER) + elif suboption[2:3] == PURGE_BOTH_BUFFERS: + self.serial.reset_input_buffer() + self.serial.reset_output_buffer() + if self.logger: + self.logger.info("purge both") + self.rfc2217_send_subnegotiation(SERVER_PURGE_DATA, PURGE_BOTH_BUFFERS) + else: + if self.logger: + self.logger.error("undefined PURGE_DATA: {!r}".format(list(suboption[2:]))) + else: + if self.logger: + self.logger.error("undefined COM_PORT_OPTION: {!r}".format(list(suboption[1:]))) + else: + if self.logger: + self.logger.warning("unknown subnegotiation: {!r}".format(suboption)) + + +# simple client test +if __name__ == '__main__': + import sys + s = Serial('rfc2217://localhost:7000', 115200) + sys.stdout.write('{}\n'.format(s)) + + sys.stdout.write("write...\n") + s.write(b"hello\n") + s.flush() + sys.stdout.write("read: {}\n".format(s.read(5))) + s.close() diff --git a/tools/python-embed/Lib/site-packages/serial/rs485.py b/tools/python-embed/Lib/site-packages/serial/rs485.py new file mode 100644 index 0000000..d7aff6f --- /dev/null +++ b/tools/python-embed/Lib/site-packages/serial/rs485.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python + +# RS485 support +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2015 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause + +"""\ +The settings for RS485 are stored in a dedicated object that can be applied to +serial ports (where supported). +NOTE: Some implementations may only support a subset of the settings. +""" + +from __future__ import absolute_import + +import time +import serial + + +class RS485Settings(object): + def __init__( + self, + rts_level_for_tx=True, + rts_level_for_rx=False, + loopback=False, + delay_before_tx=None, + delay_before_rx=None): + self.rts_level_for_tx = rts_level_for_tx + self.rts_level_for_rx = rts_level_for_rx + self.loopback = loopback + self.delay_before_tx = delay_before_tx + self.delay_before_rx = delay_before_rx + + +class RS485(serial.Serial): + """\ + A subclass that replaces the write method with one that toggles RTS + according to the RS485 settings. + + NOTE: This may work unreliably on some serial ports (control signals not + synchronized or delayed compared to data). Using delays may be + unreliable (varying times, larger than expected) as the OS may not + support very fine grained delays (no smaller than in the order of + tens of milliseconds). + + NOTE: Some implementations support this natively. Better performance + can be expected when the native version is used. + + NOTE: The loopback property is ignored by this implementation. The actual + behavior depends on the used hardware. + + Usage: + + ser = RS485(...) + ser.rs485_mode = RS485Settings(...) + ser.write(b'hello') + """ + + def __init__(self, *args, **kwargs): + super(RS485, self).__init__(*args, **kwargs) + self._alternate_rs485_settings = None + + def write(self, b): + """Write to port, controlling RTS before and after transmitting.""" + if self._alternate_rs485_settings is not None: + # apply level for TX and optional delay + self.setRTS(self._alternate_rs485_settings.rts_level_for_tx) + if self._alternate_rs485_settings.delay_before_tx is not None: + time.sleep(self._alternate_rs485_settings.delay_before_tx) + # write and wait for data to be written + super(RS485, self).write(b) + super(RS485, self).flush() + # optional delay and apply level for RX + if self._alternate_rs485_settings.delay_before_rx is not None: + time.sleep(self._alternate_rs485_settings.delay_before_rx) + self.setRTS(self._alternate_rs485_settings.rts_level_for_rx) + else: + super(RS485, self).write(b) + + # redirect where the property stores the settings so that underlying Serial + # instance does not see them + @property + def rs485_mode(self): + """\ + Enable RS485 mode and apply new settings, set to None to disable. + See serial.rs485.RS485Settings for more info about the value. + """ + return self._alternate_rs485_settings + + @rs485_mode.setter + def rs485_mode(self, rs485_settings): + self._alternate_rs485_settings = rs485_settings diff --git a/tools/python-embed/Lib/site-packages/serial/serialcli.py b/tools/python-embed/Lib/site-packages/serial/serialcli.py new file mode 100644 index 0000000..4614736 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/serial/serialcli.py @@ -0,0 +1,253 @@ +#! python +# +# Backend for .NET/Mono (IronPython), .NET >= 2 +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2008-2015 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import absolute_import + +import System +import System.IO.Ports +from serial.serialutil import * + +# must invoke function with byte array, make a helper to convert strings +# to byte arrays +sab = System.Array[System.Byte] + + +def as_byte_array(string): + return sab([ord(x) for x in string]) # XXX will require adaption when run with a 3.x compatible IronPython + + +class Serial(SerialBase): + """Serial port implementation for .NET/Mono.""" + + BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800, + 9600, 19200, 38400, 57600, 115200) + + def open(self): + """\ + Open port with current settings. This may throw a SerialException + if the port cannot be opened. + """ + if self._port is None: + raise SerialException("Port must be configured before it can be used.") + if self.is_open: + raise SerialException("Port is already open.") + try: + self._port_handle = System.IO.Ports.SerialPort(self.portstr) + except Exception as msg: + self._port_handle = None + raise SerialException("could not open port %s: %s" % (self.portstr, msg)) + + # if RTS and/or DTR are not set before open, they default to True + if self._rts_state is None: + self._rts_state = True + if self._dtr_state is None: + self._dtr_state = True + + self._reconfigure_port() + self._port_handle.Open() + self.is_open = True + if not self._dsrdtr: + self._update_dtr_state() + if not self._rtscts: + self._update_rts_state() + self.reset_input_buffer() + + def _reconfigure_port(self): + """Set communication parameters on opened port.""" + if not self._port_handle: + raise SerialException("Can only operate on a valid port handle") + + #~ self._port_handle.ReceivedBytesThreshold = 1 + + if self._timeout is None: + self._port_handle.ReadTimeout = System.IO.Ports.SerialPort.InfiniteTimeout + else: + self._port_handle.ReadTimeout = int(self._timeout * 1000) + + # if self._timeout != 0 and self._interCharTimeout is not None: + # timeouts = (int(self._interCharTimeout * 1000),) + timeouts[1:] + + if self._write_timeout is None: + self._port_handle.WriteTimeout = System.IO.Ports.SerialPort.InfiniteTimeout + else: + self._port_handle.WriteTimeout = int(self._write_timeout * 1000) + + # Setup the connection info. + try: + self._port_handle.BaudRate = self._baudrate + except IOError as e: + # catch errors from illegal baudrate settings + raise ValueError(str(e)) + + if self._bytesize == FIVEBITS: + self._port_handle.DataBits = 5 + elif self._bytesize == SIXBITS: + self._port_handle.DataBits = 6 + elif self._bytesize == SEVENBITS: + self._port_handle.DataBits = 7 + elif self._bytesize == EIGHTBITS: + self._port_handle.DataBits = 8 + else: + raise ValueError("Unsupported number of data bits: %r" % self._bytesize) + + if self._parity == PARITY_NONE: + self._port_handle.Parity = getattr(System.IO.Ports.Parity, 'None') # reserved keyword in Py3k + elif self._parity == PARITY_EVEN: + self._port_handle.Parity = System.IO.Ports.Parity.Even + elif self._parity == PARITY_ODD: + self._port_handle.Parity = System.IO.Ports.Parity.Odd + elif self._parity == PARITY_MARK: + self._port_handle.Parity = System.IO.Ports.Parity.Mark + elif self._parity == PARITY_SPACE: + self._port_handle.Parity = System.IO.Ports.Parity.Space + else: + raise ValueError("Unsupported parity mode: %r" % self._parity) + + if self._stopbits == STOPBITS_ONE: + self._port_handle.StopBits = System.IO.Ports.StopBits.One + elif self._stopbits == STOPBITS_ONE_POINT_FIVE: + self._port_handle.StopBits = System.IO.Ports.StopBits.OnePointFive + elif self._stopbits == STOPBITS_TWO: + self._port_handle.StopBits = System.IO.Ports.StopBits.Two + else: + raise ValueError("Unsupported number of stop bits: %r" % self._stopbits) + + if self._rtscts and self._xonxoff: + self._port_handle.Handshake = System.IO.Ports.Handshake.RequestToSendXOnXOff + elif self._rtscts: + self._port_handle.Handshake = System.IO.Ports.Handshake.RequestToSend + elif self._xonxoff: + self._port_handle.Handshake = System.IO.Ports.Handshake.XOnXOff + else: + self._port_handle.Handshake = getattr(System.IO.Ports.Handshake, 'None') # reserved keyword in Py3k + + #~ def __del__(self): + #~ self.close() + + def close(self): + """Close port""" + if self.is_open: + if self._port_handle: + try: + self._port_handle.Close() + except System.IO.Ports.InvalidOperationException: + # ignore errors. can happen for unplugged USB serial devices + pass + self._port_handle = None + self.is_open = False + + # - - - - - - - - - - - - - - - - - - - - - - - - + + @property + def in_waiting(self): + """Return the number of characters currently in the input buffer.""" + if not self.is_open: + raise PortNotOpenError() + return self._port_handle.BytesToRead + + def read(self, size=1): + """\ + Read size bytes from the serial port. If a timeout is set it may + return less characters as requested. With no timeout it will block + until the requested number of bytes is read. + """ + if not self.is_open: + raise PortNotOpenError() + # must use single byte reads as this is the only way to read + # without applying encodings + data = bytearray() + while size: + try: + data.append(self._port_handle.ReadByte()) + except System.TimeoutException: + break + else: + size -= 1 + return bytes(data) + + def write(self, data): + """Output the given string over the serial port.""" + if not self.is_open: + raise PortNotOpenError() + #~ if not isinstance(data, (bytes, bytearray)): + #~ raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data))) + try: + # must call overloaded method with byte array argument + # as this is the only one not applying encodings + self._port_handle.Write(as_byte_array(data), 0, len(data)) + except System.TimeoutException: + raise SerialTimeoutException('Write timeout') + return len(data) + + def reset_input_buffer(self): + """Clear input buffer, discarding all that is in the buffer.""" + if not self.is_open: + raise PortNotOpenError() + self._port_handle.DiscardInBuffer() + + def reset_output_buffer(self): + """\ + Clear output buffer, aborting the current output and + discarding all that is in the buffer. + """ + if not self.is_open: + raise PortNotOpenError() + self._port_handle.DiscardOutBuffer() + + def _update_break_state(self): + """ + Set break: Controls TXD. When active, to transmitting is possible. + """ + if not self.is_open: + raise PortNotOpenError() + self._port_handle.BreakState = bool(self._break_state) + + def _update_rts_state(self): + """Set terminal status line: Request To Send""" + if not self.is_open: + raise PortNotOpenError() + self._port_handle.RtsEnable = bool(self._rts_state) + + def _update_dtr_state(self): + """Set terminal status line: Data Terminal Ready""" + if not self.is_open: + raise PortNotOpenError() + self._port_handle.DtrEnable = bool(self._dtr_state) + + @property + def cts(self): + """Read terminal status line: Clear To Send""" + if not self.is_open: + raise PortNotOpenError() + return self._port_handle.CtsHolding + + @property + def dsr(self): + """Read terminal status line: Data Set Ready""" + if not self.is_open: + raise PortNotOpenError() + return self._port_handle.DsrHolding + + @property + def ri(self): + """Read terminal status line: Ring Indicator""" + if not self.is_open: + raise PortNotOpenError() + #~ return self._port_handle.XXX + return False # XXX an error would be better + + @property + def cd(self): + """Read terminal status line: Carrier Detect""" + if not self.is_open: + raise PortNotOpenError() + return self._port_handle.CDHolding + + # - - platform specific - - - - + # none diff --git a/tools/python-embed/Lib/site-packages/serial/serialjava.py b/tools/python-embed/Lib/site-packages/serial/serialjava.py new file mode 100644 index 0000000..0789a78 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/serial/serialjava.py @@ -0,0 +1,251 @@ +#!jython +# +# Backend Jython with JavaComm +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2002-2015 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import absolute_import + +from serial.serialutil import * + + +def my_import(name): + mod = __import__(name) + components = name.split('.') + for comp in components[1:]: + mod = getattr(mod, comp) + return mod + + +def detect_java_comm(names): + """try given list of modules and return that imports""" + for name in names: + try: + mod = my_import(name) + mod.SerialPort + return mod + except (ImportError, AttributeError): + pass + raise ImportError("No Java Communications API implementation found") + + +# Java Communications API implementations +# http://mho.republika.pl/java/comm/ + +comm = detect_java_comm([ + 'javax.comm', # Sun/IBM + 'gnu.io', # RXTX +]) + + +def device(portnumber): + """Turn a port number into a device name""" + enum = comm.CommPortIdentifier.getPortIdentifiers() + ports = [] + while enum.hasMoreElements(): + el = enum.nextElement() + if el.getPortType() == comm.CommPortIdentifier.PORT_SERIAL: + ports.append(el) + return ports[portnumber].getName() + + +class Serial(SerialBase): + """\ + Serial port class, implemented with Java Communications API and + thus usable with jython and the appropriate java extension. + """ + + def open(self): + """\ + Open port with current settings. This may throw a SerialException + if the port cannot be opened. + """ + if self._port is None: + raise SerialException("Port must be configured before it can be used.") + if self.is_open: + raise SerialException("Port is already open.") + if type(self._port) == type(''): # strings are taken directly + portId = comm.CommPortIdentifier.getPortIdentifier(self._port) + else: + portId = comm.CommPortIdentifier.getPortIdentifier(device(self._port)) # numbers are transformed to a comport id obj + try: + self.sPort = portId.open("python serial module", 10) + except Exception as msg: + self.sPort = None + raise SerialException("Could not open port: %s" % msg) + self._reconfigurePort() + self._instream = self.sPort.getInputStream() + self._outstream = self.sPort.getOutputStream() + self.is_open = True + + def _reconfigurePort(self): + """Set communication parameters on opened port.""" + if not self.sPort: + raise SerialException("Can only operate on a valid port handle") + + self.sPort.enableReceiveTimeout(30) + if self._bytesize == FIVEBITS: + jdatabits = comm.SerialPort.DATABITS_5 + elif self._bytesize == SIXBITS: + jdatabits = comm.SerialPort.DATABITS_6 + elif self._bytesize == SEVENBITS: + jdatabits = comm.SerialPort.DATABITS_7 + elif self._bytesize == EIGHTBITS: + jdatabits = comm.SerialPort.DATABITS_8 + else: + raise ValueError("unsupported bytesize: %r" % self._bytesize) + + if self._stopbits == STOPBITS_ONE: + jstopbits = comm.SerialPort.STOPBITS_1 + elif self._stopbits == STOPBITS_ONE_POINT_FIVE: + jstopbits = comm.SerialPort.STOPBITS_1_5 + elif self._stopbits == STOPBITS_TWO: + jstopbits = comm.SerialPort.STOPBITS_2 + else: + raise ValueError("unsupported number of stopbits: %r" % self._stopbits) + + if self._parity == PARITY_NONE: + jparity = comm.SerialPort.PARITY_NONE + elif self._parity == PARITY_EVEN: + jparity = comm.SerialPort.PARITY_EVEN + elif self._parity == PARITY_ODD: + jparity = comm.SerialPort.PARITY_ODD + elif self._parity == PARITY_MARK: + jparity = comm.SerialPort.PARITY_MARK + elif self._parity == PARITY_SPACE: + jparity = comm.SerialPort.PARITY_SPACE + else: + raise ValueError("unsupported parity type: %r" % self._parity) + + jflowin = jflowout = 0 + if self._rtscts: + jflowin |= comm.SerialPort.FLOWCONTROL_RTSCTS_IN + jflowout |= comm.SerialPort.FLOWCONTROL_RTSCTS_OUT + if self._xonxoff: + jflowin |= comm.SerialPort.FLOWCONTROL_XONXOFF_IN + jflowout |= comm.SerialPort.FLOWCONTROL_XONXOFF_OUT + + self.sPort.setSerialPortParams(self._baudrate, jdatabits, jstopbits, jparity) + self.sPort.setFlowControlMode(jflowin | jflowout) + + if self._timeout >= 0: + self.sPort.enableReceiveTimeout(int(self._timeout*1000)) + else: + self.sPort.disableReceiveTimeout() + + def close(self): + """Close port""" + if self.is_open: + if self.sPort: + self._instream.close() + self._outstream.close() + self.sPort.close() + self.sPort = None + self.is_open = False + + # - - - - - - - - - - - - - - - - - - - - - - - - + + @property + def in_waiting(self): + """Return the number of characters currently in the input buffer.""" + if not self.sPort: + raise PortNotOpenError() + return self._instream.available() + + def read(self, size=1): + """\ + Read size bytes from the serial port. If a timeout is set it may + return less characters as requested. With no timeout it will block + until the requested number of bytes is read. + """ + if not self.sPort: + raise PortNotOpenError() + read = bytearray() + if size > 0: + while len(read) < size: + x = self._instream.read() + if x == -1: + if self.timeout >= 0: + break + else: + read.append(x) + return bytes(read) + + def write(self, data): + """Output the given string over the serial port.""" + if not self.sPort: + raise PortNotOpenError() + if not isinstance(data, (bytes, bytearray)): + raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data))) + self._outstream.write(data) + return len(data) + + def reset_input_buffer(self): + """Clear input buffer, discarding all that is in the buffer.""" + if not self.sPort: + raise PortNotOpenError() + self._instream.skip(self._instream.available()) + + def reset_output_buffer(self): + """\ + Clear output buffer, aborting the current output and + discarding all that is in the buffer. + """ + if not self.sPort: + raise PortNotOpenError() + self._outstream.flush() + + def send_break(self, duration=0.25): + """Send break condition. Timed, returns to idle state after given duration.""" + if not self.sPort: + raise PortNotOpenError() + self.sPort.sendBreak(duration*1000.0) + + def _update_break_state(self): + """Set break: Controls TXD. When active, to transmitting is possible.""" + if self.fd is None: + raise PortNotOpenError() + raise SerialException("The _update_break_state function is not implemented in java.") + + def _update_rts_state(self): + """Set terminal status line: Request To Send""" + if not self.sPort: + raise PortNotOpenError() + self.sPort.setRTS(self._rts_state) + + def _update_dtr_state(self): + """Set terminal status line: Data Terminal Ready""" + if not self.sPort: + raise PortNotOpenError() + self.sPort.setDTR(self._dtr_state) + + @property + def cts(self): + """Read terminal status line: Clear To Send""" + if not self.sPort: + raise PortNotOpenError() + self.sPort.isCTS() + + @property + def dsr(self): + """Read terminal status line: Data Set Ready""" + if not self.sPort: + raise PortNotOpenError() + self.sPort.isDSR() + + @property + def ri(self): + """Read terminal status line: Ring Indicator""" + if not self.sPort: + raise PortNotOpenError() + self.sPort.isRI() + + @property + def cd(self): + """Read terminal status line: Carrier Detect""" + if not self.sPort: + raise PortNotOpenError() + self.sPort.isCD() diff --git a/tools/python-embed/Lib/site-packages/serial/serialposix.py b/tools/python-embed/Lib/site-packages/serial/serialposix.py new file mode 100644 index 0000000..7aceb76 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/serial/serialposix.py @@ -0,0 +1,900 @@ +#!/usr/bin/env python +# +# backend for serial IO for POSIX compatible systems, like Linux, OSX +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2001-2020 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause +# +# parts based on code from Grant B. Edwards : +# ftp://ftp.visi.com/users/grante/python/PosixSerial.py +# +# references: http://www.easysw.com/~mike/serial/serial.html + +# Collection of port names (was previously used by number_to_device which was +# removed. +# - Linux /dev/ttyS%d (confirmed) +# - cygwin/win32 /dev/com%d (confirmed) +# - openbsd (OpenBSD) /dev/cua%02d +# - bsd*, freebsd* /dev/cuad%d +# - darwin (OS X) /dev/cuad%d +# - netbsd /dev/dty%02d (NetBSD 1.6 testing by Erk) +# - irix (IRIX) /dev/ttyf%d (partially tested) names depending on flow control +# - hp (HP-UX) /dev/tty%dp0 (not tested) +# - sunos (Solaris/SunOS) /dev/tty%c (letters, 'a'..'z') (confirmed) +# - aix (AIX) /dev/tty%d + + +from __future__ import absolute_import + +# pylint: disable=abstract-method +import errno +import fcntl +import os +import select +import struct +import sys +import termios + +import serial +from serial.serialutil import SerialBase, SerialException, to_bytes, \ + PortNotOpenError, SerialTimeoutException, Timeout + + +class PlatformSpecificBase(object): + BAUDRATE_CONSTANTS = {} + + def _set_special_baudrate(self, baudrate): + raise NotImplementedError('non-standard baudrates are not supported on this platform') + + def _set_rs485_mode(self, rs485_settings): + raise NotImplementedError('RS485 not supported on this platform') + + def set_low_latency_mode(self, low_latency_settings): + raise NotImplementedError('Low latency not supported on this platform') + + def _update_break_state(self): + """\ + Set break: Controls TXD. When active, no transmitting is possible. + """ + if self._break_state: + fcntl.ioctl(self.fd, TIOCSBRK) + else: + fcntl.ioctl(self.fd, TIOCCBRK) + + +# some systems support an extra flag to enable the two in POSIX unsupported +# paritiy settings for MARK and SPACE +CMSPAR = 0 # default, for unsupported platforms, override below + +# try to detect the OS so that a device can be selected... +# this code block should supply a device() and set_special_baudrate() function +# for the platform +plat = sys.platform.lower() + +if plat[:5] == 'linux': # Linux (confirmed) # noqa + import array + + # extra termios flags + CMSPAR = 0o10000000000 # Use "stick" (mark/space) parity + + # baudrate ioctls + TCGETS2 = 0x802C542A + TCSETS2 = 0x402C542B + BOTHER = 0o010000 + + # RS485 ioctls + TIOCGRS485 = 0x542E + TIOCSRS485 = 0x542F + SER_RS485_ENABLED = 0b00000001 + SER_RS485_RTS_ON_SEND = 0b00000010 + SER_RS485_RTS_AFTER_SEND = 0b00000100 + SER_RS485_RX_DURING_TX = 0b00010000 + + class PlatformSpecific(PlatformSpecificBase): + BAUDRATE_CONSTANTS = { + 0: 0o000000, # hang up + 50: 0o000001, + 75: 0o000002, + 110: 0o000003, + 134: 0o000004, + 150: 0o000005, + 200: 0o000006, + 300: 0o000007, + 600: 0o000010, + 1200: 0o000011, + 1800: 0o000012, + 2400: 0o000013, + 4800: 0o000014, + 9600: 0o000015, + 19200: 0o000016, + 38400: 0o000017, + 57600: 0o010001, + 115200: 0o010002, + 230400: 0o010003, + 460800: 0o010004, + 500000: 0o010005, + 576000: 0o010006, + 921600: 0o010007, + 1000000: 0o010010, + 1152000: 0o010011, + 1500000: 0o010012, + 2000000: 0o010013, + 2500000: 0o010014, + 3000000: 0o010015, + 3500000: 0o010016, + 4000000: 0o010017 + } + + def set_low_latency_mode(self, low_latency_settings): + buf = array.array('i', [0] * 32) + + try: + # get serial_struct + fcntl.ioctl(self.fd, termios.TIOCGSERIAL, buf) + + # set or unset ASYNC_LOW_LATENCY flag + if low_latency_settings: + buf[4] |= 0x2000 + else: + buf[4] &= ~0x2000 + + # set serial_struct + fcntl.ioctl(self.fd, termios.TIOCSSERIAL, buf) + except IOError as e: + raise ValueError('Failed to update ASYNC_LOW_LATENCY flag to {}: {}'.format(low_latency_settings, e)) + + def _set_special_baudrate(self, baudrate): + # right size is 44 on x86_64, allow for some growth + buf = array.array('i', [0] * 64) + try: + # get serial_struct + fcntl.ioctl(self.fd, TCGETS2, buf) + # set custom speed + buf[2] &= ~termios.CBAUD + buf[2] |= BOTHER + buf[9] = buf[10] = baudrate + + # set serial_struct + fcntl.ioctl(self.fd, TCSETS2, buf) + except IOError as e: + raise ValueError('Failed to set custom baud rate ({}): {}'.format(baudrate, e)) + + def _set_rs485_mode(self, rs485_settings): + buf = array.array('i', [0] * 8) # flags, delaytx, delayrx, padding + try: + fcntl.ioctl(self.fd, TIOCGRS485, buf) + buf[0] |= SER_RS485_ENABLED + if rs485_settings is not None: + if rs485_settings.loopback: + buf[0] |= SER_RS485_RX_DURING_TX + else: + buf[0] &= ~SER_RS485_RX_DURING_TX + if rs485_settings.rts_level_for_tx: + buf[0] |= SER_RS485_RTS_ON_SEND + else: + buf[0] &= ~SER_RS485_RTS_ON_SEND + if rs485_settings.rts_level_for_rx: + buf[0] |= SER_RS485_RTS_AFTER_SEND + else: + buf[0] &= ~SER_RS485_RTS_AFTER_SEND + if rs485_settings.delay_before_tx is not None: + buf[1] = int(rs485_settings.delay_before_tx * 1000) + if rs485_settings.delay_before_rx is not None: + buf[2] = int(rs485_settings.delay_before_rx * 1000) + else: + buf[0] = 0 # clear SER_RS485_ENABLED + fcntl.ioctl(self.fd, TIOCSRS485, buf) + except IOError as e: + raise ValueError('Failed to set RS485 mode: {}'.format(e)) + + +elif plat == 'cygwin': # cygwin/win32 (confirmed) + + class PlatformSpecific(PlatformSpecificBase): + BAUDRATE_CONSTANTS = { + 128000: 0x01003, + 256000: 0x01005, + 500000: 0x01007, + 576000: 0x01008, + 921600: 0x01009, + 1000000: 0x0100a, + 1152000: 0x0100b, + 1500000: 0x0100c, + 2000000: 0x0100d, + 2500000: 0x0100e, + 3000000: 0x0100f + } + + +elif plat[:6] == 'darwin': # OS X + import array + IOSSIOSPEED = 0x80045402 # _IOW('T', 2, speed_t) + + class PlatformSpecific(PlatformSpecificBase): + osx_version = os.uname()[2].split('.') + TIOCSBRK = 0x2000747B # _IO('t', 123) + TIOCCBRK = 0x2000747A # _IO('t', 122) + + # Tiger or above can support arbitrary serial speeds + if int(osx_version[0]) >= 8: + def _set_special_baudrate(self, baudrate): + # use IOKit-specific call to set up high speeds + buf = array.array('i', [baudrate]) + fcntl.ioctl(self.fd, IOSSIOSPEED, buf, 1) + + def _update_break_state(self): + """\ + Set break: Controls TXD. When active, no transmitting is possible. + """ + if self._break_state: + fcntl.ioctl(self.fd, PlatformSpecific.TIOCSBRK) + else: + fcntl.ioctl(self.fd, PlatformSpecific.TIOCCBRK) + +elif plat[:3] == 'bsd' or \ + plat[:7] == 'freebsd' or \ + plat[:6] == 'netbsd' or \ + plat[:7] == 'openbsd': + + class ReturnBaudrate(object): + def __getitem__(self, key): + return key + + class PlatformSpecific(PlatformSpecificBase): + # Only tested on FreeBSD: + # The baud rate may be passed in as + # a literal value. + BAUDRATE_CONSTANTS = ReturnBaudrate() + + TIOCSBRK = 0x2000747B # _IO('t', 123) + TIOCCBRK = 0x2000747A # _IO('t', 122) + + + def _update_break_state(self): + """\ + Set break: Controls TXD. When active, no transmitting is possible. + """ + if self._break_state: + fcntl.ioctl(self.fd, PlatformSpecific.TIOCSBRK) + else: + fcntl.ioctl(self.fd, PlatformSpecific.TIOCCBRK) + +else: + class PlatformSpecific(PlatformSpecificBase): + pass + + +# load some constants for later use. +# try to use values from termios, use defaults from linux otherwise +TIOCMGET = getattr(termios, 'TIOCMGET', 0x5415) +TIOCMBIS = getattr(termios, 'TIOCMBIS', 0x5416) +TIOCMBIC = getattr(termios, 'TIOCMBIC', 0x5417) +TIOCMSET = getattr(termios, 'TIOCMSET', 0x5418) + +# TIOCM_LE = getattr(termios, 'TIOCM_LE', 0x001) +TIOCM_DTR = getattr(termios, 'TIOCM_DTR', 0x002) +TIOCM_RTS = getattr(termios, 'TIOCM_RTS', 0x004) +# TIOCM_ST = getattr(termios, 'TIOCM_ST', 0x008) +# TIOCM_SR = getattr(termios, 'TIOCM_SR', 0x010) + +TIOCM_CTS = getattr(termios, 'TIOCM_CTS', 0x020) +TIOCM_CAR = getattr(termios, 'TIOCM_CAR', 0x040) +TIOCM_RNG = getattr(termios, 'TIOCM_RNG', 0x080) +TIOCM_DSR = getattr(termios, 'TIOCM_DSR', 0x100) +TIOCM_CD = getattr(termios, 'TIOCM_CD', TIOCM_CAR) +TIOCM_RI = getattr(termios, 'TIOCM_RI', TIOCM_RNG) +# TIOCM_OUT1 = getattr(termios, 'TIOCM_OUT1', 0x2000) +# TIOCM_OUT2 = getattr(termios, 'TIOCM_OUT2', 0x4000) +if hasattr(termios, 'TIOCINQ'): + TIOCINQ = termios.TIOCINQ +else: + TIOCINQ = getattr(termios, 'FIONREAD', 0x541B) +TIOCOUTQ = getattr(termios, 'TIOCOUTQ', 0x5411) + +TIOCM_zero_str = struct.pack('I', 0) +TIOCM_RTS_str = struct.pack('I', TIOCM_RTS) +TIOCM_DTR_str = struct.pack('I', TIOCM_DTR) + +TIOCSBRK = getattr(termios, 'TIOCSBRK', 0x5427) +TIOCCBRK = getattr(termios, 'TIOCCBRK', 0x5428) + + +class Serial(SerialBase, PlatformSpecific): + """\ + Serial port class POSIX implementation. Serial port configuration is + done with termios and fcntl. Runs on Linux and many other Un*x like + systems. + """ + + def open(self): + """\ + Open port with current settings. This may throw a SerialException + if the port cannot be opened.""" + if self._port is None: + raise SerialException("Port must be configured before it can be used.") + if self.is_open: + raise SerialException("Port is already open.") + self.fd = None + # open + try: + self.fd = os.open(self.portstr, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK) + except OSError as msg: + self.fd = None + raise SerialException(msg.errno, "could not open port {}: {}".format(self._port, msg)) + #~ fcntl.fcntl(self.fd, fcntl.F_SETFL, 0) # set blocking + + self.pipe_abort_read_r, self.pipe_abort_read_w = None, None + self.pipe_abort_write_r, self.pipe_abort_write_w = None, None + + try: + self._reconfigure_port(force_update=True) + + try: + if not self._dsrdtr: + self._update_dtr_state() + if not self._rtscts: + self._update_rts_state() + except IOError as e: + # ignore Invalid argument and Inappropriate ioctl + if e.errno not in (errno.EINVAL, errno.ENOTTY): + raise + + self._reset_input_buffer() + + self.pipe_abort_read_r, self.pipe_abort_read_w = os.pipe() + self.pipe_abort_write_r, self.pipe_abort_write_w = os.pipe() + fcntl.fcntl(self.pipe_abort_read_r, fcntl.F_SETFL, os.O_NONBLOCK) + fcntl.fcntl(self.pipe_abort_write_r, fcntl.F_SETFL, os.O_NONBLOCK) + except BaseException: + try: + os.close(self.fd) + except Exception: + # ignore any exception when closing the port + # also to keep original exception that happened when setting up + pass + self.fd = None + + if self.pipe_abort_read_w is not None: + os.close(self.pipe_abort_read_w) + self.pipe_abort_read_w = None + if self.pipe_abort_read_r is not None: + os.close(self.pipe_abort_read_r) + self.pipe_abort_read_r = None + if self.pipe_abort_write_w is not None: + os.close(self.pipe_abort_write_w) + self.pipe_abort_write_w = None + if self.pipe_abort_write_r is not None: + os.close(self.pipe_abort_write_r) + self.pipe_abort_write_r = None + + raise + + self.is_open = True + + def _reconfigure_port(self, force_update=False): + """Set communication parameters on opened port.""" + if self.fd is None: + raise SerialException("Can only operate on a valid file descriptor") + + # if exclusive lock is requested, create it before we modify anything else + if self._exclusive is not None: + if self._exclusive: + try: + fcntl.flock(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + except IOError as msg: + raise SerialException(msg.errno, "Could not exclusively lock port {}: {}".format(self._port, msg)) + else: + fcntl.flock(self.fd, fcntl.LOCK_UN) + + custom_baud = None + + vmin = vtime = 0 # timeout is done via select + if self._inter_byte_timeout is not None: + vmin = 1 + vtime = int(self._inter_byte_timeout * 10) + try: + orig_attr = termios.tcgetattr(self.fd) + iflag, oflag, cflag, lflag, ispeed, ospeed, cc = orig_attr + except termios.error as msg: # if a port is nonexistent but has a /dev file, it'll fail here + raise SerialException("Could not configure port: {}".format(msg)) + # set up raw mode / no echo / binary + cflag |= (termios.CLOCAL | termios.CREAD) + lflag &= ~(termios.ICANON | termios.ECHO | termios.ECHOE | + termios.ECHOK | termios.ECHONL | + termios.ISIG | termios.IEXTEN) # |termios.ECHOPRT + for flag in ('ECHOCTL', 'ECHOKE'): # netbsd workaround for Erk + if hasattr(termios, flag): + lflag &= ~getattr(termios, flag) + + oflag &= ~(termios.OPOST | termios.ONLCR | termios.OCRNL) + iflag &= ~(termios.INLCR | termios.IGNCR | termios.ICRNL | termios.IGNBRK) + if hasattr(termios, 'IUCLC'): + iflag &= ~termios.IUCLC + if hasattr(termios, 'PARMRK'): + iflag &= ~termios.PARMRK + + # setup baud rate + try: + ispeed = ospeed = getattr(termios, 'B{}'.format(self._baudrate)) + except AttributeError: + try: + ispeed = ospeed = self.BAUDRATE_CONSTANTS[self._baudrate] + except KeyError: + #~ raise ValueError('Invalid baud rate: %r' % self._baudrate) + + # See if BOTHER is defined for this platform; if it is, use + # this for a speed not defined in the baudrate constants list. + try: + ispeed = ospeed = BOTHER + except NameError: + # may need custom baud rate, it isn't in our list. + ispeed = ospeed = getattr(termios, 'B38400') + + try: + custom_baud = int(self._baudrate) # store for later + except ValueError: + raise ValueError('Invalid baud rate: {!r}'.format(self._baudrate)) + else: + if custom_baud < 0: + raise ValueError('Invalid baud rate: {!r}'.format(self._baudrate)) + + # setup char len + cflag &= ~termios.CSIZE + if self._bytesize == 8: + cflag |= termios.CS8 + elif self._bytesize == 7: + cflag |= termios.CS7 + elif self._bytesize == 6: + cflag |= termios.CS6 + elif self._bytesize == 5: + cflag |= termios.CS5 + else: + raise ValueError('Invalid char len: {!r}'.format(self._bytesize)) + # setup stop bits + if self._stopbits == serial.STOPBITS_ONE: + cflag &= ~(termios.CSTOPB) + elif self._stopbits == serial.STOPBITS_ONE_POINT_FIVE: + cflag |= (termios.CSTOPB) # XXX same as TWO.. there is no POSIX support for 1.5 + elif self._stopbits == serial.STOPBITS_TWO: + cflag |= (termios.CSTOPB) + else: + raise ValueError('Invalid stop bit specification: {!r}'.format(self._stopbits)) + # setup parity + iflag &= ~(termios.INPCK | termios.ISTRIP) + if self._parity == serial.PARITY_NONE: + cflag &= ~(termios.PARENB | termios.PARODD | CMSPAR) + elif self._parity == serial.PARITY_EVEN: + cflag &= ~(termios.PARODD | CMSPAR) + cflag |= (termios.PARENB) + elif self._parity == serial.PARITY_ODD: + cflag &= ~CMSPAR + cflag |= (termios.PARENB | termios.PARODD) + elif self._parity == serial.PARITY_MARK and CMSPAR: + cflag |= (termios.PARENB | CMSPAR | termios.PARODD) + elif self._parity == serial.PARITY_SPACE and CMSPAR: + cflag |= (termios.PARENB | CMSPAR) + cflag &= ~(termios.PARODD) + else: + raise ValueError('Invalid parity: {!r}'.format(self._parity)) + # setup flow control + # xonxoff + if hasattr(termios, 'IXANY'): + if self._xonxoff: + iflag |= (termios.IXON | termios.IXOFF) # |termios.IXANY) + else: + iflag &= ~(termios.IXON | termios.IXOFF | termios.IXANY) + else: + if self._xonxoff: + iflag |= (termios.IXON | termios.IXOFF) + else: + iflag &= ~(termios.IXON | termios.IXOFF) + # rtscts + if hasattr(termios, 'CRTSCTS'): + if self._rtscts: + cflag |= (termios.CRTSCTS) + else: + cflag &= ~(termios.CRTSCTS) + elif hasattr(termios, 'CNEW_RTSCTS'): # try it with alternate constant name + if self._rtscts: + cflag |= (termios.CNEW_RTSCTS) + else: + cflag &= ~(termios.CNEW_RTSCTS) + # XXX should there be a warning if setting up rtscts (and xonxoff etc) fails?? + + # buffer + # vmin "minimal number of characters to be read. 0 for non blocking" + if vmin < 0 or vmin > 255: + raise ValueError('Invalid vmin: {!r}'.format(vmin)) + cc[termios.VMIN] = vmin + # vtime + if vtime < 0 or vtime > 255: + raise ValueError('Invalid vtime: {!r}'.format(vtime)) + cc[termios.VTIME] = vtime + # activate settings + if force_update or [iflag, oflag, cflag, lflag, ispeed, ospeed, cc] != orig_attr: + termios.tcsetattr( + self.fd, + termios.TCSANOW, + [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]) + + # apply custom baud rate, if any + if custom_baud is not None: + self._set_special_baudrate(custom_baud) + + if self._rs485_mode is not None: + self._set_rs485_mode(self._rs485_mode) + + def close(self): + """Close port""" + if self.is_open: + if self.fd is not None: + os.close(self.fd) + self.fd = None + os.close(self.pipe_abort_read_w) + os.close(self.pipe_abort_read_r) + os.close(self.pipe_abort_write_w) + os.close(self.pipe_abort_write_r) + self.pipe_abort_read_r, self.pipe_abort_read_w = None, None + self.pipe_abort_write_r, self.pipe_abort_write_w = None, None + self.is_open = False + + # - - - - - - - - - - - - - - - - - - - - - - - - + + @property + def in_waiting(self): + """Return the number of bytes currently in the input buffer.""" + #~ s = fcntl.ioctl(self.fd, termios.FIONREAD, TIOCM_zero_str) + s = fcntl.ioctl(self.fd, TIOCINQ, TIOCM_zero_str) + return struct.unpack('I', s)[0] + + # select based implementation, proved to work on many systems + def read(self, size=1): + """\ + Read size bytes from the serial port. If a timeout is set it may + return less characters as requested. With no timeout it will block + until the requested number of bytes is read. + """ + if not self.is_open: + raise PortNotOpenError() + read = bytearray() + timeout = Timeout(self._timeout) + while len(read) < size: + try: + ready, _, _ = select.select([self.fd, self.pipe_abort_read_r], [], [], timeout.time_left()) + if self.pipe_abort_read_r in ready: + os.read(self.pipe_abort_read_r, 1000) + break + # If select was used with a timeout, and the timeout occurs, it + # returns with empty lists -> thus abort read operation. + # For timeout == 0 (non-blocking operation) also abort when + # there is nothing to read. + if not ready: + break # timeout + buf = os.read(self.fd, size - len(read)) + except OSError as e: + # this is for Python 3.x where select.error is a subclass of + # OSError ignore BlockingIOErrors and EINTR. other errors are shown + # https://www.python.org/dev/peps/pep-0475. + if e.errno not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR): + raise SerialException('read failed: {}'.format(e)) + except select.error as e: + # this is for Python 2.x + # ignore BlockingIOErrors and EINTR. all errors are shown + # see also http://www.python.org/dev/peps/pep-3151/#select + if e[0] not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR): + raise SerialException('read failed: {}'.format(e)) + else: + # read should always return some data as select reported it was + # ready to read when we get to this point. + if not buf: + # Disconnected devices, at least on Linux, show the + # behavior that they are always ready to read immediately + # but reading returns nothing. + raise SerialException( + 'device reports readiness to read but returned no data ' + '(device disconnected or multiple access on port?)') + read.extend(buf) + + if timeout.expired(): + break + return bytes(read) + + def cancel_read(self): + if self.is_open: + os.write(self.pipe_abort_read_w, b"x") + + def cancel_write(self): + if self.is_open: + os.write(self.pipe_abort_write_w, b"x") + + def write(self, data): + """Output the given byte string over the serial port.""" + if not self.is_open: + raise PortNotOpenError() + d = to_bytes(data) + tx_len = length = len(d) + timeout = Timeout(self._write_timeout) + while tx_len > 0: + try: + n = os.write(self.fd, d) + if timeout.is_non_blocking: + # Zero timeout indicates non-blocking - simply return the + # number of bytes of data actually written + return n + elif not timeout.is_infinite: + # when timeout is set, use select to wait for being ready + # with the time left as timeout + if timeout.expired(): + raise SerialTimeoutException('Write timeout') + abort, ready, _ = select.select([self.pipe_abort_write_r], [self.fd], [], timeout.time_left()) + if abort: + os.read(self.pipe_abort_write_r, 1000) + break + if not ready: + raise SerialTimeoutException('Write timeout') + else: + assert timeout.time_left() is None + # wait for write operation + abort, ready, _ = select.select([self.pipe_abort_write_r], [self.fd], [], None) + if abort: + os.read(self.pipe_abort_write_r, 1) + break + if not ready: + raise SerialException('write failed (select)') + d = d[n:] + tx_len -= n + except SerialException: + raise + except OSError as e: + # this is for Python 3.x where select.error is a subclass of + # OSError ignore BlockingIOErrors and EINTR. other errors are shown + # https://www.python.org/dev/peps/pep-0475. + if e.errno not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR): + raise SerialException('write failed: {}'.format(e)) + except select.error as e: + # this is for Python 2.x + # ignore BlockingIOErrors and EINTR. all errors are shown + # see also http://www.python.org/dev/peps/pep-3151/#select + if e[0] not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR): + raise SerialException('write failed: {}'.format(e)) + if not timeout.is_non_blocking and timeout.expired(): + raise SerialTimeoutException('Write timeout') + return length - len(d) + + def flush(self): + """\ + Flush of file like objects. In this case, wait until all data + is written. + """ + if not self.is_open: + raise PortNotOpenError() + termios.tcdrain(self.fd) + + def _reset_input_buffer(self): + """Clear input buffer, discarding all that is in the buffer.""" + termios.tcflush(self.fd, termios.TCIFLUSH) + + def reset_input_buffer(self): + """Clear input buffer, discarding all that is in the buffer.""" + if not self.is_open: + raise PortNotOpenError() + self._reset_input_buffer() + + def reset_output_buffer(self): + """\ + Clear output buffer, aborting the current output and discarding all + that is in the buffer. + """ + if not self.is_open: + raise PortNotOpenError() + termios.tcflush(self.fd, termios.TCOFLUSH) + + def send_break(self, duration=0.25): + """\ + Send break condition. Timed, returns to idle state after given + duration. + """ + if not self.is_open: + raise PortNotOpenError() + termios.tcsendbreak(self.fd, int(duration / 0.25)) + + def _update_rts_state(self): + """Set terminal status line: Request To Send""" + if self._rts_state: + fcntl.ioctl(self.fd, TIOCMBIS, TIOCM_RTS_str) + else: + fcntl.ioctl(self.fd, TIOCMBIC, TIOCM_RTS_str) + + def _update_dtr_state(self): + """Set terminal status line: Data Terminal Ready""" + if self._dtr_state: + fcntl.ioctl(self.fd, TIOCMBIS, TIOCM_DTR_str) + else: + fcntl.ioctl(self.fd, TIOCMBIC, TIOCM_DTR_str) + + @property + def cts(self): + """Read terminal status line: Clear To Send""" + if not self.is_open: + raise PortNotOpenError() + s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str) + return struct.unpack('I', s)[0] & TIOCM_CTS != 0 + + @property + def dsr(self): + """Read terminal status line: Data Set Ready""" + if not self.is_open: + raise PortNotOpenError() + s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str) + return struct.unpack('I', s)[0] & TIOCM_DSR != 0 + + @property + def ri(self): + """Read terminal status line: Ring Indicator""" + if not self.is_open: + raise PortNotOpenError() + s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str) + return struct.unpack('I', s)[0] & TIOCM_RI != 0 + + @property + def cd(self): + """Read terminal status line: Carrier Detect""" + if not self.is_open: + raise PortNotOpenError() + s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str) + return struct.unpack('I', s)[0] & TIOCM_CD != 0 + + # - - platform specific - - - - + + @property + def out_waiting(self): + """Return the number of bytes currently in the output buffer.""" + #~ s = fcntl.ioctl(self.fd, termios.FIONREAD, TIOCM_zero_str) + s = fcntl.ioctl(self.fd, TIOCOUTQ, TIOCM_zero_str) + return struct.unpack('I', s)[0] + + def fileno(self): + """\ + For easier use of the serial port instance with select. + WARNING: this function is not portable to different platforms! + """ + if not self.is_open: + raise PortNotOpenError() + return self.fd + + def set_input_flow_control(self, enable=True): + """\ + Manually control flow - when software flow control is enabled. + This will send XON (true) or XOFF (false) to the other device. + WARNING: this function is not portable to different platforms! + """ + if not self.is_open: + raise PortNotOpenError() + if enable: + termios.tcflow(self.fd, termios.TCION) + else: + termios.tcflow(self.fd, termios.TCIOFF) + + def set_output_flow_control(self, enable=True): + """\ + Manually control flow of outgoing data - when hardware or software flow + control is enabled. + WARNING: this function is not portable to different platforms! + """ + if not self.is_open: + raise PortNotOpenError() + if enable: + termios.tcflow(self.fd, termios.TCOON) + else: + termios.tcflow(self.fd, termios.TCOOFF) + + def nonblocking(self): + """DEPRECATED - has no use""" + import warnings + warnings.warn("nonblocking() has no effect, already nonblocking", DeprecationWarning) + + +class PosixPollSerial(Serial): + """\ + Poll based read implementation. Not all systems support poll properly. + However this one has better handling of errors, such as a device + disconnecting while it's in use (e.g. USB-serial unplugged). + """ + + def read(self, size=1): + """\ + Read size bytes from the serial port. If a timeout is set it may + return less characters as requested. With no timeout it will block + until the requested number of bytes is read. + """ + if not self.is_open: + raise PortNotOpenError() + read = bytearray() + timeout = Timeout(self._timeout) + poll = select.poll() + poll.register(self.fd, select.POLLIN | select.POLLERR | select.POLLHUP | select.POLLNVAL) + poll.register(self.pipe_abort_read_r, select.POLLIN | select.POLLERR | select.POLLHUP | select.POLLNVAL) + if size > 0: + while len(read) < size: + # print "\tread(): size",size, "have", len(read) #debug + # wait until device becomes ready to read (or something fails) + for fd, event in poll.poll(None if timeout.is_infinite else (timeout.time_left() * 1000)): + if fd == self.pipe_abort_read_r: + break + if event & (select.POLLERR | select.POLLHUP | select.POLLNVAL): + raise SerialException('device reports error (poll)') + # we don't care if it is select.POLLIN or timeout, that's + # handled below + if fd == self.pipe_abort_read_r: + os.read(self.pipe_abort_read_r, 1000) + break + buf = os.read(self.fd, size - len(read)) + read.extend(buf) + if timeout.expired() \ + or (self._inter_byte_timeout is not None and self._inter_byte_timeout > 0) and not buf: + break # early abort on timeout + return bytes(read) + + +class VTIMESerial(Serial): + """\ + Implement timeout using vtime of tty device instead of using select. + This means that no inter character timeout can be specified and that + the error handling is degraded. + + Overall timeout is disabled when inter-character timeout is used. + + Note that this implementation does NOT support cancel_read(), it will + just ignore that. + """ + + def _reconfigure_port(self, force_update=True): + """Set communication parameters on opened port.""" + super(VTIMESerial, self)._reconfigure_port() + fcntl.fcntl(self.fd, fcntl.F_SETFL, 0) # clear O_NONBLOCK + + if self._inter_byte_timeout is not None: + vmin = 1 + vtime = int(self._inter_byte_timeout * 10) + elif self._timeout is None: + vmin = 1 + vtime = 0 + else: + vmin = 0 + vtime = int(self._timeout * 10) + try: + orig_attr = termios.tcgetattr(self.fd) + iflag, oflag, cflag, lflag, ispeed, ospeed, cc = orig_attr + except termios.error as msg: # if a port is nonexistent but has a /dev file, it'll fail here + raise serial.SerialException("Could not configure port: {}".format(msg)) + + if vtime < 0 or vtime > 255: + raise ValueError('Invalid vtime: {!r}'.format(vtime)) + cc[termios.VTIME] = vtime + cc[termios.VMIN] = vmin + + termios.tcsetattr( + self.fd, + termios.TCSANOW, + [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]) + + def read(self, size=1): + """\ + Read size bytes from the serial port. If a timeout is set it may + return less characters as requested. With no timeout it will block + until the requested number of bytes is read. + """ + if not self.is_open: + raise PortNotOpenError() + read = bytearray() + while len(read) < size: + buf = os.read(self.fd, size - len(read)) + if not buf: + break + read.extend(buf) + return bytes(read) + + # hack to make hasattr return false + cancel_read = property() diff --git a/tools/python-embed/Lib/site-packages/serial/serialutil.py b/tools/python-embed/Lib/site-packages/serial/serialutil.py new file mode 100644 index 0000000..789219e --- /dev/null +++ b/tools/python-embed/Lib/site-packages/serial/serialutil.py @@ -0,0 +1,697 @@ +#! python +# +# Base class and support functions used by various backends. +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2001-2020 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import absolute_import + +import io +import time + +# ``memoryview`` was introduced in Python 2.7 and ``bytes(some_memoryview)`` +# isn't returning the contents (very unfortunate). Therefore we need special +# cases and test for it. Ensure that there is a ``memoryview`` object for older +# Python versions. This is easier than making every test dependent on its +# existence. +try: + memoryview +except (NameError, AttributeError): + # implementation does not matter as we do not really use it. + # it just must not inherit from something else we might care for. + class memoryview(object): # pylint: disable=redefined-builtin,invalid-name + pass + +try: + unicode +except (NameError, AttributeError): + unicode = str # for Python 3, pylint: disable=redefined-builtin,invalid-name + +try: + basestring +except (NameError, AttributeError): + basestring = (str,) # for Python 3, pylint: disable=redefined-builtin,invalid-name + + +# "for byte in data" fails for python3 as it returns ints instead of bytes +def iterbytes(b): + """Iterate over bytes, returning bytes instead of ints (python3)""" + if isinstance(b, memoryview): + b = b.tobytes() + i = 0 + while True: + a = b[i:i + 1] + i += 1 + if a: + yield a + else: + break + + +# all Python versions prior 3.x convert ``str([17])`` to '[17]' instead of '\x11' +# so a simple ``bytes(sequence)`` doesn't work for all versions +def to_bytes(seq): + """convert a sequence to a bytes type""" + if isinstance(seq, bytes): + return seq + elif isinstance(seq, bytearray): + return bytes(seq) + elif isinstance(seq, memoryview): + return seq.tobytes() + elif isinstance(seq, unicode): + raise TypeError('unicode strings are not supported, please encode to bytes: {!r}'.format(seq)) + else: + # handle list of integers and bytes (one or more items) for Python 2 and 3 + return bytes(bytearray(seq)) + + +# create control bytes +XON = to_bytes([17]) +XOFF = to_bytes([19]) + +CR = to_bytes([13]) +LF = to_bytes([10]) + + +PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE = 'N', 'E', 'O', 'M', 'S' +STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO = (1, 1.5, 2) +FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS = (5, 6, 7, 8) + +PARITY_NAMES = { + PARITY_NONE: 'None', + PARITY_EVEN: 'Even', + PARITY_ODD: 'Odd', + PARITY_MARK: 'Mark', + PARITY_SPACE: 'Space', +} + + +class SerialException(IOError): + """Base class for serial port related exceptions.""" + + +class SerialTimeoutException(SerialException): + """Write timeouts give an exception""" + + +class PortNotOpenError(SerialException): + """Port is not open""" + def __init__(self): + super(PortNotOpenError, self).__init__('Attempting to use a port that is not open') + + +class Timeout(object): + """\ + Abstraction for timeout operations. Using time.monotonic() if available + or time.time() in all other cases. + + The class can also be initialized with 0 or None, in order to support + non-blocking and fully blocking I/O operations. The attributes + is_non_blocking and is_infinite are set accordingly. + """ + if hasattr(time, 'monotonic'): + # Timeout implementation with time.monotonic(). This function is only + # supported by Python 3.3 and above. It returns a time in seconds + # (float) just as time.time(), but is not affected by system clock + # adjustments. + TIME = time.monotonic + else: + # Timeout implementation with time.time(). This is compatible with all + # Python versions but has issues if the clock is adjusted while the + # timeout is running. + TIME = time.time + + def __init__(self, duration): + """Initialize a timeout with given duration""" + self.is_infinite = (duration is None) + self.is_non_blocking = (duration == 0) + self.duration = duration + if duration is not None: + self.target_time = self.TIME() + duration + else: + self.target_time = None + + def expired(self): + """Return a boolean, telling if the timeout has expired""" + return self.target_time is not None and self.time_left() <= 0 + + def time_left(self): + """Return how many seconds are left until the timeout expires""" + if self.is_non_blocking: + return 0 + elif self.is_infinite: + return None + else: + delta = self.target_time - self.TIME() + if delta > self.duration: + # clock jumped, recalculate + self.target_time = self.TIME() + self.duration + return self.duration + else: + return max(0, delta) + + def restart(self, duration): + """\ + Restart a timeout, only supported if a timeout was already set up + before. + """ + self.duration = duration + self.target_time = self.TIME() + duration + + +class SerialBase(io.RawIOBase): + """\ + Serial port base class. Provides __init__ function and properties to + get/set port settings. + """ + + # default values, may be overridden in subclasses that do not support all values + BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800, + 9600, 19200, 38400, 57600, 115200, 230400, 460800, 500000, + 576000, 921600, 1000000, 1152000, 1500000, 2000000, 2500000, + 3000000, 3500000, 4000000) + BYTESIZES = (FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS) + PARITIES = (PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE) + STOPBITS = (STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO) + + def __init__(self, + port=None, + baudrate=9600, + bytesize=EIGHTBITS, + parity=PARITY_NONE, + stopbits=STOPBITS_ONE, + timeout=None, + xonxoff=False, + rtscts=False, + write_timeout=None, + dsrdtr=False, + inter_byte_timeout=None, + exclusive=None, + **kwargs): + """\ + Initialize comm port object. If a "port" is given, then the port will be + opened immediately. Otherwise a Serial port object in closed state + is returned. + """ + + self.is_open = False + self.portstr = None + self.name = None + # correct values are assigned below through properties + self._port = None + self._baudrate = None + self._bytesize = None + self._parity = None + self._stopbits = None + self._timeout = None + self._write_timeout = None + self._xonxoff = None + self._rtscts = None + self._dsrdtr = None + self._inter_byte_timeout = None + self._rs485_mode = None # disabled by default + self._rts_state = True + self._dtr_state = True + self._break_state = False + self._exclusive = None + + # assign values using get/set methods using the properties feature + self.port = port + self.baudrate = baudrate + self.bytesize = bytesize + self.parity = parity + self.stopbits = stopbits + self.timeout = timeout + self.write_timeout = write_timeout + self.xonxoff = xonxoff + self.rtscts = rtscts + self.dsrdtr = dsrdtr + self.inter_byte_timeout = inter_byte_timeout + self.exclusive = exclusive + + # watch for backward compatible kwargs + if 'writeTimeout' in kwargs: + self.write_timeout = kwargs.pop('writeTimeout') + if 'interCharTimeout' in kwargs: + self.inter_byte_timeout = kwargs.pop('interCharTimeout') + if kwargs: + raise ValueError('unexpected keyword arguments: {!r}'.format(kwargs)) + + if port is not None: + self.open() + + # - - - - - - - - - - - - - - - - - - - - - - - - + + # to be implemented by subclasses: + # def open(self): + # def close(self): + + # - - - - - - - - - - - - - - - - - - - - - - - - + + @property + def port(self): + """\ + Get the current port setting. The value that was passed on init or using + setPort() is passed back. + """ + return self._port + + @port.setter + def port(self, port): + """\ + Change the port. + """ + if port is not None and not isinstance(port, basestring): + raise ValueError('"port" must be None or a string, not {}'.format(type(port))) + was_open = self.is_open + if was_open: + self.close() + self.portstr = port + self._port = port + self.name = self.portstr + if was_open: + self.open() + + @property + def baudrate(self): + """Get the current baud rate setting.""" + return self._baudrate + + @baudrate.setter + def baudrate(self, baudrate): + """\ + Change baud rate. It raises a ValueError if the port is open and the + baud rate is not possible. If the port is closed, then the value is + accepted and the exception is raised when the port is opened. + """ + try: + b = int(baudrate) + except TypeError: + raise ValueError("Not a valid baudrate: {!r}".format(baudrate)) + else: + if b < 0: + raise ValueError("Not a valid baudrate: {!r}".format(baudrate)) + self._baudrate = b + if self.is_open: + self._reconfigure_port() + + @property + def bytesize(self): + """Get the current byte size setting.""" + return self._bytesize + + @bytesize.setter + def bytesize(self, bytesize): + """Change byte size.""" + if bytesize not in self.BYTESIZES: + raise ValueError("Not a valid byte size: {!r}".format(bytesize)) + self._bytesize = bytesize + if self.is_open: + self._reconfigure_port() + + @property + def exclusive(self): + """Get the current exclusive access setting.""" + return self._exclusive + + @exclusive.setter + def exclusive(self, exclusive): + """Change the exclusive access setting.""" + self._exclusive = exclusive + if self.is_open: + self._reconfigure_port() + + @property + def parity(self): + """Get the current parity setting.""" + return self._parity + + @parity.setter + def parity(self, parity): + """Change parity setting.""" + if parity not in self.PARITIES: + raise ValueError("Not a valid parity: {!r}".format(parity)) + self._parity = parity + if self.is_open: + self._reconfigure_port() + + @property + def stopbits(self): + """Get the current stop bits setting.""" + return self._stopbits + + @stopbits.setter + def stopbits(self, stopbits): + """Change stop bits size.""" + if stopbits not in self.STOPBITS: + raise ValueError("Not a valid stop bit size: {!r}".format(stopbits)) + self._stopbits = stopbits + if self.is_open: + self._reconfigure_port() + + @property + def timeout(self): + """Get the current timeout setting.""" + return self._timeout + + @timeout.setter + def timeout(self, timeout): + """Change timeout setting.""" + if timeout is not None: + try: + timeout + 1 # test if it's a number, will throw a TypeError if not... + except TypeError: + raise ValueError("Not a valid timeout: {!r}".format(timeout)) + if timeout < 0: + raise ValueError("Not a valid timeout: {!r}".format(timeout)) + self._timeout = timeout + if self.is_open: + self._reconfigure_port() + + @property + def write_timeout(self): + """Get the current timeout setting.""" + return self._write_timeout + + @write_timeout.setter + def write_timeout(self, timeout): + """Change timeout setting.""" + if timeout is not None: + if timeout < 0: + raise ValueError("Not a valid timeout: {!r}".format(timeout)) + try: + timeout + 1 # test if it's a number, will throw a TypeError if not... + except TypeError: + raise ValueError("Not a valid timeout: {!r}".format(timeout)) + + self._write_timeout = timeout + if self.is_open: + self._reconfigure_port() + + @property + def inter_byte_timeout(self): + """Get the current inter-character timeout setting.""" + return self._inter_byte_timeout + + @inter_byte_timeout.setter + def inter_byte_timeout(self, ic_timeout): + """Change inter-byte timeout setting.""" + if ic_timeout is not None: + if ic_timeout < 0: + raise ValueError("Not a valid timeout: {!r}".format(ic_timeout)) + try: + ic_timeout + 1 # test if it's a number, will throw a TypeError if not... + except TypeError: + raise ValueError("Not a valid timeout: {!r}".format(ic_timeout)) + + self._inter_byte_timeout = ic_timeout + if self.is_open: + self._reconfigure_port() + + @property + def xonxoff(self): + """Get the current XON/XOFF setting.""" + return self._xonxoff + + @xonxoff.setter + def xonxoff(self, xonxoff): + """Change XON/XOFF setting.""" + self._xonxoff = xonxoff + if self.is_open: + self._reconfigure_port() + + @property + def rtscts(self): + """Get the current RTS/CTS flow control setting.""" + return self._rtscts + + @rtscts.setter + def rtscts(self, rtscts): + """Change RTS/CTS flow control setting.""" + self._rtscts = rtscts + if self.is_open: + self._reconfigure_port() + + @property + def dsrdtr(self): + """Get the current DSR/DTR flow control setting.""" + return self._dsrdtr + + @dsrdtr.setter + def dsrdtr(self, dsrdtr=None): + """Change DsrDtr flow control setting.""" + if dsrdtr is None: + # if not set, keep backwards compatibility and follow rtscts setting + self._dsrdtr = self._rtscts + else: + # if defined independently, follow its value + self._dsrdtr = dsrdtr + if self.is_open: + self._reconfigure_port() + + @property + def rts(self): + return self._rts_state + + @rts.setter + def rts(self, value): + self._rts_state = value + if self.is_open: + self._update_rts_state() + + @property + def dtr(self): + return self._dtr_state + + @dtr.setter + def dtr(self, value): + self._dtr_state = value + if self.is_open: + self._update_dtr_state() + + @property + def break_condition(self): + return self._break_state + + @break_condition.setter + def break_condition(self, value): + self._break_state = value + if self.is_open: + self._update_break_state() + + # - - - - - - - - - - - - - - - - - - - - - - - - + # functions useful for RS-485 adapters + + @property + def rs485_mode(self): + """\ + Enable RS485 mode and apply new settings, set to None to disable. + See serial.rs485.RS485Settings for more info about the value. + """ + return self._rs485_mode + + @rs485_mode.setter + def rs485_mode(self, rs485_settings): + self._rs485_mode = rs485_settings + if self.is_open: + self._reconfigure_port() + + # - - - - - - - - - - - - - - - - - - - - - - - - + + _SAVED_SETTINGS = ('baudrate', 'bytesize', 'parity', 'stopbits', 'xonxoff', + 'dsrdtr', 'rtscts', 'timeout', 'write_timeout', + 'inter_byte_timeout') + + def get_settings(self): + """\ + Get current port settings as a dictionary. For use with + apply_settings(). + """ + return dict([(key, getattr(self, '_' + key)) for key in self._SAVED_SETTINGS]) + + def apply_settings(self, d): + """\ + Apply stored settings from a dictionary returned from + get_settings(). It's allowed to delete keys from the dictionary. These + values will simply left unchanged. + """ + for key in self._SAVED_SETTINGS: + if key in d and d[key] != getattr(self, '_' + key): # check against internal "_" value + setattr(self, key, d[key]) # set non "_" value to use properties write function + + # - - - - - - - - - - - - - - - - - - - - - - - - + + def __repr__(self): + """String representation of the current port settings and its state.""" + return '{name}(port={p.portstr!r}, ' \ + 'baudrate={p.baudrate!r}, bytesize={p.bytesize!r}, parity={p.parity!r}, ' \ + 'stopbits={p.stopbits!r}, timeout={p.timeout!r}, xonxoff={p.xonxoff!r}, ' \ + 'rtscts={p.rtscts!r}, dsrdtr={p.dsrdtr!r})'.format( + name=self.__class__.__name__, id=id(self), p=self) + + # - - - - - - - - - - - - - - - - - - - - - - - - + # compatibility with io library + # pylint: disable=invalid-name,missing-docstring + + def readable(self): + return True + + def writable(self): + return True + + def seekable(self): + return False + + def readinto(self, b): + data = self.read(len(b)) + n = len(data) + try: + b[:n] = data + except TypeError as err: + import array + if not isinstance(b, array.array): + raise err + b[:n] = array.array('b', data) + return n + + # - - - - - - - - - - - - - - - - - - - - - - - - + # context manager + + def __enter__(self): + if self._port is not None and not self.is_open: + self.open() + return self + + def __exit__(self, *args, **kwargs): + self.close() + + # - - - - - - - - - - - - - - - - - - - - - - - - + + def send_break(self, duration=0.25): + """\ + Send break condition. Timed, returns to idle state after given + duration. + """ + if not self.is_open: + raise PortNotOpenError() + self.break_condition = True + time.sleep(duration) + self.break_condition = False + + # - - - - - - - - - - - - - - - - - - - - - - - - + # backwards compatibility / deprecated functions + + def flushInput(self): + self.reset_input_buffer() + + def flushOutput(self): + self.reset_output_buffer() + + def inWaiting(self): + return self.in_waiting + + def sendBreak(self, duration=0.25): + self.send_break(duration) + + def setRTS(self, value=1): + self.rts = value + + def setDTR(self, value=1): + self.dtr = value + + def getCTS(self): + return self.cts + + def getDSR(self): + return self.dsr + + def getRI(self): + return self.ri + + def getCD(self): + return self.cd + + def setPort(self, port): + self.port = port + + @property + def writeTimeout(self): + return self.write_timeout + + @writeTimeout.setter + def writeTimeout(self, timeout): + self.write_timeout = timeout + + @property + def interCharTimeout(self): + return self.inter_byte_timeout + + @interCharTimeout.setter + def interCharTimeout(self, interCharTimeout): + self.inter_byte_timeout = interCharTimeout + + def getSettingsDict(self): + return self.get_settings() + + def applySettingsDict(self, d): + self.apply_settings(d) + + def isOpen(self): + return self.is_open + + # - - - - - - - - - - - - - - - - - - - - - - - - + # additional functionality + + def read_all(self): + """\ + Read all bytes currently available in the buffer of the OS. + """ + return self.read(self.in_waiting) + + def read_until(self, expected=LF, size=None): + """\ + Read until an expected sequence is found ('\n' by default), the size + is exceeded or until timeout occurs. + """ + lenterm = len(expected) + line = bytearray() + timeout = Timeout(self._timeout) + while True: + c = self.read(1) + if c: + line += c + if line[-lenterm:] == expected: + break + if size is not None and len(line) >= size: + break + else: + break + if timeout.expired(): + break + return bytes(line) + + def iread_until(self, *args, **kwargs): + """\ + Read lines, implemented as generator. It will raise StopIteration on + timeout (empty read). + """ + while True: + line = self.read_until(*args, **kwargs) + if not line: + break + yield line + + +# - - - - - - - - - - - - - - - - - - - - - - - - - +if __name__ == '__main__': + import sys + s = SerialBase() + sys.stdout.write('port name: {}\n'.format(s.name)) + sys.stdout.write('baud rates: {}\n'.format(s.BAUDRATES)) + sys.stdout.write('byte sizes: {}\n'.format(s.BYTESIZES)) + sys.stdout.write('parities: {}\n'.format(s.PARITIES)) + sys.stdout.write('stop bits: {}\n'.format(s.STOPBITS)) + sys.stdout.write('{}\n'.format(s)) diff --git a/tools/python-embed/Lib/site-packages/serial/serialwin32.py b/tools/python-embed/Lib/site-packages/serial/serialwin32.py new file mode 100644 index 0000000..e7da929 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/serial/serialwin32.py @@ -0,0 +1,477 @@ +#! python +# +# backend for Windows ("win32" incl. 32/64 bit support) +# +# (C) 2001-2020 Chris Liechti +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# SPDX-License-Identifier: BSD-3-Clause +# +# Initial patch to use ctypes by Giovanni Bajo + +from __future__ import absolute_import + +# pylint: disable=invalid-name,too-few-public-methods +import ctypes +import time +from serial import win32 + +import serial +from serial.serialutil import SerialBase, SerialException, to_bytes, PortNotOpenError, SerialTimeoutException + + +class Serial(SerialBase): + """Serial port implementation for Win32 based on ctypes.""" + + BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800, + 9600, 19200, 38400, 57600, 115200) + + def __init__(self, *args, **kwargs): + self._port_handle = None + self._overlapped_read = None + self._overlapped_write = None + super(Serial, self).__init__(*args, **kwargs) + + def open(self): + """\ + Open port with current settings. This may throw a SerialException + if the port cannot be opened. + """ + if self._port is None: + raise SerialException("Port must be configured before it can be used.") + if self.is_open: + raise SerialException("Port is already open.") + # the "\\.\COMx" format is required for devices other than COM1-COM8 + # not all versions of windows seem to support this properly + # so that the first few ports are used with the DOS device name + port = self.name + try: + if port.upper().startswith('COM') and int(port[3:]) > 8: + port = '\\\\.\\' + port + except ValueError: + # for like COMnotanumber + pass + self._port_handle = win32.CreateFile( + port, + win32.GENERIC_READ | win32.GENERIC_WRITE, + 0, # exclusive access + None, # no security + win32.OPEN_EXISTING, + win32.FILE_ATTRIBUTE_NORMAL | win32.FILE_FLAG_OVERLAPPED, + 0) + if self._port_handle == win32.INVALID_HANDLE_VALUE: + self._port_handle = None # 'cause __del__ is called anyway + raise SerialException("could not open port {!r}: {!r}".format(self.portstr, ctypes.WinError())) + + try: + self._overlapped_read = win32.OVERLAPPED() + self._overlapped_read.hEvent = win32.CreateEvent(None, 1, 0, None) + self._overlapped_write = win32.OVERLAPPED() + #~ self._overlapped_write.hEvent = win32.CreateEvent(None, 1, 0, None) + self._overlapped_write.hEvent = win32.CreateEvent(None, 0, 0, None) + + # Setup a 4k buffer + win32.SetupComm(self._port_handle, 4096, 4096) + + # Save original timeout values: + self._orgTimeouts = win32.COMMTIMEOUTS() + win32.GetCommTimeouts(self._port_handle, ctypes.byref(self._orgTimeouts)) + + self._reconfigure_port() + + # Clear buffers: + # Remove anything that was there + win32.PurgeComm( + self._port_handle, + win32.PURGE_TXCLEAR | win32.PURGE_TXABORT | + win32.PURGE_RXCLEAR | win32.PURGE_RXABORT) + except: + try: + self._close() + except: + # ignore any exception when closing the port + # also to keep original exception that happened when setting up + pass + self._port_handle = None + raise + else: + self.is_open = True + + def _reconfigure_port(self): + """Set communication parameters on opened port.""" + if not self._port_handle: + raise SerialException("Can only operate on a valid port handle") + + # Set Windows timeout values + # timeouts is a tuple with the following items: + # (ReadIntervalTimeout,ReadTotalTimeoutMultiplier, + # ReadTotalTimeoutConstant,WriteTotalTimeoutMultiplier, + # WriteTotalTimeoutConstant) + timeouts = win32.COMMTIMEOUTS() + if self._timeout is None: + pass # default of all zeros is OK + elif self._timeout == 0: + timeouts.ReadIntervalTimeout = win32.MAXDWORD + else: + timeouts.ReadTotalTimeoutConstant = max(int(self._timeout * 1000), 1) + if self._timeout != 0 and self._inter_byte_timeout is not None: + timeouts.ReadIntervalTimeout = max(int(self._inter_byte_timeout * 1000), 1) + + if self._write_timeout is None: + pass + elif self._write_timeout == 0: + timeouts.WriteTotalTimeoutConstant = win32.MAXDWORD + else: + timeouts.WriteTotalTimeoutConstant = max(int(self._write_timeout * 1000), 1) + win32.SetCommTimeouts(self._port_handle, ctypes.byref(timeouts)) + + win32.SetCommMask(self._port_handle, win32.EV_ERR) + + # Setup the connection info. + # Get state and modify it: + comDCB = win32.DCB() + win32.GetCommState(self._port_handle, ctypes.byref(comDCB)) + comDCB.BaudRate = self._baudrate + + if self._bytesize == serial.FIVEBITS: + comDCB.ByteSize = 5 + elif self._bytesize == serial.SIXBITS: + comDCB.ByteSize = 6 + elif self._bytesize == serial.SEVENBITS: + comDCB.ByteSize = 7 + elif self._bytesize == serial.EIGHTBITS: + comDCB.ByteSize = 8 + else: + raise ValueError("Unsupported number of data bits: {!r}".format(self._bytesize)) + + if self._parity == serial.PARITY_NONE: + comDCB.Parity = win32.NOPARITY + comDCB.fParity = 0 # Disable Parity Check + elif self._parity == serial.PARITY_EVEN: + comDCB.Parity = win32.EVENPARITY + comDCB.fParity = 1 # Enable Parity Check + elif self._parity == serial.PARITY_ODD: + comDCB.Parity = win32.ODDPARITY + comDCB.fParity = 1 # Enable Parity Check + elif self._parity == serial.PARITY_MARK: + comDCB.Parity = win32.MARKPARITY + comDCB.fParity = 1 # Enable Parity Check + elif self._parity == serial.PARITY_SPACE: + comDCB.Parity = win32.SPACEPARITY + comDCB.fParity = 1 # Enable Parity Check + else: + raise ValueError("Unsupported parity mode: {!r}".format(self._parity)) + + if self._stopbits == serial.STOPBITS_ONE: + comDCB.StopBits = win32.ONESTOPBIT + elif self._stopbits == serial.STOPBITS_ONE_POINT_FIVE: + comDCB.StopBits = win32.ONE5STOPBITS + elif self._stopbits == serial.STOPBITS_TWO: + comDCB.StopBits = win32.TWOSTOPBITS + else: + raise ValueError("Unsupported number of stop bits: {!r}".format(self._stopbits)) + + comDCB.fBinary = 1 # Enable Binary Transmission + # Char. w/ Parity-Err are replaced with 0xff (if fErrorChar is set to TRUE) + if self._rs485_mode is None: + if self._rtscts: + comDCB.fRtsControl = win32.RTS_CONTROL_HANDSHAKE + else: + comDCB.fRtsControl = win32.RTS_CONTROL_ENABLE if self._rts_state else win32.RTS_CONTROL_DISABLE + comDCB.fOutxCtsFlow = self._rtscts + else: + # checks for unsupported settings + # XXX verify if platform really does not have a setting for those + if not self._rs485_mode.rts_level_for_tx: + raise ValueError( + 'Unsupported value for RS485Settings.rts_level_for_tx: {!r} (only True is allowed)'.format( + self._rs485_mode.rts_level_for_tx,)) + if self._rs485_mode.rts_level_for_rx: + raise ValueError( + 'Unsupported value for RS485Settings.rts_level_for_rx: {!r} (only False is allowed)'.format( + self._rs485_mode.rts_level_for_rx,)) + if self._rs485_mode.delay_before_tx is not None: + raise ValueError( + 'Unsupported value for RS485Settings.delay_before_tx: {!r} (only None is allowed)'.format( + self._rs485_mode.delay_before_tx,)) + if self._rs485_mode.delay_before_rx is not None: + raise ValueError( + 'Unsupported value for RS485Settings.delay_before_rx: {!r} (only None is allowed)'.format( + self._rs485_mode.delay_before_rx,)) + if self._rs485_mode.loopback: + raise ValueError( + 'Unsupported value for RS485Settings.loopback: {!r} (only False is allowed)'.format( + self._rs485_mode.loopback,)) + comDCB.fRtsControl = win32.RTS_CONTROL_TOGGLE + comDCB.fOutxCtsFlow = 0 + + if self._dsrdtr: + comDCB.fDtrControl = win32.DTR_CONTROL_HANDSHAKE + else: + comDCB.fDtrControl = win32.DTR_CONTROL_ENABLE if self._dtr_state else win32.DTR_CONTROL_DISABLE + comDCB.fOutxDsrFlow = self._dsrdtr + comDCB.fOutX = self._xonxoff + comDCB.fInX = self._xonxoff + comDCB.fNull = 0 + comDCB.fErrorChar = 0 + comDCB.fAbortOnError = 0 + comDCB.XonChar = serial.XON + comDCB.XoffChar = serial.XOFF + + if not win32.SetCommState(self._port_handle, ctypes.byref(comDCB)): + raise SerialException( + 'Cannot configure port, something went wrong. ' + 'Original message: {!r}'.format(ctypes.WinError())) + + #~ def __del__(self): + #~ self.close() + + def _close(self): + """internal close port helper""" + if self._port_handle is not None: + # Restore original timeout values: + win32.SetCommTimeouts(self._port_handle, self._orgTimeouts) + if self._overlapped_read is not None: + self.cancel_read() + win32.CloseHandle(self._overlapped_read.hEvent) + self._overlapped_read = None + if self._overlapped_write is not None: + self.cancel_write() + win32.CloseHandle(self._overlapped_write.hEvent) + self._overlapped_write = None + win32.CloseHandle(self._port_handle) + self._port_handle = None + + def close(self): + """Close port""" + if self.is_open: + self._close() + self.is_open = False + + # - - - - - - - - - - - - - - - - - - - - - - - - + + @property + def in_waiting(self): + """Return the number of bytes currently in the input buffer.""" + flags = win32.DWORD() + comstat = win32.COMSTAT() + if not win32.ClearCommError(self._port_handle, ctypes.byref(flags), ctypes.byref(comstat)): + raise SerialException("ClearCommError failed ({!r})".format(ctypes.WinError())) + return comstat.cbInQue + + def read(self, size=1): + """\ + Read size bytes from the serial port. If a timeout is set it may + return less characters as requested. With no timeout it will block + until the requested number of bytes is read. + """ + if not self.is_open: + raise PortNotOpenError() + if size > 0: + win32.ResetEvent(self._overlapped_read.hEvent) + flags = win32.DWORD() + comstat = win32.COMSTAT() + if not win32.ClearCommError(self._port_handle, ctypes.byref(flags), ctypes.byref(comstat)): + raise SerialException("ClearCommError failed ({!r})".format(ctypes.WinError())) + n = min(comstat.cbInQue, size) if self.timeout == 0 else size + if n > 0: + buf = ctypes.create_string_buffer(n) + rc = win32.DWORD() + read_ok = win32.ReadFile( + self._port_handle, + buf, + n, + ctypes.byref(rc), + ctypes.byref(self._overlapped_read)) + if not read_ok and win32.GetLastError() not in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING): + raise SerialException("ReadFile failed ({!r})".format(ctypes.WinError())) + result_ok = win32.GetOverlappedResult( + self._port_handle, + ctypes.byref(self._overlapped_read), + ctypes.byref(rc), + True) + if not result_ok: + if win32.GetLastError() != win32.ERROR_OPERATION_ABORTED: + raise SerialException("GetOverlappedResult failed ({!r})".format(ctypes.WinError())) + read = buf.raw[:rc.value] + else: + read = bytes() + else: + read = bytes() + return bytes(read) + + def write(self, data): + """Output the given byte string over the serial port.""" + if not self.is_open: + raise PortNotOpenError() + #~ if not isinstance(data, (bytes, bytearray)): + #~ raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data))) + # convert data (needed in case of memoryview instance: Py 3.1 io lib), ctypes doesn't like memoryview + data = to_bytes(data) + if data: + #~ win32event.ResetEvent(self._overlapped_write.hEvent) + n = win32.DWORD() + success = win32.WriteFile(self._port_handle, data, len(data), ctypes.byref(n), self._overlapped_write) + if self._write_timeout != 0: # if blocking (None) or w/ write timeout (>0) + if not success and win32.GetLastError() not in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING): + raise SerialException("WriteFile failed ({!r})".format(ctypes.WinError())) + + # Wait for the write to complete. + #~ win32.WaitForSingleObject(self._overlapped_write.hEvent, win32.INFINITE) + win32.GetOverlappedResult(self._port_handle, self._overlapped_write, ctypes.byref(n), True) + if win32.GetLastError() == win32.ERROR_OPERATION_ABORTED: + return n.value # canceled IO is no error + if n.value != len(data): + raise SerialTimeoutException('Write timeout') + return n.value + else: + errorcode = win32.ERROR_SUCCESS if success else win32.GetLastError() + if errorcode in (win32.ERROR_INVALID_USER_BUFFER, win32.ERROR_NOT_ENOUGH_MEMORY, + win32.ERROR_OPERATION_ABORTED): + return 0 + elif errorcode in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING): + # no info on true length provided by OS function in async mode + return len(data) + else: + raise SerialException("WriteFile failed ({!r})".format(ctypes.WinError())) + else: + return 0 + + def flush(self): + """\ + Flush of file like objects. In this case, wait until all data + is written. + """ + while self.out_waiting: + time.sleep(0.05) + # XXX could also use WaitCommEvent with mask EV_TXEMPTY, but it would + # require overlapped IO and it's also only possible to set a single mask + # on the port--- + + def reset_input_buffer(self): + """Clear input buffer, discarding all that is in the buffer.""" + if not self.is_open: + raise PortNotOpenError() + win32.PurgeComm(self._port_handle, win32.PURGE_RXCLEAR | win32.PURGE_RXABORT) + + def reset_output_buffer(self): + """\ + Clear output buffer, aborting the current output and discarding all + that is in the buffer. + """ + if not self.is_open: + raise PortNotOpenError() + win32.PurgeComm(self._port_handle, win32.PURGE_TXCLEAR | win32.PURGE_TXABORT) + + def _update_break_state(self): + """Set break: Controls TXD. When active, to transmitting is possible.""" + if not self.is_open: + raise PortNotOpenError() + if self._break_state: + win32.SetCommBreak(self._port_handle) + else: + win32.ClearCommBreak(self._port_handle) + + def _update_rts_state(self): + """Set terminal status line: Request To Send""" + if self._rts_state: + win32.EscapeCommFunction(self._port_handle, win32.SETRTS) + else: + win32.EscapeCommFunction(self._port_handle, win32.CLRRTS) + + def _update_dtr_state(self): + """Set terminal status line: Data Terminal Ready""" + if self._dtr_state: + win32.EscapeCommFunction(self._port_handle, win32.SETDTR) + else: + win32.EscapeCommFunction(self._port_handle, win32.CLRDTR) + + def _GetCommModemStatus(self): + if not self.is_open: + raise PortNotOpenError() + stat = win32.DWORD() + win32.GetCommModemStatus(self._port_handle, ctypes.byref(stat)) + return stat.value + + @property + def cts(self): + """Read terminal status line: Clear To Send""" + return win32.MS_CTS_ON & self._GetCommModemStatus() != 0 + + @property + def dsr(self): + """Read terminal status line: Data Set Ready""" + return win32.MS_DSR_ON & self._GetCommModemStatus() != 0 + + @property + def ri(self): + """Read terminal status line: Ring Indicator""" + return win32.MS_RING_ON & self._GetCommModemStatus() != 0 + + @property + def cd(self): + """Read terminal status line: Carrier Detect""" + return win32.MS_RLSD_ON & self._GetCommModemStatus() != 0 + + # - - platform specific - - - - + + def set_buffer_size(self, rx_size=4096, tx_size=None): + """\ + Recommend a buffer size to the driver (device driver can ignore this + value). Must be called after the port is opened. + """ + if tx_size is None: + tx_size = rx_size + win32.SetupComm(self._port_handle, rx_size, tx_size) + + def set_output_flow_control(self, enable=True): + """\ + Manually control flow - when software flow control is enabled. + This will do the same as if XON (true) or XOFF (false) are received + from the other device and control the transmission accordingly. + WARNING: this function is not portable to different platforms! + """ + if not self.is_open: + raise PortNotOpenError() + if enable: + win32.EscapeCommFunction(self._port_handle, win32.SETXON) + else: + win32.EscapeCommFunction(self._port_handle, win32.SETXOFF) + + @property + def out_waiting(self): + """Return how many bytes the in the outgoing buffer""" + flags = win32.DWORD() + comstat = win32.COMSTAT() + if not win32.ClearCommError(self._port_handle, ctypes.byref(flags), ctypes.byref(comstat)): + raise SerialException("ClearCommError failed ({!r})".format(ctypes.WinError())) + return comstat.cbOutQue + + def _cancel_overlapped_io(self, overlapped): + """Cancel a blocking read operation, may be called from other thread""" + # check if read operation is pending + rc = win32.DWORD() + err = win32.GetOverlappedResult( + self._port_handle, + ctypes.byref(overlapped), + ctypes.byref(rc), + False) + if not err and win32.GetLastError() in (win32.ERROR_IO_PENDING, win32.ERROR_IO_INCOMPLETE): + # cancel, ignoring any errors (e.g. it may just have finished on its own) + win32.CancelIoEx(self._port_handle, overlapped) + + def cancel_read(self): + """Cancel a blocking read operation, may be called from other thread""" + self._cancel_overlapped_io(self._overlapped_read) + + def cancel_write(self): + """Cancel a blocking write operation, may be called from other thread""" + self._cancel_overlapped_io(self._overlapped_write) + + @SerialBase.exclusive.setter + def exclusive(self, exclusive): + """Change the exclusive access setting.""" + if exclusive is not None and not exclusive: + raise ValueError('win32 only supports exclusive access (not: {})'.format(exclusive)) + else: + serial.SerialBase.exclusive.__set__(self, exclusive) diff --git a/tools/python-embed/Lib/site-packages/serial/threaded/__init__.py b/tools/python-embed/Lib/site-packages/serial/threaded/__init__.py new file mode 100644 index 0000000..b8940b6 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/serial/threaded/__init__.py @@ -0,0 +1,297 @@ +#!/usr/bin/env python3 +# +# Working with threading and pySerial +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2015-2016 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause +"""\ +Support threading with serial ports. +""" +from __future__ import absolute_import + +import serial +import threading + + +class Protocol(object): + """\ + Protocol as used by the ReaderThread. This base class provides empty + implementations of all methods. + """ + + def connection_made(self, transport): + """Called when reader thread is started""" + + def data_received(self, data): + """Called with snippets received from the serial port""" + + def connection_lost(self, exc): + """\ + Called when the serial port is closed or the reader loop terminated + otherwise. + """ + if isinstance(exc, Exception): + raise exc + + +class Packetizer(Protocol): + """ + Read binary packets from serial port. Packets are expected to be terminated + with a TERMINATOR byte (null byte by default). + + The class also keeps track of the transport. + """ + + TERMINATOR = b'\0' + + def __init__(self): + self.buffer = bytearray() + self.transport = None + + def connection_made(self, transport): + """Store transport""" + self.transport = transport + + def connection_lost(self, exc): + """Forget transport""" + self.transport = None + super(Packetizer, self).connection_lost(exc) + + def data_received(self, data): + """Buffer received data, find TERMINATOR, call handle_packet""" + self.buffer.extend(data) + while self.TERMINATOR in self.buffer: + packet, self.buffer = self.buffer.split(self.TERMINATOR, 1) + self.handle_packet(packet) + + def handle_packet(self, packet): + """Process packets - to be overridden by subclassing""" + raise NotImplementedError('please implement functionality in handle_packet') + + +class FramedPacket(Protocol): + """ + Read binary packets. Packets are expected to have a start and stop marker. + + The class also keeps track of the transport. + """ + + START = b'(' + STOP = b')' + + def __init__(self): + self.packet = bytearray() + self.in_packet = False + self.transport = None + + def connection_made(self, transport): + """Store transport""" + self.transport = transport + + def connection_lost(self, exc): + """Forget transport""" + self.transport = None + self.in_packet = False + del self.packet[:] + super(FramedPacket, self).connection_lost(exc) + + def data_received(self, data): + """Find data enclosed in START/STOP, call handle_packet""" + for byte in serial.iterbytes(data): + if byte == self.START: + self.in_packet = True + elif byte == self.STOP: + self.in_packet = False + self.handle_packet(bytes(self.packet)) # make read-only copy + del self.packet[:] + elif self.in_packet: + self.packet.extend(byte) + else: + self.handle_out_of_packet_data(byte) + + def handle_packet(self, packet): + """Process packets - to be overridden by subclassing""" + raise NotImplementedError('please implement functionality in handle_packet') + + def handle_out_of_packet_data(self, data): + """Process data that is received outside of packets""" + pass + + +class LineReader(Packetizer): + """ + Read and write (Unicode) lines from/to serial port. + The encoding is applied. + """ + + TERMINATOR = b'\r\n' + ENCODING = 'utf-8' + UNICODE_HANDLING = 'replace' + + def handle_packet(self, packet): + self.handle_line(packet.decode(self.ENCODING, self.UNICODE_HANDLING)) + + def handle_line(self, line): + """Process one line - to be overridden by subclassing""" + raise NotImplementedError('please implement functionality in handle_line') + + def write_line(self, text): + """ + Write text to the transport. ``text`` is a Unicode string and the encoding + is applied before sending ans also the newline is append. + """ + # + is not the best choice but bytes does not support % or .format in py3 and we want a single write call + self.transport.write(text.encode(self.ENCODING, self.UNICODE_HANDLING) + self.TERMINATOR) + + +class ReaderThread(threading.Thread): + """\ + Implement a serial port read loop and dispatch to a Protocol instance (like + the asyncio.Protocol) but do it with threads. + + Calls to close() will close the serial port but it is also possible to just + stop() this thread and continue the serial port instance otherwise. + """ + + def __init__(self, serial_instance, protocol_factory): + """\ + Initialize thread. + + Note that the serial_instance' timeout is set to one second! + Other settings are not changed. + """ + super(ReaderThread, self).__init__() + self.daemon = True + self.serial = serial_instance + self.protocol_factory = protocol_factory + self.alive = True + self._lock = threading.Lock() + self._connection_made = threading.Event() + self.protocol = None + + def stop(self): + """Stop the reader thread""" + self.alive = False + if hasattr(self.serial, 'cancel_read'): + self.serial.cancel_read() + self.join(2) + + def run(self): + """Reader loop""" + if not hasattr(self.serial, 'cancel_read'): + self.serial.timeout = 1 + self.protocol = self.protocol_factory() + try: + self.protocol.connection_made(self) + except Exception as e: + self.alive = False + self.protocol.connection_lost(e) + self._connection_made.set() + return + error = None + self._connection_made.set() + while self.alive and self.serial.is_open: + try: + # read all that is there or wait for one byte (blocking) + data = self.serial.read(self.serial.in_waiting or 1) + except serial.SerialException as e: + # probably some I/O problem such as disconnected USB serial + # adapters -> exit + error = e + break + else: + if data: + # make a separated try-except for called user code + try: + self.protocol.data_received(data) + except Exception as e: + error = e + break + self.alive = False + self.protocol.connection_lost(error) + self.protocol = None + + def write(self, data): + """Thread safe writing (uses lock)""" + with self._lock: + return self.serial.write(data) + + def close(self): + """Close the serial port and exit reader thread (uses lock)""" + # use the lock to let other threads finish writing + with self._lock: + # first stop reading, so that closing can be done on idle port + self.stop() + self.serial.close() + + def connect(self): + """ + Wait until connection is set up and return the transport and protocol + instances. + """ + if self.alive: + self._connection_made.wait() + if not self.alive: + raise RuntimeError('connection_lost already called') + return (self, self.protocol) + else: + raise RuntimeError('already stopped') + + # - - context manager, returns protocol + + def __enter__(self): + """\ + Enter context handler. May raise RuntimeError in case the connection + could not be created. + """ + self.start() + self._connection_made.wait() + if not self.alive: + raise RuntimeError('connection_lost already called') + return self.protocol + + def __exit__(self, exc_type, exc_val, exc_tb): + """Leave context: close port""" + self.close() + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# test +if __name__ == '__main__': + # pylint: disable=wrong-import-position + import sys + import time + import traceback + + #~ PORT = 'spy:///dev/ttyUSB0' + PORT = 'loop://' + + class PrintLines(LineReader): + def connection_made(self, transport): + super(PrintLines, self).connection_made(transport) + sys.stdout.write('port opened\n') + self.write_line('hello world') + + def handle_line(self, data): + sys.stdout.write('line received: {!r}\n'.format(data)) + + def connection_lost(self, exc): + if exc: + traceback.print_exc(exc) + sys.stdout.write('port closed\n') + + ser = serial.serial_for_url(PORT, baudrate=115200, timeout=1) + with ReaderThread(ser, PrintLines) as protocol: + protocol.write_line('hello') + time.sleep(2) + + # alternative usage + ser = serial.serial_for_url(PORT, baudrate=115200, timeout=1) + t = ReaderThread(ser, PrintLines) + t.start() + transport, protocol = t.connect() + protocol.write_line('hello') + time.sleep(2) + t.close() diff --git a/tools/python-embed/Lib/site-packages/serial/threaded/__pycache__/__init__.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/threaded/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..b9cb7a6 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/threaded/__pycache__/__init__.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/tools/__init__.py b/tools/python-embed/Lib/site-packages/serial/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/__init__.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..32df641 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/__init__.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/hexlify_codec.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/hexlify_codec.cpython-312.pyc new file mode 100644 index 0000000..b17596a Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/hexlify_codec.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/list_ports.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/list_ports.cpython-312.pyc new file mode 100644 index 0000000..e361358 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/list_ports.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/list_ports_common.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/list_ports_common.cpython-312.pyc new file mode 100644 index 0000000..6f0d5d1 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/list_ports_common.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/list_ports_linux.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/list_ports_linux.cpython-312.pyc new file mode 100644 index 0000000..02cfada Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/list_ports_linux.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/list_ports_osx.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/list_ports_osx.cpython-312.pyc new file mode 100644 index 0000000..882dd0f Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/list_ports_osx.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/list_ports_posix.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/list_ports_posix.cpython-312.pyc new file mode 100644 index 0000000..28d177b Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/list_ports_posix.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/list_ports_windows.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/list_ports_windows.cpython-312.pyc new file mode 100644 index 0000000..bf5b306 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/list_ports_windows.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/miniterm.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/miniterm.cpython-312.pyc new file mode 100644 index 0000000..f64dbf0 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/tools/__pycache__/miniterm.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/tools/hexlify_codec.py b/tools/python-embed/Lib/site-packages/serial/tools/hexlify_codec.py new file mode 100644 index 0000000..bd8f6b0 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/serial/tools/hexlify_codec.py @@ -0,0 +1,126 @@ +#! python +# +# This is a codec to create and decode hexdumps with spaces between characters. used by miniterm. +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2015-2016 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause +"""\ +Python 'hex' Codec - 2-digit hex with spaces content transfer encoding. + +Encode and decode may be a bit missleading at first sight... + +The textual representation is a hex dump: e.g. "40 41" +The "encoded" data of this is the binary form, e.g. b"@A" + +Therefore decoding is binary to text and thus converting binary data to hex dump. + +""" + +from __future__ import absolute_import + +import codecs +import serial + + +try: + unicode +except (NameError, AttributeError): + unicode = str # for Python 3, pylint: disable=redefined-builtin,invalid-name + + +HEXDIGITS = '0123456789ABCDEF' + + +# Codec APIs + +def hex_encode(data, errors='strict'): + """'40 41 42' -> b'@ab'""" + return (serial.to_bytes([int(h, 16) for h in data.split()]), len(data)) + + +def hex_decode(data, errors='strict'): + """b'@ab' -> '40 41 42'""" + return (unicode(''.join('{:02X} '.format(ord(b)) for b in serial.iterbytes(data))), len(data)) + + +class Codec(codecs.Codec): + def encode(self, data, errors='strict'): + """'40 41 42' -> b'@ab'""" + return serial.to_bytes([int(h, 16) for h in data.split()]) + + def decode(self, data, errors='strict'): + """b'@ab' -> '40 41 42'""" + return unicode(''.join('{:02X} '.format(ord(b)) for b in serial.iterbytes(data))) + + +class IncrementalEncoder(codecs.IncrementalEncoder): + """Incremental hex encoder""" + + def __init__(self, errors='strict'): + self.errors = errors + self.state = 0 + + def reset(self): + self.state = 0 + + def getstate(self): + return self.state + + def setstate(self, state): + self.state = state + + def encode(self, data, final=False): + """\ + Incremental encode, keep track of digits and emit a byte when a pair + of hex digits is found. The space is optional unless the error + handling is defined to be 'strict'. + """ + state = self.state + encoded = [] + for c in data.upper(): + if c in HEXDIGITS: + z = HEXDIGITS.index(c) + if state: + encoded.append(z + (state & 0xf0)) + state = 0 + else: + state = 0x100 + (z << 4) + elif c == ' ': # allow spaces to separate values + if state and self.errors == 'strict': + raise UnicodeError('odd number of hex digits') + state = 0 + else: + if self.errors == 'strict': + raise UnicodeError('non-hex digit found: {!r}'.format(c)) + self.state = state + return serial.to_bytes(encoded) + + +class IncrementalDecoder(codecs.IncrementalDecoder): + """Incremental decoder""" + def decode(self, data, final=False): + return unicode(''.join('{:02X} '.format(ord(b)) for b in serial.iterbytes(data))) + + +class StreamWriter(Codec, codecs.StreamWriter): + """Combination of hexlify codec and StreamWriter""" + + +class StreamReader(Codec, codecs.StreamReader): + """Combination of hexlify codec and StreamReader""" + + +def getregentry(): + """encodings module API""" + return codecs.CodecInfo( + name='hexlify', + encode=hex_encode, + decode=hex_decode, + incrementalencoder=IncrementalEncoder, + incrementaldecoder=IncrementalDecoder, + streamwriter=StreamWriter, + streamreader=StreamReader, + #~ _is_text_encoding=True, + ) diff --git a/tools/python-embed/Lib/site-packages/serial/tools/list_ports.py b/tools/python-embed/Lib/site-packages/serial/tools/list_ports.py new file mode 100644 index 0000000..0d7e3d4 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/serial/tools/list_ports.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python +# +# Serial port enumeration. Console tool and backend selection. +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2011-2015 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause + +"""\ +This module will provide a function called comports that returns an +iterable (generator or list) that will enumerate available com ports. Note that +on some systems non-existent ports may be listed. + +Additionally a grep function is supplied that can be used to search for ports +based on their descriptions or hardware ID. +""" + +from __future__ import absolute_import + +import sys +import os +import re + +# chose an implementation, depending on os +#~ if sys.platform == 'cli': +#~ else: +if os.name == 'nt': # sys.platform == 'win32': + from serial.tools.list_ports_windows import comports +elif os.name == 'posix': + from serial.tools.list_ports_posix import comports +#~ elif os.name == 'java': +else: + raise ImportError("Sorry: no implementation for your platform ('{}') available".format(os.name)) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +def grep(regexp, include_links=False): + """\ + Search for ports using a regular expression. Port name, description and + hardware ID are searched. The function returns an iterable that returns the + same tuples as comport() would do. + """ + r = re.compile(regexp, re.I) + for info in comports(include_links): + port, desc, hwid = info + if r.search(port) or r.search(desc) or r.search(hwid): + yield info + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +def main(): + import argparse + + parser = argparse.ArgumentParser(description='Serial port enumeration') + + parser.add_argument( + 'regexp', + nargs='?', + help='only show ports that match this regex') + + parser.add_argument( + '-v', '--verbose', + action='store_true', + help='show more messages') + + parser.add_argument( + '-q', '--quiet', + action='store_true', + help='suppress all messages') + + parser.add_argument( + '-n', + type=int, + help='only output the N-th entry') + + parser.add_argument( + '-s', '--include-links', + action='store_true', + help='include entries that are symlinks to real devices') + + args = parser.parse_args() + + hits = 0 + # get iteraror w/ or w/o filter + if args.regexp: + if not args.quiet: + sys.stderr.write("Filtered list with regexp: {!r}\n".format(args.regexp)) + iterator = sorted(grep(args.regexp, include_links=args.include_links)) + else: + iterator = sorted(comports(include_links=args.include_links)) + # list them + for n, (port, desc, hwid) in enumerate(iterator, 1): + if args.n is None or args.n == n: + sys.stdout.write("{:20}\n".format(port)) + if args.verbose: + sys.stdout.write(" desc: {}\n".format(desc)) + sys.stdout.write(" hwid: {}\n".format(hwid)) + hits += 1 + if not args.quiet: + if hits: + sys.stderr.write("{} ports found\n".format(hits)) + else: + sys.stderr.write("no ports found\n") + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# test +if __name__ == '__main__': + main() diff --git a/tools/python-embed/Lib/site-packages/serial/tools/list_ports_common.py b/tools/python-embed/Lib/site-packages/serial/tools/list_ports_common.py new file mode 100644 index 0000000..617f3dc --- /dev/null +++ b/tools/python-embed/Lib/site-packages/serial/tools/list_ports_common.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python +# +# This is a helper module for the various platform dependent list_port +# implementations. +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2015 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import absolute_import + +import re +import glob +import os +import os.path + + +def numsplit(text): + """\ + Convert string into a list of texts and numbers in order to support a + natural sorting. + """ + result = [] + for group in re.split(r'(\d+)', text): + if group: + try: + group = int(group) + except ValueError: + pass + result.append(group) + return result + + +class ListPortInfo(object): + """Info collection base class for serial ports""" + + def __init__(self, device, skip_link_detection=False): + self.device = device + self.name = os.path.basename(device) + self.description = 'n/a' + self.hwid = 'n/a' + # USB specific data + self.vid = None + self.pid = None + self.serial_number = None + self.location = None + self.manufacturer = None + self.product = None + self.interface = None + # special handling for links + if not skip_link_detection and device is not None and os.path.islink(device): + self.hwid = 'LINK={}'.format(os.path.realpath(device)) + + def usb_description(self): + """return a short string to name the port based on USB info""" + if self.interface is not None: + return '{} - {}'.format(self.product, self.interface) + elif self.product is not None: + return self.product + else: + return self.name + + def usb_info(self): + """return a string with USB related information about device""" + return 'USB VID:PID={:04X}:{:04X}{}{}'.format( + self.vid or 0, + self.pid or 0, + ' SER={}'.format(self.serial_number) if self.serial_number is not None else '', + ' LOCATION={}'.format(self.location) if self.location is not None else '') + + def apply_usb_info(self): + """update description and hwid from USB data""" + self.description = self.usb_description() + self.hwid = self.usb_info() + + def __eq__(self, other): + return isinstance(other, ListPortInfo) and self.device == other.device + + def __hash__(self): + return hash(self.device) + + def __lt__(self, other): + if not isinstance(other, ListPortInfo): + raise TypeError('unorderable types: {}() and {}()'.format( + type(self).__name__, + type(other).__name__)) + return numsplit(self.device) < numsplit(other.device) + + def __str__(self): + return '{} - {}'.format(self.device, self.description) + + def __getitem__(self, index): + """Item access: backwards compatible -> (port, desc, hwid)""" + if index == 0: + return self.device + elif index == 1: + return self.description + elif index == 2: + return self.hwid + else: + raise IndexError('{} > 2'.format(index)) + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +def list_links(devices): + """\ + search all /dev devices and look for symlinks to known ports already + listed in devices. + """ + links = [] + for device in glob.glob('/dev/*'): + if os.path.islink(device) and os.path.realpath(device) in devices: + links.append(device) + return links + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# test +if __name__ == '__main__': + print(ListPortInfo('dummy')) diff --git a/tools/python-embed/Lib/site-packages/serial/tools/list_ports_linux.py b/tools/python-embed/Lib/site-packages/serial/tools/list_ports_linux.py new file mode 100644 index 0000000..c8c1cfc --- /dev/null +++ b/tools/python-embed/Lib/site-packages/serial/tools/list_ports_linux.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python +# +# This is a module that gathers a list of serial ports including details on +# GNU/Linux systems. +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2011-2015 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import absolute_import + +import glob +import os +from serial.tools import list_ports_common + + +class SysFS(list_ports_common.ListPortInfo): + """Wrapper for easy sysfs access and device info""" + + def __init__(self, device): + super(SysFS, self).__init__(device) + # special handling for links + if device is not None and os.path.islink(device): + device = os.path.realpath(device) + is_link = True + else: + is_link = False + self.usb_device_path = None + if os.path.exists('/sys/class/tty/{}/device'.format(self.name)): + self.device_path = os.path.realpath('/sys/class/tty/{}/device'.format(self.name)) + self.subsystem = os.path.basename(os.path.realpath(os.path.join(self.device_path, 'subsystem'))) + else: + self.device_path = None + self.subsystem = None + # check device type + if self.subsystem == 'usb-serial': + self.usb_interface_path = os.path.dirname(self.device_path) + elif self.subsystem == 'usb': + self.usb_interface_path = self.device_path + else: + self.usb_interface_path = None + # fill-in info for USB devices + if self.usb_interface_path is not None: + self.usb_device_path = os.path.dirname(self.usb_interface_path) + + try: + num_if = int(self.read_line(self.usb_device_path, 'bNumInterfaces')) + except ValueError: + num_if = 1 + + self.vid = int(self.read_line(self.usb_device_path, 'idVendor'), 16) + self.pid = int(self.read_line(self.usb_device_path, 'idProduct'), 16) + self.serial_number = self.read_line(self.usb_device_path, 'serial') + if num_if > 1: # multi interface devices like FT4232 + self.location = os.path.basename(self.usb_interface_path) + else: + self.location = os.path.basename(self.usb_device_path) + + self.manufacturer = self.read_line(self.usb_device_path, 'manufacturer') + self.product = self.read_line(self.usb_device_path, 'product') + self.interface = self.read_line(self.usb_interface_path, 'interface') + + if self.subsystem in ('usb', 'usb-serial'): + self.apply_usb_info() + #~ elif self.subsystem in ('pnp', 'amba'): # PCI based devices, raspi + elif self.subsystem == 'pnp': # PCI based devices + self.description = self.name + self.hwid = self.read_line(self.device_path, 'id') + elif self.subsystem == 'amba': # raspi + self.description = self.name + self.hwid = os.path.basename(self.device_path) + + if is_link: + self.hwid += ' LINK={}'.format(device) + + def read_line(self, *args): + """\ + Helper function to read a single line from a file. + One or more parameters are allowed, they are joined with os.path.join. + Returns None on errors.. + """ + try: + with open(os.path.join(*args)) as f: + line = f.readline().strip() + return line + except IOError: + return None + + +def comports(include_links=False): + devices = glob.glob('/dev/ttyS*') # built-in serial ports + devices.extend(glob.glob('/dev/ttyUSB*')) # usb-serial with own driver + devices.extend(glob.glob('/dev/ttyXRUSB*')) # xr-usb-serial port exar (DELL Edge 3001) + devices.extend(glob.glob('/dev/ttyACM*')) # usb-serial with CDC-ACM profile + devices.extend(glob.glob('/dev/ttyAMA*')) # ARM internal port (raspi) + devices.extend(glob.glob('/dev/rfcomm*')) # BT serial devices + devices.extend(glob.glob('/dev/ttyAP*')) # Advantech multi-port serial controllers + if include_links: + devices.extend(list_ports_common.list_links(devices)) + return [info + for info in [SysFS(d) for d in devices] + if info.subsystem != "platform"] # hide non-present internal serial ports + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# test +if __name__ == '__main__': + for info in sorted(comports()): + print("{0}: {0.subsystem}".format(info)) diff --git a/tools/python-embed/Lib/site-packages/serial/tools/list_ports_osx.py b/tools/python-embed/Lib/site-packages/serial/tools/list_ports_osx.py new file mode 100644 index 0000000..51b4e8c --- /dev/null +++ b/tools/python-embed/Lib/site-packages/serial/tools/list_ports_osx.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python +# +# This is a module that gathers a list of serial ports including details on OSX +# +# code originally from https://github.com/makerbot/pyserial/tree/master/serial/tools +# with contributions from cibomahto, dgs3, FarMcKon, tedbrandston +# and modifications by cliechti, hoihu, hardkrash +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2013-2020 +# +# SPDX-License-Identifier: BSD-3-Clause + + +# List all of the callout devices in OS/X by querying IOKit. + +# See the following for a reference of how to do this: +# http://developer.apple.com/library/mac/#documentation/DeviceDrivers/Conceptual/WorkingWSerial/WWSerial_SerialDevs/SerialDevices.html#//apple_ref/doc/uid/TP30000384-CIHGEAFD + +# More help from darwin_hid.py + +# Also see the 'IORegistryExplorer' for an idea of what we are actually searching + +from __future__ import absolute_import + +import ctypes + +from serial.tools import list_ports_common + +iokit = ctypes.cdll.LoadLibrary('/System/Library/Frameworks/IOKit.framework/IOKit') +cf = ctypes.cdll.LoadLibrary('/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation') + +# kIOMasterPortDefault is no longer exported in BigSur but no biggie, using NULL works just the same +kIOMasterPortDefault = 0 # WAS: ctypes.c_void_p.in_dll(iokit, "kIOMasterPortDefault") +kCFAllocatorDefault = ctypes.c_void_p.in_dll(cf, "kCFAllocatorDefault") + +kCFStringEncodingMacRoman = 0 +kCFStringEncodingUTF8 = 0x08000100 + +# defined in `IOKit/usb/USBSpec.h` +kUSBVendorString = 'USB Vendor Name' +kUSBSerialNumberString = 'USB Serial Number' + +# `io_name_t` defined as `typedef char io_name_t[128];` +# in `device/device_types.h` +io_name_size = 128 + +# defined in `mach/kern_return.h` +KERN_SUCCESS = 0 +# kern_return_t defined as `typedef int kern_return_t;` in `mach/i386/kern_return.h` +kern_return_t = ctypes.c_int + +iokit.IOServiceMatching.restype = ctypes.c_void_p + +iokit.IOServiceGetMatchingServices.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] +iokit.IOServiceGetMatchingServices.restype = kern_return_t + +iokit.IORegistryEntryGetParentEntry.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] +iokit.IOServiceGetMatchingServices.restype = kern_return_t + +iokit.IORegistryEntryCreateCFProperty.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32] +iokit.IORegistryEntryCreateCFProperty.restype = ctypes.c_void_p + +iokit.IORegistryEntryGetPath.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] +iokit.IORegistryEntryGetPath.restype = kern_return_t + +iokit.IORegistryEntryGetName.argtypes = [ctypes.c_void_p, ctypes.c_void_p] +iokit.IORegistryEntryGetName.restype = kern_return_t + +iokit.IOObjectGetClass.argtypes = [ctypes.c_void_p, ctypes.c_void_p] +iokit.IOObjectGetClass.restype = kern_return_t + +iokit.IOObjectRelease.argtypes = [ctypes.c_void_p] + + +cf.CFStringCreateWithCString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int32] +cf.CFStringCreateWithCString.restype = ctypes.c_void_p + +cf.CFStringGetCStringPtr.argtypes = [ctypes.c_void_p, ctypes.c_uint32] +cf.CFStringGetCStringPtr.restype = ctypes.c_char_p + +cf.CFStringGetCString.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_long, ctypes.c_uint32] +cf.CFStringGetCString.restype = ctypes.c_bool + +cf.CFNumberGetValue.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p] +cf.CFNumberGetValue.restype = ctypes.c_void_p + +# void CFRelease ( CFTypeRef cf ); +cf.CFRelease.argtypes = [ctypes.c_void_p] +cf.CFRelease.restype = None + +# CFNumber type defines +kCFNumberSInt8Type = 1 +kCFNumberSInt16Type = 2 +kCFNumberSInt32Type = 3 +kCFNumberSInt64Type = 4 + + +def get_string_property(device_type, property): + """ + Search the given device for the specified string property + + @param device_type Type of Device + @param property String to search for + @return Python string containing the value, or None if not found. + """ + key = cf.CFStringCreateWithCString( + kCFAllocatorDefault, + property.encode("utf-8"), + kCFStringEncodingUTF8) + + CFContainer = iokit.IORegistryEntryCreateCFProperty( + device_type, + key, + kCFAllocatorDefault, + 0) + output = None + + if CFContainer: + output = cf.CFStringGetCStringPtr(CFContainer, 0) + if output is not None: + output = output.decode('utf-8') + else: + buffer = ctypes.create_string_buffer(io_name_size); + success = cf.CFStringGetCString(CFContainer, ctypes.byref(buffer), io_name_size, kCFStringEncodingUTF8) + if success: + output = buffer.value.decode('utf-8') + cf.CFRelease(CFContainer) + return output + + +def get_int_property(device_type, property, cf_number_type): + """ + Search the given device for the specified string property + + @param device_type Device to search + @param property String to search for + @param cf_number_type CFType number + + @return Python string containing the value, or None if not found. + """ + key = cf.CFStringCreateWithCString( + kCFAllocatorDefault, + property.encode("utf-8"), + kCFStringEncodingUTF8) + + CFContainer = iokit.IORegistryEntryCreateCFProperty( + device_type, + key, + kCFAllocatorDefault, + 0) + + if CFContainer: + if (cf_number_type == kCFNumberSInt32Type): + number = ctypes.c_uint32() + elif (cf_number_type == kCFNumberSInt16Type): + number = ctypes.c_uint16() + cf.CFNumberGetValue(CFContainer, cf_number_type, ctypes.byref(number)) + cf.CFRelease(CFContainer) + return number.value + return None + +def IORegistryEntryGetName(device): + devicename = ctypes.create_string_buffer(io_name_size); + res = iokit.IORegistryEntryGetName(device, ctypes.byref(devicename)) + if res != KERN_SUCCESS: + return None + # this works in python2 but may not be valid. Also I don't know if + # this encoding is guaranteed. It may be dependent on system locale. + return devicename.value.decode('utf-8') + +def IOObjectGetClass(device): + classname = ctypes.create_string_buffer(io_name_size) + iokit.IOObjectGetClass(device, ctypes.byref(classname)) + return classname.value + +def GetParentDeviceByType(device, parent_type): + """ Find the first parent of a device that implements the parent_type + @param IOService Service to inspect + @return Pointer to the parent type, or None if it was not found. + """ + # First, try to walk up the IOService tree to find a parent of this device that is a IOUSBDevice. + parent_type = parent_type.encode('utf-8') + while IOObjectGetClass(device) != parent_type: + parent = ctypes.c_void_p() + response = iokit.IORegistryEntryGetParentEntry( + device, + "IOService".encode("utf-8"), + ctypes.byref(parent)) + # If we weren't able to find a parent for the device, we're done. + if response != KERN_SUCCESS: + return None + device = parent + return device + + +def GetIOServicesByType(service_type): + """ + returns iterator over specified service_type + """ + serial_port_iterator = ctypes.c_void_p() + + iokit.IOServiceGetMatchingServices( + kIOMasterPortDefault, + iokit.IOServiceMatching(service_type.encode('utf-8')), + ctypes.byref(serial_port_iterator)) + + services = [] + while iokit.IOIteratorIsValid(serial_port_iterator): + service = iokit.IOIteratorNext(serial_port_iterator) + if not service: + break + services.append(service) + iokit.IOObjectRelease(serial_port_iterator) + return services + + +def location_to_string(locationID): + """ + helper to calculate port and bus number from locationID + """ + loc = ['{}-'.format(locationID >> 24)] + while locationID & 0xf00000: + if len(loc) > 1: + loc.append('.') + loc.append('{}'.format((locationID >> 20) & 0xf)) + locationID <<= 4 + return ''.join(loc) + + +class SuitableSerialInterface(object): + pass + + +def scan_interfaces(): + """ + helper function to scan USB interfaces + returns a list of SuitableSerialInterface objects with name and id attributes + """ + interfaces = [] + for service in GetIOServicesByType('IOSerialBSDClient'): + device = get_string_property(service, "IOCalloutDevice") + if device: + usb_device = GetParentDeviceByType(service, "IOUSBInterface") + if usb_device: + name = get_string_property(usb_device, "USB Interface Name") or None + locationID = get_int_property(usb_device, "locationID", kCFNumberSInt32Type) or '' + i = SuitableSerialInterface() + i.id = locationID + i.name = name + interfaces.append(i) + return interfaces + + +def search_for_locationID_in_interfaces(serial_interfaces, locationID): + for interface in serial_interfaces: + if (interface.id == locationID): + return interface.name + return None + + +def comports(include_links=False): + # XXX include_links is currently ignored. are links in /dev even supported here? + # Scan for all iokit serial ports + services = GetIOServicesByType('IOSerialBSDClient') + ports = [] + serial_interfaces = scan_interfaces() + for service in services: + # First, add the callout device file. + device = get_string_property(service, "IOCalloutDevice") + if device: + info = list_ports_common.ListPortInfo(device) + # If the serial port is implemented by IOUSBDevice + # NOTE IOUSBDevice was deprecated as of 10.11 and finally on Apple Silicon + # devices has been completely removed. Thanks to @oskay for this patch. + usb_device = GetParentDeviceByType(service, "IOUSBHostDevice") + if not usb_device: + usb_device = GetParentDeviceByType(service, "IOUSBDevice") + if usb_device: + # fetch some useful informations from properties + info.vid = get_int_property(usb_device, "idVendor", kCFNumberSInt16Type) + info.pid = get_int_property(usb_device, "idProduct", kCFNumberSInt16Type) + info.serial_number = get_string_property(usb_device, kUSBSerialNumberString) + # We know this is a usb device, so the + # IORegistryEntryName should always be aliased to the + # usb product name string descriptor. + info.product = IORegistryEntryGetName(usb_device) or 'n/a' + info.manufacturer = get_string_property(usb_device, kUSBVendorString) + locationID = get_int_property(usb_device, "locationID", kCFNumberSInt32Type) + info.location = location_to_string(locationID) + info.interface = search_for_locationID_in_interfaces(serial_interfaces, locationID) + info.apply_usb_info() + ports.append(info) + return ports + +# test +if __name__ == '__main__': + for port, desc, hwid in sorted(comports()): + print("{}: {} [{}]".format(port, desc, hwid)) diff --git a/tools/python-embed/Lib/site-packages/serial/tools/list_ports_posix.py b/tools/python-embed/Lib/site-packages/serial/tools/list_ports_posix.py new file mode 100644 index 0000000..79bc8ed --- /dev/null +++ b/tools/python-embed/Lib/site-packages/serial/tools/list_ports_posix.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python +# +# This is a module that gathers a list of serial ports on POSIXy systems. +# For some specific implementations, see also list_ports_linux, list_ports_osx +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2011-2015 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause + +"""\ +The ``comports`` function is expected to return an iterable that yields tuples +of 3 strings: port name, human readable description and a hardware ID. + +As currently no method is known to get the second two strings easily, they are +currently just identical to the port name. +""" + +from __future__ import absolute_import + +import glob +import sys +import os +from serial.tools import list_ports_common + +# try to detect the OS so that a device can be selected... +plat = sys.platform.lower() + +if plat[:5] == 'linux': # Linux (confirmed) # noqa + from serial.tools.list_ports_linux import comports + +elif plat[:6] == 'darwin': # OS X (confirmed) + from serial.tools.list_ports_osx import comports + +elif plat == 'cygwin': # cygwin/win32 + # cygwin accepts /dev/com* in many contexts + # (such as 'open' call, explicit 'ls'), but 'glob.glob' + # and bare 'ls' do not; so use /dev/ttyS* instead + def comports(include_links=False): + devices = glob.glob('/dev/ttyS*') + if include_links: + devices.extend(list_ports_common.list_links(devices)) + return [list_ports_common.ListPortInfo(d) for d in devices] + +elif plat[:7] == 'openbsd': # OpenBSD + def comports(include_links=False): + devices = glob.glob('/dev/cua*') + if include_links: + devices.extend(list_ports_common.list_links(devices)) + return [list_ports_common.ListPortInfo(d) for d in devices] + +elif plat[:3] == 'bsd' or plat[:7] == 'freebsd': + def comports(include_links=False): + devices = glob.glob('/dev/cua*[!.init][!.lock]') + if include_links: + devices.extend(list_ports_common.list_links(devices)) + return [list_ports_common.ListPortInfo(d) for d in devices] + +elif plat[:6] == 'netbsd': # NetBSD + def comports(include_links=False): + """scan for available ports. return a list of device names.""" + devices = glob.glob('/dev/dty*') + if include_links: + devices.extend(list_ports_common.list_links(devices)) + return [list_ports_common.ListPortInfo(d) for d in devices] + +elif plat[:4] == 'irix': # IRIX + def comports(include_links=False): + """scan for available ports. return a list of device names.""" + devices = glob.glob('/dev/ttyf*') + if include_links: + devices.extend(list_ports_common.list_links(devices)) + return [list_ports_common.ListPortInfo(d) for d in devices] + +elif plat[:2] == 'hp': # HP-UX (not tested) + def comports(include_links=False): + """scan for available ports. return a list of device names.""" + devices = glob.glob('/dev/tty*p0') + if include_links: + devices.extend(list_ports_common.list_links(devices)) + return [list_ports_common.ListPortInfo(d) for d in devices] + +elif plat[:5] == 'sunos': # Solaris/SunOS + def comports(include_links=False): + """scan for available ports. return a list of device names.""" + devices = glob.glob('/dev/tty*c') + if include_links: + devices.extend(list_ports_common.list_links(devices)) + return [list_ports_common.ListPortInfo(d) for d in devices] + +elif plat[:3] == 'aix': # AIX + def comports(include_links=False): + """scan for available ports. return a list of device names.""" + devices = glob.glob('/dev/tty*') + if include_links: + devices.extend(list_ports_common.list_links(devices)) + return [list_ports_common.ListPortInfo(d) for d in devices] + +else: + # platform detection has failed... + import serial + sys.stderr.write("""\ +don't know how to enumerate ttys on this system. +! I you know how the serial ports are named send this information to +! the author of this module: + +sys.platform = {!r} +os.name = {!r} +pySerial version = {} + +also add the naming scheme of the serial ports and with a bit luck you can get +this module running... +""".format(sys.platform, os.name, serial.VERSION)) + raise ImportError("Sorry: no implementation for your platform ('{}') available".format(os.name)) + +# test +if __name__ == '__main__': + for port, desc, hwid in sorted(comports()): + print("{}: {} [{}]".format(port, desc, hwid)) diff --git a/tools/python-embed/Lib/site-packages/serial/tools/list_ports_windows.py b/tools/python-embed/Lib/site-packages/serial/tools/list_ports_windows.py new file mode 100644 index 0000000..0b4a5b1 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/serial/tools/list_ports_windows.py @@ -0,0 +1,427 @@ +#! python +# +# Enumerate serial ports on Windows including a human readable description +# and hardware information. +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2001-2016 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import absolute_import + +# pylint: disable=invalid-name,too-few-public-methods +import re +import ctypes +from ctypes.wintypes import BOOL +from ctypes.wintypes import HWND +from ctypes.wintypes import DWORD +from ctypes.wintypes import WORD +from ctypes.wintypes import LONG +from ctypes.wintypes import ULONG +from ctypes.wintypes import HKEY +from ctypes.wintypes import BYTE +import serial +from serial.win32 import ULONG_PTR +from serial.tools import list_ports_common + + +def ValidHandle(value, func, arguments): + if value == 0: + raise ctypes.WinError() + return value + + +NULL = 0 +HDEVINFO = ctypes.c_void_p +LPCTSTR = ctypes.c_wchar_p +PCTSTR = ctypes.c_wchar_p +PTSTR = ctypes.c_wchar_p +LPDWORD = PDWORD = ctypes.POINTER(DWORD) +#~ LPBYTE = PBYTE = ctypes.POINTER(BYTE) +LPBYTE = PBYTE = ctypes.c_void_p # XXX avoids error about types + +ACCESS_MASK = DWORD +REGSAM = ACCESS_MASK + + +class GUID(ctypes.Structure): + _fields_ = [ + ('Data1', DWORD), + ('Data2', WORD), + ('Data3', WORD), + ('Data4', BYTE * 8), + ] + + def __str__(self): + return "{{{:08x}-{:04x}-{:04x}-{}-{}}}".format( + self.Data1, + self.Data2, + self.Data3, + ''.join(["{:02x}".format(d) for d in self.Data4[:2]]), + ''.join(["{:02x}".format(d) for d in self.Data4[2:]]), + ) + + +class SP_DEVINFO_DATA(ctypes.Structure): + _fields_ = [ + ('cbSize', DWORD), + ('ClassGuid', GUID), + ('DevInst', DWORD), + ('Reserved', ULONG_PTR), + ] + + def __str__(self): + return "ClassGuid:{} DevInst:{}".format(self.ClassGuid, self.DevInst) + + +PSP_DEVINFO_DATA = ctypes.POINTER(SP_DEVINFO_DATA) + +PSP_DEVICE_INTERFACE_DETAIL_DATA = ctypes.c_void_p + +setupapi = ctypes.windll.LoadLibrary("setupapi") +SetupDiDestroyDeviceInfoList = setupapi.SetupDiDestroyDeviceInfoList +SetupDiDestroyDeviceInfoList.argtypes = [HDEVINFO] +SetupDiDestroyDeviceInfoList.restype = BOOL + +SetupDiClassGuidsFromName = setupapi.SetupDiClassGuidsFromNameW +SetupDiClassGuidsFromName.argtypes = [PCTSTR, ctypes.POINTER(GUID), DWORD, PDWORD] +SetupDiClassGuidsFromName.restype = BOOL + +SetupDiEnumDeviceInfo = setupapi.SetupDiEnumDeviceInfo +SetupDiEnumDeviceInfo.argtypes = [HDEVINFO, DWORD, PSP_DEVINFO_DATA] +SetupDiEnumDeviceInfo.restype = BOOL + +SetupDiGetClassDevs = setupapi.SetupDiGetClassDevsW +SetupDiGetClassDevs.argtypes = [ctypes.POINTER(GUID), PCTSTR, HWND, DWORD] +SetupDiGetClassDevs.restype = HDEVINFO +SetupDiGetClassDevs.errcheck = ValidHandle + +SetupDiGetDeviceRegistryProperty = setupapi.SetupDiGetDeviceRegistryPropertyW +SetupDiGetDeviceRegistryProperty.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, DWORD, PDWORD, PBYTE, DWORD, PDWORD] +SetupDiGetDeviceRegistryProperty.restype = BOOL + +SetupDiGetDeviceInstanceId = setupapi.SetupDiGetDeviceInstanceIdW +SetupDiGetDeviceInstanceId.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, PTSTR, DWORD, PDWORD] +SetupDiGetDeviceInstanceId.restype = BOOL + +SetupDiOpenDevRegKey = setupapi.SetupDiOpenDevRegKey +SetupDiOpenDevRegKey.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, DWORD, DWORD, DWORD, REGSAM] +SetupDiOpenDevRegKey.restype = HKEY + +advapi32 = ctypes.windll.LoadLibrary("Advapi32") +RegCloseKey = advapi32.RegCloseKey +RegCloseKey.argtypes = [HKEY] +RegCloseKey.restype = LONG + +RegQueryValueEx = advapi32.RegQueryValueExW +RegQueryValueEx.argtypes = [HKEY, LPCTSTR, LPDWORD, LPDWORD, LPBYTE, LPDWORD] +RegQueryValueEx.restype = LONG + +cfgmgr32 = ctypes.windll.LoadLibrary("Cfgmgr32") +CM_Get_Parent = cfgmgr32.CM_Get_Parent +CM_Get_Parent.argtypes = [PDWORD, DWORD, ULONG] +CM_Get_Parent.restype = LONG + +CM_Get_Device_IDW = cfgmgr32.CM_Get_Device_IDW +CM_Get_Device_IDW.argtypes = [DWORD, PTSTR, ULONG, ULONG] +CM_Get_Device_IDW.restype = LONG + +CM_MapCrToWin32Err = cfgmgr32.CM_MapCrToWin32Err +CM_MapCrToWin32Err.argtypes = [DWORD, DWORD] +CM_MapCrToWin32Err.restype = DWORD + + +DIGCF_PRESENT = 2 +DIGCF_DEVICEINTERFACE = 16 +INVALID_HANDLE_VALUE = 0 +ERROR_INSUFFICIENT_BUFFER = 122 +ERROR_NOT_FOUND = 1168 +SPDRP_HARDWAREID = 1 +SPDRP_FRIENDLYNAME = 12 +SPDRP_LOCATION_PATHS = 35 +SPDRP_MFG = 11 +DICS_FLAG_GLOBAL = 1 +DIREG_DEV = 0x00000001 +KEY_READ = 0x20019 + + +MAX_USB_DEVICE_TREE_TRAVERSAL_DEPTH = 5 + + +def get_parent_serial_number(child_devinst, child_vid, child_pid, depth=0, last_serial_number=None): + """ Get the serial number of the parent of a device. + + Args: + child_devinst: The device instance handle to get the parent serial number of. + child_vid: The vendor ID of the child device. + child_pid: The product ID of the child device. + depth: The current iteration depth of the USB device tree. + """ + + # If the traversal depth is beyond the max, abandon attempting to find the serial number. + if depth > MAX_USB_DEVICE_TREE_TRAVERSAL_DEPTH: + return '' if not last_serial_number else last_serial_number + + # Get the parent device instance. + devinst = DWORD() + ret = CM_Get_Parent(ctypes.byref(devinst), child_devinst, 0) + + if ret: + win_error = CM_MapCrToWin32Err(DWORD(ret), DWORD(0)) + + # If there is no parent available, the child was the root device. We cannot traverse + # further. + if win_error == ERROR_NOT_FOUND: + return '' if not last_serial_number else last_serial_number + + raise ctypes.WinError(win_error) + + # Get the ID of the parent device and parse it for vendor ID, product ID, and serial number. + parentHardwareID = ctypes.create_unicode_buffer(250) + + ret = CM_Get_Device_IDW( + devinst, + parentHardwareID, + ctypes.sizeof(parentHardwareID) - 1, + 0) + + if ret: + raise ctypes.WinError(CM_MapCrToWin32Err(DWORD(ret), DWORD(0))) + + parentHardwareID_str = parentHardwareID.value + m = re.search(r'VID_([0-9a-f]{4})(&PID_([0-9a-f]{4}))?(&MI_(\d{2}))?(\\(.*))?', + parentHardwareID_str, + re.I) + + # return early if we have no matches (likely malformed serial, traversed too far) + if not m: + return '' if not last_serial_number else last_serial_number + + vid = None + pid = None + serial_number = None + if m.group(1): + vid = int(m.group(1), 16) + if m.group(3): + pid = int(m.group(3), 16) + if m.group(7): + serial_number = m.group(7) + + # store what we found as a fallback for malformed serial values up the chain + found_serial_number = serial_number + + # Check that the USB serial number only contains alpha-numeric characters. It may be a windows + # device ID (ephemeral ID). + if serial_number and not re.match(r'^\w+$', serial_number): + serial_number = None + + if not vid or not pid: + # If pid and vid are not available at this device level, continue to the parent. + return get_parent_serial_number(devinst, child_vid, child_pid, depth + 1, found_serial_number) + + if pid != child_pid or vid != child_vid: + # If the VID or PID has changed, we are no longer looking at the same physical device. The + # serial number is unknown. + return '' if not last_serial_number else last_serial_number + + # In this case, the vid and pid of the parent device are identical to the child. However, if + # there still isn't a serial number available, continue to the next parent. + if not serial_number: + return get_parent_serial_number(devinst, child_vid, child_pid, depth + 1, found_serial_number) + + # Finally, the VID and PID are identical to the child and a serial number is present, so return + # it. + return serial_number + + +def iterate_comports(): + """Return a generator that yields descriptions for serial ports""" + PortsGUIDs = (GUID * 8)() # so far only seen one used, so hope 8 are enough... + ports_guids_size = DWORD() + if not SetupDiClassGuidsFromName( + "Ports", + PortsGUIDs, + ctypes.sizeof(PortsGUIDs), + ctypes.byref(ports_guids_size)): + raise ctypes.WinError() + + ModemsGUIDs = (GUID * 8)() # so far only seen one used, so hope 8 are enough... + modems_guids_size = DWORD() + if not SetupDiClassGuidsFromName( + "Modem", + ModemsGUIDs, + ctypes.sizeof(ModemsGUIDs), + ctypes.byref(modems_guids_size)): + raise ctypes.WinError() + + GUIDs = PortsGUIDs[:ports_guids_size.value] + ModemsGUIDs[:modems_guids_size.value] + + # repeat for all possible GUIDs + for index in range(len(GUIDs)): + bInterfaceNumber = None + g_hdi = SetupDiGetClassDevs( + ctypes.byref(GUIDs[index]), + None, + NULL, + DIGCF_PRESENT) # was DIGCF_PRESENT|DIGCF_DEVICEINTERFACE which misses CDC ports + + devinfo = SP_DEVINFO_DATA() + devinfo.cbSize = ctypes.sizeof(devinfo) + index = 0 + while SetupDiEnumDeviceInfo(g_hdi, index, ctypes.byref(devinfo)): + index += 1 + + # get the real com port name + hkey = SetupDiOpenDevRegKey( + g_hdi, + ctypes.byref(devinfo), + DICS_FLAG_GLOBAL, + 0, + DIREG_DEV, # DIREG_DRV for SW info + KEY_READ) + port_name_buffer = ctypes.create_unicode_buffer(250) + port_name_length = ULONG(ctypes.sizeof(port_name_buffer)) + RegQueryValueEx( + hkey, + "PortName", + None, + None, + ctypes.byref(port_name_buffer), + ctypes.byref(port_name_length)) + RegCloseKey(hkey) + + # unfortunately does this method also include parallel ports. + # we could check for names starting with COM or just exclude LPT + # and hope that other "unknown" names are serial ports... + if port_name_buffer.value.startswith('LPT'): + continue + + # hardware ID + szHardwareID = ctypes.create_unicode_buffer(250) + # try to get ID that includes serial number + if not SetupDiGetDeviceInstanceId( + g_hdi, + ctypes.byref(devinfo), + #~ ctypes.byref(szHardwareID), + szHardwareID, + ctypes.sizeof(szHardwareID) - 1, + None): + # fall back to more generic hardware ID if that would fail + if not SetupDiGetDeviceRegistryProperty( + g_hdi, + ctypes.byref(devinfo), + SPDRP_HARDWAREID, + None, + ctypes.byref(szHardwareID), + ctypes.sizeof(szHardwareID) - 1, + None): + # Ignore ERROR_INSUFFICIENT_BUFFER + if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER: + raise ctypes.WinError() + # stringify + szHardwareID_str = szHardwareID.value + + info = list_ports_common.ListPortInfo(port_name_buffer.value, skip_link_detection=True) + + # in case of USB, make a more readable string, similar to that form + # that we also generate on other platforms + if szHardwareID_str.startswith('USB'): + m = re.search(r'VID_([0-9a-f]{4})(&PID_([0-9a-f]{4}))?(&MI_(\d{2}))?(\\(.*))?', szHardwareID_str, re.I) + if m: + info.vid = int(m.group(1), 16) + if m.group(3): + info.pid = int(m.group(3), 16) + if m.group(5): + bInterfaceNumber = int(m.group(5)) + + # Check that the USB serial number only contains alpha-numeric characters. It + # may be a windows device ID (ephemeral ID) for composite devices. + if m.group(7) and re.match(r'^\w+$', m.group(7)): + info.serial_number = m.group(7) + else: + info.serial_number = get_parent_serial_number(devinfo.DevInst, info.vid, info.pid) + + # calculate a location string + loc_path_str = ctypes.create_unicode_buffer(250) + if SetupDiGetDeviceRegistryProperty( + g_hdi, + ctypes.byref(devinfo), + SPDRP_LOCATION_PATHS, + None, + ctypes.byref(loc_path_str), + ctypes.sizeof(loc_path_str) - 1, + None): + m = re.finditer(r'USBROOT\((\w+)\)|#USB\((\w+)\)', loc_path_str.value) + location = [] + for g in m: + if g.group(1): + location.append('{:d}'.format(int(g.group(1)) + 1)) + else: + if len(location) > 1: + location.append('.') + else: + location.append('-') + location.append(g.group(2)) + if bInterfaceNumber is not None: + location.append(':{}.{}'.format( + 'x', # XXX how to determine correct bConfigurationValue? + bInterfaceNumber)) + if location: + info.location = ''.join(location) + info.hwid = info.usb_info() + elif szHardwareID_str.startswith('FTDIBUS'): + m = re.search(r'VID_([0-9a-f]{4})\+PID_([0-9a-f]{4})(\+(\w+))?', szHardwareID_str, re.I) + if m: + info.vid = int(m.group(1), 16) + info.pid = int(m.group(2), 16) + if m.group(4): + info.serial_number = m.group(4) + # USB location is hidden by FDTI driver :( + info.hwid = info.usb_info() + else: + info.hwid = szHardwareID_str + + # friendly name + szFriendlyName = ctypes.create_unicode_buffer(250) + if SetupDiGetDeviceRegistryProperty( + g_hdi, + ctypes.byref(devinfo), + SPDRP_FRIENDLYNAME, + #~ SPDRP_DEVICEDESC, + None, + ctypes.byref(szFriendlyName), + ctypes.sizeof(szFriendlyName) - 1, + None): + info.description = szFriendlyName.value + #~ else: + # Ignore ERROR_INSUFFICIENT_BUFFER + #~ if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER: + #~ raise IOError("failed to get details for %s (%s)" % (devinfo, szHardwareID.value)) + # ignore errors and still include the port in the list, friendly name will be same as port name + + # manufacturer + szManufacturer = ctypes.create_unicode_buffer(250) + if SetupDiGetDeviceRegistryProperty( + g_hdi, + ctypes.byref(devinfo), + SPDRP_MFG, + #~ SPDRP_DEVICEDESC, + None, + ctypes.byref(szManufacturer), + ctypes.sizeof(szManufacturer) - 1, + None): + info.manufacturer = szManufacturer.value + yield info + SetupDiDestroyDeviceInfoList(g_hdi) + + +def comports(include_links=False): + """Return a list of info objects about serial ports""" + return list(iterate_comports()) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# test +if __name__ == '__main__': + for port, desc, hwid in sorted(comports()): + print("{}: {} [{}]".format(port, desc, hwid)) diff --git a/tools/python-embed/Lib/site-packages/serial/tools/miniterm.py b/tools/python-embed/Lib/site-packages/serial/tools/miniterm.py new file mode 100644 index 0000000..2cceff6 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/serial/tools/miniterm.py @@ -0,0 +1,1042 @@ +#!/usr/bin/env python +# +# Very simple serial terminal +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C)2002-2020 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import absolute_import + +import codecs +import os +import sys +import threading + +import serial +from serial.tools.list_ports import comports +from serial.tools import hexlify_codec + +# pylint: disable=wrong-import-order,wrong-import-position + +codecs.register(lambda c: hexlify_codec.getregentry() if c == 'hexlify' else None) + +try: + raw_input +except NameError: + # pylint: disable=redefined-builtin,invalid-name + raw_input = input # in python3 it's "raw" + unichr = chr + + +def key_description(character): + """generate a readable description for a key""" + ascii_code = ord(character) + if ascii_code < 32: + return 'Ctrl+{:c}'.format(ord('@') + ascii_code) + else: + return repr(character) + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +class ConsoleBase(object): + """OS abstraction for console (input/output codec, no echo)""" + + def __init__(self): + if sys.version_info >= (3, 0): + self.byte_output = sys.stdout.buffer + else: + self.byte_output = sys.stdout + self.output = sys.stdout + + def setup(self): + """Set console to read single characters, no echo""" + + def cleanup(self): + """Restore default console settings""" + + def getkey(self): + """Read a single key from the console""" + return None + + def write_bytes(self, byte_string): + """Write bytes (already encoded)""" + self.byte_output.write(byte_string) + self.byte_output.flush() + + def write(self, text): + """Write string""" + self.output.write(text) + self.output.flush() + + def cancel(self): + """Cancel getkey operation""" + + # - - - - - - - - - - - - - - - - - - - - - - - - + # context manager: + # switch terminal temporary to normal mode (e.g. to get user input) + + def __enter__(self): + self.cleanup() + return self + + def __exit__(self, *args, **kwargs): + self.setup() + + +if os.name == 'nt': # noqa + import msvcrt + import ctypes + import platform + + class Out(object): + """file-like wrapper that uses os.write""" + + def __init__(self, fd): + self.fd = fd + + def flush(self): + pass + + def write(self, s): + os.write(self.fd, s) + + class Console(ConsoleBase): + fncodes = { + ';': '\1bOP', # F1 + '<': '\1bOQ', # F2 + '=': '\1bOR', # F3 + '>': '\1bOS', # F4 + '?': '\1b[15~', # F5 + '@': '\1b[17~', # F6 + 'A': '\1b[18~', # F7 + 'B': '\1b[19~', # F8 + 'C': '\1b[20~', # F9 + 'D': '\1b[21~', # F10 + } + navcodes = { + 'H': '\x1b[A', # UP + 'P': '\x1b[B', # DOWN + 'K': '\x1b[D', # LEFT + 'M': '\x1b[C', # RIGHT + 'G': '\x1b[H', # HOME + 'O': '\x1b[F', # END + 'R': '\x1b[2~', # INSERT + 'S': '\x1b[3~', # DELETE + 'I': '\x1b[5~', # PGUP + 'Q': '\x1b[6~', # PGDN + } + + def __init__(self): + super(Console, self).__init__() + self._saved_ocp = ctypes.windll.kernel32.GetConsoleOutputCP() + self._saved_icp = ctypes.windll.kernel32.GetConsoleCP() + ctypes.windll.kernel32.SetConsoleOutputCP(65001) + ctypes.windll.kernel32.SetConsoleCP(65001) + # ANSI handling available through SetConsoleMode since Windows 10 v1511 + # https://en.wikipedia.org/wiki/ANSI_escape_code#cite_note-win10th2-1 + if platform.release() == '10' and int(platform.version().split('.')[2]) > 10586: + ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 + import ctypes.wintypes as wintypes + if not hasattr(wintypes, 'LPDWORD'): # PY2 + wintypes.LPDWORD = ctypes.POINTER(wintypes.DWORD) + SetConsoleMode = ctypes.windll.kernel32.SetConsoleMode + GetConsoleMode = ctypes.windll.kernel32.GetConsoleMode + GetStdHandle = ctypes.windll.kernel32.GetStdHandle + mode = wintypes.DWORD() + GetConsoleMode(GetStdHandle(-11), ctypes.byref(mode)) + if (mode.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0: + SetConsoleMode(GetStdHandle(-11), mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING) + self._saved_cm = mode + self.output = codecs.getwriter('UTF-8')(Out(sys.stdout.fileno()), 'replace') + # the change of the code page is not propagated to Python, manually fix it + sys.stderr = codecs.getwriter('UTF-8')(Out(sys.stderr.fileno()), 'replace') + sys.stdout = self.output + self.output.encoding = 'UTF-8' # needed for input + + def __del__(self): + ctypes.windll.kernel32.SetConsoleOutputCP(self._saved_ocp) + ctypes.windll.kernel32.SetConsoleCP(self._saved_icp) + try: + ctypes.windll.kernel32.SetConsoleMode(ctypes.windll.kernel32.GetStdHandle(-11), self._saved_cm) + except AttributeError: # in case no _saved_cm + pass + + def getkey(self): + while True: + z = msvcrt.getwch() + if z == unichr(13): + return unichr(10) + elif z is unichr(0) or z is unichr(0xe0): + try: + code = msvcrt.getwch() + if z is unichr(0): + return self.fncodes[code] + else: + return self.navcodes[code] + except KeyError: + pass + else: + return z + + def cancel(self): + # CancelIo, CancelSynchronousIo do not seem to work when using + # getwch, so instead, send a key to the window with the console + hwnd = ctypes.windll.kernel32.GetConsoleWindow() + ctypes.windll.user32.PostMessageA(hwnd, 0x100, 0x0d, 0) + +elif os.name == 'posix': + import atexit + import termios + import fcntl + + class Console(ConsoleBase): + def __init__(self): + super(Console, self).__init__() + self.fd = sys.stdin.fileno() + self.old = termios.tcgetattr(self.fd) + atexit.register(self.cleanup) + if sys.version_info < (3, 0): + self.enc_stdin = codecs.getreader(sys.stdin.encoding)(sys.stdin) + else: + self.enc_stdin = sys.stdin + + def setup(self): + new = termios.tcgetattr(self.fd) + new[3] = new[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG + new[6][termios.VMIN] = 1 + new[6][termios.VTIME] = 0 + termios.tcsetattr(self.fd, termios.TCSANOW, new) + + def getkey(self): + c = self.enc_stdin.read(1) + if c == unichr(0x7f): + c = unichr(8) # map the BS key (which yields DEL) to backspace + return c + + def cancel(self): + fcntl.ioctl(self.fd, termios.TIOCSTI, b'\0') + + def cleanup(self): + termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old) + +else: + raise NotImplementedError( + 'Sorry no implementation for your platform ({}) available.'.format(sys.platform)) + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +class Transform(object): + """do-nothing: forward all data unchanged""" + def rx(self, text): + """text received from serial port""" + return text + + def tx(self, text): + """text to be sent to serial port""" + return text + + def echo(self, text): + """text to be sent but displayed on console""" + return text + + +class CRLF(Transform): + """ENTER sends CR+LF""" + + def tx(self, text): + return text.replace('\n', '\r\n') + + +class CR(Transform): + """ENTER sends CR""" + + def rx(self, text): + return text.replace('\r', '\n') + + def tx(self, text): + return text.replace('\n', '\r') + + +class LF(Transform): + """ENTER sends LF""" + + +class NoTerminal(Transform): + """remove typical terminal control codes from input""" + + REPLACEMENT_MAP = dict((x, 0x2400 + x) for x in range(32) if unichr(x) not in '\r\n\b\t') + REPLACEMENT_MAP.update( + { + 0x7F: 0x2421, # DEL + 0x9B: 0x2425, # CSI + }) + + def rx(self, text): + return text.translate(self.REPLACEMENT_MAP) + + echo = rx + + +class NoControls(NoTerminal): + """Remove all control codes, incl. CR+LF""" + + REPLACEMENT_MAP = dict((x, 0x2400 + x) for x in range(32)) + REPLACEMENT_MAP.update( + { + 0x20: 0x2423, # visual space + 0x7F: 0x2421, # DEL + 0x9B: 0x2425, # CSI + }) + + +class Printable(Transform): + """Show decimal code for all non-ASCII characters and replace most control codes""" + + def rx(self, text): + r = [] + for c in text: + if ' ' <= c < '\x7f' or c in '\r\n\b\t': + r.append(c) + elif c < ' ': + r.append(unichr(0x2400 + ord(c))) + else: + r.extend(unichr(0x2080 + ord(d) - 48) for d in '{:d}'.format(ord(c))) + r.append(' ') + return ''.join(r) + + echo = rx + + +class Colorize(Transform): + """Apply different colors for received and echo""" + + def __init__(self): + # XXX make it configurable, use colorama? + self.input_color = '\x1b[37m' + self.echo_color = '\x1b[31m' + + def rx(self, text): + return self.input_color + text + + def echo(self, text): + return self.echo_color + text + + +class DebugIO(Transform): + """Print what is sent and received""" + + def rx(self, text): + sys.stderr.write(' [RX:{!r}] '.format(text)) + sys.stderr.flush() + return text + + def tx(self, text): + sys.stderr.write(' [TX:{!r}] '.format(text)) + sys.stderr.flush() + return text + + +# other ideas: +# - add date/time for each newline +# - insert newline after: a) timeout b) packet end character + +EOL_TRANSFORMATIONS = { + 'crlf': CRLF, + 'cr': CR, + 'lf': LF, +} + +TRANSFORMATIONS = { + 'direct': Transform, # no transformation + 'default': NoTerminal, + 'nocontrol': NoControls, + 'printable': Printable, + 'colorize': Colorize, + 'debug': DebugIO, +} + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +def ask_for_port(): + """\ + Show a list of ports and ask the user for a choice. To make selection + easier on systems with long device names, also allow the input of an + index. + """ + sys.stderr.write('\n--- Available ports:\n') + ports = [] + for n, (port, desc, hwid) in enumerate(sorted(comports()), 1): + sys.stderr.write('--- {:2}: {:20} {!r}\n'.format(n, port, desc)) + ports.append(port) + while True: + port = raw_input('--- Enter port index or full name: ') + try: + index = int(port) - 1 + if not 0 <= index < len(ports): + sys.stderr.write('--- Invalid index!\n') + continue + except ValueError: + pass + else: + port = ports[index] + return port + + +class Miniterm(object): + """\ + Terminal application. Copy data from serial port to console and vice versa. + Handle special keys from the console to show menu etc. + """ + + def __init__(self, serial_instance, echo=False, eol='crlf', filters=()): + self.console = Console() + self.serial = serial_instance + self.echo = echo + self.raw = False + self.input_encoding = 'UTF-8' + self.output_encoding = 'UTF-8' + self.eol = eol + self.filters = filters + self.update_transformations() + self.exit_character = unichr(0x1d) # GS/CTRL+] + self.menu_character = unichr(0x14) # Menu: CTRL+T + self.alive = None + self._reader_alive = None + self.receiver_thread = None + self.rx_decoder = None + self.tx_decoder = None + + def _start_reader(self): + """Start reader thread""" + self._reader_alive = True + # start serial->console thread + self.receiver_thread = threading.Thread(target=self.reader, name='rx') + self.receiver_thread.daemon = True + self.receiver_thread.start() + + def _stop_reader(self): + """Stop reader thread only, wait for clean exit of thread""" + self._reader_alive = False + if hasattr(self.serial, 'cancel_read'): + self.serial.cancel_read() + self.receiver_thread.join() + + def start(self): + """start worker threads""" + self.alive = True + self._start_reader() + # enter console->serial loop + self.transmitter_thread = threading.Thread(target=self.writer, name='tx') + self.transmitter_thread.daemon = True + self.transmitter_thread.start() + self.console.setup() + + def stop(self): + """set flag to stop worker threads""" + self.alive = False + + def join(self, transmit_only=False): + """wait for worker threads to terminate""" + self.transmitter_thread.join() + if not transmit_only: + if hasattr(self.serial, 'cancel_read'): + self.serial.cancel_read() + self.receiver_thread.join() + + def close(self): + self.serial.close() + + def update_transformations(self): + """take list of transformation classes and instantiate them for rx and tx""" + transformations = [EOL_TRANSFORMATIONS[self.eol]] + [TRANSFORMATIONS[f] + for f in self.filters] + self.tx_transformations = [t() for t in transformations] + self.rx_transformations = list(reversed(self.tx_transformations)) + + def set_rx_encoding(self, encoding, errors='replace'): + """set encoding for received data""" + self.input_encoding = encoding + self.rx_decoder = codecs.getincrementaldecoder(encoding)(errors) + + def set_tx_encoding(self, encoding, errors='replace'): + """set encoding for transmitted data""" + self.output_encoding = encoding + self.tx_encoder = codecs.getincrementalencoder(encoding)(errors) + + def dump_port_settings(self): + """Write current settings to sys.stderr""" + sys.stderr.write("\n--- Settings: {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits}\n".format( + p=self.serial)) + sys.stderr.write('--- RTS: {:8} DTR: {:8} BREAK: {:8}\n'.format( + ('active' if self.serial.rts else 'inactive'), + ('active' if self.serial.dtr else 'inactive'), + ('active' if self.serial.break_condition else 'inactive'))) + try: + sys.stderr.write('--- CTS: {:8} DSR: {:8} RI: {:8} CD: {:8}\n'.format( + ('active' if self.serial.cts else 'inactive'), + ('active' if self.serial.dsr else 'inactive'), + ('active' if self.serial.ri else 'inactive'), + ('active' if self.serial.cd else 'inactive'))) + except serial.SerialException: + # on RFC 2217 ports, it can happen if no modem state notification was + # yet received. ignore this error. + pass + sys.stderr.write('--- software flow control: {}\n'.format('active' if self.serial.xonxoff else 'inactive')) + sys.stderr.write('--- hardware flow control: {}\n'.format('active' if self.serial.rtscts else 'inactive')) + sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding)) + sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding)) + sys.stderr.write('--- EOL: {}\n'.format(self.eol.upper())) + sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters))) + + def reader(self): + """loop and copy serial->console""" + try: + while self.alive and self._reader_alive: + # read all that is there or wait for one byte + data = self.serial.read(self.serial.in_waiting or 1) + if data: + if self.raw: + self.console.write_bytes(data) + else: + text = self.rx_decoder.decode(data) + for transformation in self.rx_transformations: + text = transformation.rx(text) + self.console.write(text) + except serial.SerialException: + self.alive = False + self.console.cancel() + raise # XXX handle instead of re-raise? + + def writer(self): + """\ + Loop and copy console->serial until self.exit_character character is + found. When self.menu_character is found, interpret the next key + locally. + """ + menu_active = False + try: + while self.alive: + try: + c = self.console.getkey() + except KeyboardInterrupt: + c = '\x03' + if not self.alive: + break + if menu_active: + self.handle_menu_key(c) + menu_active = False + elif c == self.menu_character: + menu_active = True # next char will be for menu + elif c == self.exit_character: + self.stop() # exit app + break + else: + #~ if self.raw: + text = c + for transformation in self.tx_transformations: + text = transformation.tx(text) + self.serial.write(self.tx_encoder.encode(text)) + if self.echo: + echo_text = c + for transformation in self.tx_transformations: + echo_text = transformation.echo(echo_text) + self.console.write(echo_text) + except: + self.alive = False + raise + + def handle_menu_key(self, c): + """Implement a simple menu / settings""" + if c == self.menu_character or c == self.exit_character: + # Menu/exit character again -> send itself + self.serial.write(self.tx_encoder.encode(c)) + if self.echo: + self.console.write(c) + elif c == '\x15': # CTRL+U -> upload file + self.upload_file() + elif c in '\x08hH?': # CTRL+H, h, H, ? -> Show help + sys.stderr.write(self.get_help_text()) + elif c == '\x12': # CTRL+R -> Toggle RTS + self.serial.rts = not self.serial.rts + sys.stderr.write('--- RTS {} ---\n'.format('active' if self.serial.rts else 'inactive')) + elif c == '\x04': # CTRL+D -> Toggle DTR + self.serial.dtr = not self.serial.dtr + sys.stderr.write('--- DTR {} ---\n'.format('active' if self.serial.dtr else 'inactive')) + elif c == '\x02': # CTRL+B -> toggle BREAK condition + self.serial.break_condition = not self.serial.break_condition + sys.stderr.write('--- BREAK {} ---\n'.format('active' if self.serial.break_condition else 'inactive')) + elif c == '\x05': # CTRL+E -> toggle local echo + self.echo = not self.echo + sys.stderr.write('--- local echo {} ---\n'.format('active' if self.echo else 'inactive')) + elif c == '\x06': # CTRL+F -> edit filters + self.change_filter() + elif c == '\x0c': # CTRL+L -> EOL mode + modes = list(EOL_TRANSFORMATIONS) # keys + eol = modes.index(self.eol) + 1 + if eol >= len(modes): + eol = 0 + self.eol = modes[eol] + sys.stderr.write('--- EOL: {} ---\n'.format(self.eol.upper())) + self.update_transformations() + elif c == '\x01': # CTRL+A -> set encoding + self.change_encoding() + elif c == '\x09': # CTRL+I -> info + self.dump_port_settings() + #~ elif c == '\x01': # CTRL+A -> cycle escape mode + #~ elif c == '\x0c': # CTRL+L -> cycle linefeed mode + elif c in 'pP': # P -> change port + self.change_port() + elif c in 'zZ': # S -> suspend / open port temporarily + self.suspend_port() + elif c in 'bB': # B -> change baudrate + self.change_baudrate() + elif c == '8': # 8 -> change to 8 bits + self.serial.bytesize = serial.EIGHTBITS + self.dump_port_settings() + elif c == '7': # 7 -> change to 8 bits + self.serial.bytesize = serial.SEVENBITS + self.dump_port_settings() + elif c in 'eE': # E -> change to even parity + self.serial.parity = serial.PARITY_EVEN + self.dump_port_settings() + elif c in 'oO': # O -> change to odd parity + self.serial.parity = serial.PARITY_ODD + self.dump_port_settings() + elif c in 'mM': # M -> change to mark parity + self.serial.parity = serial.PARITY_MARK + self.dump_port_settings() + elif c in 'sS': # S -> change to space parity + self.serial.parity = serial.PARITY_SPACE + self.dump_port_settings() + elif c in 'nN': # N -> change to no parity + self.serial.parity = serial.PARITY_NONE + self.dump_port_settings() + elif c == '1': # 1 -> change to 1 stop bits + self.serial.stopbits = serial.STOPBITS_ONE + self.dump_port_settings() + elif c == '2': # 2 -> change to 2 stop bits + self.serial.stopbits = serial.STOPBITS_TWO + self.dump_port_settings() + elif c == '3': # 3 -> change to 1.5 stop bits + self.serial.stopbits = serial.STOPBITS_ONE_POINT_FIVE + self.dump_port_settings() + elif c in 'xX': # X -> change software flow control + self.serial.xonxoff = (c == 'X') + self.dump_port_settings() + elif c in 'rR': # R -> change hardware flow control + self.serial.rtscts = (c == 'R') + self.dump_port_settings() + elif c in 'qQ': + self.stop() # Q -> exit app + else: + sys.stderr.write('--- unknown menu character {} --\n'.format(key_description(c))) + + def upload_file(self): + """Ask user for filenname and send its contents""" + sys.stderr.write('\n--- File to upload: ') + sys.stderr.flush() + with self.console: + filename = sys.stdin.readline().rstrip('\r\n') + if filename: + try: + with open(filename, 'rb') as f: + sys.stderr.write('--- Sending file {} ---\n'.format(filename)) + while True: + block = f.read(1024) + if not block: + break + self.serial.write(block) + # Wait for output buffer to drain. + self.serial.flush() + sys.stderr.write('.') # Progress indicator. + sys.stderr.write('\n--- File {} sent ---\n'.format(filename)) + except IOError as e: + sys.stderr.write('--- ERROR opening file {}: {} ---\n'.format(filename, e)) + + def change_filter(self): + """change the i/o transformations""" + sys.stderr.write('\n--- Available Filters:\n') + sys.stderr.write('\n'.join( + '--- {:<10} = {.__doc__}'.format(k, v) + for k, v in sorted(TRANSFORMATIONS.items()))) + sys.stderr.write('\n--- Enter new filter name(s) [{}]: '.format(' '.join(self.filters))) + with self.console: + new_filters = sys.stdin.readline().lower().split() + if new_filters: + for f in new_filters: + if f not in TRANSFORMATIONS: + sys.stderr.write('--- unknown filter: {!r}\n'.format(f)) + break + else: + self.filters = new_filters + self.update_transformations() + sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters))) + + def change_encoding(self): + """change encoding on the serial port""" + sys.stderr.write('\n--- Enter new encoding name [{}]: '.format(self.input_encoding)) + with self.console: + new_encoding = sys.stdin.readline().strip() + if new_encoding: + try: + codecs.lookup(new_encoding) + except LookupError: + sys.stderr.write('--- invalid encoding name: {}\n'.format(new_encoding)) + else: + self.set_rx_encoding(new_encoding) + self.set_tx_encoding(new_encoding) + sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding)) + sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding)) + + def change_baudrate(self): + """change the baudrate""" + sys.stderr.write('\n--- Baudrate: ') + sys.stderr.flush() + with self.console: + backup = self.serial.baudrate + try: + self.serial.baudrate = int(sys.stdin.readline().strip()) + except ValueError as e: + sys.stderr.write('--- ERROR setting baudrate: {} ---\n'.format(e)) + self.serial.baudrate = backup + else: + self.dump_port_settings() + + def change_port(self): + """Have a conversation with the user to change the serial port""" + with self.console: + try: + port = ask_for_port() + except KeyboardInterrupt: + port = None + if port and port != self.serial.port: + # reader thread needs to be shut down + self._stop_reader() + # save settings + settings = self.serial.getSettingsDict() + try: + new_serial = serial.serial_for_url(port, do_not_open=True) + # restore settings and open + new_serial.applySettingsDict(settings) + new_serial.rts = self.serial.rts + new_serial.dtr = self.serial.dtr + new_serial.open() + new_serial.break_condition = self.serial.break_condition + except Exception as e: + sys.stderr.write('--- ERROR opening new port: {} ---\n'.format(e)) + new_serial.close() + else: + self.serial.close() + self.serial = new_serial + sys.stderr.write('--- Port changed to: {} ---\n'.format(self.serial.port)) + # and restart the reader thread + self._start_reader() + + def suspend_port(self): + """\ + open port temporarily, allow reconnect, exit and port change to get + out of the loop + """ + # reader thread needs to be shut down + self._stop_reader() + self.serial.close() + sys.stderr.write('\n--- Port closed: {} ---\n'.format(self.serial.port)) + do_change_port = False + while not self.serial.is_open: + sys.stderr.write('--- Quit: {exit} | p: port change | any other key to reconnect ---\n'.format( + exit=key_description(self.exit_character))) + k = self.console.getkey() + if k == self.exit_character: + self.stop() # exit app + break + elif k in 'pP': + do_change_port = True + break + try: + self.serial.open() + except Exception as e: + sys.stderr.write('--- ERROR opening port: {} ---\n'.format(e)) + if do_change_port: + self.change_port() + else: + # and restart the reader thread + self._start_reader() + sys.stderr.write('--- Port opened: {} ---\n'.format(self.serial.port)) + + def get_help_text(self): + """return the help text""" + # help text, starts with blank line! + return """ +--- pySerial ({version}) - miniterm - help +--- +--- {exit:8} Exit program (alias {menu} Q) +--- {menu:8} Menu escape key, followed by: +--- Menu keys: +--- {menu:7} Send the menu character itself to remote +--- {exit:7} Send the exit character itself to remote +--- {info:7} Show info +--- {upload:7} Upload file (prompt will be shown) +--- {repr:7} encoding +--- {filter:7} edit filters +--- Toggles: +--- {rts:7} RTS {dtr:7} DTR {brk:7} BREAK +--- {echo:7} echo {eol:7} EOL +--- +--- Port settings ({menu} followed by the following): +--- p change port +--- 7 8 set data bits +--- N E O S M change parity (None, Even, Odd, Space, Mark) +--- 1 2 3 set stop bits (1, 2, 1.5) +--- b change baud rate +--- x X disable/enable software flow control +--- r R disable/enable hardware flow control +""".format(version=getattr(serial, 'VERSION', 'unknown version'), + exit=key_description(self.exit_character), + menu=key_description(self.menu_character), + rts=key_description('\x12'), + dtr=key_description('\x04'), + brk=key_description('\x02'), + echo=key_description('\x05'), + info=key_description('\x09'), + upload=key_description('\x15'), + repr=key_description('\x01'), + filter=key_description('\x06'), + eol=key_description('\x0c')) + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# default args can be used to override when calling main() from an other script +# e.g to create a miniterm-my-device.py +def main(default_port=None, default_baudrate=9600, default_rts=None, default_dtr=None): + """Command line tool, entry point""" + + import argparse + + parser = argparse.ArgumentParser( + description='Miniterm - A simple terminal program for the serial port.') + + parser.add_argument( + 'port', + nargs='?', + help='serial port name ("-" to show port list)', + default=default_port) + + parser.add_argument( + 'baudrate', + nargs='?', + type=int, + help='set baud rate, default: %(default)s', + default=default_baudrate) + + group = parser.add_argument_group('port settings') + + group.add_argument( + '--parity', + choices=['N', 'E', 'O', 'S', 'M'], + type=lambda c: c.upper(), + help='set parity, one of {N E O S M}, default: N', + default='N') + + group.add_argument( + '--rtscts', + action='store_true', + help='enable RTS/CTS flow control (default off)', + default=False) + + group.add_argument( + '--xonxoff', + action='store_true', + help='enable software flow control (default off)', + default=False) + + group.add_argument( + '--rts', + type=int, + help='set initial RTS line state (possible values: 0, 1)', + default=default_rts) + + group.add_argument( + '--dtr', + type=int, + help='set initial DTR line state (possible values: 0, 1)', + default=default_dtr) + + group.add_argument( + '--non-exclusive', + dest='exclusive', + action='store_false', + help='disable locking for native ports', + default=True) + + group.add_argument( + '--ask', + action='store_true', + help='ask again for port when open fails', + default=False) + + group = parser.add_argument_group('data handling') + + group.add_argument( + '-e', '--echo', + action='store_true', + help='enable local echo (default off)', + default=False) + + group.add_argument( + '--encoding', + dest='serial_port_encoding', + metavar='CODEC', + help='set the encoding for the serial port (e.g. hexlify, Latin1, UTF-8), default: %(default)s', + default='UTF-8') + + group.add_argument( + '-f', '--filter', + action='append', + metavar='NAME', + help='add text transformation', + default=[]) + + group.add_argument( + '--eol', + choices=['CR', 'LF', 'CRLF'], + type=lambda c: c.upper(), + help='end of line mode', + default='CRLF') + + group.add_argument( + '--raw', + action='store_true', + help='Do no apply any encodings/transformations', + default=False) + + group = parser.add_argument_group('hotkeys') + + group.add_argument( + '--exit-char', + type=int, + metavar='NUM', + help='Unicode of special character that is used to exit the application, default: %(default)s', + default=0x1d) # GS/CTRL+] + + group.add_argument( + '--menu-char', + type=int, + metavar='NUM', + help='Unicode code of special character that is used to control miniterm (menu), default: %(default)s', + default=0x14) # Menu: CTRL+T + + group = parser.add_argument_group('diagnostics') + + group.add_argument( + '-q', '--quiet', + action='store_true', + help='suppress non-error messages', + default=False) + + group.add_argument( + '--develop', + action='store_true', + help='show Python traceback on error', + default=False) + + args = parser.parse_args() + + if args.menu_char == args.exit_char: + parser.error('--exit-char can not be the same as --menu-char') + + if args.filter: + if 'help' in args.filter: + sys.stderr.write('Available filters:\n') + sys.stderr.write('\n'.join( + '{:<10} = {.__doc__}'.format(k, v) + for k, v in sorted(TRANSFORMATIONS.items()))) + sys.stderr.write('\n') + sys.exit(1) + filters = args.filter + else: + filters = ['default'] + + while True: + # no port given on command line -> ask user now + if args.port is None or args.port == '-': + try: + args.port = ask_for_port() + except KeyboardInterrupt: + sys.stderr.write('\n') + parser.error('user aborted and port is not given') + else: + if not args.port: + parser.error('port is not given') + try: + serial_instance = serial.serial_for_url( + args.port, + args.baudrate, + parity=args.parity, + rtscts=args.rtscts, + xonxoff=args.xonxoff, + do_not_open=True) + + if not hasattr(serial_instance, 'cancel_read'): + # enable timeout for alive flag polling if cancel_read is not available + serial_instance.timeout = 1 + + if args.dtr is not None: + if not args.quiet: + sys.stderr.write('--- forcing DTR {}\n'.format('active' if args.dtr else 'inactive')) + serial_instance.dtr = args.dtr + if args.rts is not None: + if not args.quiet: + sys.stderr.write('--- forcing RTS {}\n'.format('active' if args.rts else 'inactive')) + serial_instance.rts = args.rts + + if isinstance(serial_instance, serial.Serial): + serial_instance.exclusive = args.exclusive + + serial_instance.open() + except serial.SerialException as e: + sys.stderr.write('could not open port {!r}: {}\n'.format(args.port, e)) + if args.develop: + raise + if not args.ask: + sys.exit(1) + else: + args.port = '-' + else: + break + + miniterm = Miniterm( + serial_instance, + echo=args.echo, + eol=args.eol.lower(), + filters=filters) + miniterm.exit_character = unichr(args.exit_char) + miniterm.menu_character = unichr(args.menu_char) + miniterm.raw = args.raw + miniterm.set_rx_encoding(args.serial_port_encoding) + miniterm.set_tx_encoding(args.serial_port_encoding) + + if not args.quiet: + sys.stderr.write('--- Miniterm on {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits} ---\n'.format( + p=miniterm.serial)) + sys.stderr.write('--- Quit: {} | Menu: {} | Help: {} followed by {} ---\n'.format( + key_description(miniterm.exit_character), + key_description(miniterm.menu_character), + key_description(miniterm.menu_character), + key_description('\x08'))) + + miniterm.start() + try: + miniterm.join(True) + except KeyboardInterrupt: + pass + if not args.quiet: + sys.stderr.write('\n--- exit ---\n') + miniterm.join() + miniterm.close() + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +if __name__ == '__main__': + main() diff --git a/tools/python-embed/Lib/site-packages/serial/urlhandler/__init__.py b/tools/python-embed/Lib/site-packages/serial/urlhandler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/python-embed/Lib/site-packages/serial/urlhandler/__pycache__/__init__.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/urlhandler/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..ce1b80f Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/urlhandler/__pycache__/__init__.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/urlhandler/__pycache__/protocol_alt.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/urlhandler/__pycache__/protocol_alt.cpython-312.pyc new file mode 100644 index 0000000..83a5fe9 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/urlhandler/__pycache__/protocol_alt.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/urlhandler/__pycache__/protocol_cp2110.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/urlhandler/__pycache__/protocol_cp2110.cpython-312.pyc new file mode 100644 index 0000000..7253d11 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/urlhandler/__pycache__/protocol_cp2110.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/urlhandler/__pycache__/protocol_hwgrep.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/urlhandler/__pycache__/protocol_hwgrep.cpython-312.pyc new file mode 100644 index 0000000..88d1078 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/urlhandler/__pycache__/protocol_hwgrep.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/urlhandler/__pycache__/protocol_loop.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/urlhandler/__pycache__/protocol_loop.cpython-312.pyc new file mode 100644 index 0000000..54422ab Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/urlhandler/__pycache__/protocol_loop.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/urlhandler/__pycache__/protocol_rfc2217.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/urlhandler/__pycache__/protocol_rfc2217.cpython-312.pyc new file mode 100644 index 0000000..4dac283 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/urlhandler/__pycache__/protocol_rfc2217.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/urlhandler/__pycache__/protocol_socket.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/urlhandler/__pycache__/protocol_socket.cpython-312.pyc new file mode 100644 index 0000000..61de719 Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/urlhandler/__pycache__/protocol_socket.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/urlhandler/__pycache__/protocol_spy.cpython-312.pyc b/tools/python-embed/Lib/site-packages/serial/urlhandler/__pycache__/protocol_spy.cpython-312.pyc new file mode 100644 index 0000000..5c5937d Binary files /dev/null and b/tools/python-embed/Lib/site-packages/serial/urlhandler/__pycache__/protocol_spy.cpython-312.pyc differ diff --git a/tools/python-embed/Lib/site-packages/serial/urlhandler/protocol_alt.py b/tools/python-embed/Lib/site-packages/serial/urlhandler/protocol_alt.py new file mode 100644 index 0000000..2e666ca --- /dev/null +++ b/tools/python-embed/Lib/site-packages/serial/urlhandler/protocol_alt.py @@ -0,0 +1,57 @@ +#! python +# +# This module implements a special URL handler that allows selecting an +# alternate implementation provided by some backends. +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2015 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause +# +# URL format: alt://port[?option[=value][&option[=value]]] +# options: +# - class=X used class named X instead of Serial +# +# example: +# use poll based implementation on Posix (Linux): +# python -m serial.tools.miniterm alt:///dev/ttyUSB0?class=PosixPollSerial + +from __future__ import absolute_import + +try: + import urlparse +except ImportError: + import urllib.parse as urlparse + +import serial + + +def serial_class_for_url(url): + """extract host and port from an URL string""" + parts = urlparse.urlsplit(url) + if parts.scheme != 'alt': + raise serial.SerialException( + 'expected a string in the form "alt://port[?option[=value][&option[=value]]]": ' + 'not starting with alt:// ({!r})'.format(parts.scheme)) + class_name = 'Serial' + try: + for option, values in urlparse.parse_qs(parts.query, True).items(): + if option == 'class': + class_name = values[0] + else: + raise ValueError('unknown option: {!r}'.format(option)) + except ValueError as e: + raise serial.SerialException( + 'expected a string in the form ' + '"alt://port[?option[=value][&option[=value]]]": {!r}'.format(e)) + if not hasattr(serial, class_name): + raise ValueError('unknown class: {!r}'.format(class_name)) + cls = getattr(serial, class_name) + if not issubclass(cls, serial.Serial): + raise ValueError('class {!r} is not an instance of Serial'.format(class_name)) + return (''.join([parts.netloc, parts.path]), cls) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +if __name__ == '__main__': + s = serial.serial_for_url('alt:///dev/ttyS0?class=PosixPollSerial') + print(s) diff --git a/tools/python-embed/Lib/site-packages/serial/urlhandler/protocol_cp2110.py b/tools/python-embed/Lib/site-packages/serial/urlhandler/protocol_cp2110.py new file mode 100644 index 0000000..44ad4eb --- /dev/null +++ b/tools/python-embed/Lib/site-packages/serial/urlhandler/protocol_cp2110.py @@ -0,0 +1,258 @@ +#! python +# +# Backend for Silicon Labs CP2110/4 HID-to-UART devices. +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2001-2015 Chris Liechti +# (C) 2019 Google LLC +# +# SPDX-License-Identifier: BSD-3-Clause + +# This backend implements support for HID-to-UART devices manufactured +# by Silicon Labs and marketed as CP2110 and CP2114. The +# implementation is (mostly) OS-independent and in userland. It relies +# on cython-hidapi (https://github.com/trezor/cython-hidapi). + +# The HID-to-UART protocol implemented by CP2110/4 is described in the +# AN434 document from Silicon Labs: +# https://www.silabs.com/documents/public/application-notes/AN434-CP2110-4-Interface-Specification.pdf + +# TODO items: + +# - rtscts support is configured for hardware flow control, but the +# signaling is missing (AN434 suggests this is done through GPIO). +# - Cancelling reads and writes is not supported. +# - Baudrate validation is not implemented, as it depends on model and configuration. + +import struct +import threading + +try: + import urlparse +except ImportError: + import urllib.parse as urlparse + +try: + import Queue +except ImportError: + import queue as Queue + +import hid # hidapi + +import serial +from serial.serialutil import SerialBase, SerialException, PortNotOpenError, to_bytes, Timeout + + +# Report IDs and related constant +_REPORT_GETSET_UART_ENABLE = 0x41 +_DISABLE_UART = 0x00 +_ENABLE_UART = 0x01 + +_REPORT_SET_PURGE_FIFOS = 0x43 +_PURGE_TX_FIFO = 0x01 +_PURGE_RX_FIFO = 0x02 + +_REPORT_GETSET_UART_CONFIG = 0x50 + +_REPORT_SET_TRANSMIT_LINE_BREAK = 0x51 +_REPORT_SET_STOP_LINE_BREAK = 0x52 + + +class Serial(SerialBase): + # This is not quite correct. AN343 specifies that the minimum + # baudrate is different between CP2110 and CP2114, and it's halved + # when using non-8-bit symbols. + BAUDRATES = (300, 375, 600, 1200, 1800, 2400, 4800, 9600, 19200, + 38400, 57600, 115200, 230400, 460800, 500000, 576000, + 921600, 1000000) + + def __init__(self, *args, **kwargs): + self._hid_handle = None + self._read_buffer = None + self._thread = None + super(Serial, self).__init__(*args, **kwargs) + + def open(self): + if self._port is None: + raise SerialException("Port must be configured before it can be used.") + if self.is_open: + raise SerialException("Port is already open.") + + self._read_buffer = Queue.Queue() + + self._hid_handle = hid.device() + try: + portpath = self.from_url(self.portstr) + self._hid_handle.open_path(portpath) + except OSError as msg: + raise SerialException(msg.errno, "could not open port {}: {}".format(self._port, msg)) + + try: + self._reconfigure_port() + except: + try: + self._hid_handle.close() + except: + pass + self._hid_handle = None + raise + else: + self.is_open = True + self._thread = threading.Thread(target=self._hid_read_loop) + self._thread.setDaemon(True) + self._thread.setName('pySerial CP2110 reader thread for {}'.format(self._port)) + self._thread.start() + + def from_url(self, url): + parts = urlparse.urlsplit(url) + if parts.scheme != "cp2110": + raise SerialException( + 'expected a string in the forms ' + '"cp2110:///dev/hidraw9" or "cp2110://0001:0023:00": ' + 'not starting with cp2110:// {{!r}}'.format(parts.scheme)) + if parts.netloc: # cp2100://BUS:DEVICE:ENDPOINT, for libusb + return parts.netloc.encode('utf-8') + return parts.path.encode('utf-8') + + def close(self): + self.is_open = False + if self._thread: + self._thread.join(1) # read timeout is 0.1 + self._thread = None + self._hid_handle.close() + self._hid_handle = None + + def _reconfigure_port(self): + parity_value = None + if self._parity == serial.PARITY_NONE: + parity_value = 0x00 + elif self._parity == serial.PARITY_ODD: + parity_value = 0x01 + elif self._parity == serial.PARITY_EVEN: + parity_value = 0x02 + elif self._parity == serial.PARITY_MARK: + parity_value = 0x03 + elif self._parity == serial.PARITY_SPACE: + parity_value = 0x04 + else: + raise ValueError('Invalid parity: {!r}'.format(self._parity)) + + if self.rtscts: + flow_control_value = 0x01 + else: + flow_control_value = 0x00 + + data_bits_value = None + if self._bytesize == 5: + data_bits_value = 0x00 + elif self._bytesize == 6: + data_bits_value = 0x01 + elif self._bytesize == 7: + data_bits_value = 0x02 + elif self._bytesize == 8: + data_bits_value = 0x03 + else: + raise ValueError('Invalid char len: {!r}'.format(self._bytesize)) + + stop_bits_value = None + if self._stopbits == serial.STOPBITS_ONE: + stop_bits_value = 0x00 + elif self._stopbits == serial.STOPBITS_ONE_POINT_FIVE: + stop_bits_value = 0x01 + elif self._stopbits == serial.STOPBITS_TWO: + stop_bits_value = 0x01 + else: + raise ValueError('Invalid stop bit specification: {!r}'.format(self._stopbits)) + + configuration_report = struct.pack( + '>BLBBBB', + _REPORT_GETSET_UART_CONFIG, + self._baudrate, + parity_value, + flow_control_value, + data_bits_value, + stop_bits_value) + + self._hid_handle.send_feature_report(configuration_report) + + self._hid_handle.send_feature_report( + bytes((_REPORT_GETSET_UART_ENABLE, _ENABLE_UART))) + self._update_break_state() + + @property + def in_waiting(self): + return self._read_buffer.qsize() + + def reset_input_buffer(self): + if not self.is_open: + raise PortNotOpenError() + self._hid_handle.send_feature_report( + bytes((_REPORT_SET_PURGE_FIFOS, _PURGE_RX_FIFO))) + # empty read buffer + while self._read_buffer.qsize(): + self._read_buffer.get(False) + + def reset_output_buffer(self): + if not self.is_open: + raise PortNotOpenError() + self._hid_handle.send_feature_report( + bytes((_REPORT_SET_PURGE_FIFOS, _PURGE_TX_FIFO))) + + def _update_break_state(self): + if not self._hid_handle: + raise PortNotOpenError() + + if self._break_state: + self._hid_handle.send_feature_report( + bytes((_REPORT_SET_TRANSMIT_LINE_BREAK, 0))) + else: + # Note that while AN434 states "There are no data bytes in + # the payload other than the Report ID", either hidapi or + # Linux does not seem to send the report otherwise. + self._hid_handle.send_feature_report( + bytes((_REPORT_SET_STOP_LINE_BREAK, 0))) + + def read(self, size=1): + if not self.is_open: + raise PortNotOpenError() + + data = bytearray() + try: + timeout = Timeout(self._timeout) + while len(data) < size: + if self._thread is None: + raise SerialException('connection failed (reader thread died)') + buf = self._read_buffer.get(True, timeout.time_left()) + if buf is None: + return bytes(data) + data += buf + if timeout.expired(): + break + except Queue.Empty: # -> timeout + pass + return bytes(data) + + def write(self, data): + if not self.is_open: + raise PortNotOpenError() + data = to_bytes(data) + tx_len = len(data) + while tx_len > 0: + to_be_sent = min(tx_len, 0x3F) + report = to_bytes([to_be_sent]) + data[:to_be_sent] + self._hid_handle.write(report) + + data = data[to_be_sent:] + tx_len = len(data) + + def _hid_read_loop(self): + try: + while self.is_open: + data = self._hid_handle.read(64, timeout_ms=100) + if not data: + continue + data_len = data.pop(0) + assert data_len == len(data) + self._read_buffer.put(bytearray(data)) + finally: + self._thread = None diff --git a/tools/python-embed/Lib/site-packages/serial/urlhandler/protocol_hwgrep.py b/tools/python-embed/Lib/site-packages/serial/urlhandler/protocol_hwgrep.py new file mode 100644 index 0000000..1a288c9 --- /dev/null +++ b/tools/python-embed/Lib/site-packages/serial/urlhandler/protocol_hwgrep.py @@ -0,0 +1,91 @@ +#! python +# +# This module implements a special URL handler that uses the port listing to +# find ports by searching the string descriptions. +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2011-2015 Chris Liechti +# +# SPDX-License-Identifier: BSD-3-Clause +# +# URL format: hwgrep://&