pax_global_header00006660000000000000000000000064147003264360014517gustar00rootroot0000000000000052 comment=b094466829e51f63e8e92c15c5ff3049105f2fb0 bleak-0.22.3/000077500000000000000000000000001470032643600126615ustar00rootroot00000000000000bleak-0.22.3/.editorconfig000066400000000000000000000004441470032643600153400ustar00rootroot00000000000000# http://editorconfig.org root = true [*] indent_style = space indent_size = 4 trim_trailing_whitespace = true insert_final_newline = true charset = utf-8 end_of_line = lf [*.bat] indent_style = tab end_of_line = crlf [LICENSE] insert_final_newline = false [Makefile] indent_style = tab bleak-0.22.3/.gitattributes000066400000000000000000000047201470032643600155570ustar00rootroot00000000000000############################################################################### # Set default behavior to automatically normalize line endings. ############################################################################### * text=auto ############################################################################### # Set default behavior for command prompt diff. # # This is need for earlier builds of msysgit that does not have it on by # default for csharp files. # Note: This is only used by command line ############################################################################### #*.cs diff=csharp ############################################################################### # Set the merge driver for project and solution files # # Merging from the command prompt will add diff markers to the files if there # are conflicts (Merging from VS is not affected by the settings below, in VS # the diff markers are never inserted). Diff markers may cause the following # file extensions to fail to load in VS. An alternative would be to treat # these files as binary and thus will always conflict and require user # intervention with every merge. To do so, just uncomment the entries below ############################################################################### #*.sln merge=binary #*.csproj merge=binary #*.vbproj merge=binary #*.vcxproj merge=binary #*.vcproj merge=binary #*.dbproj merge=binary #*.fsproj merge=binary #*.lsproj merge=binary #*.wixproj merge=binary #*.modelproj merge=binary #*.sqlproj merge=binary #*.wwaproj merge=binary ############################################################################### # behavior for image files # # image files are treated as binary by default. ############################################################################### *.jpg binary *.png binary *.gif binary ############################################################################### # diff behavior for common document formats # # Convert binary document formats to text before diffing them. This feature # is only available from the command line. Turn it on by uncommenting the # entries below. ############################################################################### #*.doc diff=astextplain #*.DOC diff=astextplain #*.docx diff=astextplain #*.DOCX diff=astextplain #*.dot diff=astextplain #*.DOT diff=astextplain #*.pdf diff=astextplain #*.PDF diff=astextplain #*.rtf diff=astextplain #*.RTF diff=astextplain bleak-0.22.3/.github/000077500000000000000000000000001470032643600142215ustar00rootroot00000000000000bleak-0.22.3/.github/ISSUE_TEMPLATE.md000066400000000000000000000021341470032643600167260ustar00rootroot00000000000000* bleak version: * Python version: * Operating System: * BlueZ version (`bluetoothctl -v`) in case of Linux: ### Description Describe what you were trying to get done. Tell us what happened, what went wrong, and what you expected to happen. ### What I Did It is preferable if an issue contains a [Miminal Workable Example](https://stackoverflow.com/help/minimal-reproducible-example). This will otherwise be one of the first questions you will get as a response. It is also preferable if that example is not dependent on a specific peripheral device, but can be run and reproduced with other BLE peripherals as well. ``` Paste the command(s) you ran and the output. If there was a crash, please include the traceback here as well. ``` ### Logs Include any relevant logs here. Logs are essential to understand what is going on behind the scenes. See https://bleak.readthedocs.io/en/latest/troubleshooting.html for information on how to collect debug logs. If you receive an OS error (`WinError`, `BleakDBusError`, `NSError`, etc.), Wireshark logs of Bluetooth packets are required to understand the issue! bleak-0.22.3/.github/pull_request_template.md000066400000000000000000000014521470032643600211640ustar00rootroot00000000000000Pull Request Guidelines for Bleak --------------------------------- Before you submit a pull request, check that it meets these guidelines: 1. If the pull request adds functionality, the docs should be updated. 2. Modify the `CHANGELOG.rst`, describing your changes as is specified by the guidelines in that document. 3. The pull request should work for Python 3.8+ on the following platforms: - Windows 10, version 16299 (Fall Creators Update) and greater - Linux distributions with BlueZ >= 5.43 - OS X / macOS >= 10.11 4. Squash all your commits on your PR branch, if the commits are not solving different problems and you are committing them in the same PR. In that case, consider making several PRs instead. 5. Feel free to add your name as a contributor to the `AUTHORS.rst` file! bleak-0.22.3/.github/workflows/000077500000000000000000000000001470032643600162565ustar00rootroot00000000000000bleak-0.22.3/.github/workflows/build_and_test.yml000066400000000000000000000033721470032643600217660ustar00rootroot00000000000000name: Build and Test on: push: branches: [ master, develop ] pull_request: branches: [ master, develop ] jobs: build_desktop: name: "Build and test" runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13-dev'] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 id: py with: python-version: ${{ matrix.python-version }} - name: Create virtual environment run: pipx run poetry env use '${{ steps.py.outputs.python-path }}' - name: Install dependencies # work around https://github.com/python-poetry/poetry/issues/7161 env: SYSTEM_VERSION_COMPAT: 0 run: pipx run poetry install --only main,test - name: Test with pytest run: | pipx run poetry run pytest tests --junitxml=junit/test-results-${{ matrix.os }}-${{ matrix.python-version }}.xml --cov=com --cov-report=xml --cov-report=html - name: Upload pytest test results uses: actions/upload-artifact@v4 with: name: pytest-results-${{ matrix.os }}-${{ matrix.python-version }} path: junit/test-results-${{ matrix.os }}-${{ matrix.python-version }}.xml # Use always() to always run this step to publish test results when there are test failures if: ${{ always() }} bleak-0.22.3/.github/workflows/build_android.yml000066400000000000000000000017741470032643600216110ustar00rootroot00000000000000name: Build and Test on: workflow_dispatch jobs: build_android: name: "Build Android" runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: Install dependencies run: pip install buildozer cython - name: Cache buildozer files uses: actions/cache@v4 id: buildozer-cache with: path: | ~/.buildozer examples/kivy/.buildozer key: build-cache-buildozer - name: Clean bleak recipe for cache if: steps.buildozer-cache.outputs.cache-hit == 'true' working-directory: examples/kivy run: buildozer android p4a -- clean-recipe-build --local-recipes $(pwd)/../../bleak/backends/p4android/recipes bleak - name: Build Kivy example working-directory: examples/kivy run: buildozer android debug bleak-0.22.3/.github/workflows/format_and_lint.yml000066400000000000000000000016401470032643600221420ustar00rootroot00000000000000name: Format and Lint on: push: branches: [ master, develop ] pull_request: branches: [ master, develop ] jobs: format_and_lint: name: "Format and lint" runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 - name: Install development dependencies run: pipx run poetry install --only docs,lint - name: Check import sort with isort run: pipx run poetry run isort . --check --diff - name: Check code formatting with black run: pipx run poetry run black . --check --diff - name: Lint with flake8 run: pipx run poetry run flake8 . --count --show-source --statistics - name: Build docs run: pipx run poetry run make -C docs html bleak-0.22.3/.github/workflows/pypi-publish.yml000066400000000000000000000011641470032643600214300ustar00rootroot00000000000000# This workflows will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries name: Upload Python Package on: release: types: [published] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build run: pipx run poetry build - name: Publish env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: pipx run twine upload dist/* bleak-0.22.3/.gitignore000066400000000000000000000111271470032643600146530ustar00rootroot00000000000000examples/notcommit/ .idea/ .vs/ [Bb]in/ [Oo]bj/ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ # pyenv python configuration file .python-version .idea/vcs.xml ### macOS template # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### Windows template # Windows thumbnail cache files Thumbs.db ehthumbs.db ehthumbs_vista.db # Dump file *.stackdump # Folder config file [Dd]esktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msm *.msp # Windows shortcuts *.lnk ### JetBrains template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff: .idea/**/workspace.xml .idea/**/tasks.xml .idea/dictionaries # Sensitive or high-churn files: .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.xml .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml # Gradle: .idea/**/gradle.xml .idea/**/libraries # CMake cmake-build-debug/ # Mongo Explorer plugin: .idea/**/mongoSettings.xml ## File-based project format: *.iws ## Plugin-specific files: # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties ### VisualStudioCode template .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json ### Python template # Byte-compiled / optimized / DLL files # C extensions # Distribution / packaging wheels/ MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. # Installer logs # Unit test / coverage reports *.cover # Translations # Django stuff: .static_storage/ .media/ local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation # PyBuilder # Jupyter Notebook .ipynb_checkpoints # pyenv # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ ### Linux template *~ # temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden* # KDE directory preferences .directory # Linux trash folder which might appear on any partition or disk .Trash-* # .nfs files are created when an open file is removed but is still being accessed .nfs* ### SublimeText template # Cache files for Sublime Text *.tmlanguage.cache *.tmPreferences.cache *.stTheme.cache # Workspace files are user-specific *.sublime-workspace # Project files should be checked into the repository, unless a significant # proportion of contributors will probably not be using Sublime Text # *.sublime-project # SFTP configuration file sftp-config.json # Package control specific files Package Control.last-run Package Control.ca-list Package Control.ca-bundle Package Control.system-ca-bundle Package Control.cache/ Package Control.ca-certs/ Package Control.merged-ca-bundle Package Control.user-ca-bundle oscrypto-ca-bundle.crt bh_unicode_properties.cache # Sublime-github package stores a github token in this file # https://packagecontrol.io/packages/sublime-github GitHub.sublime-settings bleak-0.22.3/.readthedocs.yml000066400000000000000000000017111470032643600157470ustar00rootroot00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-24.04 tools: python: "3.12" jobs: post_create_environment: # Install poetry # https://python-poetry.org/docs/#installing-manually - pip install poetry post_install: # Install dependencies with 'docs' dependency group # https://python-poetry.org/docs/managing-dependencies/#dependency-groups # VIRTUAL_ENV needs to be set manually for now. # See https://github.com/readthedocs/readthedocs.org/pull/11152/ - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install --only docs # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py # If using Sphinx, optionally build your docs in additional formats such as PDF formats: - pdf bleak-0.22.3/.vscode/000077500000000000000000000000001470032643600142225ustar00rootroot00000000000000bleak-0.22.3/.vscode/extensions.json000066400000000000000000000010621470032643600173130ustar00rootroot00000000000000{ // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp // List of extensions which should be recommended for users of this workspace. "recommendations": [ "ms-python.python", "ms-python.black-formatter", "ms-python.isort", "ms-python.flake8" ], // List of extensions recommended by VS Code that should not be recommended for users of this workspace. "unwantedRecommendations": [] } bleak-0.22.3/.vscode/settings.json000066400000000000000000000006561470032643600167640ustar00rootroot00000000000000{ "[python]": { "editor.defaultFormatter": "ms-python.black-formatter", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.organizeImports": "explicit" }, }, "black-formatter.importStrategy": "fromEnvironment", "isort.importStrategy": "fromEnvironment", "isort.args":["--profile", "black"], "flake8.importStrategy": "fromEnvironment" } bleak-0.22.3/AUTHORS.rst000066400000000000000000000012231470032643600145360ustar00rootroot00000000000000======= Credits ======= Development Lead ---------------- * Henrik Blidh Development Team / Collaborators -------------------------------- * David Lechner Contributors ------------ * Chad Spensky * Bernie Conrad * Jonathan Soto * Kyle J. Williams * Edward Betts * Robbe Gaeremynck * David Johansen * JP Hutchins * Bram Duvigneau Sponsors -------- * Nabu Casa bleak-0.22.3/Bleak_logo.png000066400000000000000000000330531470032643600154310ustar00rootroot00000000000000PNG  IHDR QbKGD pHYs  tIME7 ̇ IDATxyUo1  1AM^W` @A%pUpeEY2섰 {Bǩ@&SO9$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$IaDGduieJ$RP Lڲךg+=Kn[m$I$@4`1$GߕK.U$Itǒ/pTMR^$I8 ? Sh\B3$Ic2ph<ڬe02$Ic0H$Wh1B t-I$ mYF=tmpE$I 8! _'pAD$bAD$I&,xLtH&pCI$H,|8#=gzDP$R18i$I2 *YY7D$R\{y!$I0&cpZ=ݪ$I*VW.`OdI$@R ቹͫ' !.+IT"Vjcڶn۲Vlx$I 8oߑ~"Id):|TvcAC C%I CH4=j$I2> !L3H$@ !d Id1|BU!D$dֺ7!d!d Idi6ed$I2 Ch썥!d VB$hL !4pe,I$HCÛICH#DA$2dҹ !8 $I@:2<9D$26!dڲ +I$ GXH $I9744:%IdyڡWfk$I2tm$I AGtsUX$I%])NH$I2@$aCHjL$Is 0FG4IÇ!sN$EfB"D$'ц=I$Hai6! H$EВб3uB!3󦾺}%;*6ҋ/<Rǁy#>~`QMƱ 7fxPIO1]QQ;Z=ioGdזہ۲߁ǼfB # iO @_ #M끫V$IuJs6cb,QÞ 1,rQI^3T*OBbH[ 'c$DUV$IU p`I4[N7$Jĥw !ijuI^ buHTb0k?[[(hV$I 2j:zlme$uU!IRYHlB!VU!IRï !鳇N tX$/xsgIdai$I*K蝌{BaXZ[ $!xSg){2gX $@\Rʌ~lhUHjq+CHcYI.I Bg[U؛a$IJ0x3WCY9* IRzěvbfg^[ d,p IRJwUn)){4^$)CHYyj%IJ&RVm lh5HԿM !@8oߑ@;K'ɫ1=5V%,oZ`"0 :3ep8\f!(lI*CHCH* M IR XyCHmC'z@x9e|~RR5 ,{/IRH=}[0T֐)~Ib)(B*ZO/a"l`K#@7jp2JXׂʵDH$Ćr~֞חnEet[AjK'OCB M0G)WPczI߿-~o[qCJ6K؊`Ck0:M]<^w!ݒ95(`s kXX%{ 0v/dxgВ׽;>mK'B ;N/#xA厶K5ބ}vch;ُ^KL~ \ EHn)(x涬% YIwg[8mWgmӹHo !A[|U_ d3.` ӒVvC C3taE Wb^CH5.Nަ%_.X%:W4 #:pb$H%1>wvZ8 aMX% %=zTs,a8 y(aAE y2\b$Yќ|6ۀ {{ٗv^+M8+ 9@*!ѐ66XI1 ,W~OgX N) IqVCt"973w!\!a)˟(",W[WGH$t*Oe|0_ql$Gt3TXCC5UcVJئL0W!N|6B y7->V>k5t`Ďi;"W̹o? _ !m^FJc3:!?7{Y Ey"dz0 g hkœcgĜ_Vuq{#9>T#k`<eU{_Ds-Fe߁MmAA;rQ+a<{z}`u@j#f1|H o`}0[BF+fnw)d4aN\2}aC@B-욅7"LVY)π6A.jgN9a4@`3i>JNjAʫ+j:Hg7԰'CXja(xp|7n&l24$¸VAXx{eb}<MXF]%{moJ;cd$aq~݇mp/.N{50zGG2yzjo)cWvNs# *༈}>:}0?6\FXaBc(iwN5x@S-~X5acƏVyH1&|(1'>l5vx|N]_# M8hzzFp?  v/r}BwZ8aF, @MZwC3}~⍺I7؅eOBNrV21KB蝸.2"a=cFgG.s;/*pUݜtH7WR%z jQф}:01{mFqy f;l\Ĝs8mpHv>ٛwab0=o( ,틄XCk(v"B "#>He]NP<{Ӷ%lL1VlZLЖ7@HM20(? lgbzG+!X>LX%A"7;0/_ȹ<&n.nMΉXd`Gz Zo1x̩llx㇭ܬBD>3wr.4wR$-BݜHTxҘ疪m#_~kU@ !kjZZ90 "W"L[/a/)ڔdnND5x+ލ6rЯUj$ ק7|H˘0-R9Wi`=\s͜c%F s|y(Eip%n 2.$;msr~fbg5zW8HqmJ~o^rُݣVG4S<70hny?G^ٹkRz^1b> Ç͝!@oY7g 2 ؽI5X0@ 26~oTΫ$qvwVu BH>$'LH,Ӫ{uoryu[`-&S@WZ`#“FkNswr/o1>\=A<)}#6-"pks~D}VZ2 m{# >x\"aGj!!Qg$=R9/A2!Rõ0T뎒^X|熵xRI4L$^ߌ>Tk $ T{HϪh-"&-Q<~ TNz@&ɹ"}1t[7HZ7aN3LXV~b$/ijYG}s9{ZU-@*roXR9L^JT/O@ 5_7yjU)R7A j{՘= KT/1yyw#w'c..@pF)Sy!B{yV Dz/n6[,kF؈orUz!"[&'N~v,t 3,+>5e}ŠvkfVj`z! }Z#0{@.$vӁf"Bci= 0ҞtTv(nnTwn*R#Wap¼pXϰ) 'mRb3“*̡[5s gpP~eg1 o4" i PkTx% y1e3N@G|8Ŧ9 ?ʮЍ שٻֳ lK|p3p9YB/ᣜ~LÏÇMj"`kࡂ80\ԭ(\B+`2!6ksT#t>nQ^l cd h}8x!{S:w𾄎gMRbɍ! \qKъR2 x?0'{aT7?̈́Md, [pn=eo%Sm\sMVTjdmr6Ԅq *mz1VA-B?.FXX% p':i.r3M`IT= '9V–lL(glw+3JԍԀ/T!9<{@NU K@*?,%ftkn`0ƞ: *{u2&6F>cr~7&,qC8[PZ('Z5"!č K@ CH<鷢z3 *8R60jUpqcœ9ƭc#.p!5X1&xZdiOE6s7ga嫳:\ˋ|`:]IG2l^FJ 2?l U!zb!_7sr ].݆9dc»oX&8R3WTnV2ZkFODbBAgAhg5y?@@a]/X X .*!m'rau&_˾;"bayѻߘݬn70Qz_9f$OƄ _nRzFY{qNڝ |!Uŵ@ ۽r.ߍ > ! joRzTV"ڀTvtvTIAVKƄ%pÇ!dHGߍZo$t`ՂnPupJBOH6cs'rÇ!![. V}!=Gk;`}U~vw -&ɛ(t2 =U ube#7[x}_LdIDAT-6$m0 l/!p#4V0hӾ1!u۱6O9Hǟo9|BTq@ 7/bYZݥ!; k";c28t4 uz *w %D,k5iAnj2"l& hK5w+=t)w>> !15+7E1{@Z8I/O4h o>Ƅ% I> !%0֪/$mu7!;@c~zy俢j6Y;n0Hq^[Lq@!cO# ܓ6|BGe_PL>/ώp;Ƅ &>]ePUlUP,e&MQH| mN}!q1a$.C jj *~`u7lKs=e38>8pcBAÇIJѦVjʾ/,nk#wsBcLޜˉ1}$av%@fAB 5 RrV2b24.փ1GF{rܘ0̏lCᣖjZ`seJ.Yl <@'wLm6ˊ :p:t1%xImmKG%5$컵]~zKR7;{]uRq1%ME9) -%n.:Һ7>j>M?mW&R({4"~ɒ!K&).WNNWc~H-x=OJXڇpHUc6+1uUSo> |Ju# mͪW47&L4$y6b0|ݴ{/m_g@v/Il@sV$@RֲTnPΎ.Vwz$nÇ#0!YF_wD*k/ХԽ=q.2&@v QyqcH7mÇ aZ`O6 ])DE(c(d\Ͽjbs)xrܘ06mKzʊBN/~肏qJ]Z\ƭC/Duj7T/ [Q1ab$O2!G?aR^;K8,Z?o&~cw0ay>rlP{P`9;R9!] YB{ hIc8e6JF6PX2.M5_;3sunJ,]>&|EÇᣴ簖7TXܛ8::؛WIny6r=l@[ lhW5SXJvJҋ!vu)Z"t QaXŋv|Akmq8[ ]̋Px@;Dm1|<c[[6͠?T}wF񾸁soXX8%s2|D*ˍ  >A6|U^W'vli #7&GgD(:[VU1Q\`eAUIJn>I.cfy,#kQvc(pay+IA e WEU{7Xks=FXHXn8΃7P&=y^$ަ V{U,|5㬆a3_Xu޹|?t.|7?t Z/M}j{y[8N!t҆r14QN'F!3-ڄEA¨,;:^"|^ ꁈѸr$8ߪ7|DW0tLwa9Z {4¦hi9ٍp,\3h ! 2t&1aB10,/89> [i˂χczo ^g0QWd9%VtTF`M0xZ1$󛑤kp^ AܥyW0B`,tz:&eyM|=3h0z 2 e˾ k&oGXw?"`k(g^383,q0v!-Sd:pMPRE{v~3s%n]q4k]s8/n%,]WnA[5Dzo|]ȺYe4Ri,la\8JCwGҜy ut,nr.?TTܘ0jX5?aUQaP ‚y>q}oQ^5}aE,Ql6#W[XQ\C&TWρs|jX%\˹VO&  >a6|# ]kUD%ŠR]B݄@ّY7&L 3mGbzho1k0!,Ѫ0QTwQ u!a5@{=su>aT nLH= K]5wo* 0fU &+b7PM7n\{cW{j窇e  Eko=  qnr=HsNE5US۴wc i8'ke lb!ە[,aѬ+ Ou]汜a湛CYwv6e&K<|Hg](aN O3>*|kce0x';m}-< L>ϲ+=/V(:eHm'IxP;?pدx~DA ;{A `>>V,y*)Q~};eߣA!xڹ{: !1% Q%y]Ql(Ywm@V~){bIࣄM wcv!abaW~9LxnSLKXAWa!J7T3] 8z_V.I09׍I~TY1*>4s1Qc~NMC{K Mp{a}i| _hbrc‘  bޏyV``3njg9; s=ʼ,g=fqsǺ_h~B{,F 7&QuH=o~@6|E{K/Ul>jFK^/d7K^Og? gG$ 7$I2 I8!1*z$IdC4|4bF$I !] 1Dhoq$I2i> 1$I2 3t|a{$I24TnX=!IdiT{*NJ7|Z $Ifn`: LA$Gk1|>$I ÇC$I 5 !Ç$Ib0|H$i#*:zۀˀUXӶaX֒}>fY$ICH+p\ITڕ$I2DNԕҙw8$I2$B'w-3I$H!- !SlR!ztZ$I2 R&r%Id)eiސ2fʕ$I*Adp:$|{=$I !U1$aף۪$I2T9L2 $sI$H]#b$I$RY24P id.Id+ȴ,L28rI$"mY``&0+I$@#00 YF7an!I$HHax~?8y=t^$I q,lKZIO8nG.I$HZdJD/JSIRuKM%Id)u0Wv`wH"f^uR$I + $ I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$I$IzYZ!cmnIENDB`bleak-0.22.3/Bleak_logo2.png000066400000000000000000000422361470032643600155160ustar00rootroot00000000000000PNG  IHDRo>zTXtRaw profile type exifxmP[ )v8ten/@J5K!nj$KN!Ed7QYĻN%ZW z L[j.FicO1|3w58WGf?lub:܎<uB?$T B eq(I9` JYc?V=iCCPICC profilex}=H@_[KT ␡:EEkP! :\MGbYWWAqvpRtZxp܏wwT'edRI!_B#!#.1S4<=||,s>`2''nX3y8ʒB|N w"bKGDC pHYs  tIME 9]_ IDATxw|TUNz2$ Б*͆b]EYVE"**R􇸲Y" G2RoǯU~y#k?\hccciԨc!-Db0\:u廄ߔ~P<<}hܟk0䐖ƫʐ!Cxزe 2^"[8t\::K_p{QTT$V߄CH;uý!USO1}tΞ=+Xğ3dOǷyE5`̙kΐHE8O yR<T/ 1k,>C***dgH$.Blަ3x ~HnW]țoɿoL%,pX5XrV.*6x+Fnn.*9993$_|Ao=LFOGN%9%gyf +x >n๠;|~T ј~JL}  \BAR0`BB]B)//cǎk.Gvve`LBB|%_M9 S `@´<hqD# sWKtt4}!03M4gϒO?ҥKtlժU+D"0N?qkT*0}UfvWAQ#FڶZw\PZ[˖d2HLL =z_oaX9ʗ_~)s%_b>>yw4oPvDW)Upsȴix뭷X/@y/bׄDb8ohxe#<,,Çs9.dddi_Y")8zZs{9ѐqa?ל1[oؼyėCv}_>={qD"puܹipJ̹ʢ!s&m( ;viӦ/))!//O>Gy%vҩX)heu"+g `Æ IMMumii\qH$$%"'c,NZUGr(p2/_ 4Loޡ;wN>q*0 F<@LvșM<@cF 8TYTPWN2RF9Զ*!V2Ξ={^$+**BHhh(uԡnݺDEEa2 iVb (..ܹsTUUaZAݺu1͘L&L&3E8Q a =,#gjW ؽ{7۶mc۶mQTTDaa9Lhh(DEEѲeKz 7@RRf0"R]]ͩSdlݺLJJJ(--D(y^<6mʍ7u]Gbb"111veueVc}3..2dD0h 7~a@e/^̖-[ʺjܚz,[ µ^˰aׯ7!())!==+W?ؕ _Ί+0ʹhт[oAqwC^^?<}vJU?~< BpJ8NaHa$8/Є hv~zΝˆ >e͚5Y:Os뭷{:w7n>`Y ##>;3G}$ƪO>? /@="6pnrf7̍&{kBPTNDDS\޾Ç3e|AV\r4ƌٷo `YVvO?=]&ܹs:t(_~%~,_ٳgf;֣(DžConM0hTո@9\)""z=o۶QF1k,W,}17b0e˖1tP>cI믌;7xcz:uCRRSLaÆN㠛k{C<**oQ]]j-WGFͺu+̞=͛7zK/u]kuX8Ku2<)b\(jbB ['Gի:!FXiO<^17׻f&N֕=̞=w}2ߪlZYt)~mƎm?ZΊb]*͌+Yq!yLfRSS}۷3~zL[<&L/{UVV3k,[o3nBxW2dcƌGO\YE S8#ͯi[آaʊ_q>#&''Ӷm[Ç3i$4.&==ӧsi ޏ?kf/̜9Ǐĸ8qӦM{2ɓe,WB/tսzϳ4c eΝK޽uŋٶm{6lGn <p*volE̚5KvC=… 뮻d2*Ю];L‚ HHH3Xp!pfee`۬Y3g={N:' uL0y8#G ;.~Nj{x )p.O#d{V< *|3jRPPƍ?~<#Gtxbȑ 4'VVoFg 7̜9s׵eg]3u^xƌ}:((;3>++lC3gk}ޕɓ6;rcݶ2hxٓĖ͵E?ݻYjwesL0ኁѣ|$%%??e VU>}O&''+Wruٕ%K‡~ء怀 ’%Kqq!4M3$ xb^oX6mA|>;h ޶m}QYV***(//efƌ>Eiׯ'##CWoi: {oѣG%VٺV1cP, {TWW&CxL<#jl;4:shwQ]$''3w\^y JKKfذa.VU.]}P^\\֭[D4q(wqK5o\]YYa*D2k,Drr2&MA˰ՀhZy4@BBÇ{e˖>χM6򤊍otYzMұcG~ƍwkEQ߿?m۶رc:t_~KnMӮ];6\ĸsQ~}Njݗ?s^oX>}:~WBYرcӧ-Z"MEEE&..-[gϞ,ZȮrر+ t| iTWW/Ǚ3g~رc4i5#ې{=ݺm۶1c 'Ǐg0eŌrxb$5ѧG Z0bZ^;deeٳKTU%>>l㽣(8BBB~Ŧiyqq+;FeeWs1])E?LPPaǷ_ GPhR_a ;vyf6oLBBr C !%%ŧʃ !8xnod06lHtt3݃ 3eEQta{[8JKK3g?]OIIaĉ^Z돢\nQtքؾ*SBo}>ncn+@jV+;vнjڴKFFFtCg]TVVFUU+xz-5\c1pШ^(QN8qH0̝;ロ$lܸQWh2t Ǿ} ]G;KFFӧO{Cѷo_pq "S4xw2ydç-pDFFZBBBtsΑoؾջ"+.. *77W^yECqjѸX<@cA<¼ܹ]qJyy9EEE̙3SN1c P>}Zp٘d%EEExom6g"ÑJO;YTTTG_j~zvɍ7+ GhusάY.[*z>*??'N믿zj㔐,] fΜILLdnn+,,d.U iTUUϙ3gٳ呗G~~>QXXHYYеyR8lyͲeر=|^8-F^yxD@@DGGӼysn رcٷoVO?u8CEhҤ SL!<<0}%emdffr-x,YCZRR‘#G$##[r)(,,4T@g9p/پ;FMvS8B@Dp-&0RRRHNNnc̙v]IMM宻2T;#ػpJKKٻw/7no.ٮ4*B 9s&OVV+W$))e%A%hI<Dx>[À={6wuhv.] ]^Y4s(yX< {UawV]oE{bbbx* 'p)܏QzJ83g?˖-3 {YhK<6&ƐY4'Geقv v7#/55ѣG;6==?mURR߅ϥ\NT+ G31ڹs'[l1l6#"T!CQѰe(뫭#Ls>T}ݺje_ȏ?a{?77)S8t>uBBBtЁΝ;ӡC^z &0|4g6 0yX:+zT34@LvȪ']qnݚ} 5Ϟ=k#hYf9]0ʹhт^zѩS'7oNtt4u!44@L&B(++c>՟㩧l6s 7?nφ tX8 *pcIOjG!$00~9dN<ɑ#G !Fg\R2%]@j_؝Rѿ FǎX,L+:~;_I&!#:X~rӪat5Ax(~($$$+m{Ȏ;5gЛ 11n'ڵzˡCp[9ԁRNߕ/h1yJ _?ѰaC _|СC:ޝEG)ĸ(VEVR4j & W4lڴhٲ%M41=jՊMji&N>ػw/Ge۶-7ߑ~yyK#==q#9L&+]|FFk׮j@=n*|x|X5 ?B2224iSinvrDFqM7j?2CW]]իu(vmA t(yٳg6pΝc…~[Y011^zj駟z5 i0̵U&`]_JBlذm۶9>tPem3I=ƶK,ȑ#,]TW?oy޽{!6lpȵm};d]ceǎ^ tɓ֪Bi*p@㉆Yk*~$deeo2l0DW^mP( iFW 68mYlvJV.:u8TgdڵycaƌN]8ESNƊ]bZ q\W\O˥۷3o<]x^vUQ^=c߾}ر!!,**b٬[Ω64hСCuYn"] [(w\Lj ]^=!Bz=8}4k֬V곢?Ν;ٿ?.ݫ;]*L& UjN83gބ|8ÇwihԨ'Ndy4i};woqƄQUUӧIOOgݺul3gy.AHLL[n,_ܡ~?>Vk׮у)//ìZ;v\bccytĊCQ:t@rr27o7]ԙTqnWI4h'X[nL2 |#*RQn&Əs={dee_|k=z4z˨3|pV^9>-Z1cHMMe۶m򍒊ܮ xzw}8bSo6]TӡC~˺ Fͣ>jk[y'P޽{ӿ\Q $00PWLIQQ7ȪJ޽/[vrx: ХÒyC44f j\G}Djj.72esμL/r%0an$*)SԜ":{Ԥ0`6tp*6Ѱ?GgW^'99'E{y9r׮[n̙3$ڧ2eR%>>3fr{ %FM~_Y< ֱ w5D{$\J6yꩧX|9&LCo_iӦ̘1ɓ'{^w{Ԫ- C/5k }xxϣGRUUSc$%%T]m<r0b}8E7Eì >̌W^;,_3gҾ}K&e4i1)))_Z,L2{v9j G孷ҕ^:t~Ϲ]QZZTo{K c2&+ 0CvPbEdd$;wcǎ$%%ѲeKfP.RSSYh˫fnfqvJppKȑnݚ^{ͩDCOҪUK \DDM4EII gϞaÆ>36TUG$$$  KTHÖ~S_}(V+ݻ]vaA dn5-(G5LlQD⋘T7*3ְJE^+Ee0hOTUz&D"*gVQwIoTyZfFI-,GD"RUU l'UzA<&DFQ(H| @(7oW 17sVVa0+'QR4$oQ|(UBD;JXEcAyH['YaM*09%qHU6/S#==Tܦ͆tswE#9>R4$O"j5.Hn(H`_, and this project adheres to `Semantic Versioning `_. `Unreleased`_ ============= `0.22.3`_ (2024-10-05) ====================== Changed ------- * Don't change ctypes' global state ``bleak.backends.winrt.util``. * Improved performance of BlueZ backend when there are many adapters. * Added support for Python 3.13. `0.22.2`_ (2024-06-01) ====================== Changed ------- * Retrieve the BLE address required by ``BleakClientWinRT`` from scan response if advertising is None (WinRT). * Changed type hint for ``adv`` attribute of ``bleak.backends.winrt.scanner._RawAdvData``. * ``BleakGATTCharacteristic.max_write_without_response_size`` is now dynamic. Fixed ----- * Fixed ``discovered_devices_and_advertisement_data`` returning devices that should be filtered out by service UUIDs. Fixes #1576. * Fixed a ``Descriptor None was not found!`` exception occurring in ``start_notify()`` on Android. Fixes #823. * Fixed exception raised when starting ``BleakScanner`` while running in a Windows GUI app. `0.22.1`_ (2024-05-07) ====================== Added ----- * Added ``bleak.backends.winrt.util.allow_sta()`` method to allow integration with graphical user interfaces on Windows. Fixes #1565. `0.22.0`_ (2024-05-04) ====================== Added ----- * Added ``BleakCharacteristicNotFoundError`` which is raised if a device does not support a characteristic. * Added utility function to work around ``pywin32`` setting threading model to STA on Windows. Changed ------- * Updated PyObjC dependency on macOS to v10.x. * Updated missing Bluetooth SIG characteristics and service UUIDs. * Updated ``BlueZManager`` to remove empty interfaces from `_properties` during InterfacesRemoved message. * Updated PyWinRT dependency to v2. Fixes #1529. * Raise exception when trying to scan while in a single-treaded apartment (STA) on Windows. Fixes #1132. Fixed ----- * Fixed BlueZ version in passive scanning error message. Fixes #1433. * Fixed mypy requiring ``Unpack[ExtraArgs]`` that were intended to be optional. Fixes #1487. * Fixed ``KeyError`` in BlueZ ``is_connected()`` and ``get_global_bluez_manager()`` when device is not present. Fixes #1507. * Fixed BlueZ ``_wait_removed`` completion on invalid object path. Fixes #1489. * Fixed rare unhandled exception when scanning on macOS when using ``use_bdaddr``. Fixes #1523. * Fixed scanning silently failing on Windows when Bluetooth is off. Fixes #1535. * Fixed using wrong value for ``tx_power`` in Android backend. Fixes #1532. * Fixed 4-character UUIDs not working on ``BleakClient.*_gatt_char`` methods. Fixes #1498. * Fixed race condition with getting max PDU size on Windows. Fixes #1497. [REVERTED in v0.22.2] * Fixed filtering advertisement data by service UUID when multiple apps are scanning. Fixes #1534. `0.21.1`_ (2023-09-08) ====================== Changed ------- * Changed ``dbus-fast`` dependency to include v2.x. Fixes #1412. `0.21.0`_ (2023-09-02) ====================== Added ----- * Added ``bleak.uuids.normalize_uuid_16()`` function. * Added ``bleak.uuids.normalize_uuid_32()`` function. * Added ``advertisement_data()`` async iterator method to ``BleakScanner``. Merged #1361. * Added type hints for kwargs on ``BleakScanner`` class methods. * Added support for Python 3.12. Changed ------- * Improved error messages when failing to get services in WinRT backend. * Improved error messages with enum values in WinRT backend. Fixes #1284. * Scanner backends modified to allow multiple advertisement callbacks. Merged #1367. * Changed default handling of the ``response`` argument in ``BleakClient.write_gatt_char``. Fixes #909. * Bleak recipe now automatically installs bleak from GitHub release in Kivy example. * Changed `BlueZManager` methods to raise `BleakError` when device is not in BlueZ. * Optimized BlueZ backend device watchers and condition callbacks to avoid linear searches. * Changed type hint for buffer protocol to ``collections.abc.Buffer``. Fixed ----- * Fixed handling all access denied errors when enumerating characteristics on Windows. Fixes #1291. * Added support for 32bit UUIDs. Fixes #1314. * Fixed typing for ``BaseBleakScanner`` detection callback. * Fixed possible crash in ``_stopped_handler()`` in WinRT backend. Fixes #1330. * Reduced expensive logging in the BlueZ backend. Merged #1376. * Fixed race condition with ``"InterfaceRemoved"`` when getting services in BlueZ backend. * Fixed missing permissions and requirements in android Kivy example. Fixes #1184. * Fixed WinRT backend sometimes hanging forever when a device goes out of range during connection. Fixes #1359. Removed ------- Dropped support for Python 3.7. `0.20.2`_ (2023-04-19) ====================== Fixed ----- * Fixed ``org.bluez.Error.InProgress`` in characteristic and descriptor read and write methods in BlueZ backend. * Fixed ``OSError: [WinError -2147483629] The object has been closed`` when connecting on Windows. Fixes #1280. `0.20.1`_ (2023-03-24) ====================== Fixed ----- * Fixed possible garbage collection of running async callback from ``BleakClient.start_notify()``. * Fixed possible garbage collection of running async callback from ``BleakScanner(detection_callback=)``. * Fixed possible garbage collection of disconnect monitor in BlueZ backend. Fixed #1258. `0.20.0`_ (2023-03-17) ====================== Added ----- * Added ``BLEAK_DBUS_AUTH_UID`` environment variable for hardcoding D-Bus UID. Merged #1182. * Added return type ``None`` to some scanner methods. * Added optional hack to use Bluetooth address instead of UUID on macOS. Merged #1073. * Added ``BleakScanner.find_device_by_name()`` class method. * Added optional command line argument to use debug log level to all applicable examples. * Added ``bleak.uuids.normalize_uuid_str()`` function. * Added optional ``services`` argument to ``BleakClient()`` to filter services. Merged #654. * Added automatic retry on ``le-connection-abort-by-local`` in BlueZ backend. Fixes #1220. Changed ------- * Dropped ``async-timeout`` dependency on Python >= 3.11. * Deprecated ``BLEDevice.rssi`` and ``BLEDevice.metadata``. Fixes #1025. * ``BLEDevice`` now uses ``__slots__`` to reduce memory usage. Merged #1117. * ``BaseBleakClient.services`` is now ``None`` instead of empty service collection until services are discovered. * Include thread name in ``BLEAK_LOGGING`` output. Merged #1144. * Updated PyObjC dependency on macOS to v9.x. Fixed ----- * Fixed invalid UTF-8 in ``uuids.uuid16_dict``. * Fixed ``AttributeError`` in ``_ensure_success`` in WinRT backend. * Fixed ``BleakScanner.stop()`` can raise ``BleakDBusError`` with ``org.bluez.Error.NotReady`` in BlueZ backend. * Fixed ``BleakScanner.stop()`` hanging in WinRT backend when Bluetooth is disabled. * Fixed leaking services when ``get_services()`` is cancelled in WinRT backend. * Fixed disconnect monitor task not always cancelled on the BlueZ client. Merged #1159. * Fixed WinRT scanner never calling ``detection_callback`` when a device does not send a scan response. Fixes #1211. * Fixed ``BLEDevice`` name sometimes incorrectly ``None``. * Fixed unhandled exception in ``CentralManagerDelegate`` destructor on macOS. Fixes #1219. * Fixed object passed to ``disconnected_callback`` is not ``BleakClient``. Fixes #1200. `0.19.5`_ (2022-11-19) ====================== Fixed ----- * Fixed more issues with getting services in WinRT backend. `0.19.4`_ (2022-11-06) ====================== Fixed ----- * Fixed ``TypeError`` in WinRT backend introduced in v0.19.3. `0.19.3`_ (2022-11-06) ====================== Fixed ----- * Fixed ``TimeoutError`` when connecting to certain devices with WinRT backend. Fixes #604. `0.19.2`_ (2022-11-06) ====================== Fixed ------ * Fixed crash when getting services in WinRT backend in Python 3.11. Fixes #1112. * Fixed cache mode when retrying get services in WinRT backend. Merged #1102. * Fixed ``KeyError`` crash in BlueZ backend when removing non-existent property. Fixes #1107. `0.19.1`_ (2022-10-29) ====================== Fixed ----- * Fixed crash in Android backend introduced in v0.19.0. Fixes #1085. * Fixed service discovery blocking forever if device disconnects in BlueZ backend. Merged #1092. * Fixed ``AttributeError`` crash when scanning on Windows builds < 19041. Fixes #1094. `0.19.0`_ (2022-10-13) ====================== Added ----- * Added support for Python 3.11. Merged #990. * Added better error message for Bluetooth not authorized on macOS. Merged #1033. * Added ``BleakDeviceNotFoundError`` which should is raised if a device can not be found by ``connect``, ``pair`` and ``unpair``. Merged #1022. * Added ``rssi`` attribute to ``AdvertisementData``. Merged #1047. * Added ``BleakScanner.discovered_devices_and_advertisement_data`` property. Merged #1047. * Added ``return_adv`` argument to ``BleakScanner.discover`` method. Merged #1047. * Added ``BleakClient.unpair()`` implementation for BlueZ backend. Merged #1067. Changed ------- * Changed ``AdvertisementData`` to a named tuple. Merged #1047. * A faster ``unpack_variants`` is now provided by dbus-fast. Merged #1055. Fixed ----- * On BlueZ, support creating additional instances running on a different event loops (i.e. multiple pytest-asyncio cases). Merged #1034. * Fixed unhandled exception in ``max_pdu_size_changed_handler`` in WinRT backend. Fixes #1039. * Fixed stale services in WinRT backend causing ``WinError -2147483629``. Fixes #1061. Removed ------- Removed ``bleak.__version__``. Use ``importlib.metadata.version('bleak')`` instead. `0.18.1`_ (2022-09-25) ====================== Fixed ----- * Reverted unintentional breaking parameter name changes. Fixes #1028. `0.18.0`_ (2022-09-23) ====================== Changed ------- * Relaxed ``async-timeout`` dependency version to support different installations. Merged #1009. * ``BleakClient.unpair()`` in WinRT backend can be called without being connected first. Merged #1012. * Use relative imports internally. Merged #1007. * ``BleakScanner`` and ``BleakClient`` are now concrete classes. Fixes #582. * Deprecated ``BleakScanner.register_detection_callback()``. * Deprecated ``BleakScanner.set_scanning_filter()``. * Deprecated ``BleakClient.set_disconnected_callback()``. * Deprecated ``BleakClient.get_services()``. * Refactored common code in ``BleakClient.start_notify()``. * (BREAKING) Changed notification callback argument from ``int`` to ``BleakGattCharacteristic``. Fixes #759. Fixed ----- * Fixed ``tx_power`` not included in ``AdvertisementData.__repr__`` when 0. Merged #1017. `0.17.0`_ (2022-09-12) ====================== Added ----- * ``AdvertisementData`` class now has an attribute ``tx_power``. Merged #987. Changed ------- * ``BleakClient`` methods now raise ``BleakError`` if called when not connected in WinRT backend. Merged #973. * Extended disconnect timeout to 120 seconds in WinRT backend. Fixes #807. * Changed version check for BlueZ battery workaround to exclude versions >= 5.55. Merged #976. * Use Poetry for build system and dependencies. Merged #978. * The BlueZ D-Bus backend implements a services cache between connections to significancy improve reconnect performance. To use the cache, call ``connect`` and ``get_services`` with the ``dangerous_use_bleak_cache`` argument to avoid services being resolved again. Merged #923. * The BlueZ D-Bus backend now uses ``dbus-fast`` package instead of ``dbus-next`` which significantly improves performance. Merged #988. * The BlueZ D-Bus backend will not avoid trying to connect to devices that are already connected. Fixes #992. * Updated logging to lazy version and replaced format by f-string for ``BleakClientWinRT``. #1000. * Added deprecation warning to ``discover()`` method. Merged #1005. * BlueZ adapter is chosen dynamically if not provided, instead of using hardcoded "hci0". Fixes #513. Fixed ----- * Fixed wrong error message for BlueZ "Operation failed with ATT error". Merged #975. * Fixed possible ``AttributeError`` when enabling notifications for battery service in BlueZ backend. Merged #976. * Fixed use of wrong enum in unpair function of WinRT backend. Merged #986. * Fixed inconsistent return types for ``properties`` and ``descriptors`` properties of ``BleakGATTCharacteristic``. Merged #989. * Handle device being removed before ``GetManagedObjects`` returns in BlueZ backend. Fixes #996. * Fixed crash in ``max_pdu_size_changed_handler`` in WinRT backend. Fixes #998. * Fixes a race in the BlueZ D-Bus backend where the disconnect monitor would be removed before it could be awaited. Merged #999. Removed ------- * Removed ``BLEDeviceCoreBluetooth`` type from CoreBluetooth backend. Merged #977. `0.16.0`_ (2022-08-31) ====================== Added ----- * Added ``BleakGattCharacteristic.max_write_without_response_size`` property. Fixes #738. Fixed ----- * Fixed regression in v0.15 where devices removed from BlueZ while scanning were still listed in ``BleakScanner.discovered_devices``. Fixes #942. * Fixed possible bad connection state in BlueZ backend. Fixes #951. Changed ------- * Made BlueZ D-Bus signal callback logging lazy to improve performance. Merged #912. * Switch to using ``async_timeout`` instead of ``asyncio.wait_for for performance``. Merged #916. * Improved performance of ``BlueZManager.get_services()``. Fixes #927. Removed ------- * Removed explicit inheritance from object in class declarations. Merged #922. * Removed first seen filter in ``BleakScanner`` detection callbacks on BlueZ backend. Merged #964. `0.15.1`_ (2022-08-03) ====================== Fixed ----- * The global BlueZ manager now disconnects correctly on exception. Merged #918. * Handle the race in the BlueZ D-Bus backend where the device disconnects during the connection process which presented as ``Failed to cancel connection``. Merged #919. * Ensure the BlueZ D-Bus scanner can reconnect after DBus disconnection. Merged #920. * Adjust default timeout for ``read_gatt_char()`` with CoreBluetooth to 20s. Fixes #926. `0.15.0`_ (2022-07-29) ====================== Added ----- * Added new ``assigned_numbers`` module and ``AdvertisementDataType`` enum. * Added new ``bluez`` kwarg to ``BleakScanner`` in BlueZ backend. * Added support for passive scanning in the BlueZ backend. Fixes #606. * Added option to use cached services, characteristics and descriptors in WinRT backend. Fixes #686. * Added ``PendingDeprecationWarning`` to use of ``address_type`` as keyword argument. It will be moved into the ``winrt`` keyword instead according to #623. * Added better error message when adapter is not present in BlueZ backend. Fixes #889. Changed ------- * Add ``py.typed`` file so mypy discovers Bleak's type annotations. * UUID descriptions updated to 2022-03-16 assigned numbers document. * Replace use of deprecated ``asyncio.get_event_loop()`` in Android backend. * Adjust default timeout for ``read_gatt_char()`` with CoreBluetooth to 10s. Merged #891. * ``BleakScanner()`` args ``detection_callback`` and ``service_uuids`` are no longer keyword-only. * ``BleakScanner()`` arg ``scanning_mode`` is no longer Windows-only and is no longer keyword-only. * All ``BleakScanner()`` instances in BlueZ backend now use common D-Bus object manager. * Deprecated ``filters`` kwarg in ``BleakScanner`` in BlueZ backend. * BlueZ version is now checked on first connection instead of import to avoid import side effects. Merged #907. Fixed ----- * Documentation fixes. * On empty characteristic description from WinRT, use the lookup table instead of returning empty string. * Fixed detection of first advertisement in BlueZ backend. Merged #903. * Fixed performance issues in BlueZ backend caused by calling "GetManagedObjects" each time a ``BleakScanner`` scans or ``BleakClient`` is connected. Fixes #500. * Fixed not handling "InterfacesRemoved" in ``BleakClient`` in BlueZ backend. Fixes #882. * Fixed leaking D-Bus socket file descriptors in BlueZ backend. Fixes #805. Removed ------- * Removed fallback to call "ConnectDevice" when "Connect" fails in Bluez backend. Fixes #806. `0.14.3`_ (2022-04-29) ====================== Changed ------- * Suppress macOS 12 scanner bug error message for macOS 12.3 and higher. Fixes #720. * Added filters ``Discoverable`` and ``Pattern`` to BlueZ D-Bus scanner. Fixes #790. Fixed ----- * Fixed reading the battery level returns a zero-filled ``bytearray`` on BlueZ >= 5.48. Fixes #750. * Fixed unpairing does not work on windows with WinRT. Fixes #699 * Fixed leak of ``_disconnect_futures`` in ``CentralManagerDelegate``. * Fixed callback not removed from ``_disconnect_callbacks`` on disconnect in ``CentralManagerDelegate``. `0.14.2`_ (2022-01-26) ====================== Changed ------- * Updated ``bleak-winrt`` dependency to v1.1.1. Fixes #741. Fixed ----- * Fixed ``name`` is ``'Unknown'`` in WinRT backend. Fixes #736. `0.14.1`_ (2022-01-12) ====================== Fixed ----- * Fixed ``AttributeError`` when passing ``BLEDevice`` to ``BleakClient`` constructor on WinRT backend. Fixes #731. `0.14.0`_ (2022-01-10) ====================== Added ----- * Added ``service_uuids`` kwarg to ``BleakScanner``. This can be used to work around issue of scanning not working on macOS 12. Fixes #230. Works around #635. * Added UUIDs for LEGO Powered Up Smart Hubs. Changed ------- * Changed WinRT backend to use GATT session status instead of actual device connection status. * Changed handling of scan response data on WinRT backend. Advertising data and scan response data is now combined in callbacks like other platforms. * Updated ``bleak-winrt`` dependency to v1.1.0. Fixes #698. Fixed ----- * Fixed ``InvalidStateError`` in CoreBluetooth backend when read and notification of the same characteristic are used. Fixes #675. * Fixed reading a characteristic on CoreBluetooth backend also triggers notification callback. * Fixed in Linux, scanner callback not setting metadata parameters. Merged #715. `0.13.0`_ (2021-10-20) ====================== Added ----- * Allow 16-bit UUID string arguments to ``get_service()`` and ``get_characteristic()``. * Added ``register_uuids()`` to augment the uuid-to-description mapping. * Added support for Python 3.10. * Added ``force_indicate`` keyword argument for WinRT backend client's ``start_notify`` method. Fixes #526. * Added python-for-android backend. Changed ------- * Changed from ``winrt`` dependency to ``bleak-winrt``. * Improved error when connecting to device fails in WinRT backend. Fixes #647. * Changed examples to use ``asyncio.run()``. * Changed the default notify method for the WinRT backend from ``Indicate`` to ``Notify``. * Refactored GATT error handling in WinRT backend. * Changed Windows Bluetooth packet capture instructions. Fixes #653. * Replaced usage of deprecated ``@abc.abstractproperty``. * Use ``asyncio.get_running_loop()`` instead of ``asyncio.get_event_loop()``. * Changed "service is already present" exception to logged error in BlueZ backend. Merged #622. Removed ------- * Removed ``dotnet`` backend. * Dropped support for Python 3.6. * Removed ``use_cached`` kwarg from ``BleakClient`` ``connect()`` and ``get_services()`` methods. Fixes #646. Fixed ----- * Fixed unused timeout in the implementation of BleakScanner's ``find_device_by_address()`` function. * Fixed BleakClient ignoring the ``adapter`` kwarg. Fixes #607. * Fixed writing descriptors in WinRT backend. Fixes #615. * Fixed race on disconnect and cleanup of BlueZ matches when device disconnects early. Fixes #603. * Fixed memory leaks on Windows. * Fixed protocol error code descriptions on WinRT backend. Fixes #532. * Fixed race condition hitting assentation in BlueZ ``disconnect()`` method. Fixes #641. * Fixed enumerating services on a device with HID service on WinRT backend. Fixes #599. * Fixed subprocess running to check BlueZ version each time a client is created. Fixes #602. * Fixed exception when discovering services after reconnecting in CoreBluetooth backend. `0.12.1`_ (2021-07-07) ====================== Changed ------- * Changed minimum ``winrt`` package version to 1.0.21033.1. Fixes #589. Fixed ----- * Fixed unawaited future when writing without response on CoreBluetooth backend. Fixes #586. `0.12.0`_ (2021-06-19) ====================== Added ----- * Added ``mtu_size`` property for clients. * Added WinRT backend. * Added ``BleakScanner.discovered_devices`` property. * Added an event to await when stopping scanners in WinRT and pythonnet backends. Fixes #556. * Added ``BleakScanner.find_device_by_filter`` static method. * Added ``scanner_byname.py`` example. * Added optional command line argument to specify device to all applicable examples. Changed ------- * Added ``Programming Language :: Python :: 3.9`` classifier in ``setup.py``. * Deprecated ``BleakScanner.get_discovered_devices()`` async method. * Added capability to handle async functions as detection callbacks in ``BleakScanner``. * Added error description in addition to error name when ``BleakDBusError`` is converted to string. * Change typing of data parameter in write methods to ``Union[bytes, bytearray, memoryview]``. * Improved type hints in CoreBluetooth backend. * Use delegate callbacks for ``get_rssi()`` on CoreBluetooth backend. * Use ``@objc.python_method`` where possible in ``PeripheralDelegate`` class. * Using ObjC key-value observer to wait for ``BleakScanner.start()`` and ``stop()`` in CoreBluetooth backend. Fixed ----- * Fixed ``KeyError`` when trying to connect to ``BLEDevice`` from advertising data callback on macOS. Fixes #448. * Handling of undetected devices in ``connect_by_bledevice.py`` example. Fixes #487. * Added ``Optional`` typehint for ``BleakScanner.find_device_by_address``. * Fixed ``linux_autodoc_mock_import`` in ``docs/conf.py``. * Minor fix for disconnection event handling in BlueZ backend. Fixes #491. * Corrections for the Philips Hue lamp example. Merged #505. * Fixed ``BleakClientBlueZDBus.pair()`` method always returning ``True``. Fixes #503. * Fixed waiting for notification start/stop to complete in CoreBluetooth backend. * Fixed write without response on BlueZ < 5.51. * Fixed error propagation for CoreBluetooth events. * Fixed failed import on CI server when BlueZ is not installed. * Fixed notification ``value`` should be ``bytearray`` on CoreBluetooth. Fixes #560. * Fixed crash when cancelling connection when Python runtime shuts down on CoreBluetooth backend. Fixes #538. * Fixed connecting to multiple devices using a single ``BleakScanner`` on CoreBluetooth backend. * Fixed deadlock in CoreBluetooth backend when device disconnects while callbacks are pending. Fixes #535. * Fixed deadlock when using more than one service, characteristic or descriptor with the same UUID on CoreBluetooth backend. * Fixed exception raised when calling ``BleakScanner.stop()`` when already stopped in CoreBluetooth backend. `0.11.0`_ (2021-03-17) ====================== Added ----- * Updated ``dotnet.client.BleakClientDotNet`` connect method docstring. * Added ``AdvertisementServiceData`` in BLEDevice in macOS devices * Protection levels (encryption) in Windows backend pairing. Solves #405. * Philips Hue lamp example script. Relates to #405. * Keyword arguments to ``get_services`` method on ``BleakClient``. * Keyword argument ``use_cached`` on .NET backend, to enable uncached reading of services, characteristics and descriptors in Windows. * Documentation on troubleshooting OS level caches for services. * New example added: Async callbacks with a queue and external consumer * ``handle`` property on ``BleakGATTService`` objects * ``service_handle`` property on ``BleakGATTCharacteristic`` objects * Added more specific type hints for ``BleakGATTServiceCollection`` properties. * Added ``asyncio`` task to disconnect devices on event loop crash in BlueZ backend. * Added filtering on advertisement data callbacks on BlueZ backend so that callbacks only occur when advertising data changes like on macOS backend. * Added fallback to try ``org.bluez.Adapter1.ConnectDevice`` when trying to connect a device in BlueZ backend. * Added UART service example. Fixed ----- * Fixed wrong OS write method called in ``write_gatt_descriptor()`` in Windows backend. Merged #403. * Fixed ``BaseBleakClient.services_resolved`` not reset on disconnect on BlueZ backend. Merged #401. * Fixed RSSI missing in discovered devices on macOS backend. Merged #400. * Fixed scan result shows 'Unknown' name of the ``BLEDevice``. Fixes #371. * Fixed a broken check for the correct adapter in ``BleakClientBlueZDBus``. * Fixed #445 and #362 for Windows. Changed ------- * Using handles to identify the services. Added `handle` abstract property to `BleakGATTService` and storing the services by handle instead of UUID. * Changed ``BleakScanner.set_scanning_filter()`` from async method to normal method. * Changed BlueZ backend to use ``dbus-next`` instead of ``txdbus``. * Changed ``BleakClient.is_connected`` from async method to property. * Consolidated D-Bus signal debug messages in BlueZ backend. Removed ------- * Removed all ``__str__`` methods from backend service, characteristic and descriptor implementations in favour of those in the abstract base classes. `0.10.0`_ (2020-12-11) ====================== Added ----- * Added ``AdvertisementData`` class used with detection callbacks across all supported platforms. Merged #334. * Added ``BleakError`` raised during import on unsupported platforms. * Added ``rssi`` parameter to ``BLEDevice`` constructor. * Added ``detection_callback`` kwarg to ``BleakScanner`` constructor. Changed ------- * Updated minimum PyObjC version to 7.0.1. * Consolidated implementation of ``BleakScanner.register_detection_callback()``. All platforms now take callback with ``BLEDevice`` and ``AdvertisementData`` arguments. * Consolidated ``BleakScanner.find_device_by_address()`` implementations. * Renamed "device" kwarg to "adapter" in BleakClient and BleakScanner. Fixes #381. Fixed ----- * Fixed use of bare exceptions. * Fixed ``BleakClientBlueZDBus.start_notify()`` misses initial notifications with fast Bluetooth devices. Fixed #374. * Fix event callbacks on Windows not running in asyncio event loop thread. * Fixed ``BleakScanner.discover()`` on older versions of macOS. Fixes #331. * Fixed disconnect callback on BlueZ backend. * Fixed calling ``BleakClient.is_connected()`` on Mac before connection. * Fixed kwargs ignored in ``BleakScanner.find_device_by_address()`` in BlueZ backend. Fixes #360. Removed ------- * Removed duplicate definition of ``BLEDevice`` in BlueZ backend. * Removed unused imports. * Removed separate implementation of global ``discover`` method. `0.9.1`_ (2020-10-22) ===================== Added ----- * Added new attribute ``_device_info`` on ``BleakClientBlueZDBus``. Merges #347. * Added Pull Request Template. Changed ------- * Updated instructions on how to contribute, file issues and make PRs. * Updated ``AUTHORS.rst`` file with development team. Fixed ----- * Fix well-known services not converted to UUIDs in ``BLEDevice.metadata`` in CoreBluetooth backend. Fixes #342. * Fix advertising data replaced instead of merged in scanner in CoreBluetooth backend. Merged #343. * Fix CBCentralManager not properly waited for during initialization in some cases. * Fix AttributeError in CoreBluetooth when using BLEDeviceCoreBluetooth object. `0.9.0`_ (2020-10-20) ===================== Added ----- * Timeout for BlueZ backend connect call to avoid potential infinite hanging. Merged #306. * Added Interfaces API docs again. * Troubleshooting documentation. * noqa flags added to ``BleakBridge`` imports. * Adding a timeout on OSX so that the connect cannot hang forever. Merge #336. Changed ------- * ``BleakCharacteristic.description()`` on .NET now returns the same value as other platforms. * Changed all adding and removal of .NET event handler from ``+=``/``-=`` syntax to calling ``add_`` and ``remove_`` methods instead. This allows for proper removal of event handlers in .NET backend. * All code dependence on the ``BleakBridge`` is now removed. It is only imported to allow for access to UWP namespaces. * Removing internal method ``_start_notify`` in the .NET backend. * ``GattSession`` object now manages lifetime of .NET ``BleakClient`` connection. * ``BleakClient`` in .NET backend will reuse previous device information when reconnecting so that it doesn't have to scan/discover again. Fixed ----- * UUID property bug fixed in BlueZ backend. Merged #307. * Fix for broken RTD documentation. * Fix UUID string arguments should not be case sensitive. * Fix ``BleakGATTService.get_characteristic()`` method overridden with ``NotImplementedError`` in BlueZ backend. * Fix ``AttributeError`` when trying to connect using CoreBluetooth backend. Merged #323. * Fix disconnect callback called multiple times in .NET backend. Fixes #312. * Fix ``BleakClient.disconnect()`` method failing when called multiple times in .NET backend. Fixes #313. * Fix ``BleakClient.disconnect()`` method failing when called multiple times in Core Bluetooth backend. Merge #333. * Catch RemoteError in ``is_connected`` in BlueZ backend. Fixes #310, * Prevent overwriting address in constructor of ``BleakClient`` in BlueZ backend. Merge #311. * Fix nordic uart UUID. Merge #339. `0.8.0`_ (2020-09-22) ===================== Added ----- * Implemented ``set_disconnected_callback`` in the .NET backend ``BleakClient`` implementation. * Added ``find_device_by_address`` method to the ``BleakScanner`` interface, for stopping scanning when a desired address is found. * Implemented ``find_device_by_address`` in the .NET backend ``BleakScanner`` implementation and switched its ``BleakClient`` implementation to use that method in ``connect``. * Implemented ``find_device_by_address`` in the BlueZ backend ``BleakScanner`` implementation and switched its ``BleakClient`` implementation to use that method in ``connect``. * Implemented ``find_device_by_address`` in the Core Bluetooth backend ``BleakScanner`` implementation and switched its ``BleakClient`` implementation to use that method in ``connect``. * Added text representations of Protocol Errors that are visible in the .NET backend. Added these texts to errors raised. * Added pairing method in ``BleakClient`` interface. * Implemented pairing method in .NET backend. * Implemented pairing method in the BlueZ backend. * Added stumps and ``NotImplementedError`` on pairing in macOS backend. * Added the possibility to connect using ``BLEDevice`` instead of a string address. This allows for skipping the discovery call when connecting. Removed ------- * Support for Python 3.5. Changed ------- * **BREAKING CHANGE** All notifications now have the characteristic's integer **handle** instead of its UUID as a string as the first argument ``sender`` sent to notification callbacks. This provides the uniqueness of sender in notifications as well. * Renamed ``BleakClient`` argument ``address`` to ``address_or_ble_device``. * Version 0.5.0 of BleakUWPBridge, with some modified methods and implementing ``IDisposable``. * Merged #224. All storing and passing of event loops in bleak is removed. * Removed Objective C delegate compliance checks. Merged #253. * Made context managers for .NET ``DataReader`` and ``DataWriter``. Fixed ----- * .NET backend loop handling bug entered by #224 fixed. * Removed default ``DEBUG`` level set to bleak logger. Fixes #251. * More coherency in logger uses over all backends. Fixes #258 * Attempted fix of #255 and #133: cleanups, disposing of objects and creating new ``BleakBridge`` instances each disconnect. * Fixed some type hints and docstrings. * Modified the ``connected_peripheral_delegate`` handling in macOS backend to fix #213 and #116. * Merged #270, fixing a critical bug in ``get_services`` method in Core Bluetooth backend. * Improved handling of disconnections and ``is_connected`` in BlueZ backend to fix #259. * Fix for ``set_disconnected_callback`` on Core Bluetooth. Fixes #276. * Safer `Core Bluetooth` presence check. Merged #280. `0.7.1`_ (2020-07-02) ===================== Changed ------- * Improved, more explanatory error on BlueZ backend when ``BleakClient`` cannot find the desired device when trying to connect. (#238) * Better-than-nothing documentation about scanning filters added (#230). * Ran black on code which was forgotten in 0.7.0. Large diffs due to that. * Re-adding Python 3.8 CI "tests" on Windows again. Fixed ----- * Fix when characteristic updates value faster than asyncio schedule (#240 & #241) * Incorrect ``MANIFEST.in`` corrected. (#244) `0.7.0`_ (2020-06-30) ===================== Added ----- * Better feedback of communication errors to user in .NET backend and implementing error details proposed in #174. * Two devices example file to use for e.g. debugging. * Detection/discovery callbacks in Core Bluetooth backend ``Scanner`` implemented. * Characteristic handle printout in ``service_explorer.py``. * Added scanning filters to .NET backend's ``discover`` method. Changed ------- * Replace ``NSRunLoop`` with dispatch queue in Core Bluetooth backend. This causes callbacks to be dispatched on a background thread instead of on the main dispatch queue on the main thread. ``call_soon_threadsafe()`` is used to synchronize the events with the event loop where the central manager was created. Fixes #111. * The Central Manager is no longer global in the Core Bluetooth backend. A new one is created for each ``BleakClient`` and ``BleakScanner``. Fixes #206 and #105. * Merged #167 and reworked characteristics handling in Bleak. Implemented in all backends; bleak now uses the characteristics' handle to identify and keep track of them. Fixes #139 and #159 and allows connection for devices with multiple instances of the same characteristic UUIDs. * In ``requirements.txt`` and ``Pipfile``, the requirement on ``pythonnet`` was bumped to version 2.5.1, which seems to solve issues described in #217 and #225. * Renamed ``HISTORY.rst`` to ``CHANGELOG.rst`` and adopted the `Keep a Changelog `_ format. * Python 3.5 support from macOS is officially removed since pyobjc>6 requires 3.6+ * Pin ``pyobjc`` dependencies to use at least version 6.2. (PR #194) * Pin development requirement on `bump2version` to version 1.0.0 * Added ``.pyup.yml`` for Pyup * Using CBManagerState constants from pyobj instead of integers. Removed ------- * Removed documentation note about not using new event loops in Linux. This was fixed by #143. * ``_central_manager_delegate_ready`` was removed in macOS backend. * Removed the ``bleak.backends.bluez.utils.get_gatt_service_path`` method. It is not used by bleak and possibly generates errors. Fixed ----- * Improved handling of the txdbus connection to avoid hanging of disconnection clients in BlueZ backend. Fixes #216, #219 & #221. * #150 hints at the device path not being possible to create as is done in the `get_device_object_path` method. Now, we try to get it from BlueZ first. Otherwise, use the old fallback. * Minor documentation errors corrected. * ``CBManagerStatePoweredOn`` is now properly handled in Core Bluetooth. * Device enumeration in ``discover``and ``Scanner`` corrected. Fixes #211 * Updated documentation about scanning filters. * Added workaround for ``isScanning`` attribute added in macOS 10.13. Fixes #234. `0.6.4`_ (2020-05-20) ===================== Fixed ----- * Fix for bumpversion usage `0.6.3`_ (2020-05-20) ===================== Added ----- * Building and releasing from Github Actions Removed ------- * Building and releasing on Azure Pipelines `0.6.2`_ (2020-05-15) ===================== Added ----- * Added ``disconnection_callback`` functionality for Core Bluetooth (#184 & #186) * Added ``requirements.txt`` Fixed ----- * Better cleanup of Bluez notifications (#154) * Fix for ``read_gatt_char`` in Core Bluetooth (#177) * Fix for ``is_disconnected`` in Core Bluetooth (#187 & #185) * Documentation fixes `0.6.1`_ (2020-03-09) ===================== Fixed ----- * Including #156, lost notifications on macOS backend, which was accidentally missed on previous release. `0.6.0`_ (2020-03-09) ===================== * New Scanner object to allow for async device scanning. * Updated ``txdbus`` requirement to version 1.1.1 (Merged #122) * Implemented ``write_gatt_descriptor`` for Bluez backend. * Large change in Bluez backend handling of Twisted reactors. Fixes #143 * Modified ``set_disconnected_callback`` to actually call the callback as a callback. Fixes #108. * Added another required parameter to disconnect callbacks. * Added Discovery filter option in BlueZ backend (Merged #124) * Merge #138: comments about Bluez version check. * Improved scanning data for macOS backend. Merge #126. * Merges #141, a critical fix for macOS. * Fix for #114, write with response on macOS. * Fix for #87, DIctionary changes size on .NET backend. * Fix for #127, uuid or str on macOS. * Handles str/uuid for characteristics better. * Merge #148, Run .NET backend notifications on event loop instead of main loop. * Merge #146, adapt characteristic write log to account for WriteWithoutResponse on macOS. * Fix for #145, Error in cleanup on Bluez backend. * Fix for #151, only subscribe to BlueZ messages on DBus. Merge #152. * Fix for #142, Merge #144, Improved scanning for macOS backend. * Fix for #155, Merge #156, lost notifications on macOS backend. * Improved type hints * Improved error handling for .NET backend. * Documentation fixes. 0.5.1 (2019-10-09) ================== * Active Scanning on Windows, #99 potentially solving #95 * Longer timeout in service discovery on BlueZ * Added ``timeout`` to constructors and connect methods * Fix for ``get_services`` on macOS. Relates to #101 * Fixes for disconnect callback on BlueZ, #86 and #83 * Fixed reading of device name in BlueZ. It is not readable as regular characteristic. #104 * Removed logger feedback in BlueZ discovery method. * More verbose exceptions on macOS, #117 and #107 0.5.0 (2019-08-02) ================== * macOS support added (thanks to @kevincar) * Merged #90 which fixed #89: Leaking callbacks in BlueZ * Merged #92 which fixed #91, Prevent leaking of DBus connections on discovery * Merged #96: Regex patterns * Merged #86 which fixed #83 and #82 * Recovered old .NET discovery method to try for #95 * Merged #80: macOS development 0.4.3 (2019-06-30) ================== * Fix for #76 * Fix for #69 * Fix for #74 * Fix for #68 * Fix for #70 * Merged #66 0.4.2 (2019-05-17) ================== * Fix for missed part of PR #61. 0.4.1 (2019-05-17) ================== * Merging of PR #61, improvements and fixes for multiple issues for BlueZ backend * Implementation of issue #57 * Fixing issue #59 * Documentation fixes. 0.4.0 (2019-04-10) ================== * Transferred code from the BleakUWPBridge C# support project to pythonnet code * Fixed BlueZ >= 5.48 issues regarding Battery Service * Fix for issue #55 0.3.0 (2019-03-18) ================== * Fix for issue #53: Windows and Python 3.7 error * Azure Pipelines used for CI 0.2.4 (2018-11-30) ================== * Fix for issue #52: Timing issue getting characteristics * Additional fix for issue #51. * Bugfix for string method for BLEDevice. 0.2.3 (2018-11-28) ================== * Fix for issue #51: ``dpkg-query not found on all Linux systems`` 0.2.2 (2018-11-08) ================== * Made it compliant with Python 3.5 by removing f-strings 0.2.1 (2018-06-28) ================== * Improved logging on .NET discover method * Some type annotation fixes in .NET code 0.2.0 (2018-04-26) ================== * Project added to Github * First version on PyPI. * Working Linux (BlueZ DBus API) backend. * Working Windows (UWP Bluetooth API) backend. 0.1.0 (2017-10-23) ================== * Bleak created. .. _Unreleased: https://github.com/hbldh/bleak/compare/v0.22.3...develop .. _0.22.3: https://github.com/hbldh/bleak/compare/v0.22.2...v0.22.3 .. _0.22.2: https://github.com/hbldh/bleak/compare/v0.22.1...v0.22.2 .. _0.22.1: https://github.com/hbldh/bleak/compare/v0.22.0...v0.22.1 .. _0.22.0: https://github.com/hbldh/bleak/compare/v0.21.1...v0.22.0 .. _0.21.1: https://github.com/hbldh/bleak/compare/v0.21.0...v0.21.1 .. _0.21.0: https://github.com/hbldh/bleak/compare/v0.20.2...v0.21.0 .. _0.20.2: https://github.com/hbldh/bleak/compare/v0.20.1...v0.20.2 .. _0.20.1: https://github.com/hbldh/bleak/compare/v0.20.0...v0.20.1 .. _0.20.0: https://github.com/hbldh/bleak/compare/v0.19.5...v0.20.0 .. _0.19.5: https://github.com/hbldh/bleak/compare/v0.19.4...v0.19.5 .. _0.19.4: https://github.com/hbldh/bleak/compare/v0.19.3...v0.19.4 .. _0.19.3: https://github.com/hbldh/bleak/compare/v0.19.2...v0.19.3 .. _0.19.2: https://github.com/hbldh/bleak/compare/v0.19.1...v0.19.2 .. _0.19.1: https://github.com/hbldh/bleak/compare/v0.19.0...v0.19.1 .. _0.19.0: https://github.com/hbldh/bleak/compare/v0.18.1...v0.19.0 .. _0.18.1: https://github.com/hbldh/bleak/compare/v0.18.0...v0.18.1 .. _0.18.0: https://github.com/hbldh/bleak/compare/v0.17.0...v0.18.0 .. _0.17.0: https://github.com/hbldh/bleak/compare/v0.16.0...v0.17.0 .. _0.16.0: https://github.com/hbldh/bleak/compare/v0.15.1...v0.16.0 .. _0.15.1: https://github.com/hbldh/bleak/compare/v0.15.0...v0.15.1 .. _0.15.0: https://github.com/hbldh/bleak/compare/v0.14.3...v0.15.0 .. _0.14.3: https://github.com/hbldh/bleak/compare/v0.14.2...v0.14.3 .. _0.14.2: https://github.com/hbldh/bleak/compare/v0.14.1...v0.14.2 .. _0.14.1: https://github.com/hbldh/bleak/compare/v0.14.0...v0.14.1 .. _0.14.0: https://github.com/hbldh/bleak/compare/v0.13.0...v0.14.0 .. _0.13.0: https://github.com/hbldh/bleak/compare/v0.12.1...v0.13.0 .. _0.12.1: https://github.com/hbldh/bleak/compare/v0.12.0...v0.12.1 .. _0.12.0: https://github.com/hbldh/bleak/compare/v0.11.0...v0.12.0 .. _0.11.0: https://github.com/hbldh/bleak/compare/v0.10.0...v0.11.0 .. _0.10.0: https://github.com/hbldh/bleak/compare/v0.9.1...v0.10.0 .. _0.9.1: https://github.com/hbldh/bleak/compare/v0.9.0...v0.9.1 .. _0.9.0: https://github.com/hbldh/bleak/compare/v0.8.0...v0.9.0 .. _0.8.0: https://github.com/hbldh/bleak/compare/v0.7.1...v0.8.0 .. _0.7.1: https://github.com/hbldh/bleak/compare/v0.7.0...v0.7.1 .. _0.7.0: https://github.com/hbldh/bleak/compare/v0.6.4...v0.7.0 .. _0.6.4: https://github.com/hbldh/bleak/compare/v0.6.4...v0.6.3 .. _0.6.3: https://github.com/hbldh/bleak/compare/v0.6.3...v0.6.2 .. _0.6.2: https://github.com/hbldh/bleak/compare/v0.6.2...v0.6.1 .. _0.6.1: https://github.com/hbldh/bleak/compare/v0.6.1...v0.6.0 .. _0.6.0: https://github.com/hbldh/bleak/compare/v0.6.0...v0.5.1 bleak-0.22.3/CONTRIBUTING.rst000066400000000000000000000063451470032643600153320ustar00rootroot00000000000000.. highlight:: shell ============ Contributing ============ Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. You can contribute in many ways: Types of Contributions ---------------------- Report Bugs ~~~~~~~~~~~ Report bugs at https://github.com/hbldh/bleak/issues. If you are reporting a bug, please include: * Your operating system name and version. * Any details about your local setup that might be helpful in troubleshooting. * Detailed steps to reproduce the bug. Fix Bugs ~~~~~~~~ Look through the GitHub issues for bugs. Anything tagged with "bug" and "help wanted" is open to whoever wants to implement it. Implement Features ~~~~~~~~~~~~~~~~~~ Look through the GitHub issues for features. Anything tagged with "enhancement" and "help wanted" is open to whoever wants to implement it. Write Documentation ~~~~~~~~~~~~~~~~~~~ bleak could always use more documentation, whether as part of the official bleak docs, in docstrings, or even on the web in blog posts, articles, and such. Submit Feedback ~~~~~~~~~~~~~~~ The best way to send feedback is to file an issue at https://github.com/hbldh/bleak/issues. If you are proposing a feature: * Explain in detail how it would work. * Keep the scope as narrow as possible, to make it easier to implement. * Remember that this is a volunteer-driven project, and that contributions are welcome :) Get Started! ------------ Ready to contribute? Here's how to set up ``bleak`` for local development. You will need `Git `_ and `Poetry `_ and your favorite text editor. And Python of course. 1. Fork the ``bleak`` repo on GitHub. 2. Clone your fork locally:: $ git clone https://github.com:your_name_here/bleak.git 3. Set up the Python environment:: $ cd bleak/ $ poetry install 4. Create a branch for local development, originating from the ``develop`` branch:: $ git checkout -b name-of-your-bugfix-or-feature develop Now you can make your changes locally. 5. When you're done making changes, check that your changes pass linting and the tests:: $ poetry run isort . $ poetry run black . $ poetry run flake8 $ poetry run pytest 6. Commit your changes and push your branch to GitHub:: $ git add . $ git commit -m "Your detailed description of your changes." $ git push origin name-of-your-bugfix-or-feature 7. Submit a pull request through the GitHub website. Pull Request Guidelines ----------------------- Before you submit a pull request, check that it meets these guidelines: 1. If the pull request adds functionality, the docs should be updated. 2. Modify the ``CHANGELOG.rst``, describing your changes as is specified by the guidelines in that document. 3. The pull request should work for Python 3.8+ on the following platforms: - Windows 10, version 16299 (Fall Creators Update) and greater - Linux distributions with BlueZ >= 5.43 - OS X / macOS >= 10.11 4. Squash all your commits on your PR branch, if the commits are not solving different problems and you are committing them in the same PR. In that case, consider making several PRs instead. 5. Feel free to add your name as a contributor to the ``AUTHORS.rst`` file! bleak-0.22.3/LICENSE000066400000000000000000000020601470032643600136640ustar00rootroot00000000000000 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. bleak-0.22.3/README.rst000066400000000000000000000052161470032643600143540ustar00rootroot00000000000000===== bleak ===== .. image:: https://raw.githubusercontent.com/hbldh/bleak/master/Bleak_logo2.png :target: https://github.com/hbldh/bleak :alt: Bleak Logo .. 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 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 10, version 16299 (Fall Creators Update) or greater * Supports Linux distributions with BlueZ >= 5.43 * OS X/macOS support via Core Bluetooth API, from at least OS X version 10.11 * 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("Model Number: {0}".format("".join(map(chr, model_number)))) 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 `_ bleak-0.22.3/bleak/000077500000000000000000000000001470032643600137375ustar00rootroot00000000000000bleak-0.22.3/bleak/__init__.py000066400000000000000000000773421470032643600160650ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Top-level package for bleak.""" from __future__ import annotations __author__ = """Henrik Blidh""" __email__ = "henrik.blidh@gmail.com" import asyncio import functools import inspect import logging import os import sys import uuid from types import TracebackType from typing import ( TYPE_CHECKING, AsyncGenerator, Awaitable, Callable, Dict, Iterable, List, Literal, Optional, Set, Tuple, Type, TypedDict, Union, overload, ) from warnings import warn if sys.version_info < (3, 12): from typing_extensions import Buffer else: from collections.abc import Buffer if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout from typing_extensions import Unpack else: from asyncio import timeout as async_timeout from typing import Unpack from .backends.characteristic import BleakGATTCharacteristic from .backends.client import BaseBleakClient, get_platform_client_backend_type from .backends.device import BLEDevice from .backends.scanner import ( AdvertisementData, AdvertisementDataCallback, AdvertisementDataFilter, BaseBleakScanner, get_platform_scanner_backend_type, ) from .backends.service import BleakGATTServiceCollection from .exc import BleakCharacteristicNotFoundError, BleakError from .uuids import normalize_uuid_str if TYPE_CHECKING: from .backends.bluezdbus.scanner import BlueZScannerArgs from .backends.corebluetooth.scanner import CBScannerArgs from .backends.winrt.client import WinRTClientArgs _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.stdout) 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] = 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. """ 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, ) -> None: PlatformBleakScanner = ( get_platform_scanner_backend_type() if backend is None else backend ) self._backend = PlatformBleakScanner( detection_callback, service_uuids, scanning_mode, bluez=bluez, cb=cb, **kwargs, ) async def __aenter__(self) -> BleakScanner: await self._backend.start() return self async def __aexit__( self, exc_type: Type[BaseException], exc_val: BaseException, exc_tb: TracebackType, ) -> None: await self._backend.stop() def register_detection_callback( self, callback: Optional[AdvertisementDataCallback] ) -> None: """ Register a callback that is called when a device is discovered or has a property changed. .. deprecated:: 0.17.0 This method will be removed in a future version of Bleak. Pass the callback directly to the :class:`BleakScanner` constructor instead. Args: callback: A function, coroutine or ``None``. """ warn( "This method will be removed in a future version of Bleak. Use the detection_callback of the BleakScanner constructor instead.", FutureWarning, stacklevel=2, ) try: unregister = getattr(self, "_unregister_") except AttributeError: pass else: unregister() if callback is not None: unregister = self._backend.register_detection_callback(callback) setattr(self, "_unregister_", unregister) async def start(self) -> None: """Start scanning for devices""" await self._backend.start() async def stop(self) -> None: """Stop scanning for devices""" await self._backend.stop() def set_scanning_filter(self, **kwargs) -> None: """ Set scanning filter for the BleakScanner. .. deprecated:: 0.17.0 This method will be removed in a future version of Bleak. Pass arguments directly to the :class:`BleakScanner` constructor instead. Args: **kwargs: The filter details. """ warn( "This method will be removed in a future version of Bleak. Use BleakScanner constructor args instead.", FutureWarning, stacklevel=2, ) self._backend.set_scanning_filter(**kwargs) 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() 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). """ @overload @classmethod async def discover( cls, timeout: float = 5.0, *, return_adv: Literal[False] = False, **kwargs ) -> List[BLEDevice]: ... @overload @classmethod async def discover( cls, timeout: float = 5.0, *, return_adv: Literal[True], **kwargs ) -> Dict[str, Tuple[BLEDevice, AdvertisementData]]: ... @classmethod async def discover( cls, timeout=5.0, *, return_adv=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 self._backend.seen_devices async def get_discovered_devices(self) -> List[BLEDevice]: """Gets the devices registered by the BleakScanner. .. deprecated:: 0.11.0 This method will be removed in a future version of Bleak. Use the :attr:`.discovered_devices` property instead. Returns: A list of the devices that the scanner has discovered during the scanning. """ warn( "This method will be removed in a future version of Bleak. Use the `discovered_devices` property instead.", FutureWarning, stacklevel=2, ) return self.discovered_devices @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 except asyncio.TimeoutError: return None 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 10.0. 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. .. 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. """ def __init__( self, address_or_ble_device: Union[BLEDevice, str], disconnected_callback: Optional[Callable[[BleakClient], None]] = None, services: Optional[Iterable[str]] = None, *, timeout: float = 10.0, winrt: WinRTClientArgs = {}, backend: Optional[Type[BaseBleakClient]] = None, **kwargs, ) -> None: PlatformBleakClient = ( get_platform_client_backend_type() if backend is None else backend ) 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, winrt=winrt, **kwargs, ) # device info @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) -> BleakClient: await self.connect() return self async def __aexit__( self, exc_type: Type[BaseException], exc_val: BaseException, exc_tb: TracebackType, ) -> None: await self.disconnect() # Connectivity methods def set_disconnected_callback( self, callback: Optional[Callable[[BleakClient], None]], **kwargs ) -> None: """Set the disconnect callback. .. deprecated:: 0.17.0 This method will be removed in a future version of Bleak. Pass the callback to the :class:`BleakClient` constructor instead. Args: callback: callback to be called on disconnection. """ warn( "This method will be removed future version, pass the callback to the BleakClient constructor instead.", FutureWarning, stacklevel=2, ) self._backend.set_disconnected_callback( None if callback is None else functools.partial(callback, self), **kwargs ) async def connect(self, **kwargs) -> bool: """Connect to the specified GATT server. Args: **kwargs: For backwards compatibility - should not be used. Returns: Always returns ``True`` for backwards compatibility. """ return await self._backend.connect(**kwargs) async def disconnect(self) -> bool: """Disconnect from the specified GATT server. Returns: Always returns ``True`` for backwards compatibility. """ return await self._backend.disconnect() async def pair(self, *args, **kwargs) -> bool: """ 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. Returns: Always returns ``True`` for backwards compatibility. """ return await self._backend.pair(*args, **kwargs) async def unpair(self) -> bool: """ 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. Returns: Always returns ``True`` for backwards compatibility. """ return 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 async def get_services(self, **kwargs) -> BleakGATTServiceCollection: """Get all services registered for this GATT server. .. deprecated:: 0.17.0 This method will be removed in a future version of Bleak. Use the :attr:`services` property instead. Returns: A :class:`bleak.backends.service.BleakGATTServiceCollection` with this device's services tree. """ warn( "This method will be removed future version, use the services property instead.", FutureWarning, stacklevel=2, ) return await self._backend.get_services(**kwargs) @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], **kwargs, ) -> 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. Returns: The read data. """ return await self._backend.read_gatt_char(char_specifier, **kwargs) async def write_gatt_char( self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], data: Buffer, response: bool = None, ) -> None: """ 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. .. tip:: Explicit is better than implicit. Best practice is to always include an explicit ``response=True`` or ``response=False`` when calling this method. 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. If omitted or ``None``, the "best" operation will be used based on the reported properties of the characteristic. .. versionchanged:: 0.21 The default behavior when ``response=`` is omitted was changed. Example:: MY_CHAR_UUID = "1234" ... await client.write_gatt_char(MY_CHAR_UUID, b"\x00\x01\x02\x03", response=True) """ if isinstance(char_specifier, BleakGATTCharacteristic): characteristic = char_specifier else: characteristic = self.services.get_characteristic(char_specifier) if not characteristic: raise BleakCharacteristicNotFoundError(char_specifier) 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. 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]] ], **kwargs, ) -> 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. .. versionchanged:: 0.18 The first argument of the callback is now a :class:`BleakGATTCharacteristic` instead of an ``int``. """ if not self.is_connected: raise BleakError("Not connected") if not isinstance(char_specifier, BleakGATTCharacteristic): characteristic = self.services.get_characteristic(char_specifier) else: characteristic = char_specifier if not characteristic: raise BleakCharacteristicNotFoundError(char_specifier) 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) await self._backend.start_notify(characteristic, wrapped_callback, **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. .. 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. """ await self._backend.stop_notify(char_specifier) async def read_gatt_descriptor(self, handle: int, **kwargs) -> bytearray: """ Perform read operation on the specified GATT descriptor. Args: handle: The handle of the descriptor to read from. Returns: The read data. """ return await self._backend.read_gatt_descriptor(handle, **kwargs) async def write_gatt_descriptor(self, handle: int, data: Buffer) -> 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. """ await self._backend.write_gatt_descriptor(handle, data) # for backward compatibility def discover(*args, **kwargs): """ .. deprecated:: 0.17.0 This method will be removed in a future version of Bleak. Use :meth:`BleakScanner.discover` instead. """ warn( "The discover function will removed in a future version, use BleakScanner.discover instead.", FutureWarning, stacklevel=2, ) return BleakScanner.discover(*args, **kwargs) def cli() -> None: import argparse parser = argparse.ArgumentParser( description="Perform Bluetooth Low Energy device scan" ) parser.add_argument("-i", dest="adapter", default=None, help="HCI device") parser.add_argument( "-t", dest="timeout", type=int, default=5, help="Duration to scan for" ) args = parser.parse_args() out = asyncio.run(discover(adapter=args.adapter, timeout=float(args.timeout))) for o in out: print(str(o)) if __name__ == "__main__": cli() bleak-0.22.3/bleak/assigned_numbers.py000066400000000000000000000020001470032643600176310ustar00rootroot00000000000000""" Bluetooth Assigned Numbers -------------------------- This module contains useful assigned numbers from the Bluetooth spec. See . """ from enum import IntEnum 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 bleak-0.22.3/bleak/backends/000077500000000000000000000000001470032643600155115ustar00rootroot00000000000000bleak-0.22.3/bleak/backends/__init__.py000066400000000000000000000001521470032643600176200ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ __init__.py Created on 2017-11-19 by hbldh """ bleak-0.22.3/bleak/backends/_manufacturers.py000066400000000000000000002041251470032643600211050ustar00rootroot00000000000000""" 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.", } bleak-0.22.3/bleak/backends/bluezdbus/000077500000000000000000000000001470032643600175105ustar00rootroot00000000000000bleak-0.22.3/bleak/backends/bluezdbus/__init__.py000066400000000000000000000000251470032643600216160ustar00rootroot00000000000000"""BlueZ backend.""" bleak-0.22.3/bleak/backends/bluezdbus/advertisement_monitor.py000066400000000000000000000071631470032643600245120ustar00rootroot00000000000000""" Advertisement Monitor --------------------- This module contains types associated with the BlueZ D-Bus `advertisement monitor api `. """ import logging from typing import Iterable, NamedTuple, Tuple, Union, no_type_check from dbus_fast.service import PropertyAccess, ServiceInterface, dbus_property, method from ...assigned_numbers import AdvertisementDataType from . import defs logger = logging.getLogger(__name__) 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 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 bleak-0.22.3/bleak/backends/bluezdbus/characteristic.py000066400000000000000000000064431470032643600230610ustar00rootroot00000000000000from typing import Callable, List, Union from uuid import UUID from ..characteristic import BleakGATTCharacteristic from ..descriptor import BleakGATTDescriptor from .defs import GattCharacteristic1 from .utils import extract_service_handle_from_path _GattCharacteristicsFlagsEnum = { 0x0001: "broadcast", 0x0002: "read", 0x0004: "write-without-response", 0x0008: "write", 0x0010: "notify", 0x0020: "indicate", 0x0040: "authenticated-signed-writes", 0x0080: "extended-properties", 0x0100: "reliable-write", 0x0200: "writable-auxiliaries", # "encrypt-read" # "encrypt-write" # "encrypt-authenticated-read" # "encrypt-authenticated-write" # "secure-read" #(Server only) # "secure-write" #(Server only) # "authorize" } class BleakGATTCharacteristicBlueZDBus(BleakGATTCharacteristic): """GATT Characteristic implementation for the BlueZ DBus backend""" def __init__( self, obj: GattCharacteristic1, object_path: str, service_uuid: str, service_handle: int, max_write_without_response_size: Callable[[], int], ): super(BleakGATTCharacteristicBlueZDBus, self).__init__( obj, max_write_without_response_size ) self.__descriptors = [] self.__path = object_path self.__service_uuid = service_uuid self.__service_handle = service_handle self._handle = extract_service_handle_from_path(object_path) @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 handle of the Service containing this characteristic""" return self.__service_handle @property def handle(self) -> int: """The handle of this characteristic""" return self._handle @property def uuid(self) -> str: """The uuid of this characteristic""" return self.obj.get("UUID") @property def properties(self) -> List[str]: """Properties of this characteristic Returns the characteristics `Flags` present in the DBus API. """ return self.obj["Flags"] @property def descriptors(self) -> List[BleakGATTDescriptor]: """List of descriptors for this service""" return self.__descriptors def get_descriptor( self, specifier: Union[int, str, UUID] ) -> Union[BleakGATTDescriptor, None]: """Get a descriptor by handle (int) or UUID (str or uuid.UUID)""" try: if isinstance(specifier, int): return next(filter(lambda x: x.handle == specifier, self.descriptors)) else: return next( filter(lambda x: x.uuid == str(specifier), self.descriptors) ) except StopIteration: 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. """ self.__descriptors.append(descriptor) @property def path(self) -> str: """The DBus path. Mostly needed by `bleak`, not by end user""" return self.__path bleak-0.22.3/bleak/backends/bluezdbus/client.py000066400000000000000000001126601470032643600213460ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ BLE Client for BlueZ on Linux """ import asyncio import logging import os import sys import warnings from typing import Callable, Dict, Optional, Set, Union, cast from uuid import UUID if sys.version_info < (3, 12): from typing_extensions import Buffer else: from collections.abc import Buffer if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout else: from asyncio import timeout as async_timeout 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 ... import BleakScanner from ...exc import ( BleakCharacteristicNotFoundError, BleakDBusError, BleakDeviceNotFoundError, BleakError, ) from ..characteristic import BleakGATTCharacteristic from ..client import BaseBleakClient, NotifyCallback from ..device import BLEDevice from ..service import BleakGATTServiceCollection from . import defs from .characteristic import BleakGATTCharacteristicBlueZDBus from .manager import get_global_bluez_manager from .scanner import BleakScannerBlueZDBus from .utils import assert_reply, get_dbus_authenticator from .version import BlueZFeatures logger = logging.getLogger(__name__) # prevent tasks from being garbage collected _background_tasks: Set[asyncio.Task] = 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. Keyword Args: timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0. 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. adapter (str): Bluetooth adapter to use for discovery. """ def __init__( self, address_or_ble_device: Union[BLEDevice, str], services: Optional[Set[str]] = None, **kwargs, ): super(BleakClientBlueZDBus, self).__init__(address_or_ble_device, **kwargs) # kwarg "device" is for backwards compatibility self._adapter: Optional[str] = kwargs.get("adapter", kwargs.get("device")) # 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 # 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] = {} # used to override mtu_size property self._mtu_size: Optional[int] = None # Connectivity methods async def connect(self, dangerous_use_bleak_cache: bool = False, **kwargs) -> bool: """Connect to the specified GATT server. Keyword Args: timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0. Returns: Boolean representing connection status. 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.43.") # 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, 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." ) manager = await get_global_bluez_manager() async with async_timeout(timeout): while True: # Each BLE connection session needs a new D-Bus connection to avoid a # BlueZ quirk where notifications are automatically enabled on reconnect. self._bus = await MessageBus( bus_type=BusType.SYSTEM, negotiate_unix_fd=True, auth=get_dbus_authenticator(), ).connect() 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() ) try: try: # # 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 ) reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, interface=defs.DEVICE_INTERFACE, path=self._device_path, member="Connect", ) ) assert reply is not None 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 == "org.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 ) return True except BaseException: # Calling Disconnect cancels any pending connect request. Also, # if connection was successful but get_services() raises (e.g. # because task was cancelled), the 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: reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, interface=defs.DEVICE_INTERFACE, path=self._device_path, member="Disconnect", ) ) try: assert_reply(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}" ) raise except BaseException: # this effectively cancels the disconnect monitor in case the event # was not triggered by a D-Bus callback local_disconnect_monitor_event.set() self._cleanup_all() raise @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. await bus.send( 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 # Try to disconnect the System Bus. try: self._bus.disconnect() except Exception as e: logger.error( "Attempt to disconnect system bus failed (%s): %s", self._device_path, e, ) else: # Critical to remove the `self._bus` object here to since it was # closed above. If not, calls made to it later could lead to # a stuck client. self._bus = None # Reset all stored services. self.services = None async def disconnect(self) -> bool: """Disconnect from the specified GATT server. Returns: Boolean representing if device is disconnected. 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 True 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() elif self.is_connected: self._disconnecting_event = asyncio.Event() try: # 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 # sanity check to make sure _cleanup_all() was triggered by the # "PropertiesChanged" signal handler and that it completed successfully assert self._bus is None return True async def pair(self, *args, **kwargs) -> bool: """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. Returns: Boolean regarding success of pairing. """ # See if it is already paired. reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, path=self._device_path, interface=defs.PROPERTIES_INTERFACE, member="Get", signature="ss", body=[defs.DEVICE_INTERFACE, "Paired"], ) ) assert_reply(reply) if reply.body[0].value: logger.debug("BLE device @ %s is already paired", self.address) return True # Set device as trusted. 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) reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, path=self._device_path, interface=defs.DEVICE_INTERFACE, member="Pair", ) ) assert_reply(reply) reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, path=self._device_path, interface=defs.PROPERTIES_INTERFACE, member="Get", signature="ss", body=[defs.DEVICE_INTERFACE, "Paired"], ) ) assert_reply(reply) return reply.body[0].value async def unpair(self) -> bool: """Unpair with the peripheral. Returns: Boolean regarding success of unpairing. """ 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 try: reply = await manager._bus.call( 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 == "org.bluez.Error.DoesNotExist": raise BleakDeviceNotFoundError( self.address, f"Device with address {self.address} was not found." ) from e raise return True @property def is_connected(self) -> bool: """Check connection status between this client and the server. Returns: Boolean representing connection status. """ return self._DeprecatedIsConnectedReturn( 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. """ # 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 ) reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, path=char.path, 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 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 ) -> 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 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 async def read_gatt_char( self, char_specifier: Union[BleakGATTCharacteristicBlueZDBus, int, str, UUID], **kwargs, ) -> bytearray: """Perform read operation on the specified GATT characteristic. Args: char_specifier (BleakGATTCharacteristicBlueZDBus, int, str or UUID): The characteristic to read from, specified by either integer handle, UUID or directly by the BleakGATTCharacteristicBlueZDBus object representing it. Returns: (bytearray) The read data. """ if not self.is_connected: raise BleakError("Not connected") if not isinstance(char_specifier, BleakGATTCharacteristicBlueZDBus): characteristic = self.services.get_characteristic(char_specifier) else: characteristic = char_specifier if not characteristic: # Special handling for BlueZ >= 5.48, where Battery Service (0000180f-0000-1000-8000-00805f9b34fb:) # has been moved to interface org.bluez.Battery1 instead of as a regular service. if ( str(char_specifier) == "00002a19-0000-1000-8000-00805f9b34fb" and BlueZFeatures.hides_battery_characteristic ): reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, path=self._device_path, interface=defs.PROPERTIES_INTERFACE, member="GetAll", signature="s", body=[defs.BATTERY_INTERFACE], ) ) assert_reply(reply) # Simulate regular characteristics read to be consistent over all platforms. value = bytearray([reply.body[0]["Percentage"].value]) logger.debug( "Read Battery Level {0} | {1}: {2}".format( char_specifier, self._device_path, value ) ) return value if ( str(char_specifier) == "00002a00-0000-1000-8000-00805f9b34fb" and BlueZFeatures.hides_device_name_characteristic ): # Simulate regular characteristics read to be consistent over all platforms. manager = await get_global_bluez_manager() value = bytearray(manager.get_device_name(self._device_path).encode()) logger.debug( "Read Device Name {0} | {1}: {2}".format( char_specifier, self._device_path, value ) ) return value raise BleakCharacteristicNotFoundError(char_specifier) while True: assert self._bus reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, path=characteristic.path, interface=defs.GATT_CHARACTERISTIC_INTERFACE, member="ReadValue", signature="a{sv}", body=[{}], ) ) assert reply if reply.error_name == "org.bluez.Error.InProgress": 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_reply(reply) break value = bytearray(reply.body[0]) logger.debug( "Read Characteristic {0} | {1}: {2}".format( characteristic.uuid, characteristic.path, value ) ) return value async def read_gatt_descriptor(self, handle: int, **kwargs) -> bytearray: """Perform read operation on the specified GATT descriptor. Args: handle (int): The handle of the descriptor to read from. Returns: (bytearray) The read data. """ if not self.is_connected: raise BleakError("Not connected") descriptor = self.services.get_descriptor(handle) if not descriptor: raise BleakError("Descriptor with handle {0} was not found!".format(handle)) while True: assert self._bus reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, path=descriptor.path, interface=defs.GATT_DESCRIPTOR_INTERFACE, member="ReadValue", signature="a{sv}", body=[{}], ) ) assert reply if reply.error_name == "org.bluez.Error.InProgress": 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_reply(reply) break value = bytearray(reply.body[0]) logger.debug("Read Descriptor %s | %s: %s", handle, descriptor.path, value) return value async def write_gatt_char( self, characteristic: BleakGATTCharacteristic, data: Buffer, response: bool, ) -> None: if not self.is_connected: raise BleakError("Not connected") # See docstring for details about this handling. if not response and not BlueZFeatures.can_write_without_response: raise BleakError("Write without response requires at least BlueZ 5.46") if response or not BlueZFeatures.write_without_response_workaround_needed: while True: assert self._bus reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, path=characteristic.path, interface=defs.GATT_CHARACTERISTIC_INTERFACE, member="WriteValue", signature="aya{sv}", body=[ bytes(data), { "type": Variant( "s", "request" if response else "command" ) }, ], ) ) assert reply if reply.error_name == "org.bluez.Error.InProgress": 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_reply(reply) break else: # Older versions of BlueZ don't have the "type" option, so we have # to write the hard way. This isn't the most efficient way of doing # things, but it works. reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, path=characteristic.path, interface=defs.GATT_CHARACTERISTIC_INTERFACE, member="AcquireWrite", signature="a{sv}", body=[{}], ) ) assert_reply(reply) fd = reply.unix_fds[0] try: os.write(fd, data) finally: os.close(fd) logger.debug( "Write Characteristic %s | %s: %s", characteristic.uuid, characteristic.path, data, ) async def write_gatt_descriptor(self, handle: int, data: Buffer) -> 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") descriptor = self.services.get_descriptor(handle) if not descriptor: raise BleakError(f"Descriptor with handle {handle} was not found!") while True: assert self._bus reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, path=descriptor.path, interface=defs.GATT_DESCRIPTOR_INTERFACE, member="WriteValue", signature="aya{sv}", body=[bytes(data), {"type": Variant("s", "command")}], ) ) assert reply if reply.error_name == "org.bluez.Error.InProgress": 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_reply(reply) break logger.debug("Write Descriptor %s | %s: %s", handle, descriptor.path, data) async def start_notify( self, characteristic: BleakGATTCharacteristic, callback: NotifyCallback, **kwargs, ) -> None: """ Activate notifications/indications on a characteristic. """ characteristic = cast(BleakGATTCharacteristicBlueZDBus, characteristic) self._notification_callbacks[characteristic.path] = callback assert self._bus is not None reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, path=characteristic.path, interface=defs.GATT_CHARACTERISTIC_INTERFACE, member="StartNotify", ) ) assert_reply(reply) async def stop_notify( self, char_specifier: Union[BleakGATTCharacteristicBlueZDBus, int, str, UUID], ) -> None: """Deactivate notification/indication on a specified characteristic. Args: char_specifier (BleakGATTCharacteristicBlueZDBus, int, str or UUID): The characteristic to deactivate notification/indication on, specified by either integer handle, UUID or directly by the BleakGATTCharacteristicBlueZDBus object representing it. """ if not self.is_connected: raise BleakError("Not connected") if not isinstance(char_specifier, BleakGATTCharacteristicBlueZDBus): characteristic = self.services.get_characteristic(char_specifier) else: characteristic = char_specifier if not characteristic: raise BleakCharacteristicNotFoundError(char_specifier) reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, path=characteristic.path, interface=defs.GATT_CHARACTERISTIC_INTERFACE, member="StopNotify", ) ) assert_reply(reply) self._notification_callbacks.pop(characteristic.path, None) bleak-0.22.3/bleak/backends/bluezdbus/defs.py000066400000000000000000000103051470032643600210020ustar00rootroot00000000000000# -*- coding: utf-8 -*- from typing import Dict, List, Literal, Tuple, TypedDict # 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" # 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[ 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", ] ] 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 bleak-0.22.3/bleak/backends/bluezdbus/descriptor.py000066400000000000000000000025551470032643600222470ustar00rootroot00000000000000from ..descriptor import BleakGATTDescriptor from .defs import GattDescriptor1 class BleakGATTDescriptorBlueZDBus(BleakGATTDescriptor): """GATT Descriptor implementation for BlueZ DBus backend""" def __init__( self, obj: GattDescriptor1, object_path: str, characteristic_uuid: str, characteristic_handle: int, ): super(BleakGATTDescriptorBlueZDBus, self).__init__(obj) self.__path = object_path self.__characteristic_uuid = characteristic_uuid self.__characteristic_handle = characteristic_handle self.__handle = int(self.path.split("/")[-1].replace("desc", ""), 16) @property def characteristic_handle(self) -> int: """Handle for the characteristic that this descriptor belongs to""" return self.__characteristic_handle @property def characteristic_uuid(self) -> str: """UUID for the characteristic that this descriptor belongs to""" return self.__characteristic_uuid @property def uuid(self) -> str: """UUID for this descriptor""" return self.obj["UUID"] @property def handle(self) -> int: """Integer handle for this descriptor""" return self.__handle @property def path(self) -> str: """The DBus path. Mostly needed by `bleak`, not by end user""" return self.__path bleak-0.22.3/bleak/backends/bluezdbus/manager.py000066400000000000000000001134641470032643600215050ustar00rootroot00000000000000""" BlueZ D-Bus manager module -------------------------- This module contains code for the global BlueZ D-Bus object manager that is used internally by Bleak. """ import asyncio import contextlib import logging import os from collections import defaultdict from typing import ( Any, Callable, Coroutine, Dict, List, MutableMapping, NamedTuple, Optional, Set, cast, ) from weakref import WeakKeyDictionary from dbus_fast import BusType, Message, MessageType, Variant, unpack_variants from dbus_fast.aio.message_bus import MessageBus from ...exc import BleakDBusError, BleakError from ..service import BleakGATTServiceCollection from . import defs from .advertisement_monitor import AdvertisementMonitor, OrPatternLike from .characteristic import BleakGATTCharacteristicBlueZDBus from .defs import Device1, GattCharacteristic1, GattDescriptor1, GattService1 from .descriptor import BleakGATTDescriptorBlueZDBus from .service import BleakGATTServiceBlueZDBus from .signals import MatchRules, add_match from .utils import ( assert_reply, device_path_from_characteristic_path, get_dbus_authenticator, ) 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", } class BlueZManager: """ BlueZ D-Bus object manager. Use :func:`bleak.backends.bluezdbus.get_global_bluez_manager` to get the global instance. """ def __init__(self): 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()) await bus.connect() try: # Add signal listeners bus.add_message_handler(self._parse_msg) 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 # 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: BleakError: if there are no Bluetooth adapters or if none of the adapters are powered """ if not any(self._adapters): raise BleakError("No Bluetooth adapters found.") for adapter_path in self._adapters: if cast( defs.Adapter1, self._properties[adapter_path][defs.ADAPTER_INTERFACE] )["Powered"]: return adapter_path raise BleakError("No powered Bluetooth adapters found.") async def active_scan( self, adapter_path: str, filters: Dict[str, Variant], advertisement_callback: AdvertisementCallback, device_removed_callback: DeviceRemovedCallback, ) -> Callable[[], Coroutine]: """ 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: # 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: 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 != "org.bluez.Error.NotReady": 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]: """ 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: # 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: 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 = BleakGATTServiceBlueZDBus(service_props, service_path) 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 = BleakGATTCharacteristicBlueZDBus( char_props, char_path, service.uuid, service.handle, # "MTU" property was added in BlueZ 5.62, otherwise fall # back to minimum MTU according to Bluetooth spec. lambda: char_props.get("MTU", 23) - 3, ) 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 = BleakGATTDescriptorBlueZDBus( desc_props, desc_path, char.uuid, char.handle, ) 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 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 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 _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: 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 callback in callbacks: name = callback.property_name if name in changed: callback.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()) _global_instances: MutableMapping[Any, BlueZManager] = WeakKeyDictionary() 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: instance = _global_instances[loop] = BlueZManager() await instance.async_init() return instance bleak-0.22.3/bleak/backends/bluezdbus/scanner.py000066400000000000000000000223171470032643600215200ustar00rootroot00000000000000import logging from typing import Callable, Coroutine, Dict, List, Literal, Optional, TypedDict from warnings import warn from dbus_fast import Variant from ...exc import BleakError from ..scanner import AdvertisementData, AdvertisementDataCallback, BaseBleakScanner from .advertisement_monitor import OrPatternLike from .defs import Device1 from .manager import get_global_bluez_manager from .utils import bdaddr_from_device_path logger = logging.getLogger(__name__) 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 BlueZScannerArgs(TypedDict, total=False): """ :class:`BleakScanner` args that are specific to the BlueZ backend. """ 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 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. **adapter (str): Bluetooth adapter to use for discovery. """ def __init__( self, detection_callback: Optional[AdvertisementDataCallback], service_uuids: Optional[List[str]], scanning_mode: Literal["active", "passive"], *, bluez: BlueZScannerArgs, **kwargs, ): super(BleakScannerBlueZDBus, self).__init__(detection_callback, service_uuids) self._scanning_mode = scanning_mode # kwarg "device" is for backwards compatibility self._adapter: Optional[str] = kwargs.get("adapter", kwargs.get("device")) # callback from manager for stopping scanning if it has been started self._stop: Optional[Callable[[], Coroutine]] = 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 = kwargs.get("filters") if filters is None: filters = bluez.get("filters") else: warn( "the 'filters' kwarg is deprecated, use 'bluez' kwarg instead", FutureWarning, stacklevel=2, ) 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") 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": 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, ) async def stop(self) -> None: if self._stop: # avoid reentrancy stop, self._stop = self._stop, None await stop() def set_scanning_filter(self, **kwargs) -> 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( props["Address"], 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: bdaddr = bdaddr_from_device_path(device_path) del self.seen_devices[bdaddr] 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 bleak-0.22.3/bleak/backends/bluezdbus/service.py000066400000000000000000000025451470032643600215300ustar00rootroot00000000000000from typing import Any, List from ..service import BleakGATTService from .characteristic import BleakGATTCharacteristicBlueZDBus from .utils import extract_service_handle_from_path class BleakGATTServiceBlueZDBus(BleakGATTService): """GATT Service implementation for the BlueZ DBus backend""" def __init__(self, obj: Any, path: str): super().__init__(obj) self.__characteristics = [] self.__path = path self.__handle = extract_service_handle_from_path(path) @property def uuid(self) -> str: """The UUID to this service""" return self.obj["UUID"] @property def handle(self) -> int: """The integer handle of this service""" return self.__handle @property def characteristics(self) -> List[BleakGATTCharacteristicBlueZDBus]: """List of characteristics for this service""" return self.__characteristics def add_characteristic( self, characteristic: BleakGATTCharacteristicBlueZDBus ) -> None: """Add a :py:class:`~BleakGATTCharacteristicBlueZDBus` to the service. Should not be used by end user, but rather by `bleak` itself. """ self.__characteristics.append(characteristic) @property def path(self) -> str: """The DBus path. Mostly needed by `bleak`, not by end user""" return self.__path bleak-0.22.3/bleak/backends/bluezdbus/signals.py000066400000000000000000000136141470032643600215270ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import annotations import re from typing import Any, Coroutine, Dict, 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, ): assert_bus_name_valid(type) self.type: str = type if sender: assert_bus_name_valid(sender) self.sender: str = sender else: self.sender = None if interface: assert_interface_name_valid(interface) self.interface: str = interface else: self.interface = None if member: assert_member_name_valid(member) self.member: str = member else: self.member = None if path: assert_object_path_valid(path) self.path: str = path else: self.path = None if path_namespace: assert_object_path_valid(path_namespace) self.path_namespace: 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: str = destination else: self.destination = None if arg0namespace: assert_bus_name_valid(arg0namespace) self.arg0namespace: 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: 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})" def add_match(bus: MessageBus, rules: MatchRules) -> Coroutine[Any, Any, Message]: """Calls org.freedesktop.DBus.AddMatch using ``rules``.""" return bus.call( Message( destination="org.freedesktop.DBus", interface="org.freedesktop.DBus", path="/org/freedesktop/DBus", member="AddMatch", signature="s", body=[str(rules)], ) ) def remove_match(bus: MessageBus, rules: MatchRules) -> Coroutine[Any, Any, Message]: """Calls org.freedesktop.DBus.RemoveMatch using ``rules``.""" return bus.call( Message( destination="org.freedesktop.DBus", interface="org.freedesktop.DBus", path="/org/freedesktop/DBus", member="RemoveMatch", signature="s", body=[str(rules)], ) ) bleak-0.22.3/bleak/backends/bluezdbus/utils.py000066400000000000000000000034771470032643600212350ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os from typing import Optional from dbus_fast.auth import AuthExternal from dbus_fast.constants import MessageType from dbus_fast.message import Message from ...exc import BleakDBusError, BleakError 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: raise BleakDBusError(reply.error_name, reply.body) assert reply.message_type == MessageType.METHOD_RETURN 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 bdaddr_from_device_path(device_path: str) -> str: """ Scrape the Bluetooth address from a D-Bus device path. Args: device_path: The D-Bus object path of the device. Returns: A Bluetooth address as a string. """ return ":".join(device_path[-17:].split("_")) 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[:37] 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 bleak-0.22.3/bleak/backends/bluezdbus/version.py000066400000000000000000000043741470032643600215570ustar00rootroot00000000000000import asyncio import contextlib import logging import re from typing import Optional logger = logging.getLogger(__name__) async def _get_bluetoothctl_version() -> Optional[re.Match]: """Get the version of bluetoothctl.""" with contextlib.suppress(Exception): proc = await asyncio.create_subprocess_exec( "bluetoothctl", "--version", stdout=asyncio.subprocess.PIPE ) 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 can_write_without_response = True write_without_response_workaround_needed = False hides_battery_characteristic = True hides_device_name_characteristic = 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 >= 34 cls.can_write_without_response = major == 5 and minor >= 46 cls.write_without_response_workaround_needed = not ( major == 5 and minor >= 51 ) cls.hides_battery_characteristic = major == 5 and minor >= 48 and minor < 55 cls.hides_device_name_characteristic = major == 5 and minor >= 48 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. logging.warning( "Could not determine BlueZ version, bluetoothctl not available, assuming 5.51+" ) cls._check_bluez_event.set() cls.checked_bluez_version = True bleak-0.22.3/bleak/backends/characteristic.py000066400000000000000000000101431470032643600210520ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Interface class for the Bleak representation of a GATT Characteristic Created on 2019-03-19 by hbldh """ import abc import enum from typing import Any, Callable, List, Union from uuid import UUID from ..uuids import uuidstr_to_str from .descriptor import BleakGATTDescriptor 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(abc.ABC): """Interface for the Bleak representation of a GATT Characteristic""" def __init__(self, obj: Any, max_write_without_response_size: Callable[[], int]): """ 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. """ self.obj = obj self._max_write_without_response_size = max_write_without_response_size def __str__(self): return f"{self.uuid} (Handle: {self.handle}): {self.description}" @property @abc.abstractmethod def service_uuid(self) -> str: """The UUID of the Service containing this characteristic""" raise NotImplementedError() @property @abc.abstractmethod def service_handle(self) -> int: """The integer handle of the Service containing this characteristic""" raise NotImplementedError() @property @abc.abstractmethod def handle(self) -> int: """The handle for this characteristic""" raise NotImplementedError() @property @abc.abstractmethod def uuid(self) -> str: """The UUID for this characteristic""" raise NotImplementedError() @property def description(self) -> str: """Description for this characteristic""" return uuidstr_to_str(self.uuid) @property @abc.abstractmethod def properties(self) -> List[str]: """Properties of this characteristic""" raise NotImplementedError() @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 @abc.abstractmethod def descriptors(self) -> List[BleakGATTDescriptor]: """List of descriptors for this service""" raise NotImplementedError() @abc.abstractmethod def get_descriptor( self, specifier: Union[int, str, UUID] ) -> Union[BleakGATTDescriptor, None]: """Get a descriptor by handle (int) or UUID (str or uuid.UUID)""" raise NotImplementedError() @abc.abstractmethod 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. """ raise NotImplementedError() bleak-0.22.3/bleak/backends/client.py000066400000000000000000000202231470032643600173400ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Base class for backend clients. Created on 2018-04-23 by hbldh """ import abc import asyncio import os import platform import sys import uuid from typing import Callable, Optional, Type, Union from warnings import warn if sys.version_info < (3, 12): from typing_extensions import Buffer else: from collections.abc import Buffer from ..exc import BleakError from .characteristic import BleakGATTCharacteristic from .device import BLEDevice from .service import BleakGATTServiceCollection 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. Defaults to 10.0. 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): 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.get("timeout", 10.0) self._disconnected_callback: Optional[Callable[[], None]] = kwargs.get( "disconnected_callback" ) @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 ) -> 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, **kwargs) -> bool: """Connect to the specified GATT server. Returns: Boolean representing connection status. """ raise NotImplementedError() @abc.abstractmethod async def disconnect(self) -> bool: """Disconnect from the specified GATT server. Returns: Boolean representing connection status. """ raise NotImplementedError() @abc.abstractmethod async def pair(self, *args, **kwargs) -> bool: """Pair with the peripheral.""" raise NotImplementedError() @abc.abstractmethod async def unpair(self) -> bool: """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() class _DeprecatedIsConnectedReturn: """Wrapper for ``is_connected`` return value to provide deprecation warning.""" def __init__(self, value: bool): self._value = value def __bool__(self): return self._value def __call__(self) -> bool: warn( "is_connected has been changed to a property. Calling it as an async method will be removed in a future version", FutureWarning, stacklevel=2, ) f = asyncio.Future() f.set_result(self._value) return f def __repr__(self) -> str: return repr(self._value) # GATT services methods @abc.abstractmethod async def get_services(self, **kwargs) -> BleakGATTServiceCollection: """Get all services registered for this GATT server. Returns: A :py:class:`bleak.backends.service.BleakGATTServiceCollection` with this device's services tree. """ raise NotImplementedError() # I/O methods @abc.abstractmethod async def read_gatt_char( self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], **kwargs, ) -> bytearray: """Perform read operation on the specified GATT characteristic. Args: char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to read from, specified by either integer handle, UUID or directly by the BleakGATTCharacteristic object representing it. Returns: (bytearray) The read data. """ raise NotImplementedError() @abc.abstractmethod async def read_gatt_descriptor(self, handle: int, **kwargs) -> bytearray: """Perform read operation on the specified GATT descriptor. Args: handle (int): The handle of the descriptor to read from. Returns: (bytearray) The read data. """ raise NotImplementedError() @abc.abstractmethod async def write_gatt_char( self, characteristic: BleakGATTCharacteristic, data: Buffer, 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, handle: int, data: Buffer) -> 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). """ raise NotImplementedError() @abc.abstractmethod async def start_notify( self, characteristic: BleakGATTCharacteristic, callback: NotifyCallback, **kwargs, ) -> 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, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID] ) -> None: """Deactivate notification/indication on a specified characteristic. Args: char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to deactivate notification/indication on, specified by either integer handle, UUID or directly by the BleakGATTCharacteristic object representing it. """ raise NotImplementedError() def get_platform_client_backend_type() -> Type[BaseBleakClient]: """ Gets the platform-specific :class:`BaseBleakClient` type. """ if os.environ.get("P4A_BOOTSTRAP") is not None: from bleak.backends.p4android.client import BleakClientP4Android return BleakClientP4Android if platform.system() == "Linux": from bleak.backends.bluezdbus.client import BleakClientBlueZDBus return BleakClientBlueZDBus if platform.system() == "Darwin": from bleak.backends.corebluetooth.client import BleakClientCoreBluetooth return BleakClientCoreBluetooth if platform.system() == "Windows": from bleak.backends.winrt.client import BleakClientWinRT return BleakClientWinRT raise BleakError(f"Unsupported platform: {platform.system()}") bleak-0.22.3/bleak/backends/corebluetooth/000077500000000000000000000000001470032643600203675ustar00rootroot00000000000000bleak-0.22.3/bleak/backends/corebluetooth/CentralManagerDelegate.py000066400000000000000000000314231470032643600252620ustar00rootroot00000000000000""" CentralManagerDelegate will implement the CBCentralManagerDelegate protocol to manage CoreBluetooth services and resources on the Central End Created on June, 25 2019 by kevincar """ import asyncio import logging import sys import threading from typing import Any, Callable, Dict, List, Optional if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout else: from asyncio import timeout as async_timeout import objc from CoreBluetooth import ( CBUUID, CBCentralManager, CBManagerStatePoweredOff, CBManagerStatePoweredOn, CBManagerStateResetting, CBManagerStateUnauthorized, CBManagerStateUnknown, CBManagerStateUnsupported, CBPeripheral, ) from Foundation import ( NSUUID, NSArray, NSDictionary, NSError, NSKeyValueChangeNewKey, NSKeyValueObservingOptionNew, NSNumber, NSObject, NSString, ) from libdispatch import DISPATCH_QUEUE_SERIAL, dispatch_queue_create from ...exc import BleakError logger = logging.getLogger(__name__) CBCentralManagerDelegate = objc.protocolNamed("CBCentralManagerDelegate") DisconnectCallback = Callable[[], None] class CentralManagerDelegate(NSObject): """macOS conforming python class for managing the CentralManger for BLE""" ___pyobjc_protocols__ = [CBCentralManagerDelegate] def init(self) -> Optional["CentralManagerDelegate"]: """macOS init function for NSObject""" self = objc.super(CentralManagerDelegate, self).init() if self is None: return None self.event_loop = asyncio.get_running_loop() self._connect_futures: Dict[NSUUID, asyncio.Future] = {} self.callbacks: Dict[ int, Callable[[CBPeripheral, Dict[str, Any], int], None] ] = {} self._disconnect_callbacks: Dict[NSUUID, DisconnectCallback] = {} self._disconnect_futures: Dict[NSUUID, asyncio.Future] = {} self._did_update_state_event = threading.Event() self.central_manager = CBCentralManager.alloc().initWithDelegate_queue_( self, dispatch_queue_create(b"bleak.corebluetooth", DISPATCH_QUEUE_SERIAL) ) # 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. # It doesn't take long for the callback to occur, so we should be able # to do a blocking wait here without anyone complaining. self._did_update_state_event.wait(1) if self.central_manager.state() == CBManagerStateUnsupported: raise BleakError("BLE is unsupported") if self.central_manager.state() == CBManagerStateUnauthorized: raise BleakError("BLE is not authorized - check macOS privacy settings") if self.central_manager.state() != CBManagerStatePoweredOn: raise BleakError("Bluetooth device is turned off") # isScanning property was added in 10.13 if objc.macos_available(10, 13): self.central_manager.addObserver_forKeyPath_options_context_( self, "isScanning", NSKeyValueObservingOptionNew, 0 ) self._did_start_scanning_event: Optional[asyncio.Event] = None self._did_stop_scanning_event: Optional[asyncio.Event] = None return self def __del__(self) -> None: if objc.macos_available(10, 13): try: self.central_manager.removeObserver_forKeyPath_(self, "isScanning") except IndexError: # If self.init() raised an exception before calling # addObserver_forKeyPath_options_context_, attempting # to remove the observer will fail with IndexError pass # User defined functions @objc.python_method async def start_scan(self, service_uuids: Optional[List[str]]) -> None: service_uuids = ( NSArray.alloc().initWithArray_( list(map(CBUUID.UUIDWithString_, service_uuids)) ) if service_uuids else None ) self.central_manager.scanForPeripheralsWithServices_options_( service_uuids, None ) # The `isScanning` property was added in macOS 10.13, so before that # just waiting some will have to do. if objc.macos_available(10, 13): event = asyncio.Event() self._did_start_scanning_event = event if not self.central_manager.isScanning(): await event.wait() else: await asyncio.sleep(0.1) @objc.python_method async def stop_scan(self) -> None: self.central_manager.stopScan() # The `isScanning` property was added in macOS 10.13, so before that # just waiting some will have to do. if objc.macos_available(10, 13): event = asyncio.Event() self._did_stop_scanning_event = event if self.central_manager.isScanning(): await event.wait() else: await asyncio.sleep(0.1) @objc.python_method 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 @objc.python_method 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()] @objc.python_method def _changed_is_scanning(self, is_scanning: bool) -> None: if is_scanning: if self._did_start_scanning_event: self._did_start_scanning_event.set() else: if self._did_stop_scanning_event: self._did_stop_scanning_event.set() def observeValueForKeyPath_ofObject_change_context_( self, keyPath: NSString, object: Any, change: NSDictionary, context: int ) -> None: logger.debug("'%s' changed", keyPath) if keyPath != "isScanning": return is_scanning = bool(change[NSKeyValueChangeNewKey]) self.event_loop.call_soon_threadsafe(self._changed_is_scanning, is_scanning) # Protocol Functions def centralManagerDidUpdateState_(self, centralManager: CBCentralManager) -> None: logger.debug("centralManagerDidUpdateState_") if centralManager.state() == CBManagerStateUnknown: logger.debug("Cannot detect bluetooth device") elif centralManager.state() == CBManagerStateResetting: logger.debug("Bluetooth is resetting") elif centralManager.state() == CBManagerStateUnsupported: logger.debug("Bluetooth is unsupported") elif centralManager.state() == CBManagerStateUnauthorized: logger.debug("Bluetooth is unauthorized") elif centralManager.state() == CBManagerStatePoweredOff: logger.debug("Bluetooth powered off") elif centralManager.state() == CBManagerStatePoweredOn: logger.debug("Bluetooth powered on") self._did_update_state_event.set() @objc.python_method def did_discover_peripheral( self, central: CBCentralManager, peripheral: CBPeripheral, advertisementData: NSDictionary, 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(): if callback: callback(peripheral, advertisementData, RSSI) logger.debug( "Discovered device %s: %s @ RSSI: %d (kCBAdvData %r) and Central: %r", uuid_string, peripheral.name(), RSSI, advertisementData.keys(), central, ) def centralManager_didDiscoverPeripheral_advertisementData_RSSI_( self, central: CBCentralManager, peripheral: CBPeripheral, advertisementData: NSDictionary, RSSI: NSNumber, ) -> None: logger.debug("centralManager_didDiscoverPeripheral_advertisementData_RSSI_") self.event_loop.call_soon_threadsafe( self.did_discover_peripheral, central, peripheral, advertisementData, RSSI, ) @objc.python_method 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 centralManager_didConnectPeripheral_( self, central: CBCentralManager, peripheral: CBPeripheral ) -> None: logger.debug("centralManager_didConnectPeripheral_") self.event_loop.call_soon_threadsafe( self.did_connect_peripheral, central, peripheral, ) @objc.python_method 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 centralManager_didFailToConnectPeripheral_error_( self, centralManager: CBCentralManager, peripheral: CBPeripheral, error: Optional[NSError], ) -> None: logger.debug("centralManager_didFailToConnectPeripheral_error_") self.event_loop.call_soon_threadsafe( self.did_fail_to_connect_peripheral, centralManager, peripheral, error, ) @objc.python_method 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() def centralManager_didDisconnectPeripheral_error_( self, central: CBCentralManager, peripheral: CBPeripheral, error: Optional[NSError], ) -> None: logger.debug("centralManager_didDisconnectPeripheral_error_") self.event_loop.call_soon_threadsafe( self.did_disconnect_peripheral, central, peripheral, error, ) bleak-0.22.3/bleak/backends/corebluetooth/PeripheralDelegate.py000066400000000000000000000520671470032643600245010ustar00rootroot00000000000000""" PeripheralDelegate Created by kevincar """ from __future__ import annotations import asyncio import itertools import logging import sys from typing import Any, Dict, Iterable, NewType, Optional if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout else: from asyncio import timeout as async_timeout import objc from CoreBluetooth import ( CBCharacteristic, CBCharacteristicWriteWithResponse, CBDescriptor, CBPeripheral, CBService, ) from Foundation import NSUUID, NSArray, NSData, NSError, NSNumber, NSObject, NSString from ...exc import BleakError from ..client import NotifyCallback # logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) CBPeripheralDelegate = objc.protocolNamed("CBPeripheralDelegate") CBCharacteristicWriteType = NewType("CBCharacteristicWriteType", int) class PeripheralDelegate(NSObject): """macOS conforming python class for managing the PeripheralDelegate for BLE""" ___pyobjc_protocols__ = [CBPeripheralDelegate] def initWithPeripheral_( self, peripheral: CBPeripheral ) -> Optional[PeripheralDelegate]: """macOS init function for NSObject""" self = objc.super(PeripheralDelegate, self).init() if self is None: return None self.peripheral = peripheral self.peripheral.setDelegate_(self) 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] = {} self._characteristic_descriptor_discover_futures: Dict[int, asyncio.Future] = {} self._characteristic_read_futures: Dict[int, asyncio.Future] = {} self._characteristic_write_futures: Dict[int, asyncio.Future] = {} self._descriptor_read_futures: Dict[int, asyncio.Future] = {} self._descriptor_write_futures: Dict[int, asyncio.Future] = {} self._characteristic_notify_change_futures: Dict[int, asyncio.Future] = {} self._characteristic_notify_callbacks: Dict[int, NotifyCallback] = {} self._read_rssi_futures: Dict[NSUUID, asyncio.Future] = {} return self @objc.python_method def futures(self) -> Iterable[asyncio.Future]: """ 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(), ) @objc.python_method async def discover_services(self, services: Optional[NSArray]) -> NSArray: 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 @objc.python_method async def discover_characteristics(self, service: CBService) -> NSArray: 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()] @objc.python_method async def discover_descriptors(self, characteristic: CBCharacteristic) -> NSArray: 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() @objc.python_method async def read_characteristic( self, characteristic: CBCharacteristic, use_cached: bool = True, timeout: int = 20, ) -> NSData: if characteristic.value() is not None and use_cached: return characteristic.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()] @objc.python_method async def read_descriptor( self, descriptor: CBDescriptor, use_cached: bool = True ) -> Any: if descriptor.value() is not None and use_cached: return descriptor.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()] @objc.python_method 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 ) @objc.python_method 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()] @objc.python_method async def start_notifications( self, characteristic: CBCharacteristic, callback: NotifyCallback ) -> 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 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] @objc.python_method 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) @objc.python_method async def read_rssi(self) -> NSNumber: 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 @objc.python_method def did_discover_services( self, peripheral: CBPeripheral, services: NSArray, 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 peripheral_didDiscoverServices_( self, peripheral: CBPeripheral, error: Optional[NSError] ) -> None: logger.debug("peripheral_didDiscoverServices_") self._event_loop.call_soon_threadsafe( self.did_discover_services, peripheral, peripheral.services(), error, ) @objc.python_method def did_discover_characteristics_for_service( self, peripheral: CBPeripheral, service: CBService, characteristics: NSArray, 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 peripheral_didDiscoverCharacteristicsForService_error_( self, peripheral: CBPeripheral, service: CBService, error: Optional[NSError] ) -> None: logger.debug("peripheral_didDiscoverCharacteristicsForService_error_") self._event_loop.call_soon_threadsafe( self.did_discover_characteristics_for_service, peripheral, service, service.characteristics(), error, ) @objc.python_method 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 peripheral_didDiscoverDescriptorsForCharacteristic_error_( self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: Optional[NSError], ) -> None: logger.debug("peripheral_didDiscoverDescriptorsForCharacteristic_error_") self._event_loop.call_soon_threadsafe( self.did_discover_descriptors_for_characteristic, peripheral, characteristic, error, ) @objc.python_method def did_update_value_for_characteristic( self, peripheral: CBPeripheral, characteristic: CBCharacteristic, value: NSData, error: Optional[NSError], ) -> None: c_handle = characteristic.handle() future = self._characteristic_read_futures.get(c_handle) # If there is no pending read request, then this must be a notification # (the same delegate callback is used by both). if not future: if error is None: notify_callback = self._characteristic_notify_callbacks.get(c_handle) if notify_callback: notify_callback(bytearray(value)) return if error is not None: exception = BleakError(f"Failed to read characteristic {c_handle}: {error}") future.set_exception(exception) else: logger.debug("Read characteristic value") future.set_result(value) def peripheral_didUpdateValueForCharacteristic_error_( self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: Optional[NSError], ) -> None: logger.debug("peripheral_didUpdateValueForCharacteristic_error_") self._event_loop.call_soon_threadsafe( self.did_update_value_for_characteristic, peripheral, characteristic, characteristic.value(), error, ) @objc.python_method def did_update_value_for_descriptor( self, peripheral: CBPeripheral, descriptor: CBDescriptor, value: NSObject, 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 = BleakError( f"Failed to read descriptor {descriptor.handle()}: {error}" ) future.set_exception(exception) else: logger.debug("Read descriptor value") future.set_result(value) def peripheral_didUpdateValueForDescriptor_error_( self, peripheral: CBPeripheral, descriptor: CBDescriptor, error: Optional[NSError], ) -> None: logger.debug("peripheral_didUpdateValueForDescriptor_error_") self._event_loop.call_soon_threadsafe( self.did_update_value_for_descriptor, peripheral, descriptor, descriptor.value(), error, ) @objc.python_method 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 = BleakError( f"Failed to write characteristic {characteristic.handle()}: {error}" ) future.set_exception(exception) else: logger.debug("Write Characteristic Value") future.set_result(None) def peripheral_didWriteValueForCharacteristic_error_( self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: Optional[NSError], ) -> None: logger.debug("peripheral_didWriteValueForCharacteristic_error_") self._event_loop.call_soon_threadsafe( self.did_write_value_for_characteristic, peripheral, characteristic, error, ) @objc.python_method 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 = BleakError( f"Failed to write descriptor {descriptor.handle()}: {error}" ) future.set_exception(exception) else: logger.debug("Write Descriptor Value") future.set_result(None) def peripheral_didWriteValueForDescriptor_error_( self, peripheral: CBPeripheral, descriptor: CBDescriptor, error: Optional[NSError], ) -> None: logger.debug("peripheral_didWriteValueForDescriptor_error_") self._event_loop.call_soon_threadsafe( self.did_write_value_for_descriptor, peripheral, descriptor, error, ) @objc.python_method 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 peripheral_didUpdateNotificationStateForCharacteristic_error_( self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: Optional[NSError], ) -> None: logger.debug("peripheral_didUpdateNotificationStateForCharacteristic_error_") self._event_loop.call_soon_threadsafe( self.did_update_notification_for_characteristic, peripheral, characteristic, error, ) @objc.python_method def did_read_rssi( self, peripheral: CBPeripheral, rssi: NSNumber, 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) # peripheral_didReadRSSI_error_ method is added dynamically later # Bleak currently doesn't use the callbacks below other than for debug logging @objc.python_method def did_update_name(self, peripheral: CBPeripheral, name: NSString) -> None: logger.debug(f"name of {peripheral.identifier()} changed to {name}") def peripheralDidUpdateName_(self, peripheral: CBPeripheral) -> None: logger.debug("peripheralDidUpdateName_") self._event_loop.call_soon_threadsafe( self.did_update_name, peripheral, peripheral.name() ) @objc.python_method def did_modify_services( self, peripheral: CBPeripheral, invalidated_services: NSArray ) -> None: logger.debug( f"{peripheral.identifier()} invalidated services: {invalidated_services}" ) def peripheral_didModifyServices_( self, peripheral: CBPeripheral, invalidatedServices: NSArray ) -> None: logger.debug("peripheral_didModifyServices_") self._event_loop.call_soon_threadsafe( self.did_modify_services, peripheral, invalidatedServices ) # peripheralDidUpdateRSSI:error: was deprecated and replaced with # peripheral:didReadRSSI:error: in macOS 10.13 if objc.macos_available(10, 13): def peripheral_didReadRSSI_error_( self: PeripheralDelegate, peripheral: CBPeripheral, rssi: NSNumber, error: Optional[NSError], ) -> None: logger.debug("peripheral_didReadRSSI_error_") self._event_loop.call_soon_threadsafe( self.did_read_rssi, peripheral, rssi, error ) objc.classAddMethod( PeripheralDelegate, b"peripheral:didReadRSSI:error:", peripheral_didReadRSSI_error_, ) else: def peripheralDidUpdateRSSI_error_( self: PeripheralDelegate, peripheral: CBPeripheral, error: Optional[NSError] ) -> None: logger.debug("peripheralDidUpdateRSSI_error_") self._event_loop.call_soon_threadsafe( self.did_read_rssi, peripheral, peripheral.RSSI(), error ) objc.classAddMethod( PeripheralDelegate, b"peripheralDidUpdateRSSI:error:", peripheralDidUpdateRSSI_error_, ) bleak-0.22.3/bleak/backends/corebluetooth/__init__.py000066400000000000000000000002241470032643600224760ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ __init__.py Created on 2017-11-19 by hbldh """ import objc objc.options.verbose = True bleak-0.22.3/bleak/backends/corebluetooth/characteristic.py000066400000000000000000000077541470032643600237460ustar00rootroot00000000000000""" Interface class for the Bleak representation of a GATT Characteristic Created on 2019-06-28 by kevincar """ from enum import Enum from typing import Callable, Dict, List, Optional, Tuple, Union from CoreBluetooth import CBCharacteristic from ..characteristic import BleakGATTCharacteristic from ..descriptor import BleakGATTDescriptor from .descriptor import BleakGATTDescriptorCoreBluetooth from .utils import cb_uuid_to_str class CBCharacteristicProperties(Enum): BROADCAST = 0x1 READ = 0x2 WRITE_WITHOUT_RESPONSE = 0x4 WRITE = 0x8 NOTIFY = 0x10 INDICATE = 0x20 AUTHENTICATED_SIGNED_WRITES = 0x40 EXTENDED_PROPERTIES = 0x80 NOTIFY_ENCRYPTION_REQUIRED = 0x100 INDICATE_ENCRYPTION_REQUIRED = 0x200 _GattCharacteristicsPropertiesEnum: Dict[Optional[int], Tuple[str, str]] = { None: ("None", "The characteristic doesn’t have any properties that apply"), 1: ("Broadcast".lower(), "The characteristic supports broadcasting"), 2: ("Read".lower(), "The characteristic is readable"), 4: ( "Write-Without-Response".lower(), "The characteristic supports Write Without Response", ), 8: ("Write".lower(), "The characteristic is writable"), 16: ("Notify".lower(), "The characteristic is notifiable"), 32: ("Indicate".lower(), "The characteristic is indicatable"), 64: ( "Authenticated-Signed-Writes".lower(), "The characteristic supports signed writes", ), 128: ( "Extended-Properties".lower(), "The ExtendedProperties Descriptor is present", ), 256: ("Reliable-Writes".lower(), "The characteristic supports reliable writes"), 512: ( "Writable-Auxiliaries".lower(), "The characteristic has writable auxiliaries", ), } class BleakGATTCharacteristicCoreBluetooth(BleakGATTCharacteristic): """GATT Characteristic implementation for the CoreBluetooth backend""" def __init__( self, obj: CBCharacteristic, max_write_without_response_size: Callable[[], int] ): super().__init__(obj, max_write_without_response_size) self.__descriptors: List[BleakGATTDescriptorCoreBluetooth] = [] # self.__props = obj.properties() self.__props: List[str] = [ _GattCharacteristicsPropertiesEnum[v][0] for v in [2**n for n in range(10)] if (self.obj.properties() & v) ] self._uuid: str = cb_uuid_to_str(self.obj.UUID()) @property def service_uuid(self) -> str: """The uuid of the Service containing this characteristic""" return cb_uuid_to_str(self.obj.service().UUID()) @property def service_handle(self) -> int: return int(self.obj.service().startHandle()) @property def handle(self) -> int: """Integer handle for this characteristic""" return int(self.obj.handle()) @property def uuid(self) -> str: """The uuid of this characteristic""" return self._uuid @property def properties(self) -> List[str]: """Properties of this characteristic""" return self.__props @property def descriptors(self) -> List[BleakGATTDescriptor]: """List of descriptors for this service""" return self.__descriptors def get_descriptor(self, specifier) -> Union[BleakGATTDescriptor, None]: """Get a descriptor by handle (int) or UUID (str or uuid.UUID)""" try: if isinstance(specifier, int): return next(filter(lambda x: x.handle == specifier, self.descriptors)) else: return next( filter(lambda x: x.uuid == str(specifier), self.descriptors) ) except StopIteration: return None def add_descriptor(self, descriptor: BleakGATTDescriptor): """Add a :py:class:`~BleakGATTDescriptor` to the characteristic. Should not be used by end user, but rather by `bleak` itself. """ self.__descriptors.append(descriptor) bleak-0.22.3/bleak/backends/corebluetooth/client.py000066400000000000000000000342211470032643600222210ustar00rootroot00000000000000""" BLE Client for CoreBluetooth on macOS Created on 2019-06-26 by kevincar """ import asyncio import logging import sys import uuid from typing import Optional, Set, Union if sys.version_info < (3, 12): from typing_extensions import Buffer else: from collections.abc import Buffer from CoreBluetooth import ( CBUUID, CBCharacteristicWriteWithoutResponse, CBCharacteristicWriteWithResponse, CBPeripheral, CBPeripheralStateConnected, ) from Foundation import NSArray, NSData from ... import BleakScanner from ...exc import ( BleakCharacteristicNotFoundError, BleakDeviceNotFoundError, BleakError, ) from ..characteristic import BleakGATTCharacteristic from ..client import BaseBleakClient, NotifyCallback from ..device import BLEDevice from ..service import BleakGATTServiceCollection from .CentralManagerDelegate import CentralManagerDelegate from .characteristic import BleakGATTCharacteristicCoreBluetooth from .descriptor import BleakGATTDescriptorCoreBluetooth from .PeripheralDelegate import PeripheralDelegate from .scanner import BleakScannerCoreBluetooth from .service import BleakGATTServiceCoreBluetooth from .utils import cb_uuid_to_str 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. Keyword Args: timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0. """ def __init__( self, address_or_ble_device: Union[BLEDevice, str], services: Optional[Set[str]] = None, **kwargs, ): super(BleakClientCoreBluetooth, self).__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.alloc().initWithArray_(list(map(CBUUID.UUIDWithString_, services))) if services else None ) def __str__(self) -> str: return "BleakClientCoreBluetooth ({})".format(self.address) async def connect(self, **kwargs) -> bool: """Connect to a specified Peripheral Keyword Args: timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0. Returns: Boolean representing connection status. """ 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: self._delegate = PeripheralDelegate.alloc().initWithPeripheral_( self._peripheral ) def disconnect_callback() -> None: # Ensure that `get_services` retrieves services again, rather # than using the cached object self.services = 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 logger.debug("CentralManagerDelegate at {}".format(manager)) logger.debug("Connecting to BLE device @ {}".format(self.address)) await manager.connect(self._peripheral, disconnect_callback, timeout=timeout) # Now get services await self.get_services() return True async def disconnect(self) -> bool: """Disconnect from the peripheral device""" if ( self._peripheral is None or self._peripheral.state() != CBPeripheralStateConnected ): return True await self._central_manager_delegate.disconnect(self._peripheral) return True @property def is_connected(self) -> bool: """Checks for current active connection""" return self._DeprecatedIsConnectedReturn( False if self._peripheral is None else self._peripheral.state() == CBPeripheralStateConnected ) @property 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. return ( self._peripheral.maximumWriteValueLengthForType_( CBCharacteristicWriteWithoutResponse ) + 3 ) async def pair(self, *args, **kwargs) -> bool: """Attempt to pair with a peripheral. .. note:: This is not available on macOS since there is not explicit method to do a pairing, Instead the docs state that it "auto-pairs" when trying to read a characteristic that requires encryption, something Bleak cannot do apparently. Reference: - `Apple Docs `_ - `Stack Overflow post #1 `_ - `Stack Overflow post #2 `_ Returns: Boolean regarding success of pairing. """ raise NotImplementedError("Pairing is not available in Core Bluetooth.") async def unpair(self) -> bool: """ Returns: """ raise NotImplementedError("Pairing is not available in Core Bluetooth.") async def get_services(self, **kwargs) -> 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...") cb_services = await self._delegate.discover_services(self._requested_services) for service in cb_services: serviceUUID = service.UUID().UUIDString() logger.debug( "Retrieving characteristics for service {}".format(serviceUUID) ) characteristics = await self._delegate.discover_characteristics(service) services.add_service(BleakGATTServiceCoreBluetooth(service)) for characteristic in characteristics: cUUID = characteristic.UUID().UUIDString() logger.debug( "Retrieving descriptors for characteristic {}".format(cUUID) ) descriptors = await self._delegate.discover_descriptors(characteristic) services.add_characteristic( BleakGATTCharacteristicCoreBluetooth( characteristic, lambda: self._peripheral.maximumWriteValueLengthForType_( CBCharacteristicWriteWithoutResponse ), ) ) for descriptor in descriptors: services.add_descriptor( BleakGATTDescriptorCoreBluetooth( descriptor, cb_uuid_to_str(characteristic.UUID()), int(characteristic.handle()), ) ) logger.debug("Services resolved for %s", str(self)) self.services = services return self.services async def read_gatt_char( self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], use_cached: bool = False, **kwargs, ) -> bytearray: """Perform read operation on the specified GATT characteristic. Args: char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to read from, specified by either integer handle, UUID or directly by the BleakGATTCharacteristic object representing it. use_cached (bool): `False` forces macOS 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 isinstance(char_specifier, BleakGATTCharacteristic): characteristic = self.services.get_characteristic(char_specifier) else: characteristic = char_specifier if not characteristic: raise BleakCharacteristicNotFoundError(char_specifier) output = await self._delegate.read_characteristic( characteristic.obj, use_cached=use_cached ) value = bytearray(output) logger.debug("Read Characteristic {0} : {1}".format(characteristic.uuid, value)) return value async def read_gatt_descriptor( self, handle: int, use_cached: bool = False, **kwargs ) -> 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. """ descriptor = self.services.get_descriptor(handle) if not descriptor: raise BleakError("Descriptor {} was not found!".format(handle)) output = await self._delegate.read_descriptor( descriptor.obj, use_cached=use_cached ) if isinstance( output, str ): # Sometimes a `pyobjc_unicode`or `__NSCFString` is returned and they can be used as regular Python strings. value = bytearray(output.encode("utf-8")) else: # _NSInlineData value = bytearray(output) # value.getBytes_length_(None, len(value)) logger.debug("Read Descriptor {0} : {1}".format(handle, value)) return value async def write_gatt_char( self, characteristic: BleakGATTCharacteristic, data: Buffer, response: bool, ) -> None: value = NSData.alloc().initWithBytes_length_(data, len(data)) await self._delegate.write_characteristic( characteristic.obj, value, ( CBCharacteristicWriteWithResponse if response else CBCharacteristicWriteWithoutResponse ), ) logger.debug(f"Write Characteristic {characteristic.uuid} : {data}") async def write_gatt_descriptor(self, handle: int, data: Buffer) -> 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). """ descriptor = self.services.get_descriptor(handle) if not descriptor: raise BleakError("Descriptor {} was not found!".format(handle)) value = NSData.alloc().initWithBytes_length_(data, len(data)) await self._delegate.write_descriptor(descriptor.obj, value) logger.debug("Write Descriptor {0} : {1}".format(handle, data)) async def start_notify( self, characteristic: BleakGATTCharacteristic, callback: NotifyCallback, **kwargs, ) -> None: """ Activate notifications/indications on a characteristic. """ assert self._delegate is not None await self._delegate.start_notifications(characteristic.obj, callback) async def stop_notify( self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID] ) -> None: """Deactivate notification/indication on a specified characteristic. Args: char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to deactivate notification/indication on, specified by either integer handle, UUID or directly by the BleakGATTCharacteristic object representing it. """ if not isinstance(char_specifier, BleakGATTCharacteristic): characteristic = self.services.get_characteristic(char_specifier) else: characteristic = char_specifier if not characteristic: raise BleakCharacteristicNotFoundError(char_specifier) await self._delegate.stop_notifications(characteristic.obj) async def get_rssi(self) -> int: """To get RSSI value in dBm of the connected Peripheral""" return int(await self._delegate.read_rssi()) bleak-0.22.3/bleak/backends/corebluetooth/descriptor.py000066400000000000000000000025211470032643600231170ustar00rootroot00000000000000""" Interface class for the Bleak representation of a GATT Descriptor Created on 2019-06-28 by kevincar """ from CoreBluetooth import CBDescriptor from ..corebluetooth.utils import cb_uuid_to_str from ..descriptor import BleakGATTDescriptor class BleakGATTDescriptorCoreBluetooth(BleakGATTDescriptor): """GATT Descriptor implementation for CoreBluetooth backend""" def __init__( self, obj: CBDescriptor, characteristic_uuid: str, characteristic_handle: int ): super(BleakGATTDescriptorCoreBluetooth, self).__init__(obj) self.obj: CBDescriptor = obj self.__characteristic_uuid: str = characteristic_uuid self.__characteristic_handle: int = characteristic_handle @property def characteristic_handle(self) -> int: """handle for the characteristic that this descriptor belongs to""" return self.__characteristic_handle @property def characteristic_uuid(self) -> str: """UUID for the characteristic that this descriptor belongs to""" return self.__characteristic_uuid @property def uuid(self) -> str: """UUID for this descriptor""" return cb_uuid_to_str(self.obj.UUID()) @property def handle(self) -> int: """Integer handle for this descriptor""" return int(self.obj.handle()) bleak-0.22.3/bleak/backends/corebluetooth/scanner.py000066400000000000000000000146571470032643600224070ustar00rootroot00000000000000import logging from typing import Any, Dict, List, Literal, Optional, TypedDict import objc from CoreBluetooth import CBPeripheral from Foundation import NSBundle from ...exc import BleakError from ..scanner import AdvertisementData, AdvertisementDataCallback, BaseBleakScanner from .CentralManagerDelegate import CentralManagerDelegate from .utils import cb_uuid_to_str logger = logging.getLogger(__name__) 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 `_. """ 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 ): super(BleakScannerCoreBluetooth, self).__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.alloc().init() 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" ) async def start(self) -> None: self.seen_devices = {} def callback(p: CBPeripheral, a: Dict[str, Any], r: int) -> None: service_uuids = [ cb_uuid_to_str(u) for u in a.get("kCBAdvDataServiceUUIDs", []) ] if not self.is_allowed_uuid(service_uuids): return # Process service data service_data_dict_raw = a.get("kCBAdvDataServiceData", {}) service_data = { cb_uuid_to_str(k): bytes(v) for k, v in service_data_dict_raw.items() } # Process manufacturer data into a more friendly format manufacturer_binary_data = a.get("kCBAdvDataManufacturerData") manufacturer_data = {} 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 # set tx_power data if available tx_power = a.get("kCBAdvDataTxPowerLevel") advertisement_data = AdvertisementData( local_name=a.get("kCBAdvDataLocalName"), manufacturer_data=manufacturer_data, service_data=service_data, service_uuids=service_uuids, tx_power=tx_power, rssi=r, platform_data=(p, a, r), ) if self._use_bdaddr: # HACK: retrieveAddressForPeripheral_ is undocumented but seems to do the trick address_bytes: bytes = ( self._manager.central_manager.retrieveAddressForPeripheral_(p) ) if address_bytes is None: logger.debug( "Could not get Bluetooth address for %s. Ignoring this device.", p.identifier().UUIDString(), ) address = address_bytes.hex(":").upper() else: address = p.identifier().UUIDString() device = self.create_or_update_device( address, p.name(), (p, self._manager.central_manager.delegate()), advertisement_data, ) self.call_detection_callbacks(device, advertisement_data) self._manager.callbacks[id(self)] = callback await self._manager.start_scan(self._service_uuids) async def stop(self) -> None: await self._manager.stop_scan() self._manager.callbacks.pop(id(self), None) def set_scanning_filter(self, **kwargs) -> None: """Set scanning filter for the scanner. .. note:: This is not implemented for macOS yet. Raises: ``NotImplementedError`` """ raise NotImplementedError( "Need to evaluate which macOS versions to support first..." ) # macOS specific methods @property def is_scanning(self): # TODO: Evaluate if newer macOS than 10.11 has isScanning. try: return self._manager.isScanning_ except Exception: return None bleak-0.22.3/bleak/backends/corebluetooth/service.py000066400000000000000000000027171470032643600224100ustar00rootroot00000000000000from typing import List from CoreBluetooth import CBService from ..service import BleakGATTService from .characteristic import BleakGATTCharacteristicCoreBluetooth from .utils import cb_uuid_to_str class BleakGATTServiceCoreBluetooth(BleakGATTService): """GATT Characteristic implementation for the CoreBluetooth backend""" def __init__(self, obj: CBService): super().__init__(obj) self.__characteristics: List[BleakGATTCharacteristicCoreBluetooth] = [] # N.B. the `startHandle` method of the CBService is an undocumented Core Bluetooth feature, # which Bleak takes advantage of in order to have a service handle to use. self.__handle: int = int(self.obj.startHandle()) @property def handle(self) -> int: """The integer handle of this service""" return self.__handle @property def uuid(self) -> str: """UUID for this service.""" return cb_uuid_to_str(self.obj.UUID()) @property def characteristics(self) -> List[BleakGATTCharacteristicCoreBluetooth]: """List of characteristics for this service""" return self.__characteristics def add_characteristic( self, characteristic: BleakGATTCharacteristicCoreBluetooth ) -> None: """Add a :py:class:`~BleakGATTCharacteristicCoreBluetooth` to the service. Should not be used by end user, but rather by `bleak` itself. """ self.__characteristics.append(characteristic) bleak-0.22.3/bleak/backends/corebluetooth/utils.py000066400000000000000000000024761470032643600221120ustar00rootroot00000000000000from CoreBluetooth import CBUUID from Foundation import NSData from ...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()) def _is_uuid_16bit_compatible(_uuid: str) -> bool: test_uuid = "0000ffff-0000-1000-8000-00805f9b34fb" test_int = _convert_uuid_to_int(test_uuid) uuid_int = _convert_uuid_to_int(_uuid) result_int = uuid_int & test_int return uuid_int == result_int def _convert_uuid_to_int(_uuid: str) -> int: UUID_cb = CBUUID.alloc().initWithString_(_uuid) UUID_data = UUID_cb.data() UUID_bytes = UUID_data.getBytes_length_(None, len(UUID_data)) UUID_int = int.from_bytes(UUID_bytes, byteorder="big") return UUID_int def _convert_int_to_uuid(i: int) -> str: UUID_bytes = i.to_bytes(length=16, byteorder="big") UUID_data = NSData.alloc().initWithBytes_length_(UUID_bytes, len(UUID_bytes)) UUID_cb = CBUUID.alloc().initWithData_(UUID_data) return UUID_cb.UUIDString().lower() bleak-0.22.3/bleak/backends/descriptor.py000066400000000000000000000077001470032643600202450ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Interface class for the Bleak representation of a GATT Descriptor Created on 2019-03-19 by hbldh """ import abc from typing import Any from ..uuids import normalize_uuid_16 _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(abc.ABC): """Interface for the Bleak representation of a GATT Descriptor""" def __init__(self, obj: Any): self.obj = obj def __str__(self): return f"{self.uuid} (Handle: {self.handle}): {self.description}" @property @abc.abstractmethod def characteristic_uuid(self) -> str: """UUID for the characteristic that this descriptor belongs to""" raise NotImplementedError() @property @abc.abstractmethod def characteristic_handle(self) -> int: """handle for the characteristic that this descriptor belongs to""" raise NotImplementedError() @property @abc.abstractmethod def uuid(self) -> str: """UUID for this descriptor""" raise NotImplementedError() @property @abc.abstractmethod def handle(self) -> int: """Integer handle for this descriptor""" raise NotImplementedError() @property def description(self) -> str: """A text description of what this descriptor represents""" return _descriptor_descriptions.get(self.uuid, ["Unknown"])[0] bleak-0.22.3/bleak/backends/device.py000066400000000000000000000043411470032643600173240ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Wrapper class for Bluetooth LE servers returned from calling :py:meth:`bleak.discover`. Created on 2018-04-23 by hbldh """ 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", "_rssi", "_metadata") def __init__( self, address: str, name: Optional[str], details: Any, rssi: int, **kwargs ): #: 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 # for backwards compatibility self._rssi = rssi self._metadata = kwargs @property def rssi(self) -> int: """ Gets the RSSI of the last received advertisement. .. deprecated:: 0.19.0 Use :class:`AdvertisementData` from detection callback or :attr:`BleakScanner.discovered_devices_and_advertisement_data` instead. """ warn( "BLEDevice.rssi is deprecated and will be removed in a future version of Bleak, use AdvertisementData.rssi instead", FutureWarning, stacklevel=2, ) return self._rssi @property def metadata(self) -> dict: """ Gets additional advertisement data for the device. .. deprecated:: 0.19.0 Use :class:`AdvertisementData` from detection callback or :attr:`BleakScanner.discovered_devices_and_advertisement_data` instead. """ warn( "BLEDevice.metadata is deprecated and will be removed in a future version of Bleak, use AdvertisementData instead", FutureWarning, stacklevel=2, ) return self._metadata def __str__(self): return f"{self.address}: {self.name}" def __repr__(self): return f"BLEDevice({self.address}, {self.name})" bleak-0.22.3/bleak/backends/p4android/000077500000000000000000000000001470032643600173755ustar00rootroot00000000000000bleak-0.22.3/bleak/backends/p4android/__init__.py000066400000000000000000000000001470032643600214740ustar00rootroot00000000000000bleak-0.22.3/bleak/backends/p4android/characteristic.py000066400000000000000000000061021470032643600227360ustar00rootroot00000000000000from typing import Callable, List, Union from uuid import UUID from ...exc import BleakError from ..characteristic import BleakGATTCharacteristic from ..descriptor import BleakGATTDescriptor from . import defs class BleakGATTCharacteristicP4Android(BleakGATTCharacteristic): """GATT Characteristic implementation for the python-for-android backend""" def __init__( self, java, service_uuid: str, service_handle: int, max_write_without_response_size: Callable[[], int], ): super(BleakGATTCharacteristicP4Android, self).__init__( java, max_write_without_response_size ) self.__uuid = self.obj.getUuid().toString() self.__handle = self.obj.getInstanceId() self.__service_uuid = service_uuid self.__service_handle = service_handle self.__descriptors = [] self.__notification_descriptor = None self.__properties = [ name for flag, name in defs.CHARACTERISTIC_PROPERTY_DBUS_NAMES.items() if flag & self.obj.getProperties() ] @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 int(self.__service_handle) @property def handle(self) -> int: """The handle of this characteristic""" return self.__handle @property def uuid(self) -> str: """The uuid of this characteristic""" return self.__uuid @property def properties(self) -> List[str]: """Properties of this characteristic""" return self.__properties @property def descriptors(self) -> List[BleakGATTDescriptor]: """List of descriptors for this service""" return self.__descriptors def get_descriptor( self, specifier: Union[str, UUID] ) -> Union[BleakGATTDescriptor, None]: """Get a descriptor by UUID (str or uuid.UUID)""" if isinstance(specifier, int): raise BleakError( "The Android Bluetooth API does not provide access to descriptor handles." ) matches = [ descriptor for descriptor in self.descriptors if descriptor.uuid == str(specifier) ] if len(matches) == 0: return None return matches[0] def add_descriptor(self, descriptor: BleakGATTDescriptor): """Add a :py:class:`~BleakGATTDescriptor` to the characteristic. Should not be used by end user, but rather by `bleak` itself. """ self.__descriptors.append(descriptor) if descriptor.uuid == defs.CLIENT_CHARACTERISTIC_CONFIGURATION_UUID: self.__notification_descriptor = descriptor @property def notification_descriptor(self) -> BleakGATTDescriptor: """The notification descriptor. Mostly needed by `bleak`, not by end user""" return self.__notification_descriptor bleak-0.22.3/bleak/backends/p4android/client.py000066400000000000000000000445511470032643600212360ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ BLE Client for python-for-android """ import asyncio import logging import uuid import warnings from typing import Optional, Set, Union from android.broadcast import BroadcastReceiver from jnius import java_method from ...exc import BleakCharacteristicNotFoundError, BleakError from ..characteristic import BleakGATTCharacteristic from ..client import BaseBleakClient, NotifyCallback from ..device import BLEDevice from ..service import BleakGATTServiceCollection from . import defs, utils from .characteristic import BleakGATTCharacteristicP4Android from .descriptor import BleakGATTDescriptorP4Android from .service import BleakGATTServiceP4Android 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(BleakClientP4Android, self).__init__(address_or_ble_device, **kwargs) self._requested_services = ( set(map(defs.UUID.fromString, services)) if services else None ) # kwarg "device" is for backwards compatibility self.__adapter = kwargs.get("adapter", kwargs.get("device", None)) self.__gatt = None self.__mtu = 23 def __del__(self): if self.__gatt is not None: self.__gatt.close() self.__gatt = None # Connectivity methods async def connect(self, **kwargs) -> bool: """Connect to the specified GATT server. Returns: Boolean representing connection status. """ loop = asyncio.get_running_loop() self.__adapter = defs.BluetoothAdapter.getDefaultAdapter() if self.__adapter is None: raise BleakError("Bluetooth is not supported on this hardware platform") if self.__adapter.getState() != defs.BluetoothAdapter.STATE_ON: raise BleakError("Bluetooth is not turned on") self.__device = self.__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 return True async def disconnect(self) -> bool: """Disconnect from the specified GATT server. Returns: Boolean representing if device is disconnected. """ 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 True # 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 return True async def pair(self, *args, **kwargs) -> bool: """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. Returns: Boolean regarding success of pairing. """ 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 True 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}" ) return await bondedFuture finally: await receiver.stop() async def unpair(self) -> bool: """Unpair with the peripheral. Returns: Boolean regarding success of unpairing. """ warnings.warn( "Unpairing is seemingly unavailable in the Android API at the moment." ) return False @property 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 def mtu_size(self) -> Optional[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 = BleakGATTServiceP4Android(java_service) services.add_service(service) for java_characteristic in java_service.getCharacteristics(): characteristic = BleakGATTCharacteristicP4Android( java_characteristic, service.uuid, service.handle, lambda: self.__mtu - 3, ) services.add_characteristic(characteristic) for descriptor_index, java_descriptor in enumerate( java_characteristic.getDescriptors() ): descriptor = BleakGATTDescriptorP4Android( java_descriptor, characteristic.uuid, characteristic.handle, descriptor_index, ) services.add_descriptor(descriptor) self.services = services return self.services # IO methods async def read_gatt_char( self, char_specifier: Union[BleakGATTCharacteristicP4Android, int, str, uuid.UUID], **kwargs, ) -> bytearray: """Perform read operation on the specified GATT characteristic. Args: char_specifier (BleakGATTCharacteristicP4Android, int, str or UUID): The characteristic to read from, specified by either integer handle, UUID or directly by the BleakGATTCharacteristicP4Android object representing it. Returns: (bytearray) The read data. """ if not isinstance(char_specifier, BleakGATTCharacteristicP4Android): characteristic = self.services.get_characteristic(char_specifier) else: characteristic = char_specifier if not characteristic: raise BleakCharacteristicNotFoundError(char_specifier) (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 async def read_gatt_descriptor( self, desc_specifier: Union[BleakGATTDescriptorP4Android, str, uuid.UUID], **kwargs, ) -> bytearray: """Perform read operation on the specified GATT descriptor. Args: desc_specifier (BleakGATTDescriptorP4Android, str or UUID): The descriptor to read from, specified by either UUID or directly by the BleakGATTDescriptorP4Android object representing it. Returns: (bytearray) The read data. """ if not isinstance(desc_specifier, BleakGATTDescriptorP4Android): descriptor = self.services.get_descriptor(desc_specifier) else: descriptor = desc_specifier if not descriptor: raise BleakError(f"Descriptor with UUID {desc_specifier} was not found!") (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 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}" ) async def write_gatt_descriptor( self, desc_specifier: Union[BleakGATTDescriptorP4Android, str, uuid.UUID], data: bytearray, ) -> None: """Perform a write operation on the specified GATT descriptor. Args: desc_specifier (BleakGATTDescriptorP4Android, str or UUID): The descriptor to write to, specified by either UUID or directly by the BleakGATTDescriptorP4Android object representing it. data (bytes or bytearray): The data to send. """ if not isinstance(desc_specifier, BleakGATTDescriptorP4Android): 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}" ) 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.notification_descriptor, defs.BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE, ) async def stop_notify( self, char_specifier: Union[BleakGATTCharacteristicP4Android, int, str, uuid.UUID], ) -> None: """Deactivate notification/indication on a specified characteristic. Args: char_specifier (BleakGATTCharacteristicP4Android, int, str or UUID): The characteristic to deactivate notification/indication on, specified by either integer handle, UUID or directly by the BleakGATTCharacteristicP4Android object representing it. """ if not isinstance(char_specifier, BleakGATTCharacteristicP4Android): characteristic = self.services.get_characteristic(char_specifier) else: characteristic = char_specifier if not characteristic: raise BleakCharacteristicNotFoundError(char_specifier) await self.write_gatt_descriptor( characteristic.notification_descriptor, 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)) bleak-0.22.3/bleak/backends/p4android/defs.py000066400000000000000000000072311470032643600206730ustar00rootroot00000000000000# -*- coding: utf-8 -*- 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", } CHARACTERISTIC_PROPERTY_DBUS_NAMES = { BluetoothGattCharacteristic.PROPERTY_BROADCAST: "broadcast", BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS: "extended-properties", BluetoothGattCharacteristic.PROPERTY_INDICATE: "indicate", BluetoothGattCharacteristic.PROPERTY_NOTIFY: "notify", BluetoothGattCharacteristic.PROPERTY_READ: "read", BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE: "authenticated-signed-writes", BluetoothGattCharacteristic.PROPERTY_WRITE: "write", BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE: "write-without-response", } CLIENT_CHARACTERISTIC_CONFIGURATION_UUID = normalize_uuid_16(0x2902) bleak-0.22.3/bleak/backends/p4android/descriptor.py000066400000000000000000000026641470032643600221350ustar00rootroot00000000000000from ..descriptor import BleakGATTDescriptor class BleakGATTDescriptorP4Android(BleakGATTDescriptor): """GATT Descriptor implementation for python-for-android backend""" def __init__( self, java, characteristic_uuid: str, characteristic_handle: int, index: int ): super(BleakGATTDescriptorP4Android, self).__init__(java) self.__uuid = self.obj.getUuid().toString() self.__characteristic_uuid = characteristic_uuid self.__characteristic_handle = characteristic_handle # many devices have sequential handles and this formula will mysteriously work for them # it's possible this formula could make duplicate handles on other devices. self.__fake_handle = self.__characteristic_handle + 1 + index @property def characteristic_handle(self) -> int: """handle for the characteristic that this descriptor belongs to""" return self.__characteristic_handle @property def characteristic_uuid(self) -> str: """UUID for the characteristic that this descriptor belongs to""" return self.__characteristic_uuid @property def uuid(self) -> str: """UUID for this descriptor""" return self.__uuid @property def handle(self) -> int: """Integer handle for this descriptor""" # 2021-01 The Android Bluetooth API does not appear to provide access to descriptor handles. return self.__fake_handle bleak-0.22.3/bleak/backends/p4android/java/000077500000000000000000000000001470032643600203165ustar00rootroot00000000000000bleak-0.22.3/bleak/backends/p4android/java/com/000077500000000000000000000000001470032643600210745ustar00rootroot00000000000000bleak-0.22.3/bleak/backends/p4android/java/com/github/000077500000000000000000000000001470032643600223565ustar00rootroot00000000000000bleak-0.22.3/bleak/backends/p4android/java/com/github/hbldh/000077500000000000000000000000001470032643600234375ustar00rootroot00000000000000bleak-0.22.3/bleak/backends/p4android/java/com/github/hbldh/bleak/000077500000000000000000000000001470032643600245155ustar00rootroot00000000000000bleak-0.22.3/bleak/backends/p4android/java/com/github/hbldh/bleak/PythonBluetoothGattCallback.java000066400000000000000000000055211470032643600327670ustar00rootroot00000000000000package 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); } } bleak-0.22.3/bleak/backends/p4android/java/com/github/hbldh/bleak/PythonScanCallback.java000066400000000000000000000016151470032643600310660ustar00rootroot00000000000000package 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); } } bleak-0.22.3/bleak/backends/p4android/recipes/000077500000000000000000000000001470032643600210275ustar00rootroot00000000000000bleak-0.22.3/bleak/backends/p4android/recipes/bleak/000077500000000000000000000000001470032643600221055ustar00rootroot00000000000000bleak-0.22.3/bleak/backends/p4android/recipes/bleak/__init__.py000066400000000000000000000037461470032643600242300ustar00rootroot00000000000000import 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() bleak-0.22.3/bleak/backends/p4android/recipes/bleak/fix_setup.py000066400000000000000000000003641470032643600244700ustar00rootroot00000000000000from 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")), ) bleak-0.22.3/bleak/backends/p4android/scanner.py000066400000000000000000000255711470032643600214120ustar00rootroot00000000000000# -*- coding: utf-8 -*- import asyncio import logging import sys import warnings from typing import List, Literal, Optional if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout else: from asyncio import timeout as async_timeout from android.broadcast import BroadcastReceiver from android.permissions import Permission, request_permissions from jnius import cast, java_method from ...exc import BleakError from ..scanner import AdvertisementData, AdvertisementDataCallback, BaseBleakScanner from . import defs, utils 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(BleakScannerP4Android, self).__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.__adapter = None self.__javascanner = None self.__callback = None def __del__(self) -> None: self.__stop() 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 self.__adapter = defs.BluetoothAdapter.getDefaultAdapter() if self.__adapter is None: raise BleakError("Bluetooth is not supported on this hardware platform") if self.__adapter.getState() != defs.BluetoothAdapter.STATE_ON: raise BleakError("Bluetooth is not turned on") self.__javascanner = self.__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: self.__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: self.__adapter.enable() await stateOnFuture finally: receiver.stop() logger.debug("restarting scan ...") return await self.start() 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") async def stop(self) -> None: self.__stop() def set_scanning_filter(self, **kwargs) -> None: # If we do end up implementing this, this should accept List # and ScanSettings java objects to pass to startScan(). raise NotImplementedError("not implemented in Android backend") 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.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) bleak-0.22.3/bleak/backends/p4android/service.py000066400000000000000000000022141470032643600214060ustar00rootroot00000000000000from typing import List from ..service import BleakGATTService from .characteristic import BleakGATTCharacteristicP4Android class BleakGATTServiceP4Android(BleakGATTService): """GATT Service implementation for the python-for-android backend""" def __init__(self, java): super().__init__(java) self.__uuid = self.obj.getUuid().toString() self.__handle = self.obj.getInstanceId() self.__characteristics = [] @property def uuid(self) -> str: """The UUID to this service""" return self.__uuid @property def handle(self) -> int: """A unique identifier for this service""" return self.__handle @property def characteristics(self) -> List[BleakGATTCharacteristicP4Android]: """List of characteristics for this service""" return self.__characteristics def add_characteristic(self, characteristic: BleakGATTCharacteristicP4Android): """Add a :py:class:`~BleakGATTCharacteristicP4Android` to the service. Should not be used by end user, but rather by `bleak` itself. """ self.__characteristics.append(characteristic) bleak-0.22.3/bleak/backends/p4android/utils.py000066400000000000000000000061641470032643600211160ustar00rootroot00000000000000# -*- coding: utf-8 -*- import asyncio import logging import warnings from jnius import PythonJavaClass from ...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 bleak-0.22.3/bleak/backends/scanner.py000066400000000000000000000233231470032643600175170ustar00rootroot00000000000000import abc import asyncio import inspect import os import platform from typing import ( Any, Callable, Coroutine, Dict, Hashable, List, NamedTuple, Optional, Set, Tuple, Type, ) from ..exc import BleakError from .device import BLEDevice # prevent tasks from being garbage collected _background_tasks: Set[asyncio.Task] = 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 """ Tuple of platform specific data. This is not a stable API. The actual values may change between releases. """ def __repr__(self) -> str: kwargs = [] 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. This map must be cleared when scanning starts. """ def __init__( self, detection_callback: Optional[AdvertisementDataCallback], service_uuids: Optional[List[str]], ): super(BaseBleakScanner, self).__init__() 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 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, address: str, name: str, details: Any, adv: AdvertisementData ) -> BLEDevice: """ Creates or updates a device in :attr:`seen_devices`. Args: 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. """ # for backwards compatibility, see https://github.com/hbldh/bleak/issues/1025 metadata = dict( uuids=adv.service_uuids, manufacturer_data=adv.manufacturer_data, ) try: device, _ = self.seen_devices[address] device.name = name device._rssi = adv.rssi device._metadata = metadata except KeyError: device = BLEDevice( address, name, details, adv.rssi, **metadata, ) self.seen_devices[address] = (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() @abc.abstractmethod def set_scanning_filter(self, **kwargs) -> None: """Set scanning filter for the BleakScanner. Args: **kwargs: The filter details. This will differ a lot between backend implementations. """ raise NotImplementedError() def get_platform_scanner_backend_type() -> Type[BaseBleakScanner]: """ Gets the platform-specific :class:`BaseBleakScanner` type. """ if os.environ.get("P4A_BOOTSTRAP") is not None: from bleak.backends.p4android.scanner import BleakScannerP4Android return BleakScannerP4Android if platform.system() == "Linux": from bleak.backends.bluezdbus.scanner import BleakScannerBlueZDBus return BleakScannerBlueZDBus if platform.system() == "Darwin": from bleak.backends.corebluetooth.scanner import BleakScannerCoreBluetooth return BleakScannerCoreBluetooth if platform.system() == "Windows": from bleak.backends.winrt.scanner import BleakScannerWinRT return BleakScannerWinRT raise BleakError(f"Unsupported platform: {platform.system()}") bleak-0.22.3/bleak/backends/service.py000066400000000000000000000157071470032643600175350ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Gatt Service Collection class and interface class for the Bleak representation of a GATT Service. Created on 2019-03-19 by hbldh """ import abc import logging from typing import Any, Dict, Iterator, List, Optional, Union from uuid import UUID from ..exc import BleakError from ..uuids import normalize_uuid_str, uuidstr_to_str from .characteristic import BleakGATTCharacteristic from .descriptor import BleakGATTDescriptor logger = logging.getLogger(__name__) class BleakGATTService(abc.ABC): """Interface for the Bleak representation of a GATT Service.""" def __init__(self, obj: Any) -> None: self.obj = obj def __str__(self) -> str: return f"{self.uuid} (Handle: {self.handle}): {self.description}" @property @abc.abstractmethod def handle(self) -> int: """The handle of this service""" raise NotImplementedError() @property @abc.abstractmethod def uuid(self) -> str: """The UUID to this service""" raise NotImplementedError() @property def description(self) -> str: """String description for this service""" return uuidstr_to_str(self.uuid) @property @abc.abstractmethod def characteristics(self) -> List[BleakGATTCharacteristic]: """List of characteristics for this service""" raise NotImplementedError() @abc.abstractmethod 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. """ raise NotImplementedError() 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)) except StopIteration: return None class BleakGATTServiceCollection: """Simple data container for storing the peripheral's service complement.""" def __init__(self) -> None: self.__services = {} self.__characteristics = {} self.__descriptors = {} 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(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) bleak-0.22.3/bleak/backends/winrt/000077500000000000000000000000001470032643600166545ustar00rootroot00000000000000bleak-0.22.3/bleak/backends/winrt/__init__.py000066400000000000000000000000001470032643600207530ustar00rootroot00000000000000bleak-0.22.3/bleak/backends/winrt/characteristic.py000066400000000000000000000110341470032643600222150ustar00rootroot00000000000000# -*- coding: utf-8 -*- import sys from typing import Callable, List, Union from uuid import UUID if sys.version_info >= (3, 12): from winrt.windows.devices.bluetooth.genericattributeprofile import ( GattCharacteristic, GattCharacteristicProperties, ) else: from bleak_winrt.windows.devices.bluetooth.genericattributeprofile import ( GattCharacteristic, GattCharacteristicProperties, ) from ..characteristic import BleakGATTCharacteristic from ..descriptor import BleakGATTDescriptor _GattCharacteristicsPropertiesMap = { GattCharacteristicProperties.NONE: ( "None", "The characteristic doesn’t have any properties that apply", ), GattCharacteristicProperties.BROADCAST: ( "Broadcast".lower(), "The characteristic supports broadcasting", ), GattCharacteristicProperties.READ: ( "Read".lower(), "The characteristic is readable", ), GattCharacteristicProperties.WRITE_WITHOUT_RESPONSE: ( "Write-Without-Response".lower(), "The characteristic supports Write Without Response", ), GattCharacteristicProperties.WRITE: ( "Write".lower(), "The characteristic is writable", ), GattCharacteristicProperties.NOTIFY: ( "Notify".lower(), "The characteristic is notifiable", ), GattCharacteristicProperties.INDICATE: ( "Indicate".lower(), "The characteristic is indicatable", ), GattCharacteristicProperties.AUTHENTICATED_SIGNED_WRITES: ( "Authenticated-Signed-Writes".lower(), "The characteristic supports signed writes", ), GattCharacteristicProperties.EXTENDED_PROPERTIES: ( "Extended-Properties".lower(), "The ExtendedProperties Descriptor is present", ), GattCharacteristicProperties.RELIABLE_WRITES: ( "Reliable-Writes".lower(), "The characteristic supports reliable writes", ), GattCharacteristicProperties.WRITABLE_AUXILIARIES: ( "Writable-Auxiliaries".lower(), "The characteristic has writable auxiliaries", ), } class BleakGATTCharacteristicWinRT(BleakGATTCharacteristic): """GATT Characteristic implementation for the .NET backend, implemented with WinRT""" def __init__( self, obj: GattCharacteristic, max_write_without_response_size: Callable[[], int], ): super().__init__(obj, max_write_without_response_size) self.__descriptors = [] self.__props = [ _GattCharacteristicsPropertiesMap[v][0] for v in [2**n for n in range(10)] if (self.obj.characteristic_properties & v) ] @property def service_uuid(self) -> str: """The uuid of the Service containing this characteristic""" return str(self.obj.service.uuid) @property def service_handle(self) -> int: """The integer handle of the Service containing this characteristic""" return int(self.obj.service.attribute_handle) @property def handle(self) -> int: """The handle of this characteristic""" return int(self.obj.attribute_handle) @property def uuid(self) -> str: """The uuid of this characteristic""" return str(self.obj.uuid) @property def description(self) -> str: """Description for this characteristic""" return ( self.obj.user_description if self.obj.user_description else super().description ) @property def properties(self) -> List[str]: """Properties of this characteristic""" return self.__props @property def descriptors(self) -> List[BleakGATTDescriptor]: """List of descriptors for this characteristic""" return self.__descriptors def get_descriptor( self, specifier: Union[int, str, UUID] ) -> Union[BleakGATTDescriptor, None]: """Get a descriptor by handle (int) or UUID (str or uuid.UUID)""" try: if isinstance(specifier, int): return next(filter(lambda x: x.handle == specifier, self.descriptors)) else: return next( filter(lambda x: x.uuid == str(specifier), self.descriptors) ) except StopIteration: return None def add_descriptor(self, descriptor: BleakGATTDescriptor): """Add a :py:class:`~BleakGATTDescriptor` to the characteristic. Should not be used by end user, but rather by `bleak` itself. """ self.__descriptors.append(descriptor) bleak-0.22.3/bleak/backends/winrt/client.py000066400000000000000000001202431470032643600205060ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ BLE Client for Windows 10 systems, implemented with WinRT. Created on 2020-08-19 by hbldh """ import asyncio import logging import sys import uuid import warnings from ctypes import WinError from typing import ( Any, Dict, List, Literal, Optional, Protocol, Sequence, Set, TypedDict, Union, cast, ) if sys.version_info < (3, 12): from typing_extensions import Buffer else: from collections.abc import Buffer if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout else: from asyncio import timeout as async_timeout if sys.version_info >= (3, 12): 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, DevicePairingKinds, DevicePairingResultStatus, DeviceUnpairingResultStatus, ) from winrt.windows.foundation import ( AsyncStatus, EventRegistrationToken, IAsyncOperation, ) from winrt.windows.storage.streams import Buffer as WinBuffer else: from bleak_winrt.windows.devices.bluetooth import ( BluetoothAddressType, BluetoothCacheMode, BluetoothError, BluetoothLEDevice, ) from bleak_winrt.windows.devices.bluetooth.genericattributeprofile import ( GattCharacteristic, GattCharacteristicProperties, GattClientCharacteristicConfigurationDescriptorValue, GattCommunicationStatus, GattDescriptor, GattDeviceService, GattSession, GattSessionStatus, GattSessionStatusChangedEventArgs, GattValueChangedEventArgs, GattWriteOption, ) from bleak_winrt.windows.devices.enumeration import ( DeviceInformation, DevicePairingKinds, DevicePairingResultStatus, DeviceUnpairingResultStatus, ) from bleak_winrt.windows.foundation import ( AsyncStatus, EventRegistrationToken, IAsyncOperation, ) from bleak_winrt.windows.storage.streams import Buffer as WinBuffer from ... import BleakScanner from ...exc import ( PROTOCOL_ERROR_CODES, BleakCharacteristicNotFoundError, BleakDeviceNotFoundError, BleakError, ) from ..characteristic import BleakGATTCharacteristic from ..client import BaseBleakClient, NotifyCallback from ..device import BLEDevice from ..service import BleakGATTServiceCollection from .characteristic import BleakGATTCharacteristicWinRT from .descriptor import BleakGATTDescriptorWinRT from .scanner import BleakScannerWinRT from .service import BleakGATTServiceWinRT logger = logging.getLogger(__name__) class _Result(Protocol): status: GattCommunicationStatus protocol_error: 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: err = PROTOCOL_ERROR_CODES.get(result.protocol_error, "Unknown") raise BleakError( f"{fail_msg}: Protocol Error 0x{result.protocol_error:02X}: {err}" ) 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 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. """ 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. **timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0. """ def __init__( self, address_or_ble_device: Union[BLEDevice, str], services: Optional[Set[str]] = None, *, winrt: WinRTClientArgs, **kwargs, ): super(BleakClientWinRT, self).__init__(address_or_ble_device, **kwargs) # Backend specific. WinRT objects. if isinstance(address_or_ble_device, BLEDevice): data = address_or_ble_device.details self._device_info = (data.adv or data.scan).bluetooth_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_closed_events: List[asyncio.Event] = [] self._session: GattSession = None self._notification_callbacks: Dict[int, NotifyCallback] = {} if "address_type" in kwargs: warnings.warn( "The address_type keyword arg will in a future version be moved into the win dict input instead.", PendingDeprecationWarning, stacklevel=2, ) # os-specific options self._use_cached_services = winrt.get("use_cached_services") self._address_type = winrt.get("address_type", kwargs.get("address_type")) self._retry_on_services_changed = False self._session_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): return f"{type(self).__name__} ({self.address})" # Connectivity methods async def _create_requester(self, bluetooth_address: int) -> BluetoothLEDevice: args = [ bluetooth_address, ] if self._address_type is not None: args.append( BluetoothAddressType.PUBLIC if self._address_type == "public" else BluetoothAddressType.RANDOM ) requester = await BluetoothLEDevice.from_bluetooth_address_async(*args) # 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 async def connect(self, **kwargs) -> bool: """Connect to the specified GATT server. Keyword Args: timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0. Returns: Boolean representing connection status. """ # 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 = device.details self._device_info = (data.adv or data.scan).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) def handle_services_changed(): 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, args): 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(): 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, ): 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() for e in self._session_closed_events: e.set() 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): 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) 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) return True async def disconnect(self) -> bool: """Disconnect from the specified GATT server. Returns: Boolean representing if device is disconnected. """ logger.debug("Disconnecting from BLE device...") # Remove notifications. for handle, event_handler_token in list(self._notification_callbacks.items()): char = self.services.get_characteristic(handle) char.obj.remove_value_changed(event_handler_token) self._notification_callbacks.clear() # 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 # Without this, disposing the BluetoothLEDevice won't disconnect it if self._session: self._session.maintain_connection = False # calling self._session.close() here prevents any further GATT # session status events, so we defer that until after the session # is no longer active # Dispose of the BluetoothLEDevice and see that the session # status is now closed. if self._requester: event = asyncio.Event() self._session_closed_events.append(event) try: self._requester.close() # sometimes it can take over one minute before Windows decides # to end the GATT session/disconnect the device async with async_timeout(120): await event.wait() finally: self._session_closed_events.remove(event) return True @property def is_connected(self) -> bool: """Check connection status between this client and the server. Returns: Boolean representing connection status. """ return self._DeprecatedIsConnectedReturn( False if self._session is None else self._session.session_status == GattSessionStatus.ACTIVE ) @property def mtu_size(self) -> int: """Get ATT MTU size for active connection""" return self._session.max_pdu_size async def pair(self, protection_level: int = None, **kwargs) -> bool: """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. (This will not work in Bleak...) Returns: Boolean regarding success of pairing. """ # 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.can_pair and not device_information.pairing.is_paired ): # Currently only supporting Just Works solutions... ceremony = DevicePairingKinds.CONFIRM_ONLY custom_pairing = device_information.pairing.custom def handler(sender, args): args.accept() pairing_requested_token = custom_pairing.add_pairing_requested(handler) try: if protection_level: pairing_result = await custom_pairing.pair_async( ceremony, protection_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}") else: logger.info( "Paired to device with protection level %r.", pairing_result.protection_level_used, ) return True else: return device_information.pairing.is_paired async def unpair(self) -> bool: """Attempts to unpair from the device. N.B. unpairing also leads to disconnection in the Windows backend. Returns: Boolean on whether the unparing was successful. """ 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() return True # GATT services methods async def get_services( self, *, service_cache_mode: Optional[BluetoothCacheMode] = None, cache_mode: Optional[BluetoothCacheMode] = None, **kwargs, ) -> 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() # Each of the get_serv/char/desc_async() methods has two forms, one # with no args and one with a cache_mode argument srv_args = [] args = [] # If the os-specific use_cached_services arg was given when BleakClient # was created, the we use the second form with explicit cache mode. # Otherwise we use the first form with no explicit cache mode which # allows the OS Bluetooth stack to decide what is best. if service_cache_mode is not None: srv_args.append(service_cache_mode) if cache_mode is not None: args.append(cache_mode) def dispose_on_cancel(future): if future._cancel_requested and future._result is not None: logger.debug("disposing services object because of cancel") for service in future._result: service.close() services: Sequence[GattDeviceService] if self._requested_services is None: future = FutureLike(self._requester.get_gatt_services_async(*srv_args)) future.add_done_callback(dispose_on_cancel) services = _ensure_success( await FutureLike(self._requester.get_gatt_services_async(*srv_args)), "services", "Could not get GATT services", ) else: services = [] # REVISIT: should properly dispose services on cancel or protect from cancellation for s in self._requested_services: services.extend( _ensure_success( await FutureLike( self._requester.get_gatt_services_for_uuid_async( s, *srv_args ) ), "services", "Could not get GATT services", ) ) try: for service in services: result = await FutureLike(service.get_characteristics_async(*args)) 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})", ) new_services.add_service(BleakGATTServiceWinRT(service)) for characteristic in characteristics: descriptors: Sequence[GattDescriptor] = _ensure_success( await FutureLike(characteristic.get_descriptors_async(*args)), "descriptors", f"Could not get GATT descriptors for characteristic {characteristic.uuid} ({characteristic.attribute_handle})", ) new_services.add_characteristic( BleakGATTCharacteristicWinRT( characteristic, lambda: self._session.max_pdu_size - 3 ) ) for descriptor in descriptors: new_services.add_descriptor( BleakGATTDescriptorWinRT( descriptor, str(characteristic.uuid), characteristic.attribute_handle, ) ) 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 async def read_gatt_char( self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], **kwargs, ) -> bytearray: """Perform read operation on the specified GATT characteristic. Args: char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to read from, specified by either integer handle, UUID or directly by the BleakGATTCharacteristic object representing it. 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") use_cached = kwargs.get("use_cached", False) if not isinstance(char_specifier, BleakGATTCharacteristic): characteristic = self.services.get_characteristic(char_specifier) else: characteristic = char_specifier if not characteristic: raise BleakCharacteristicNotFoundError(char_specifier) value = bytearray( _ensure_success( await characteristic.obj.read_value_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 async def read_gatt_descriptor(self, handle: int, **kwargs) -> bytearray: """Perform read operation on the specified GATT descriptor. Args: handle (int): The handle of the descriptor 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") use_cached = kwargs.get("use_cached", False) descriptor = self.services.get_descriptor(handle) if not descriptor: raise BleakError(f"Descriptor with handle {handle} was not found!") value = bytearray( _ensure_success( await descriptor.obj.read_value_async( BluetoothCacheMode.CACHED if use_cached else BluetoothCacheMode.UNCACHED ), "value", f"Could not read Descriptor value for {handle:04X}", ) ) logger.debug("Read Descriptor %04X : %s", handle, value) return value async def write_gatt_char( self, characteristic: BleakGATTCharacteristic, data: Buffer, response: bool, ) -> None: if not self.is_connected: raise BleakError("Not connected") response = ( GattWriteOption.WRITE_WITH_RESPONSE if response else GattWriteOption.WRITE_WITHOUT_RESPONSE ) buf = WinBuffer(len(data)) buf.length = buf.capacity with memoryview(buf) as mv: mv[:] = data _ensure_success( await characteristic.obj.write_value_with_result_async(buf, response), None, f"Could not write value {data} to characteristic {characteristic.handle:04X}", ) async def write_gatt_descriptor(self, handle: int, data: Buffer) -> 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") descriptor = self.services.get_descriptor(handle) if not descriptor: raise BleakError(f"Descriptor with handle {handle} was not found!") buf = WinBuffer(len(data)) buf.length = buf.capacity with memoryview(buf) as mv: mv[:] = data _ensure_success( await descriptor.obj.write_value_with_result_async(buf), None, f"Could not write value {data!r} to descriptor {handle:04X}", ) logger.debug("Write Descriptor %04X : %s", handle, data) async def start_notify( self, characteristic: BleakGATTCharacteristic, callback: NotifyCallback, **kwargs, ) -> 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 ): value = bytearray(args.characteristic_value) return 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_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 async def stop_notify( self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID] ) -> None: """Deactivate notification/indication on a specified characteristic. Args: char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to deactivate notification/indication on, specified by either integer handle, UUID or directly by the BleakGATTCharacteristic object representing it. """ if not self.is_connected: raise BleakError("Not connected") if not isinstance(char_specifier, BleakGATTCharacteristic): characteristic = self.services.get_characteristic(char_specifier) else: characteristic = char_specifier if not characteristic: raise BleakCharacteristicNotFoundError(char_specifier) _ensure_success( await characteristic.obj.write_client_characteristic_configuration_descriptor_async( GattClientCharacteristicConfigurationDescriptorValue.NONE ), None, f"Could not stop notify on {characteristic.handle:04X}", ) event_handler_token = self._notification_callbacks.pop(characteristic.handle) characteristic.obj.remove_value_changed(event_handler_token) class FutureLike: """ 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, op: IAsyncOperation) -> None: self._op = op self._callbacks = [] self._loop = asyncio.get_running_loop() self._cancel_requested = False self._result = None def call_callbacks(): for c in self._callbacks: c(self) def call_callbacks_threadsafe(op: IAsyncOperation, status: AsyncStatus): 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) -> Any: if self._op.status == AsyncStatus.STARTED: raise asyncio.InvalidStateError if self._op.status == AsyncStatus.COMPLETED: if self._cancel_requested: raise asyncio.CancelledError 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) 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, *, context=None) -> None: self._callbacks.append(callback) def remove_done_callback(self, callback) -> None: self._callbacks.remove(callback) def cancel(self, msg=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) 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. bleak-0.22.3/bleak/backends/winrt/descriptor.py000066400000000000000000000025321470032643600214060ustar00rootroot00000000000000# -*- coding: utf-8 -*- import sys if sys.version_info >= (3, 12): from winrt.windows.devices.bluetooth.genericattributeprofile import GattDescriptor else: from bleak_winrt.windows.devices.bluetooth.genericattributeprofile import ( GattDescriptor, ) from ..descriptor import BleakGATTDescriptor class BleakGATTDescriptorWinRT(BleakGATTDescriptor): """GATT Descriptor implementation for .NET backend, implemented with WinRT""" def __init__( self, obj: GattDescriptor, characteristic_uuid: str, characteristic_handle: int ): super(BleakGATTDescriptorWinRT, self).__init__(obj) self.obj = obj self.__characteristic_uuid = characteristic_uuid self.__characteristic_handle = characteristic_handle @property def characteristic_handle(self) -> int: """handle for the characteristic that this descriptor belongs to""" return self.__characteristic_handle @property def characteristic_uuid(self) -> str: """UUID for the characteristic that this descriptor belongs to""" return self.__characteristic_uuid @property def uuid(self) -> str: """UUID for this descriptor""" return str(self.obj.uuid) @property def handle(self) -> int: """Integer handle for this descriptor""" return self.obj.attribute_handle bleak-0.22.3/bleak/backends/winrt/scanner.py000066400000000000000000000271231470032643600206640ustar00rootroot00000000000000import asyncio import logging import sys from typing import Dict, List, Literal, NamedTuple, Optional from uuid import UUID from .util import assert_mta if sys.version_info >= (3, 12): from winrt.windows.devices.bluetooth.advertisement import ( BluetoothLEAdvertisementReceivedEventArgs, BluetoothLEAdvertisementType, BluetoothLEAdvertisementWatcher, BluetoothLEAdvertisementWatcherStatus, BluetoothLEScanningMode, ) else: from bleak_winrt.windows.devices.bluetooth.advertisement import ( BluetoothLEAdvertisementReceivedEventArgs, BluetoothLEAdvertisementType, BluetoothLEAdvertisementWatcher, BluetoothLEAdvertisementWatcherStatus, BluetoothLEScanningMode, ) from ...assigned_numbers import AdvertisementDataType from ...exc import BleakError from ...uuids import normalize_uuid_str from ..scanner import AdvertisementData, AdvertisementDataCallback, BaseBleakScanner 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 se 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, ): super(BleakScannerWinRT, self).__init__(detection_callback, service_uuids) self.watcher: Optional[BluetoothLEAdvertisementWatcher] = None self._advertisement_pairs: Dict[int, _RawAdvData] = {} self._stopped_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 = None self._stopped_token = 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 = [] mfg_data = {} service_data = {} local_name = None tx_power = None for args in filter(lambda d: d is not None, raw_data): 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_d_bm is not None: tx_power = args.transmit_power_level_in_d_bm 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_d_bm, platform_data=(sender, raw_data), ) device = self.create_or_update_device( bdaddr, local_name, raw_data, advertisement_data ) self.call_detection_callbacks(device, advertisement_data) def _stopped_handler(self, sender, e): logger.debug( "%s devices found. Watcher status: %r.", len(self.seen_devices), sender.status, ) self._stopped_event.set() 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() # start with fresh list of discovered devices self.seen_devices = {} self._advertisement_pairs.clear() self.watcher = BluetoothLEAdvertisementWatcher() self.watcher.scanning_mode = self._scanning_mode event_loop = asyncio.get_running_loop() self._stopped_event = asyncio.Event() self._received_token = self.watcher.add_received( lambda s, e: event_loop.call_soon_threadsafe(self._received_handler, s, e) ) self._stopped_token = self.watcher.add_stopped( lambda s, e: event_loop.call_soon_threadsafe(self._stopped_handler, s, e) ) 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}") async def stop(self) -> None: 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 def set_scanning_filter(self, **kwargs) -> None: """Set a scanning filter for the BleakScanner. Keyword Args: SignalStrengthFilter (``Windows.Devices.Bluetooth.BluetoothSignalStrengthFilter``): A BluetoothSignalStrengthFilter object used for configuration of Bluetooth LE advertisement filtering that uses signal strength-based filtering. AdvertisementFilter (Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementFilter): A BluetoothLEAdvertisementFilter object used for configuration of Bluetooth LE advertisement filtering that uses payload section-based filtering. """ if "SignalStrengthFilter" in kwargs: # TODO: Handle SignalStrengthFilter parameters self._signal_strength_filter = kwargs["SignalStrengthFilter"] if "AdvertisementFilter" in kwargs: # TODO: Handle AdvertisementFilter parameters self._advertisement_filter = kwargs["AdvertisementFilter"] bleak-0.22.3/bleak/backends/winrt/service.py000066400000000000000000000024031470032643600206650ustar00rootroot00000000000000import sys from typing import List if sys.version_info >= (3, 12): from winrt.windows.devices.bluetooth.genericattributeprofile import ( GattDeviceService, ) else: from bleak_winrt.windows.devices.bluetooth.genericattributeprofile import ( GattDeviceService, ) from ..service import BleakGATTService from ..winrt.characteristic import BleakGATTCharacteristicWinRT class BleakGATTServiceWinRT(BleakGATTService): """GATT Characteristic implementation for the .NET backend, implemented with WinRT""" def __init__(self, obj: GattDeviceService): super().__init__(obj) self.__characteristics = [] @property def uuid(self) -> str: return str(self.obj.uuid) @property def handle(self) -> int: return self.obj.attribute_handle @property def characteristics(self) -> List[BleakGATTCharacteristicWinRT]: """List of characteristics for this service""" return self.__characteristics def add_characteristic(self, characteristic: BleakGATTCharacteristicWinRT): """Add a :py:class:`~BleakGATTCharacteristicWinRT` to the service. Should not be used by end user, but rather by `bleak` itself. """ self.__characteristics.append(characteristic) bleak-0.22.3/bleak/backends/winrt/util.py000066400000000000000000000141751470032643600202130ustar00rootroot00000000000000import asyncio import ctypes import sys from ctypes import wintypes from enum import IntEnum from typing import Tuple from ...exc import BleakError if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout else: from asyncio import timeout as async_timeout def _check_result(result, func, args): if not result: raise ctypes.WinError() return args def _check_hresult(result, func, args): 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 # 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 _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(*_): 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(): """ 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 """ 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() bleak-0.22.3/bleak/exc.py000066400000000000000000000151231470032643600150720ustar00rootroot00000000000000# -*- coding: utf-8 -*- import uuid from typing import Optional, Union class BleakError(Exception): """Base Exception for bleak.""" pass 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): """ 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 Authentication", 0x10: "Unsupported Group Type", 0x11: "Insufficient Resource", 0x12: "Database Out Of Sync", 0x13: "Value Not Allowed", # REVISIT: do we need Application Errors 0x80-0x9F? 0xFC: "Write Request Rejected", 0xFD: "Client Characteristic Configuration Descriptor Improperly Configured", 0xFE: "Procedure Already in Progress", 0xFF: "Out of Range", } bleak-0.22.3/bleak/py.typed000066400000000000000000000000001470032643600154240ustar00rootroot00000000000000bleak-0.22.3/bleak/uuids.py000066400000000000000000001376771470032643600154670ustar00rootroot00000000000000# -*- coding: utf-8 -*- from typing import Dict 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}") bleak-0.22.3/docs/000077500000000000000000000000001470032643600136115ustar00rootroot00000000000000bleak-0.22.3/docs/Makefile000066400000000000000000000151511470032643600152540ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -W -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/bleak.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/bleak.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/bleak" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/bleak" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." bleak-0.22.3/docs/api/000077500000000000000000000000001470032643600143625ustar00rootroot00000000000000bleak-0.22.3/docs/api/client.rst000066400000000000000000000057771470032643600164120ustar00rootroot00000000000000================= BleakClient class ================= .. autoclass:: bleak.BleakClient ---------------------------- Connecting and disconnecting ---------------------------- Before doing anything else with a :class:`BleakClient` object, it must be connected. :class:`bleak.BleakClient` is a an async context manager, so the recommended way of connecting is to use it as such:: import asyncio from bleak import BleakClient async def main(): async with BleakClient("XX:XX:XX:XX:XX:XX") as client: # Read a characteristic, etc. ... # Device will disconnect when block exits. ... # Using asyncio.run() is important to ensure that device disconnects on # KeyboardInterrupt or other unhandled exception. asyncio.run(main()) It is also possible to connect and disconnect without a context manager, however this can leave the device still connected when the program exits: .. automethod:: bleak.BleakClient.connect .. automethod:: bleak.BleakClient.disconnect The current connection status can be retrieved with: .. autoproperty:: bleak.BleakClient.is_connected A callback can be provided to the :class:`BleakClient` constructor via the ``disconnect_callback`` argument to be notified of disconnection events. ------------------ Device information ------------------ .. autoproperty:: bleak.BleakClient.address .. autoproperty:: bleak.BleakClient.mtu_size ---------------------- GATT Client Operations ---------------------- All Bluetooth Low Energy devices use a common Generic Attribute Profile (GATT) for interacting with the device after it is connected. Some GATT operations like discovering the services/characteristic/descriptors and negotiating the MTU are handled automatically by Bleak and/or the OS Bluetooth stack. The primary operations for the Bleak client are reading, writing and subscribing to characteristics. Services ======== The available services on a device are automatically enumerated when connecting to a device. Services describe the devices capabilities. .. autoproperty:: bleak.BleakClient.services GATT characteristics ==================== Most I/O with a device is done via the characteristics. .. automethod:: bleak.BleakClient.read_gatt_char .. automethod:: bleak.BleakClient.write_gatt_char .. automethod:: bleak.BleakClient.start_notify .. automethod:: bleak.BleakClient.stop_notify GATT descriptors ================ Descriptors can provide additional information about a characteristic. .. automethod:: bleak.BleakClient.read_gatt_descriptor .. automethod:: bleak.BleakClient.write_gatt_descriptor --------------- Pairing/bonding --------------- On some devices, some characteristics may require authentication in order to read or write the characteristic. In this case pairing/bonding the device is required. .. automethod:: bleak.BleakClient.pair .. automethod:: bleak.BleakClient.unpair ---------- Deprecated ---------- .. automethod:: bleak.BleakClient.set_disconnected_callback .. automethod:: bleak.BleakClient.get_services bleak-0.22.3/docs/api/index.rst000066400000000000000000000014061470032643600162240ustar00rootroot00000000000000============= API reference ============= Contents: .. toctree:: :maxdepth: 2 scanner client .. TODO: move everything below to separate pages Class representing BLE devices ------------------------------ Generated by :py:meth:`bleak.discover` and :py:class:`bleak.backends.scanning.BaseBleakScanner`. .. automodule:: bleak.backends.device :members: GATT objects ------------ .. automodule:: bleak.backends.service :members: .. automodule:: bleak.backends.characteristic :members: .. automodule:: bleak.backends.descriptor :members: Exceptions ---------- .. automodule:: bleak.exc :members: Utilities --------- .. automodule:: bleak.uuids :members: Deprecated ---------- .. module:: bleak .. autofunction:: bleak.discover bleak-0.22.3/docs/api/scanner.rst000066400000000000000000000053071470032643600165520ustar00rootroot00000000000000================== BleakScanner class ================== .. py:currentmodule:: bleak .. autoclass:: bleak.BleakScanner ------------ Easy methods ------------ These methods and handy for simple programs but are not recommended for more advanced use cases like long running programs, GUIs or connecting to multiple devices. .. automethod:: bleak.BleakScanner.discover .. automethod:: bleak.BleakScanner.find_device_by_name .. automethod:: bleak.BleakScanner.find_device_by_address .. automethod:: bleak.BleakScanner.find_device_by_filter .. autoclass:: bleak.BleakScanner.ExtraArgs :members: --------------------- Starting and stopping --------------------- :class:`BleakScanner` is an context manager so the recommended way to start and stop scanning is to use it in an ``async with`` statement:: import asyncio from bleak import BleakScanner async def main(): stop_event = asyncio.Event() # TODO: add something that calls stop_event.set() def callback(device, advertising_data): # TODO: do something with incoming data pass async with BleakScanner(callback) as scanner: ... # Important! Wait for an event to trigger stop, otherwise scanner # will stop immediately. await stop_event.wait() # scanner stops when block exits ... asyncio.run(main()) It can also be started and stopped without using the context manager using the following methods: .. automethod:: bleak.BleakScanner.start .. automethod:: bleak.BleakScanner.stop ------------------------------------------------- Getting discovered devices and advertisement data ------------------------------------------------- If you aren't using the "easy" class methods, there are three ways to get the discovered devices and advertisement data. For event-driven programming, you can provide a ``detection_callback`` callback to the :class:`BleakScanner` constructor. This will be called back each time and advertisement is received. Alternatively, you can utilize the asynchronous iterator to iterate over advertisements as they are received. The method below returns an async iterator that yields the same tuples as otherwise provided to ``detection_callback``. .. automethod:: bleak.BleakScanner.advertisement_data Otherwise, you can use one of the properties below after scanning has stopped. .. autoproperty:: bleak.BleakScanner.discovered_devices .. autoproperty:: bleak.BleakScanner.discovered_devices_and_advertisement_data ---------- Deprecated ---------- .. automethod:: bleak.BleakScanner.register_detection_callback .. automethod:: bleak.BleakScanner.set_scanning_filter .. automethod:: bleak.BleakScanner.get_discovered_devices bleak-0.22.3/docs/authors.rst000066400000000000000000000000341470032643600160250ustar00rootroot00000000000000.. include:: ../AUTHORS.rst bleak-0.22.3/docs/backends/000077500000000000000000000000001470032643600153635ustar00rootroot00000000000000bleak-0.22.3/docs/backends/android.rst000066400000000000000000000056511470032643600175440ustar00rootroot00000000000000Android backend =============== For an example of building an android bluetooth app, see `the example folder `_ and its accompanying README file. There are a handful of ways to run Python on Android. Presently some code has been written for the `Python-for-Android `_ build tool, and the code has only been tested using the `Kivy Framework `_. The Kivy framework provides a way to make graphical applications using bluetooth that run on both android and desktop. An alternative framework is `BeeWare `_. An implementation for BeeWare would likely be very similar to Python-for-Android, if anybody is interested in contributing one. As of 2020, the major task to tackle is making a custom template to embed Java subclasses of the Bluetooth Android interfaces, for forwarding callbacks. The Python-for-Android backend classes are found in the ``bleak.backends.p4android`` package and are automatically selected when building with python-for-android or `Buildozer `_, Kivy's automated build tool. Considerations on Android ------------------------- For one thing, the python-for-android backend has not been fully tested. Please run applications with ``adb logcat`` or ``buildozer android logcat`` and file issues that include the output, so that any compatibility concerns with devices the developer did not own can be eventually addressed. This backend was originally authored by @xloem for a project that has mostly wrapped up now, so it would be good to tag him in the issues. When fixing issues, often the Android documentation is lacking, and other resources may need to be consulted to find information on various device quirks, such as community developer forums. Sometimes device drivers will give off new, undocumented error codes. There is a developing list of these at ``bleak.backends.p4android.defs.GATT_STATUS_NAMES``. Please add to the list if you find new status codes, which is indicated by a number being reported instead of a name. Additionally a few small features are missing. Please file an issue if you need a missing feature, and ideally contribute code, so that soon they will all be implemented. Two missing features include scanning filters and indications (notifications without replies). Additionally reading from a characteristic has not been tested at all, as xloem's test device did not provide for this. On Android, Bluetooth needs permissions for access. These permissions need to be added to the android application in the buildozer.spec file, and are also requested from the user at runtime. This means that enabling bluetooth may not succeed if the user does not accept permissions. API --- Scanner ~~~~~~~ .. automodule:: bleak.backends.p4android.scanner :members: Client ~~~~~~ .. automodule:: bleak.backends.p4android.client :members: bleak-0.22.3/docs/backends/index.rst000066400000000000000000000014731470032643600172310ustar00rootroot00000000000000Backend implementations ======================= Bleak supports the following operating systems: * Windows 10, version 16299 (Fall Creators Update) and greater * Linux distributions with BlueZ >= 5.43 (See :ref:`linux-backend` for more details) * OS X/macOS support via Core Bluetooth API, from at least version 10.11 * Partial Android support mostly using Python-for-Android/Kivy. These pages document platform specific differences from the interface API. Contents: .. toctree:: :maxdepth: 2 windows linux macos android Shared Backend API ------------------ .. warning:: The backend APIs are not considered part of the stable API and may change without notice. Scanner ~~~~~~~ .. automodule:: bleak.backends.scanner :members: Client ~~~~~~ .. automodule:: bleak.backends.client :members: bleak-0.22.3/docs/backends/linux.rst000066400000000000000000000046761470032643600172710ustar00rootroot00000000000000.. _linux-backend: Linux backend ============= The Linux backend of Bleak communicates with `BlueZ `_ over DBus. Communication uses the `dbus-fast `_ package for async access to DBus messaging. Special handling for ``write_gatt_char`` ---------------------------------------- The ``type`` option to the ``Characteristic.WriteValue`` method was added to `Bluez in 5.51 `_ Before that commit, ``Characteristic.WriteValue`` was only "Write with response". ``Characteristic.AcquireWrite`` was added in `Bluez 5.46 `_ which can be used to "Write without response", but for older versions of Bluez (5.43, 5.44, 5.45), it is not possible to "Write without response". Resolving services with ``get_services`` ---------------------------------------- By default, calling ``get_services`` will wait for services to be resolved before returning the ``BleakGATTServiceCollection``. If a previous connection to the device was made, passing the ``dangerous_use_bleak_cache`` argument will return the cached services without waiting for them to be resolved again. This is useful when you know services have not changed, and you want to use the services immediately, but don't want to wait for them to be resolved again. Parallel Access --------------- Each Bleak object should be created and used from a single `asyncio event loop`_. Simple asyncio programs will only have a single event loop. It's also possible to use Bleak with multiple event loops, even at the same time, but individual Bleak objects should not be shared between event loops. Otherwise, RuntimeErrors similar to ``[...] got Future attached to a different loop`` will be thrown. D-Bus Authentication -------------------- Connecting to the host DBus from within a user namespace will fail. This is because the remapped UID will not match the UID that the hosts sees. To work around this, you can hardcode a UID with the `BLEAK_DBUS_AUTH_UID` environment variable. API --- Scanner ~~~~~~~ .. automodule:: bleak.backends.bluezdbus.scanner :members: Client ~~~~~~ .. automodule:: bleak.backends.bluezdbus.client :members: .. _`asyncio event loop`: https://docs.python.org/3/library/asyncio-eventloop.html bleak-0.22.3/docs/backends/macos.rst000066400000000000000000000026101470032643600172160ustar00rootroot00000000000000macOS backend ============= The macOS backend of Bleak is written with `pyobjc `_ directives for interfacing with `Foundation `_ and `CoreBluetooth `_ APIs. Specific features for the macOS backend --------------------------------------- The most noticeable difference between the other backends of bleak and this backend, is that CoreBluetooth doesn't scan for other devices via Bluetooth address. Instead, UUIDs are utilized that are often unique between the device that is scanning and the device that is being scanned. In the example files, this is handled in this fashion: .. code-block:: python mac_addr = ( "24:71:89:cc:09:05" if platform.system() != "Darwin" else "243E23AE-4A99-406C-B317-18F1BD7B4CBE" ) As stated above, this will however only work the macOS machine that performed the scan and thus cached the device as ``243E23AE-4A99-406C-B317-18F1BD7B4CBE``. There is also no pairing functionality implemented in macOS right now, since it does not seem to be any explicit pairing methods in the COre Bluetooth. API --- Scanner ~~~~~~~ .. automodule:: bleak.backends.corebluetooth.scanner :members: Client ~~~~~~ .. automodule:: bleak.backends.corebluetooth.client :members: bleak-0.22.3/docs/backends/windows.rst000066400000000000000000000020001470032643600175770ustar00rootroot00000000000000Windows backend =============== The Windows backend of bleak is written using `PyWinRT `_ to provide bindings for the Windows Runtime (WinRT). The Windows backend implements a ``BleakClient`` in the module ``bleak.backends.winrt.client``, a ``BleakScanner`` method in the ``bleak.backends.winrt.scanner`` module. There are also backend-specific implementations of the ``BleakGATTService``, ``BleakGATTCharacteristic`` and ``BleakGATTDescriptor`` classes. Specific features for the Windows backend ----------------------------------------- Client ~~~~~~ - The constructor keyword ``address_type`` which can have the values ``"public"`` or ``"random"``. This value makes sure that the connection is made in a fashion that suits the peripheral. API --- Utilities ~~~~~~~~~ .. automodule:: bleak.backends.winrt.util :members: Scanner ~~~~~~~ .. automodule:: bleak.backends.winrt.scanner :members: Client ~~~~~~ .. automodule:: bleak.backends.winrt.client :members: bleak-0.22.3/docs/conf.py000077500000000000000000000214011470032643600151110ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # bleak documentation build configuration file, created by # sphinx-quickstart on Tue Jul 9 22:26:36 2013. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import pathlib import sys import tomli PROJECT_ROOT_DIR = pathlib.Path(__file__).parent.parent.resolve() # If extensions (or modules to document with autodoc) are in another # directory, add these directories to sys.path here. If the directory is # relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # Get the project root dir, which is the parent dir of this cwd = os.getcwd() project_root = os.path.dirname(cwd) # Insert the project root dir as the first element in the PYTHONPATH. # This lets us ensure that the source package is imported, and that its # version is used. sys.path.insert(0, project_root) # -- General configuration --------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode", "sphinx.ext.napoleon"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "bleak" copyright = "2020, Henrik Blidh" # The version info for the project you're documenting, acts as replacement # for |version| and |release|, also used in various other places throughout # the built documents. # # The full version, including alpha/beta/rc tags. with open(PROJECT_ROOT_DIR / "pyproject.toml", "rb") as f: release = tomli.load(f)["tool"]["poetry"]["version"] # The short X.Y version. version = ".".join(release.split(".")[:2]) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to # some non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built # documents. # keep_warnings = False # -- Options for autodoc extension ------------------------------------------- autodoc_mock_imports = [ "android", "async_timeout", "bleak_winrt", "winrt", "CoreBluetooth", "dbus_fast", "Foundation", "jnius", "libdispatch", "objc", "ctypes", "typing_extensions", ] # -- Options for HTML output ------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "default" html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a # theme further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as # html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the # top of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon # of the docs. This file should be a Windows icon file (.ico) being # 16x16 or 32x32 pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) # here, relative to this directory. They are copied after the builtin # static files, so a file named "default.css" will overwrite the builtin # "default.css". # html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names # to template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. # Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. # Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages # will contain a tag referring to it. The value of this option # must be the base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "bleakdoc" # -- Options for LaTeX output ------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ("index", "bleak.tex", "bleak Documentation", "Henrik Blidh", "manual") ] # The name of an image file (relative to this directory) to place at # the top of the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings # are parts, not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output ------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [("index", "bleak", "bleak Documentation", ["Henrik Blidh"], 1)] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ---------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "bleak", "bleak Documentation", "Henrik Blidh", "bleak", "One line description of project.", "Miscellaneous", ) ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False bleak-0.22.3/docs/contributing.rst000066400000000000000000000000411470032643600170450ustar00rootroot00000000000000.. include:: ../CONTRIBUTING.rst bleak-0.22.3/docs/history.rst000066400000000000000000000000361470032643600160430ustar00rootroot00000000000000.. include:: ../CHANGELOG.rst bleak-0.22.3/docs/images/000077500000000000000000000000001470032643600150565ustar00rootroot00000000000000bleak-0.22.3/docs/images/macos-privacy-bluetooth.png000066400000000000000000016254351470032643600223640ustar00rootroot00000000000000PNG  IHDRvh6 jiCCPICC ProfileHWXS[zDzB U@B!!ذ,*vŊ(^ *ʺ 躯|;9sڝ}\$ G' bNLNa P`< w7:*ls_ L8/@ '@T3$ \:P*NS#:lPr 4Aˀv4>A,h؟'!V>*'gAl %xW763f?m>1y ZH&?K%'[>6PVB-NV>_YwPPGx26`@G@lq8;*RŧB9Ղq!ևx@.֧K,+@R#pT1a|-EQk@$ˊP-tXE aAJX~44V_#.qT@0>\Y;?kY CvC!ܱqBN$/(V9HcT ;LC&ˏST%y18Le<* 040dQkwC7 \ @UЌ1ƁD 48*<* }p4pFx q|px["x?saxaS{~ʰ bCCb01J q@\p/{(v#uB4BwQ~i6 Zqnq7臅@eVTeP鑝(y9lL { a+Z[eif|M^[b'0X#v ;ë;O#;̹ƹr,O03O% a&Gstqvq@Q>2!W.bHf|~ W 68ƓKSB4`,-x_B8 A2 u.3 6T}hG p\m: WO'x z;Џ  !t1EB$ET$#rd)A H53r9GڑC y|D1ƨ5:BYhNA3\]@ t/Z@/% _31/Ec)X:&aX)VbM>_:nN8w+8Oyx.>_o«z~_4C&23ERn!i: D"hC{1IM\NB#6ۉ$ɀ@#E;s.ctƌpLӘ7..3go0 T< |Ʋce^9Ig粛M!BC3BkB{f5#WsxjN8qsǝFElxi)l7~{QVQ⨆h͉^}?&&7 1'<;'l=nZܞwA+&&Z5''V'O NZ1qĹ/&&SH))Sz'LZ?s7L9TéSNӜƝv0'7[MmNyx/u.`Y_~k3Ra-$z-}VtVe@vRv]ZNja8K|j%"IGOit M5Kr[}3g=S<,Yf=+-i6>7eٜse16e; Pd-m5 ZiŏHHZtsmK񥢥\m\_|Ĺr ?ǁ+ZWzܺJUkyvuuZ?mRm(:"7Zn\&Au6/~ ˕[ko+qha;++Jww|+qٟ~mdJqeGUlթj=F{V֠5򚮽 XXQWs7Dh9u_6*Gg4:;tW_+)?wt1ʱm4w8eZݓO^;5T΄9yu9sG?|BE/C=/7y5m?v%ʉW\\x=znNqۯ[xpF*~ã=ٓOҞ>3}VЮ^t.Cͯl_gz&txʿj}.]>^~L'ҧvD|730 Jlhz:o*%oxnLRQ_OXy^*H( ~l b*uun*(mQI70R[>9WyU+;QO("pReXIfMM*JR(iZvASCIIScreenshot$ pHYs%%IR$iTXtXML:com.adobe.xmp Screenshot 1560 1398 2 144/1 144/1 1 4$@IDATxۏmuύ>M&M*(F(b؆ 8-_ O-y $~IbXq(D)$[$%"&>TƷV}{g]u992oysUlBA  @A  @A  @A  @A  @A  @A  @A  @A  @A  @A  @A  @A  @A  @A  @A  @A  @A  @A  @A  @A  @A  @A  @A  @A  @A  @A  @A  @A  @A  @[cSCO D,Ty*ی}ڰAɧQ7M:c cgs-/dLPw݆z< V~al2k7je3 rk069ǽ&e-Iόb~+Z ܃# F"LvrH|xs.WۊO-4OR__֟y'f$O!R׏>g.kIsgg\k#/߾oC}g`ngzP^|JpqCY>9u e.R!6@iL/+6EoeM '0>˜rH*o$ &ygYS"yp?w>qt%.$%珜rg#篣F_˞Q,rSjՑ  P85ԩT#e#O9;igXr"erH_L$gl:^QNe q!e0";d!&Nj|xf5!B;#!&`Dr5w^e7G_/ã7@R 1#b$ F7&XѸXw$aqҿ޼Rvō\A@f<7/Α]%r?r#R}1ښǂO9\#el֓ K$s&0TmNB?KeH2d>l֋sX 9+Գ,yĆ瑱/ 8e'눱a+ۍ/scIqEO?,Y̿1a|ydl3#NYg,:blX'vu}R\u_ pEp2wQ} q\ﲔ!hWfԍPK VbCN K, [;zo(h/Pֿ~:9'k1r[WG.CqgߌadZbLB󭫣l eqg֟ cxǵĘr[WG.C3Ϭ?3Ɔg}Y"''';kq ORmgB?Cػ,Y&[j^Ho"=r(/s8 &+Yd#篜?s|dt7l1<3?y~g"ϝO_ =A=A dsL"B|sx!7򋵥.Ky$x2`,.ϱ+'oy.P3qa?rb^M=Vr,d0e'/ }OcG\t)4yswA &Ը .*i2Pܲȋo͍9rˉ;ƓU_8_ǐsh#3Oܲqֱ3W6obx1d8"<3䣯mȡG 'Pܲc|R_̿c#K!cў1&}emGb88?G䖝g;g[+uw[^;Q 2IĔ dF.;>` C;u6\RH<ڐ&uW_t([\K"HAy>j6o;.2մ_[#/lfyݦ?xK)2c'&~ 3YfBy]e9yt>0?Z׌GΟ9ü^wy||4O/N!COBr2]BXCqw+cB}L[& NvLt&ĆODvXT>[j>NmA__֟yt$fG_9=1_(?b>81ܿռٰA?|:9 /? Gfv_> 2lP6Wq~Aֲ a;2~S;޽"s2Ͻ˜/g/]ԭb"m2!3gɄ 9z2 u٩~(-+:#g63o,SYڕVA>+vkY<'(v}SvK{a:v_i9fӣ>' ̿?˞7o?GRf`Ff-č2xB`>ĊqG91ʼWgw9zt9lD@b=4(aCN@nM.ȁ: e |>oޮevS/]u<ğq?I?̂?rf{>WYֿEY@߬|?xEN\Cޓ͵gֿe9N\^k>x!`@=L>e,kK0-h+ H9 wr.iW{)oԧ:/=698MScϪƯ d?v#M >w۪iK; ?7OM*<2e_߿'S3y[l&>G>W>{Y?yV~Ne!!SڑC\*N??>v4̿?Y}#o97r ~n uxG>G>Uzn?sF?C9Y,/6?e#QǯL5iW~UX~

SVG{&W)jH_TF5}_ȱHnk~/¿"wbx:|2_L?d%9KY$fF??S\{kD` +2|\^5Mm4|ʑC$@?e>ԡ#>iR*>'u2>(_E28o牥}/)8kS}'9~r?<+/{|>߲eeYdroo}8(rnW AQ&v*n?!K{>qgq23N3W}a~7x5GBX=y% -كlg6ƺzҾ?m[_ɷ.iG"hSy[ce?'2d7y`=$ynKg57/3,o9f>yx}_Gg~OuΟ}ތ(uuMc6u*:AD@Ej( &reeY~/mh״}c}ǯE1O{'rf:n!7s ~cx ?Q3Ϝ?sO>y&e2|H>}#ӟ6i׿^m?[ VF ZnF?21P re~,9ɧ Hp Yxݿ[?:_0!!u| _[g?M}'3?iǿ1b>1ǯi@cZ?v|v5c4'25#όkg?s85@u<ۮC Zg:iŗɡ};NtiƏ~}}a迶hO>BGag󯦧?y5$OpeH6ᙺA)u;؆1Ӯ}AOo%ʞ=ݓ3.N5b`C{G~C/b?njxKeeYCy>5LOܷfGx9ec W=3ǹ9rʩ7[R֟΍?\Cd{+kZerIxc=֡*s~SEzoWu/}bGG}֌r÷C{[Uuڮ  ׅA@N@b G}-Ѿ}"dSE!vWvތ3ڿsvk>Yo8RȾ"GY[Y(uwL?6?P>nkYG燽W_ݱO= ;/PcvwSWf0HDsTKu A=w**M˾Yo'm%6_\˾G)#'<_?OgPkQ?G99Mǯ;i˚ug:K?:Lcbj:zP\Τ4`R>f|e$O6\]6%,ya'x vd~d]c7g'P<./"|z)~n7 }06IcU''u _6~={?.YhͿ>}+X?cMߟ*ЧePhuuZ?Q2j޶k>!uE_?bۋ=m.29?59\5ǽ ̩Cy??ֿ˿6+QvL?XW6hGs[)?hvҾvJuˣ&vYNN4PI` >e<|4aY|r` $B$V~fU6s.eȒ>|K Gc 4__֟e>d7珜r{>7|>|?nIwHQ6([GL]ʻ#Fƚ%ч_Xi'9~tv)+_]rWq‚\[|mN?}#?|Tq!x&`<`<0t"3SozXG^Uu>6Hrh_[v{_):AIτDWcWLl2.?Ww|I@2|O8}cO_]UobaW/ ?\6Kee>g˳.Ҝ?rs>+0r1ptAg-yz~2;Ӭ?>=7SPx&>m!3޿~Cyt;}9Tfz_v8#klNY9h{Cē>a1F*dÅl.>x}ny6ejwx#)_ŭ^VVWcw>WߧO?է Q'mm 䑥R-}'^Hk_x@k~I 3eeg.VMr+Ϝ?gDօ~9L9}ߞ'z-ߺ͓.?}CUIֿ~I;o>rɏء_q\ytՇGg(o; }?>?}XO`ק. Ɯ߇e + rIm uSO]{rV\ykHR;@ֻӦn_b?f3G~wϳQ?vс/ eYc˾N[]Ocψ}w\CoK.}{/eKn*CNM?}A_ŭgh|27ulh]xkq_~YG&)# __emO"_?mkiOY'xPHee~MS f<+|r3Ob#oP;s߬|y2'mwSb|kpd2~ݿ*ˣ柾DW;N?}A2x~8ޱ.;GOjyd0< P xe ڰ: w 98°lQ׿@~Ϳ@B<|xY۱˧OƿǸk!Kr8>iOdk_֟y?#o#oY }>_s9:;i.+iNU;~b&_ow8|vZE IE2L ᓰ,y" _@ eL=O>_1RGG_?\۷bjdy:Q<6iW>]~_lk<'2d=gcka[%r/Ϝ?ss;#gjgS&A! yHȈ>O55׿G_{;AG$çNm;M4G'v ԏ?uFo%'A_/Q/>ʒ÷꓀].=!X@

r(5[ֱ̿o֟k%9{ W ?r`\'rO>wamxΟuχUܒr?1}ǯ]|vԱ!Qg_[|`*/NkGi8ic%wYr%6ڂuvh$WCɇE;CSf8R\ݮMdNY=xC^[IȐ#:U܎Iu³FB]&$9̕Q6.G>';~d~?kKl_i=(c sSl-!݋fʰ̋?f!Aږ7?vȏ<Ƕbme)C}Ž6mm!s tWۮ*I)C< |HXS6EF,.rOfl>zQH\}VV[R(x55~Ŷ~_b!6#3[]ǂq/~urw!׮E-ݷk~?e^8kl:>8#n_q̋̿弒Uxsld|`/2l_Z9-q$3s߸v5|W?FO?}wq׿-'mʑ7<bMmȒ >|mQdžPG~ߟ?eY }nOm$Ǭ?oyoxQqe;Ys">eR8\X@у[yS ݿ k1!/WOks( /C?ӆHYG_ߴwyǯ|_>cY?d> Oh#9Үkf^ũ%}U>>q K\$2u#ϼONzkgy͘@sgtrqg39庐F07rs@@(bB?rq_#n3eIr掟6ן>bvv(C?5wzv)ӷqK}du?- Dv:udxAzCv':<rv6h?U)}ƿm'g?Y߬YrgI{kbOm 2=}B؂h7_k8d} X>\"GYXWD Ǜ3vS%F>4œ~_=|>_RDY—˿:&roCYE73/_\zO$G׬?Y.᪓W$f߿5ȫ[Itew }sK'2.kW?}?GOTqKy/)sЮtoZ6c]S/NbM/;ȱo=5}x_xHݿt鿹T&GJxG|`N YXV:XNUȕ' *]-#+/_׷]?L8H[#+g5lhIǾzAYs,!}R֯?G/1_q?O]v gw^wds /$cr5˾zwsYOi4Ο?I?ЇIJb:zS&_o{5o#?r ؆GrUO]>z'oG6SGRJQ1 p;>xxvt! ɇM*Nڣ a6U^9#uIA#nQ>ru?AsmOG?E1w%2Q_;I <|ãy_[9d>d]Hsm?||?ASЁOo_iC혌쏺R'uh;>?rd I֟eR׵5e>0?r8׀Gߣ'yx ̓#ø ~wdԩV~tV}4#/k9oF ϲkI9~K[߬?X'Ѕ-u 98%6:y5}ȑLC4׮OsE3u`g&C[*Cζ*NmOǀ@_hbc_O)G6+C?(cҿ$luOyq1u]tq}~IdJϟ<ewZBsur`'O?O9@( ,d%d9?17u=WTo}⟽Ի8!Hk/<۪-ӧ |vQDż?P8~<؎-r[r?$ǯx?RvUd8ik6M W*}.@IDAT?6G?mq_=rt5Qѿz:=m+If_OeuX`ۜ6n>?zPexȡo_m/ʐʠgiWkK>#sW_wקrwӆ}$׿?>~K8I'3fLă|g+o˺7/!fr6sXbŘc^CLmW;u:iΟq~띝56˝۶/wC9w;̋O_;yv/9?XAsd'v8~ةECgVV9z!b@U{,r O;&M?ac;/;C/Ff' Oug"K]9;AiÓ#uXwe_?GE?NG/sxn:6{'Y?^w#<˲G9"=zgo_yb'x.,|?ȸwBW_];F[V=$Q({mk㷮M;:>~cؕ+ 6r'Gϔ&OqNg 0JȲÃ|`æL@Bц:m`i3_SO]r؀cGwǮmk2~?W_/G|6!t)c*nç,vض?2UH!9I#?#w}{/Oߜ?3k_9@Ο9'g˜=&f_sDVԣy׮||y/N䶓w$Ǐ.W< [޿׶~|lAN yڻt!rC8OԷ mڥ_c%quLCY`)KIvHLT;=G 9S6?b/uw>tt}d lA\!K#mWqhq[kgQb;7~5K\^O.̵C4:/w3A?k̿?Y9~$Ƚú^ /ڲ.1gwO ?YwAd<#w*\! ht3WV^" cL?:w<*քmF}y?U'+}/T\VBn%%2Dn;u:bil}|;nSz?(>?SƏsIeegH__ vO߬'gFϯSp/_rk^!'e;r+e~y'kʾ/ؾZxqVz7*}WZ?'k3WZGgMyG:GپCC}?Sǖ}q̻<{֭oy͛ 7A*ݸYnѺq.$|SuSf*߬|?WE֟1rs:#g|?%OyVs7߿୷z7|^{?ޛ7_;΋AちeR)(K;FO#=MG?)bmmQ=(#d|__?KOgolnܹwe,MYS;o=s;HϠAޗ 4Y,z"phL 453)n2d=\=VCȲfpkZk°.9 xh9O~8sz֟ -&|:Ṕgf5FYv!v!ʷꗇ뷉_"} o|+_zJ^RWޗR6e6!/̦ȣgӧQ5ȏGיE-|*N96ui%i\}ǯ<:JtqQ > Z.kP@C^]r6_YhWrȾϵ'$+gN 2wG#i*۷o|o/߹s}o歷n~o޽{Ӌ{Mabzo BA  @A  @A \/x# Y~M"ۛg{);gyf;/~w;\+qE#'mQ>~/+)ky|x7 s_We|_FOƏ>m~i"#F_x{Κ M\S7 F_)IP/RE>\ ?#EdP(cߕ-3ۉ=mr_1.Yj_Ko;G?^𢁿Z @A  @A  ΂/ xpCݻ7z?o~?T2{Sr1;\ڸog'=m˿#Vz޿"MH𩓏i }VqվvF6HԑSwʡ վl7GM>r#ER:.C }Ӄ@сmtiSm_h'a9|x?ehl<2:c+/| z=_76?|^0_,/ PA  @A  @7ؼWo Ks_{*a%Kĝ';\I!|?KȽgNڔ B{W뿊^lGdI$lӮ͓$-Bt2IP>X@RF  y 0uuw§9U;U?YH;r ±ؓ[W>O|6ԿB_/Z@0@A <eH9Ah̑/x)[ʼn>(hbO=r/),߇ *nǃɑv(~_կ}}W_}u~I"?@A +A8x0\m練SrmIlO}Y@IRA"#dg ȑE] bmۺd8ձy߽8@2$ܺ|pү| 􉏿7= L( e#ˁ 6l|񞯃/YG>i9ACdE X` uy/I|h 9lYH]GW(k=>4hnM>ho?EK7˅&@A<X/ ^AmE׋ (tzV/U{;z}߭gw@A  @%w>˿|o}[?[NwR6yZ#Q{k{žSÝ-2+m'Uq/GrၮUחAO#I] e8 ̫8uTF|؁#$D<<`Q~m_DX&Ǟl㴽/_}Wj~zY@A  0|_逗/2҇<8^?d8x UP&7U:#_\dA  @>6w2<.H~rɥ&}h> Orq,mآg_"Q>|}?e۰M<_Ȓ>6ԭ֮x]_ A nm' :PʼnlGGCQxmC=[vT–ee?ڵ~ӟ}//B A  OK< _*රK/K~ |"|_|?_Lٜ/`SUav}q3hBN4;,z/ ҝӔ{%_H]_JW}=JQ_l^ Fíoˆzp^8LU A  @Ogyfsg_g>oUռu}*m2zVqOyxqpN{~/[ uLJwǟcv˗<<|迊~S *Nl +?K^OY/ ($aR/9|}m_¿Ӧ~]g_}mG37o믿я~  A kA]h*Ԝ BuGu:}UԗO_Vek%jw_t\9/  /R /C} 'w^,1foU|a|/n᫇ޚ^BLÇU A  @W^2?޼wo>/_b߫O3>ܟr/je;aOQũ|:9<^ŭҎ=eɻ>>c_^'|a/p|Ġ8e |Hp: A$G} }Q+ m6|MSu+߱t>/[oom^}* @|o\/~*rPru9<}Ip7|Yp}y _~0/BC~};9;v ՗p/X <}A7_͗scITjE? Rz Qe b:y @A w֗>?uu/}KzXx)>wwԹCEWcg՗_%Dn_I^$l۵ÃWWY?v!DG.(;H?1>hdP#HW<Mer1?r⶟Aހ#9: ?6jz  Ӌgo}.?Q/>KN/ ַ*/L_K^_{/'ЕF`k͟MbP5ԋe|Ⲿ/F_Q\UO1_qϟJ[_rx7T/\qY+@A  k^&~ _Jr@.yʁ*eM9[V{T78v.9exonު A  D"P˄uAzWdTN=2}ׅnmnԿ­?7keo WWv G6?r%¿zs GGP[_p UpC}C( @A \&oͺ˼{,_;Od-{'Oޯr C6v_ (ˣݻ*N_ڰwlbM6OYUڨkvI;e@rQ>s3 aI%up֑,hG6*NGFyN?蟺rח>hgǧ@u6S/~7o^PA <zp2M27{ nq8 r˄CAq Ps$\^<ҡ^瑨Nݚs66[+nz/o zPlr@A  N@ba[om>ww?V`dV~oJK{TGKOv%ȓ{Jo*].| |y(Cءoڅ } ˿?cn[5oO?NrQŇa zzByUv=<Q;'|w=E;vi?y'_ }N*N6S:m@D:Ph@A"0O׋_Njs?^x67lO/* D!Pyv+呦3c|9 ó{6R+񷰼xeo\ʫt6l@A  ~Yz~ܫAOz){%G^UvulqJ:*Nw.$Yʻ?~@>ѥfqPO_{%s#wn!| QQlMrBFഡ~)M {z@ân_,`s?xgs/{#vq7F^$|m{PxWkۿFZϿV2Au_~!MvA0}^VʡNpˆ.cHA  S;es=Le'[sL{$MiSVx5[{@۶cPw SN[}t9 z?yE܁ZrT69Hٞ[cx <4K>d6FdIޜp:a_'Q6Uq%^zPqRڐ< e#Ku1YKG\V}I-%!u{Hwc|^}ᓕ*P/_U:/zP/WBF4 A  @{=s}k+=)ʴuR:d]ڦ,#}_pЮ_LCyi7iWRPgn~: ?{XGqQ$71ظl0`z1=BHH$@/IH$k)b{/ S lI꿚[d̜́9mήΞi/ڠ+6+yӣV{+me+l{%lA]$l;ޕS=R*;!='&=======P:47[6Y@ܔ/B.e:D8h Xˁ4Z6/جb/dP4Gtѯ託~Im P;Û#P=WtC%9rWpu“`P n$^ɥ.lߥFQN= B?:$2 ee|)FDDDD[t j+˖^ ~O^vB聞9jNgf$>[ 8­hw浚] bJh3n9E~YQQIdz$ɥ8*QiUY,0F+zփFrB(g+z6ʊfm[UڒO=rn-:pCJ4DhHڹWp]%#G#^,seDh7%U,˰X:X1y~S MਃV2&hKyڠX n:BC7l=)ԓr&&qcp)@At`@Jȇ,By O2B:p~xC*Sډ~C#:rqEtUL_8C7z z z z ػdn+\Ñ n$s[:%iL 0Y[Tr[$`Kl.W밮ov b;V;~6nruߨsUU7Jϖ}[bM@@@>``١GE4XZ]]Voh֝Bg8u2 Yꈣ* >7r6RxX)YRƟU.-*\҅^:t6,@H%C:U()Fp CYΒHd tђIy񊖛 ldRIe KihWZ?葧-tRO*ɪ=*\聼<м*>V+f xu> #)Ui7`>jg+%f\hɶ 8j+%VC0Z.- 4Tiߋ6h!z z zz 555jf#=hW@@@pS~ߔ߶`A|GU|U1ZU=)1V-edKR\Z|\>ɢ. MP?4!^Oz\GvY) hZ A4B798H!=H.HVZ@ C |(GzD'Lx FtC_ԇE`v%5d;fS)vh+^$}e6o],p_ m {ڹ 5t_v;U`E?lԍn/DDDtX _p:˷;uEDР(c߸p %&fMQ<A8}OLaTSCy>L* : ްTqZbi|h;|a̗j w[7RUyˇ.VNvˊGD=\242g5VF\]*uĐKHEDD4+>A0а|rJ if)v c*:a7cUyⱬ++:h3e^52oRӲL\!H^+ /H1cH9!4<`EJ aUT}Ok#%/Ъ mK\m:^K?H_lܑ~+#Hla@2H5H)ur89D8*)@x) /yÇ-@://4o^'eBy/lx&| ;ft8`====MlۑS܊Wboor~WPz2W}MVj[(y 9z VK̜pECF[#c ޺S===෎=beCep!OΡ.⠀b*nPBC=u\%C8dAG^Wpiinp՗U1^UED^`/ d4hd෰dgδv+X⧊)C.®~$ Nβy@J'ƨj,O%ZjQ/^;6J'rpt#@#Mײ$~OdȮP?uj3[X@F !iKUŔ 鉙RCpt03AzANd8Z8R=e! _=M6 2"t{~*-C O R1B*ijG?9!:\2J ?u䒗cEOYtԧ#O@>=hz|(S~Bj=5&BV====З=b/[pv7o=WO_Xl{@@s+6;GdrAÁvĕjgo[.E\@@aPPQE鷰$+RRTOGXJHUSëksM3 >/yꥏx-2\/K4.Rɐ>ѫI68W^r( 0j#p. Hi0|8R7¼*!p3R'Or$W!:E^vGz'~0I =7'͆,B@@@@9-e.H_w88.#66z{z4U MCm5k]ݗZ{ѽ聞9====PBZr*بʤ[#+4_F'/ ebYnW2I~d,{CK?t~PhJ%0.lȦ *U=N@+Ƈ4dHjo72Vpa%-At^)CK9 ,h_ <璜FD6f׿"J@; V6gAu d7G5n k]I===`[66 Rk]cC==P؋j$> _+UU/fdB>#|v+\@F{G GO:Z깐-{WuVO]QKs5^PcU yՋ>9jل|I4e 6@rg@kO.?)7;ѩv]˪<7Ud+udڏɶlR{`РAs:t݅^XCJݚ?ݦn/_*` E@6𛥎sr8j4B@@l5/mgۖpgʝWu3.u6M,EDDDDDDD (b:N𲢏LJ1y}+⪖Mb,Z|jxN2# ^Ø/kfɠBtB#>lG0 !Æ`8Ze}Z.nnnL顬ƃ!e-DJ׍F|4V@>6zF~}|d>SO@@)<knu)Zw-)sĈ.Vo|ꫯvs푶vQvÆ sZ6AXgvگR 2DElٲ>p ݚk-^-\0)7kFT1Teծ[&2z})[֖x>Z\An7\amWpq0tE6p@W=|fas M_&-@IDAT% qdho%2R |Rbe )"??K&xUײm[!l/\X{ӦMS 4tpAuu;餓u]fΜfN=T?pSLIP555_gnF/'N?_oso2~85?k>=fk\fϙV4hBFϳ'XĶdNoϱtW;MV߿ƽ=Eau?EqHw T m8a 2 2W׺ @yy0@lي'?|)I 8*)4Ï^]8dGJ>e͛.C ʷܲvd _vq{)N;n< >̕hw3ߜzaS{v>\(|~{kؑ!l;Cܦmod+|쿏l{7fx)wmwvީY}ocgy>_d{7yY/&~ޞJoJ?Wf=05t@W窍i@@@ G26p7`odCC{o{Bv{󍙮>yED;G;3YuѹL'W"W4 -8H.K?xhС:<) vzdR~ʀ^@ȫp [AnAM)0# 92F4n48xDR /*K>oSjc}wqGw*.Jo~N!i[>mv7nsi_&N&e+~ġnjrtYy.ϭp2ֱ\ :,r7[32zxwˎYd x!A 9c7ߜՏWӖ%EkfQm_6`3wfJ2oA~uJ#w"O~:Ÿ6uOw[:?M@3Dƍ}3ؙF^n.e骒Gl4WE孫'oK("6S>#b+]uR;&B@@@s[o y%/M~؆6pl4xqjdѤ聮{?*0eF yHNC>[%e} 4 d34ALj R頌-m(/ԅLꄓ !@uDGˠeƀANPC04 ahl ph9TsDkmC CY8&zЇfP-|ʦ 0#l?үQNv}oUI}k+p8yȚ!k~@eEU+Q풞W;erOwXpev|rWL8-{̀zW<|7n58|p2wyg\?}Y>3@QGzEgRaNomc?!M_uB2hwʋ$ ȯmC݈c=*oi3bFYmg֟>@' ߗ 8#h .;e//oRm)juZLZf _so~xB?nZd8k[G[Qv_rm4nu聞 .0Iǟ+/-X{݄Fީ;mb<43T-[<4nRZ\aS,Xb|L+X:}Jc%ⵊ*'#< Weu ϥ_[~E?b'rȫ=IRpP&H%O CTvJ>iABA qF!Cz9 $89]xʒ-1)s^@de; K%@?2/mP[WiŅߖ .% L cʦd`B }vWaF}%\}`#h:)9ҝxY~awu״i?Ef`U <_v?mWg%\gi2ɝ={vV9 Oc뭖Γ[fA9vqo:`a?๐?NRw/3=̕OEضi.'AϑQŁLV.&4i,sM_px~ 0_=׿-7{VzI;hwFJ{ɫk[lusbO} s3؏ZUv dVՏh{_/{_O?{ϭKlBf1(Vd5ږ}>f͜9KbMwݶnc=5``d̘>{ZkMw>c_:H 0±^J~RVq`2kU)[5j?_Vsfζ{GW9g|n\`|fn9+w\7i(GDD;C}۷mCg =}i^w 4#|u]'_U?@G_f+/b!z z z=FA{TV+ T(@KK=!OJU2)J78 ÓE@=Ȓ  e.dH/>zNiJ+/4yy 0XjdbIjxᢱ[eHx'GSW-8]7S2D:zK|I4#~RmV٨H^epeS :QY,:QY,֡3:d!30zL;8sP{`k3lGp4zcXJІJQPbРY_A*O. n|b [l1|A~ /Β'~mվͪw9.jf[\3o 0pC>->}f㌯f'ʠCLw6U8 Y-VE.r_wX?쩳 0̶~Luɀ 3 z+3&R @=#n''*X=!(vv6h`8ܖ[OhS kسկ}a4nm2I~ ) g ~J䲻\-U6g^LlwQ===P8yr+Wp?ʳ39 h7sC,?a–Y}wfc#gDb5c@=ݏ_L@k8b?*YqgI<{EK B,\7:C#J KpUpڦ2~ҫ PH3Oh`2CN(F醉F Wy)z`#GjDz鳬䃗OzɅNr#KIF?M\h/~ehaghU-T}(2](LYZ..TZ+?2](YcifȎ,4.+8,o=.t9s+>(w_ )4큦5WM=o`v}(pđsB Э[*~ YV`up\po|B+odKWwy7K4Cm<N}0,2zu].~s] r wݒe3>C` 30ۃ|ϔـK@7L i9cǎeM~w?7hk.k[rMG!z z z#lVC}`t|dt;R L֗^zٯdīLKlgCzCGc]@@@֘hG NJPP ZhIʤ$?%YZ1===*y筚򀁶:AbC|o{>RjoJ-|k!hp?g`Bvz[HmKR}LʃN)֔\ 0'uc&zڵU-_% R>,:IRɵ^Wp#Sฤ^@z G+¯J?6H?<~+z^ d#y8P񀜎PCCxG9CDt3,%\A pMcuүݒ^~.u -eCCzʲ< Lmw|֙6Ǐmsم-e kupO0yIgCnV _>%]w/뮻 >hoŇI'fv8VR-v]]cNzW%o; /twx+%(ɶE!N5V:L@Ͽy9W[[<o!!̛7}rK{p %{ :W2(wK;k=}-J lteW?c?QGcOv Oxn.?=YS<.`eܽ'; BNfxn3"rTwh'2!{ɧ a-VRޖB!M>磏>r?4iDFnILz%/|j?7xӝs1.wgop扠g]޶{ҭ]`M7x4FAK[4f 9 >0/@V 6ɆD 8ۏA+V=?l>3ۙ;3 F4aP7ff=ĻpSm l{˲˖.NTп>|X{ܱv„6̰->}F',2b>z z<Q% (XGM1a_I`*O;mGP_\b-'\Dz G.dW%W|~@Z?/#Çar(e $hxp=)xOG7ˉ[NWP?P?4P?y@m@|*S/J:ڌKV0eORV ٣xBŐ=ӲQ.bWjtP2n). .t]vYJfnkjz>Ґ1*H}.,T1$ټ.LCߝo3C>^۬J tW\w;!^?g}< oCv23Ǚ?%Yχ1Rü #?fC;>;s`xWo3ox;Zs8"ͦq~0>~vwfrMjB{y%_瞝Gsv|f!gd;L`a  翹G?Y2 gyg dR1|nbs2ܦ!HX[[W1 j[nb. Xݰ%Yլ:h6|8amy\>fo* m[B .@\"F(=xnjm\ma JPy{64뻲-wDDB~}G9 5uQU_\A |a< Yp\iaW4.O4Ȃ_zB /JClPCppaAh4XtYT L^,< ONtGMʧB22TW):$[Kl%ϔZUYEy_V@1dͽ(Cv)J)(,xagM7-W^MXj-~ԨQYK CŻ~Wi|> =8{ݵ3=6[ÌÏG~;f[RfJ !1cG;#2ZnP?oMЋ-Jl+N)B%0YƮ H,p?gq?RB01BX<[n^dUuF٬[m~[+ ? *'ML@"x2xРÏ$el-16?/!dRws#Ke~ND7,Kc!z z`ur8F pRnm K.u􌳃YqŮXA4Š@ߗ+/sClM`~U#D9zJġz&E5JBi2 gdϻ.rETvILF!N ۇ\V]]3 B_r}PWG9dg)Db*h +K80u[C yfh.vUXrO,rގvh/YG%e̘0aK>Ƕ^u,g%]%wuڎ"ϝ37'?XIНuw 1cF;Pr9{?cgptLx'^{홨Zygҥ 2l0q6-~Z/svZߜ(Nj+k?V===ikoaP^0mp!z`Ug+pnd 8\> AZkgps3m ~ޱ4  vljV8``PmMʆm(JChGc}-^D{=`|v̘ўd@7\<=%|dRb'ު20K=|ԓGFg#^RFq\꩓lҫԧS_t>Q{EI*otF#:ҼA`EcH) q(xÛ#^EOʅEIJڦz9ܪ_Ε>v:ZpY%ӪtᇇTW*ˎY%j{@KŐNHfl1dCU4AYf\r%)0s=؜fir.]s5nƌovfqd51T<|kշ 7my-c3N eRy ˟NǞ~${=Mr ?!/0 *,Yo_4R>cfwXE0f #A V.<ꗿqg}wl2 P~{m}/fZo+gp*(Ifّ;l>MʇoyǟYۂ5\@pR?7YN(Aafme PQd@@@'=0gO9vhd$[gr-@ig%`fU+Q:z@ Rh#ZYd+ p ߫˂?*rIK>μ`pA2XYkק sr3X!z z OAml2):wD.I'HTZEe#<Dž^S~2duѡ Z=oD'~dB j@̥Wt]Nu3̘b( N$gH Nx#GzNqtåOrn'/G yF!+GRV!BN=%-8](^Fd trX\bm2K/ԯdƍs%]{キ/|$d'mc{{$bp+Gwg[,dd`SO=+Ɲo9#mv}1PЦ)m'āXR\˶"hMRֽ-DF)y b3^z[!u/b| U|YtW\~ur49w{眕`^-;(, 8mgְ/` m lle/ǶO>Tod#7mgz?Ӗ7Ƀ[g$Bd 敹[*%7?  *+͏/eeO8{pÉ'讽Z:vuׄg몫rOpsά` gp+/sz}ًf+X*KљU֓cƎILzwm&X,|p^wEBOf~GLoo;5~0Q͖JiX>{20pv+;]awu{$TW1]!<У%_gΜgYHKAЃ3Fڋl#}<]k2$q73l 迲T(_F*m8ѷ()̕M!Ke@===Ё>m.Pf۞7ߜGk+{O`h&Nj0VFΈ?83y`LIoآNc`B:{>%@&|N1Pb\ҡ#FJ>$@KY4~p48uRGQz#IV{/=nM^j;iQ;id G*'?v! h%_޴,CGC05FG=)exI%Coāc*=yq6駼*s"z ۆ| .C=)zxT6R3 3e?.^{W\@& fP)T&Ȍ+=I`/[Ϩfȑ~!lW}9C\@M\(ޓQ}s\G7 +fΜZ/&a%ѧF?䓎C8qmGz3~aM}z'cܺԻ]vs#H=r0KJav)}UǛr&m﷊> g{O%ķ B7l̪"9`6Dl)63cm;&>\@hykAf{8 |{q?{i7>o5!}罾grt vi~럻~"j{e# vy= ^POb賐; `q6KD88x;vu;gߜA"cu<`+=H ewGu*W6 3bו趫ro^LQV@@@r߽VnnXmE~0c 7V Geу^z9$+8_ks/K[tf% k cƌn(lp2m Àr3v?93ӗbx, s5'z zs~ 5N%OP<QdPGY|\Ђ#~h~و}KH/tЈ>K Gt˲]GnTMQYa8 ЪuCC^ԉDO*~HuC'y']A.F @JKxn^' UJyhSj܍/qglz)Wgbݍ63.HaRW؟9Y, ^S?]VA' Yq{]GЗ8yرm/z)VoMϫafG겎Y}&CCcZ{ÌaLIw7pk\ihv|볻ug̘uC=oM6Hw|6>J>,\^|LQy~ n\nAeၹ\l{sWm\Kvq8ϖ[W.ı5A8J &dgjcr[5cOwqՕ׸~ Al,=ϝetS:`zm &l6 K|/Vڸi[̬Pf2W=L``-p * ';7wMx)jdeuf1>x}23d {ɯ6An :kڙn[#6솬n2pRr6 ʈucƌv۵5 %VoQ2(:VFߤ]y>c4bD )H* ^4W>VC**P+/zj~M(|&Wey˭_ߕ}@9B@@@+٪ouW^~O_}յA\Xay8m- n'd`;b%KL~}-Xfv3HԿ]j;7ݧUl VD˪ VGD̘62.Hhқo0؊i#;$ şFfdia:o䉙c ,~ՃPW?S~_et+-W&*~HR~ _ P,YXds @j4hX/yH?| ]3!~K.ml'nR&*bGo⨫(\1&G]xEF.[*o_wgtگ:7mZV$bAk*7>mѦI3BJ\R̨{G'φE?L$byY(3ڃϻo7ݚ쁩|l6ӌ}S_/u,؛ޙ5~s8aHG3:7pf U: a=zr ̛7ߝxhD} OE([>)rCZ&ԑoq| &*.?#DDDD.MdxۂVr*pg>_@IDATB5$`{B` @Ys|DŽvIsMyEon0/Rpޘc0D|V-`7gTDH&>H5职yTfp<?^1^RGYu*#cU*r h.).!^w|sd q57YXǁ9s"us1enmܾ[;ÒǁΜPm:y l9*K9;lKtx҇3@cj#,m`[jY6to`! V|Se؍:~P- Wx#l%{CPȳ]MW۶rnOjP|큮,;MKxo.'o{S]  ӧP()#E~Ʋ˒(We+z?m wħOHh==7ns·g"VR>GJx*3oPG}H- z+W2%X#CCzR%aPX#f;Q>L e:ܥT.1ġ&C8Xu0:.-re<Kӂ#ц8t _?CE?+husX ?\Tr>&!O n-op.s,o6b`g9܍F)sC[Xvm++sʹs pɥ t]/馛|Gxm.`^fd:g̘1I,XϩVknKhb!g͚F,%7s̐?S6Zς͛C|Y\k[Pۯ]݅we4$|1-\Ÿ=ج67'_ >߇K*-7Krb׿N} y{x||n:Lrl~b+n9B z{e+A> jk;?1$ObH+M[*`$mT*Qn@@'=@T4|* X*Ѐ#>mŜ|.8"R^.Az gى^ſ$BA/ZRÃLld_<+/5 !\R.D1:z8pmY_QW /I/uSsNr!hgDHCv`'ֽ6u==P*0Kgw&McLv\ڛ]*Gh^rdJ\V"Dd<srdVBļ@O}n5a-NJV.=eww"&z z+(ݮز*ZV2^rtCcwaUc}@RN +d`"qTi'Fʥ:RŇ-GeBъzd!74 tZI_$[BZbZm=#ݲAe nՑ":ђ"KIRE| MAAN32HF c,ntj$2 phT'yJ̅^$Dzce䢟\>ypP3eAھW-r=[- 7~ z‹BOI=ІXϸzWwZȱ%.|c^]hR@@@ <;-(2z z zXK U#[1S*dʀȆ ԓWeYC+^LNoO?xhx=cll"zxHuA}P=e'7И1FTC#::NΑj$6JRlGeR9yQ撣śK%m?z>ɣ ~ɥ@8U/C%{%xK====G^U,TeQszb*+o$By݊F!~ѠCXRIP?,^vRH?yKzՑʾP?4*~RGhH: pwCc࣌1yDcp".hdJz59jdHU:]ENK?u\C@'tz_ՂI=#d~CJ====з<mg.VN[&pʉҥ?UoծCBx'ݛotͶB;{קče MӆM+un>NlDΨ߃Ӗ&bznfš2NqQR⡊R]xUC%1U!B|"N'u䑛KIyzC %C)SNbD+֏O6\ [_(wR!QK QRP&acJxoDy @H>|(9*K&J놅x=q22䑇ا:f~ uyN]ɾYނ"B@@@@@տ\ nq.R`oC\~*Wh`\[ngjplmQ7M6]>ri;wߥ !z z z4໻/V/F>}FTGo2Ph+σ'0jW<\ DeSG*3_ | eIdx~@)c%_i_84 e4m2Fv dqInR#UGKoddתמ~Dg.kY…xKR]vqy6*o>9nWKlnM.Xܸ՝ZF)B@@@O ϨsKk82[Unv=jt3cS@I pf@_0U~ #*~*C+@x⯔g&O "G@π Ν0N߇CUP@ uRR^A =c_wn|0ܜen ul?b[v_G9>~`_W7eɜx7JO#!Ik4CDb1ֆыګyp>q{|bЯgsħGGo<3V c =b{B "}"nuYs5o5lh5&>2Rž=>W_z47O4{|Xs_"c#11k8ڲι{$RD@a ,b^ Å:?\u5R]& ʟG׆X߆aґ0az7'_W ͅڇֆ ?qrO;Sl@K3ڱPOF @OnDPֱ8i4%v}O c/UcκыS/Aq_=(?։/ cRͱ;g11pGa  12U'/L sZZfX >=-bz㋏ zcT`$/&cl\mXؘp ` x :lDzcHA\ŵC`/sXǺ6؏":ܰU~л-&@a GPmUu˒?S]UߩġhHy 9ư^n0p:T<:T8sl/-~4_2%0N"y (q=Dd@ew^XR X>Joյ:)ER[Ke1 R@uN+ǎ= 𱥱ensaN5lb.p-N<=h7lA( &E4goȘcHƒzִ!kذYN{a>ЛzD[zc#~ ulXbRcLlc-hvWHa 3捁p[/ aT y h]}7*[m0niLa+ _^ +nk:e=_S|̍0N6/׃ V|rÆ%00z}KN}E"Vk06&}oBac (#&Fn=8:[n me4#gf>XkrkC^q8 /v7W\[ .c1&mHa k``+?8{+`ci=\3j'&ƿ7U퍁"b$ [p:Txe] 0Ri;_?:6koa6 0psy{MqU@GÀֺEg|IݔڨuP^TfMcNFAӈ;zka3B9@z|!ŷެuq셲'{Q5oĒ~5lLbb#c/5.7R5v6GыN|9xħoCXK4rCz~Va 0zOiu@0W1v;>_5sv˥ 4a샑0n ӧlWn?K7-lա/~?R1^P@8.(=Z}ƩCsM03Åzni>!Z)T2F[WCY`_luol\'sz8e:&!} kϚ1z1^RX*˗A!bκ=:z}]#IȑpnFᇰ=1$c<4Sv5 bsl؈1)㏞z|s؏6惍k)i$ 030L-LM[(}M|]~>Lvoוֹo ߩo8<710p uuY*n?ۏĮO@qpW^!y q6)@Y?4Z'IPz`O_bCkEy>:,6Ao|]G|C5}}aC[~bO="I{v&0BpA+la͍;6@̱_ù/~2ױ}X"/x(-OlXaC#}%#w6ٹtkODtGa a`p៟oUWnW-wxY.?n{uju a0P`^X_Va닶UѸ5گWgJJe@a 0p:DRs&cĺ+=v|&.kW7^ [s*~_Ǝ'6'5xS_{ qZnc6c#׋;c77W$H os'QNM6}Lcaǘ9//cp䩍{Þxc2W^sa"_/UEܶHa z87 -^] u[/` m=g{zN6|~ٺ;we<`d* `o^ZQxy_$[]L>^x:PB=,~^ a 0d` g_KEKvJ-15ә^:z@ ֐kbcq>~{׉%ЉeM؏9p^ffna_U|#4L__얊e$#K6qIm37V \˜_[uaC52k1r1 ?a<š׾͎@9 }La 0nsob5ޝɁ×_D_Ez;@V;NKc-߯wka_G͘U\0%ڸt-uٲQ6;[uWVXk#a 0@a:5ImZ*2C,ִAq9Y=vj ?u1gXilŦC-v4cp.{gX%"|cr|o,z}fJLg:C4=vI4scQ1ձ"㜘J+_X#6C{|8`ЏI?@ad0PR .o 6_Ur(m/vG3Yڰz-ߵ5Lna&ϫg/ÄUm{ztm<Կ!h@a 0 P]y8\RTgίz[ޟ8ډU |ĤK|3 >^!x֏{|_o\m!M .ZCB 6A#0=bRsݹ1 ıu9Ǘ뾐c|IE bp _2كOLgLH/W6s?0{혳OrC3s se/8I@a$1x+ > <<8ԡ|?|OCjܖi.u{@LCgn s607[[L u<(TϿHa 0@8({&OjjCRꮬ[_m~eZeN\lΘ~yϼ'F_6nߋ91űF~s5ELb5'B1II:6{>4'1s혻^';s?x1/vKlgY\ܿc6a2|fD0@8 ,*uP:lxyVu1>e[m}auP׆ɧ}+?%3< M9uϫo'qi+=ܱ&c>{{{] ;"@ژ`$:7C!>ƗH1nڦzC/GglB1>zǎ5z"xrYK|Y__ӈ),r)"a 0Nu?j׷`ss뭐w:߮ƲVrX{?TmZ=뎝2@Jo<~Uup:sP;6O<3ƬeQQ6c?68 0@a ̮ Z^ ·ej\@#vNq^fclg9:鉣yXE\?֘#'b#c|ܗ߫s_"90&>øѦ̮ګs$9z_xDuL2ع_ un _%R| d<s"Ƌr[`߃i_t `XWgLb1okZHa dk2js%L:l /[ož m}OjSo/[_?sP&Äg?1z\$ 07p<3i{՞Rsnj*n)8Y"&l]͝?z.ƻ9; 0@a0`j 'W묌i詉GѺko[7НU{t^̀f͗Xcc3&G'O q?dL_<_('&*0Amކ1: k`h#{ЀwW/-cl1>ء+k>_A|Dm/уWf0@*uVqBwuۘF/6-O[WmL>Y o?Ss?Äzyk~W"xa 0@f#O/ϹBO=?ƞ/4lG~Q+~O|O.+=q1OO,pwcØӣ986Y7cѮmdxP^%& BO& 8bB:$ }bif\ĠV}N|}XIzR!Ub w1MѳQLumYg=fM[ YDZ1cg| 17z|ְCel |c]lЋ/G$z}j a 0ph.m7t5wZ9H9Z Oa'Q*Է_Ja<LHa 0@iF͓#\@9TϺzm-cx1FO^_ӦۛؠG#c ƮcMϺĎX{6zxv3WŚdݘVIk`C =c"q9q"OsI<챥Gִw{_4[c{=8q;7ڀϷЃcj!@a eGkU؝j[Ϯ4}{n{dZf@k:dRue ʯ9ܕKǁ|xY6զ+iQ2Bߏ'h'O+'5n ¾aSؘt]mz.1xA'XvgR_,f}? 0@a ~〱W ǚ1W陹mLW\4^Oֈ0+1>:]ag; 1cwOc n,ljcW/>z| ɬAL`ho I,/ģ;0C۳_Da`k~9]xp+ANGNJ0@a 0PT4mLn:S8P7k\ybgA7g>XW=1lMQ?K|' s|2R56@X^l{f\cX^){tI3Ffqc#cCqXq5E}<0xҮ_'&c\|{`S .10@Gx"o{S=zw!< #:s4vE{؇{>͐Ha 0@2sS>m^P1̞1d cƘ*Sll:/clZzD\{qz|raW6Lt}O˸ގ.aB>): ֘#C@x!(:%85X3|~+}j| sD|挱㗪Ӄﭓz|c3VX$ȩ\?kNoA$ 0@ =Xcɂ@{pp=0 O!یy&=]5?XӨl՟g|>kסkõy6C!k]>0@a 0'=Z֐nQ[jA˺au1"ڊ/D,zu5Κ2|`?m,X=~2;&⛋|vE] c3FY3Ÿ/>s_hlt^TgSV|b B΋Yo|bK_G?Ŏqհ9bla 0Id>p᣼LڷjSnmtG};0ba&wa|2j΃`.U{X3F?8O5>}O8%<=a;n[=ԸmP50@a 0p 10{"6W\QШ|"EcϘ/v^L.,'4}wa 3Ff6ŧwLϺ6?`gܽמa3 Hp{Ƃ#j Ġ9Gߌ$>&9㳎G'~ =}.1״'NhE@a 0Kwf@a 0@y pBtVi'2ZŖzuT(/rb?.뺵We2u,__R֟іx{X`"ZHo~RO &cm4s?_y,㍈w93m?`Do [י/G5ɤTz]2a 0@a 0@a RYj}1-c{#+Uө !w;\gL ;&98Ɯ^1=*.a_DlSz>eSdB1UBlm!cK/c0}ُkF)af a 0@a 0@a rr"eYS5d"֟[{eY0g,'&"slf7{1'tګ3 X ,wnoBFOGOtK3Ɔ>И4cp~D\~y=j| 7>k:Fh"a 0@a 0@a gH_Ewkcb_kc ll%/k 'RaCD M{IFbjϡkc1V 5[5_of>K2!`9ɳ9 u{CK(1w5܅ELִ_?쌏_Ɂo,s?Y~]Ϝz|1(fǯgoLlg>Ϗ0@a 0@a 0Igfi-'}_HJӎ15Rz 5Ě񘳶ubi_67`czq^=ccѹ_X8JWH`8>7C:k.>5x'&|#cRakN㇎xkYÖfk,q]tG@a 0@a 0@'뚕)5RO'zQ6\y5k5YcQcņaY./"1Yc+5il=kk'VX?u/ᇸ`k[ýbB{WfbCzE=v~XgC?e1i6\vb~#665h?]ᅱPE@a 0@a 0@VפI u\&j+?36!ztY혣? |bg?WlrqNj@$D ʋiĄk v/_kE>%Spg#<}a 0@a 0@a0@zA /ERĬSc>|gƴ~|>ĿJ Gi?~wsLիֆb?X4'QD_K[4{|l; >1,%y~.@a 0@a 0@8} P3]VDуi UX&'>L_}{3~:w JOի($s@{b!o'z_|uPe2|XQ}Wퟃ_Ha 0@a 0@aT2?"} WeZ3W]]oվGRL'_zuT+_<{08},IhC<|w͸̱߿ؾXY@IDAT/9{|z'qaXE@a 0@a 0@E_뺇keq%>t{~ulO Kyb 1qOfګI,z>41X_;ֈ_"=kǍ|~{g a 0@a 0@a 2,ɘ!yZjOM?:c欩_UQy>X[to$*|rE:Lgև0_ @&J!z" azLs b5X'_?G~G@a 0@a 0@ }-_6iQuegLCR>>S|SY^|ipȚ.^z8Pރ07l-U<AǺ1Y^=e>L|ɣ?{{a$ 0@a 0@a 0pJzK8Y}L|ٿvYS۞+ Rh,cuH~ְx97zbsk-gM>fČ0@a 0@a 0N#Dizكs9@Y[Ue5؃2|@LpoM^lŷ}Zo~ E2.uȢ@$;Z=mט#o\mY#V1|b'!{Jp0@a 0@a 0N }]/vGR+u֟7>q5{WfA@0 ̱y [M%i2 ccbblgQg'~̉IR:@a 0@a 0@8u :(DlzC{mtpϴ ~` WrM@{[gؾxccc/=l_ĵx'|=20@a 0@a 0N}g_wf?7|;Bٯ@ H_gNA|\GG o'萾hNla33<\X6܏*|}_|ְ߽?"ƫ񉄁0@a 0@a 0N#ReC]uvl [e}\U\֬[3} _ ^(lvNb ( @(cհN{lI1bcN^fGUcU;k ٌ)XG}GgLOǦ?qz_;K|[(}R  Ø s7&ޏb _ng,@r3@a 0@a 0@8 +]f~owA.~b)-#$.! سljO/OWvXaO~%=~s_+bzWG<[2GCq>4s%9T0@a 0@a 0`gekQ[O*sMj{ڵX֚6|!6/s{|0 :zb`7؂>˜35Ŷ/Uf\!nh`g`L h0u*atCz/:MƗg$ 0@a 0@a 0pX(S˜jԾM,8_l`؉_YdA`lkަհ%W^c2&QAMq >=KOL/jxLt߿:ݿ:f0@a 0@a 0N?YQ/]T]V^9hlE_׭݊lǟEZ&5g_,bA Apv@$X$\pƾآg.i3gLC\gs D;^ޖW}{Z|"a 0@a 0@a i#י)E} AON /:H/3ƞ :s~ؚv>q㳯ٷc a 0@a 0@a Vg_K[{# p-;su(c}of=:~x}fT?#G/Ľ|=cXZ0@a 0@a 0ia`\L Ԛ/{qL;;lU c_+owçnݞXr.x^&D^KyLq ^A/=H8bϼw>`"|LEw1e0@a 0@a 0N%?wꝭkmz(_wQUE]:3ۛ'Ac{Yװ E&cm4s?_y,!#*|}]K rto\uΜWQE@a 0@a 0@:Rj}1:WHTO] ۘ? `ztiGޫķ/720/@a 0@a 0@85 PݑkJ}xGUVM}Zv#[N8G}7w /* y1XCk>bclYģn\7^'1]<}+9W;41K|z'w|į!a"a 0@a 0pSJN;7&\)K+/U\-vEB$ 0͏?虳FMwMjkNʚj–1gLmV|Ϛ5YbGMY|tHܜ?/ Eg\W+#ssD5\,JH0zs#&ú_⠧!&?>غы9~ M^"Y6/>=CsU a 0@a #e`rG{V]=.VS{ɓjLKGBSjj;GJZ.?\z%"a 0N=z-='\]FmUJ :{bL>. >Z1=_eA/wOrkBOe.>z g7A0 @Do"`/!#O}гn>:ro>Wt׹[7v0kdXX]ai4}~0@a 0!^/ÂT{q]4>ڳj_2a*ܻ 98LS0@8= E=s?-fS m5ҚذF5f v4k`OLߌ1V_fgհIg/}鉁? [_9v=8؂OS/?Ģ-&:zALJ[=s1F-[;#C\{xAGvc#>B`>sĘ36z:CG$@a 0@a`rgYb2}a‹~]Wqu)SׄSJ#T @ߙۿ[izuka10 ÎF BV{kkMUxa_{mYCУg.ގuŘF\{mCУCclegMe*>{h¸zpt !qsc9ƞ50|k8_[-BŸ6\|zHOW|j=0@a 00yڰ*e-|%վjsg>L1߲=L\T{xAa O޸hW[gZ9u^o3Ɩ*:+k{D|C/A m#ao~/'/6qiڢ%bŚL#ؠDW;cxJ":-3.clB5a 5ǏF `Ús[ c=c [Ha 0@a 4Fk_Yꢫ3A\|Uhpa#ZIA|?b8|E]Vȴuu[a 0ewH민k:+QlikP;xǚ+[8.ռVl_;䂿1E?qmذ.|1c87s֌Ke)`1g](I2IlǬiOL5Ơc6Z 89w/}OL|GO~؍e a 0@a i&SM0ֿnm|I]>WXL]O?RCu)?\Eո<6A<FwWGGHhxz7@s#8cGJ$qj:?Af6ш 1c1yN\>zDct?sx`bRE@a 0@gk\>*˦GU`w]U?үZt:TAa>!D+3=uR} 5j0a_Ձ/6؇0d`N7>j0`0`:BU=[9ɊuX#koR-g]|b o>؈ON=륞ݢ7qߜq'rĞq  dp>!IMBA@Ƙb1=>q(H1\3y2fݘmP=52|~7}<r)# 0@a 0p37 uvu >XWoÃV{G.NS!*UoK"¢juKWt>'_6l|Eyn?S7W0@b(O'5Pz7 Sq|\~>X&GboOYG}_ƽ2{ƈ15{|}[*W 0 u Y7!zli}̚6ιE8C|l6ǎ/1z|K^9g.ε7}a 0@ac5}W˿=iU -ꊺnKCoӬF-^Y}yrIy ["YG?]5E$ 027O'c5lum1xwz/:>1Rw^_K=ߋ'>kD\zD1cTM1fJLg:C4=v!1z߹4D|e,>}ϸl?">^{z|j􈾳l= YQh#&\0םC:%u֐1>1YCOl|c l=8;Ĺ9O<\5<>q9-{wp$ 0@a 0p0Vj^BZAe/V;ٲkҷBvZ=$\U·\6Uz:l ]ϫ0@NRMŞ+z-WӐV7-X3_&vc~"|hv?o͹LJg|/kiKg6[Ӥ,5IH,ɡu1IԒ;;w\l9[ƽ~erx!||sm-E@a 0@K'F|{2@}CiU~b~sqXmu1}4Zir^󹦮yڟZoopLHa 3`7k}:-3Ɩ5*=MS _Vl1̋C |װubu[;}؃;e^.s|㉏=>{UBPdg]Ől|!I$ǜX#'&6=a=>v̱GqP|376a j6(67/8D@a 0@^־8BSM7y9x@a5uW)k׆T^䱼.'EM2a uU[-UiRFz_̭dl|OL񍥎~yp׻A3}a}X߳ H$HO:H5 ׵6vsɕD[b =>5lzsdnLzpږIٯ՗I ?/_k̒fF@a 0@Y Uk;Ws~nuYxƟ#ZSl7[C{w!]K_:}Vq a 0y]Ojj驅R7.[Ϭ#R\[_u_O<Xu` Ķn|P|b%1ٟH~Aoݘ\a zvdLs5lh FGgl=R51aG|| a 0@a .&/ ~j}XV.pvk5T9\/u_5l`[Fxևɓe% 0xHww1]c3%~Rv Dўm<<`cs906:r[a} [aeLG x]?zXKEGx#=6೾G/>>b ڊ?0=#>X49vpcL-GGLB"@&HO@ui 6lB#$זud t̺ 1O|^|c|F?G_|cbL=q{|lu戟Y0@t=upa|Jcv=0@7;o7pyD>Q?o{piK ǓaV+7ɴ6pO^h>#V.cG4e@8 |"~j5c7lV_l-SSezcbkIO;)7; _1̡0n`Z0@n8{߽åK M3Q a 0p>Lŀdxѿ_ͻ=a <2 G o xɃ { /X72,42 0*~1=clߎı?zz}X㾖[6>׃O,pߜb>Y%<}O s3 -sæ3R/'o/>ԏ^;zD\zt= m%3^ #a 0p;kxG+W6MĄ[#HgW$a 4/3h]*޻8;>|xpw  /62axxpw~ٚe['ឿvϰӟ[dE@aL3NZ'| fPGnj͞N8Tm#.=ZKY!ԾWG a7tQſG޸i|;lzlk؉޸CWR`g`L 1i$D1Atl5| F>7czxE1=v25s1F/.s1yw~'Wq$ 0&qpg0k\TwI=ï#?{Ұ[&Uj2 a IFI驧Ҩ}jƜ} .r!ư^K} p+>a\茇MϘ,ї_mo {l\|tHo7t?Y%1!z:0>x"#~ic{l$Jl1|tKٖG~ʽwϧNa caEmQZjO50KDgM^џX7>vv{B'~q'C\{sߺ9z4EC^h܃[7;@Ħ1Ɵ3Lי3!3-k ۊ?0@8 p['4lnm7A&&[a \K\|pߺ? W)Fg/ q9[ϨCpsܽYa eRZ?gE}W =pXg:1hi#`1l_֙s0V|숩Yw@a 2ps?1;\h0@7ݽ7;w|p.]bw=?=>w{O /ZCcEa R1o&PS>[{k{~+W.X DO@½st-k~| 2G׌Gu,va 0pۆK.|k[gl6 0N[}s~hW3;/=wU-mm¿'$l~p{#[l. 0V3SsnO-}\ϗ§Bfӣ ^A=H8#3)rKK@gUos],0a`[rjZ_<{!rX/u}'8[|da 0 jֻg(pyvKX#(&cm4s?_y,~21>~Ӿ.u߽sHa U &%uם3O{j}?z`'?yV_k7~7 __6LЏ+/_jy?׼#t|copzEv46|_acc}x[>?#`0{<Ͷ^+[(, _͞'s1w}W_8yI8`w Ӊx]oÕeWߧ`?I1 0EO,R_cZ)˜)>&cN=WeN3>4w9{|JY5Ğk!9ckN=n &&CěF?yV "A}AaC0 knZNK_$ż_|{~ 8$FakL2sHa U(F>6qc_%_w:}߳^^ ٟp>'77B~p nmo(~簾>_<|~jG0gw||b c|\Fŧ'8ůazb뎱k\8@gI75R{wx}W?؏/feiD~ykf/_>`6ԧ9m>k=`:?{`QY$۲lbc70ƔjoL!PPR `bjB %Fs7Ye[rSeuo~W޷E]oܝ3o{zw4)ݱ\?KoڰaCzNoۮh&4z#s23{WkWPYɬYZ~XzԣKy޳Wt7T}C]2]к%e{8Gݟ.+НzB 3`&r%Yri렍P*xsCHSW.V)Uj-%>ZJ}/\J;Sg\,qB=$|͉X3L >YcW DPנ-ɠ0xIׁ^S]$"ԕ~3Ď~D1is _}eĢD~G}?W'~ā|;R3# 3`3_\rtۭo:0SҌ3rroVb^7S2~[nM8Ct 跽gt?s€IçjqnH˗^Q[|ȢnQzaaa\ o6tg<&c+3s[Stǖ寧뮻J /ggώɮ[ 6E_[<6b9RϏ,3`a@Ѷ|)yUGrJSK\aKO92*\q)|-L/84s~ .:Ư~|;+<:>mlB[zͯ ?wdu~@4٠c+? |ÆxXУ$'H(E|O:1넏/##s'kQ~CR_ZZ5f )̀>Ɔ{d:%/SNM+WtTs_LoۿO0~NJ?2xӻy'~&є&] wǓ<Ǧ"qIN|UgW];y4vH/Ҧ]?k4}mmաdDZl_,6:Ow핦MJ~γU~;}G~vR\hB 3`&\Q(MʱQj㥮.S^\|.JG5RTq; bዀ%~K|tj5nRjLG.h_G#eɦ*[``MT3D}ԉ/2El5pJH?b.r׸6>m>~MK|b >i,j{zQ,5W1>3`̀ ^?ݘZ$sN{-.05kH?_ӿiiEi֬Yy{3~튯=Ɠ$z׬Yt٥W4Q~͛ӕW^8,_pxlKYӪիa a+ωdUuٻxcC-o@.:8-XppwQrҴa A^1W\1uOjBΕ Ř~uޯ`gLs88tAi}Nkb[%7ߒ}/>ˮ+rM7'u ~r-mŝb;*m-]5\jJIh-*?[ E9sO [u+lG7`UUpϳq*w%Q!qղ/-c_ _W[xF^h]/֯zY|Jw'>iixM7.IKޚuMKĿOVyBwmךdyiϽZ+>Pe6{z;>n~;鮻'XL-Zv}vsӖOm\ju8C;o`cJgikמԻ0f LP_.\H$tB_o[u፯ML#,p&Nm6oH/mz5uJ{s-oI?ҧ?9~&Y-`ozńWş?x *߲r]l$oJ܉Tg'> yȯ~%i;eOܑw<)O Ἥ ~ZNGls>Lʞq17^L|3Os㟜oȪOnm:9E/xiU*NP[ wNx8\?o)}Wey*?Vv'εk=#׷bbv(6ibחu׸s`W+셧1+cAO| &x_ 8p~?w)}a@-o9u۞9qẕGU"*<"ori\4gh=,TɢMZ Ǔjz1}ϻ;쐬?S=Ip׿qcrbOoO5їwN~slB'4btO΋S{TЕ:Q@IDATX_g3`df Oޗn~!\."M?}(լl \CX> dŕJlׂutv#|ӯe tm2K[OLdl'<ڈ㋀X谡M)f$cOj> 1i="jY?f 3`Fm-Kg[AxV!gt뭷%dl=J-[>U_ֈB-#$Z%^ie\,^H'A/|c/ko_~ӳ [؂fslÂ=IoK/"c9*?/oWqA.2v|b@[qosAH؆aKe^j'm3V8 s%/y'=Hf~c]vs9VF|P(`{N>^xϿclƖIzm۹I=sl%9.ex94Ne\hNZ7%w,K~\{hCP痹9)Rxt: o ]"lОz8'riM,B--אt.u l{#O=oA.%8 _7T+9䑋i^Oc3frM Z% 3`?ղՑr?u(?8K::b.|cAU= ՖħMlb/YJ|pჇ?:阿!;_vXt#=DN8k`((@#˗6Xjk:z&`ȇRĨC1ǟ x|K\؀O)+؀9KK>L,f 3`FeĎȑ%mٺ=z7ޜ~ [pAZ[Qwօ m;Ik/夸S;g@/g?caJ*{v\tg@] s;ⱏNSe-U'!/|5~XX/~*dQ,8J{GKϻQ|=quoyGΩK/ LnW ~FNV9.fE;v=Nni8yf 6r7@օ~;}yk_W+,"ϖKSi{H7-0OO"E'x"x^ /חsyő$-.`˂@jiNxAW0S{8厜OpŠw꣏>2HOXv>:#Ohmp̱GngAxg7c}DO/P/%vUꅯڝ—M'|@2Ol[o=]t*p (Pp6dNEG;G)&eI*`u҆z'|O}(`҆>/O$ у%t†d?~K 3`l=y@ԦdhޛIER?C*g>)JW)'oZtHưtGf_kIx{mm]䏮;yGnw'ߩ- .;z>s<|z9ް Rt,& ,cG[ޑsk lDr[ṟ8aG=zj.m>mܟg\lz=ͷMe>㻍6}#_vEz3(I*GO:ŵO]fϞ-^]v%oSs$9#ߤ&aq??_IGMR=ҵ\_=-/h)tu[ĂẀ0Cam~+]BrHvJ!?-->S9Г_U<+e?uᏭ*W,_v^XW ٗآG~͟:;6|a 6ѦOqdFGQA`M{tUe%I$=1/W J~`>jW2}j._ _%>1h*3>5߄y%NSݲevߩ8a&큚3`F%G&m{Vw c~y̝3JOzq2ETu-ܭ:ʁnK괦>,bem*oMYk3e\$'{_fœzlxG@' $Wv|"^g9i5pB5Rַu\"quoyG)kyAxwNkH"$p)t m9k-F sKS 5n9?6#l+&}}!b?:9/BS hk,_[,4#[Icـf Q`ӻzˍVYO+JrEM[yerS_آ]T%z ul'v L/.>wOqXTN\/Ciƕ=41GO[` (RBpMXƎI CI2:8D IqY}%G5C1_CG?NůkOs="utc_ĞRj| yOx\GGg̀ "/*-gG{Ek' M}eҦn˴i= 7ߴ$-)^Zj@&9${C˧6l:Wz_צf;,[] 6I *Fr;z>~_=->9ħ|鄧=%';WmCi~CmOlٚ_)|ޏ 10Q9=XbяyT:rx⥏a$gK7%<#P۫kqd?֨?bsy]ew;-y{tP|Ƽ؛S+W[#ؒMWi޼9`zzz"b;ݷ++l58oٝcJf(m:qjX'kEߋb됆(w42f yy\cgʗ NyWtN~Om|Q~U?\h#%0%Cxyd+&|O D#^>ϘJ| 7q'|x:T=R$S6g Kjte`b*}/iC?RG )NbR/ن:* buD"cc~V?*`cVo1f wY G $# /5{?>w8P+K=[9'TOw_+#HOlZvyI _Y0d>ќ8.x{4SEiqsc`G)RBwmb|Hgh]Kc}~M7JMl~ /Vf$?v}Z\ |3ع\WYzdÖz,uKy_pDćg;1Α,2b̀0-=}lby*"$R$Ŀ¡9Tsѓ-Zxp`á䤾tJ|}! ,WNȞ~JDG_濱\ {b*VK/]GQN :ڈ$tJqP_Ӗb =DTi_\O.xqa~V0f`0x%_Z+_QӠR~оWiCWR&>HJ3ϱs<⻣z!4B`|MJ74fĻt*T: #/#q>-Y4?M;e/?(hcXmmt.OW ;rU@cTv)h5yf=IW^qu?Y^>6Krץ믿Dˤ,;=!h׾2/"=яjog,0<#mcۆ]4Kͬ1f  {!\ې 9oNO"%1lIssHLB_ذ¡"urD[ťO#Eu|=-x/)%;z,:,2X̀0f`b3Rwda/L>+^xoxq;g?EevӍ7Wk0-߼ӟD>N~IV؞z?$pgM:lڸl3=ҋOza΍a`Χr!A;ñ_ܹ7޷^%KZ!Q>OH$GFbD:Cc`$ΩK:/m[w۞r y*ͼ#f:rV]ƥ[a[gpak'i<;x;z'-t˝Ka^=,K>q Ww'a4rȢ82..%ltGwłXD0rbp15Tg̀sZ7nLqBnn"mοd[(wt$ds=@I \0_mlz[18xfR焢-_M_ŏj^DX5T/PxׄObo(3!P5Ϳ=}]~ ʲU@}q7P 3`L'?O{sb$oqKNBsģ~ϗ,^}W\eS˯LE[~-xyH?tN͉_.ħ#MH"󾠧=U~+}kwNoߒ~8xAmwf_ۃMqtngᕯӶ8O9glĹte)aŋu?9>~'yuʭ:qVyc:O7\c~&F✺4Iz ZVH7ݸ#|-Kf,GB#bY+d]~xޝi slT.msf[׾U8/fT_,oxX<:Ozɋb#[n^ 8zI.Gwt)Oc-=ݽ߻?쏹3`H0@.]w"Eh+_-?:ȥRHEKY⧸`?9["|)q5$Mq *eG#%>r9Vŕ/}9/%8V_ +FpЗ:|8A]yc'Nxu|W%!XVب>xlyB ۄc6Ƨ.lֿs0f O_99?:9'b==OGSgo_G>4wqw/wl7ݵ2-[vG?Qw]~`Z?WW~=;ŖN9vkT}.w8V\.Er+s:q`#lqs'ԃX质t'>xtb˱D6/'rѧ (B:Eci#ĒJہSl٣@x:=c:3`̀0c@OoOʗ&_)Qpg߽O|&n~v XɹS?;q}4l5k Zp[^8éܦFUNwg($3EԻN9ҳUE)ƌGi76G<:}~[S$qS}LgJŖ!W_}mծW꥽'x>9|?{>/_yoN*CoN$pݶւCM۸džuV;K0f rcGxPEI'0iH;S=URH_&կI8%QFUU_ǧ\!ٗ\ub Bb*~?`)XyC%s@Gpd~mE?9nJ:e{~>_qPlG4VWۚ ,Ԗ@D_ĞCNe_Dd06ف![šCJӇ(NbC'2㠟>-bh/v3lclis o1f @״{M7-~$T{'8FCH许혃0pO;Tb<=1< s9Ϛ⍆nRE>N:-{ⅾo9gр1(4Af?/ʕOZK/.0%c9c3``US? Г%J/9SPr=qh#)>1kI6?|)^£(Kq.@ׄyPqU/_چIG J& G4  H@G~}Q6ZP%щ4 {FQ"|l78[cd|u||4'J7?3`̀ p3{0h R)3`8c`o vTeK53Kt><^:^{VĖIc%w<Y wvX}1f 7HBΔ|(_ {bB\lS" QǦ^ >%R|~Wl l maP'sG`q(1hc'V:/&|tD(rd@Jr0X}>t![D|0L>CS6c+jK3`̀b =p'by pi ƪa1hXtM-]4=]L=nxMX`{zwzi[\:\˶#;/Z9?? N췘3`TdDo-"M^895 +̳ u-'!OO%:|XLFgA+2#i,ҜV*+ Svx LK|l4&bЏP/sb uQ K]3`̀ iC`*;YmVט-E3D,I EvQG4s #?-,ЖeSU&Ɇ<*ySJiCe?RKq\k3>!侅/1d @y_K|tA(:Ƭzvؔ>zťB)|(2hס`4 C #Ԥ4iԱٗξ[$%&5yZ̀0f`1z՚Ǟ St̀0㞁8;sg-צͻsӢe21n^g="m{&~X̀0f`j2dqiVH濉KY 4,e& cC0`}uHb#y !(A':&B)_/O6}(w{ǓJ)8 > EO[ MQ?mٱxP1Ӟ1-0f Le/_ϟvmTs7f @P~Z״Uiֻfw79]Ӎ)QLLǖWW巤%z|*} RtM6=#03`̀0} 8-7>otʯ.o˧_I} a_Z5Qp(W>@?uAx"C4s?L_iP'#O|ۥ0f LQ6 +Vܝoߴŗ'_3`̀0?<=mzZtY;\ wo콗%= Zܖ.6ow4mCw:S//Q ^PGìuZL \z=;e{w2h5_G U,pe1CdC]#a($P:>ŒjN1mb"mW` >vZďf.[k|& 3`0VJ3OO_w}wNaJH )ʣǾ㇈rQm.A8j+j||01v_C ) _s>>a0} ;GFdPJ>MFfL :}GK4'_s$Bħ.CO?5V?Fb̀0S{7mܸ1࠴뮳k[l{lJɛ3`3u,2p!CbyWWO[N>ymhؿ7\f/iUs{z)xܙ6%Eh2 LuЦ >FU?ől)D?b4k<:i4vaWXq _u駍 O?1+r,؀9xE|T3`̀0-֬y ]ݍ~H֭O6lH7o΋ =Z3cf 3`Ɩ{{gk?ޖNϞ6usꝧˡ1]6]Ҵ/Hݿxqa``VP-i+}?/jڲi%Nڮ4cNZnY4~EY4f 3Иˆ)US ~Cj $L@ &$uO]Lg/C_—h\A_>—5O$F:c ˼+EcV]3`̀0ʼnKo5͜93͝?7niΜ3_O۬]3f 30v а5bƞ[w?sڐzM޷)M4ܳ+T6ߔV?{uZթg/C6w=3iƞiM顇֥{/?U[fỲ0f 40']Ե^/X9+N|*rϊA[VU+%A UH_H]qP_c>1Ƃ_xOx jk+nvh VPJG-5X|9b K=:= B|Oc>6e/=>')uG5=cW17)JGb̀0f,4pX̀0f /zS5?)]GfJ:t˼-ޗݛ}齩L:3l.;g8 ̎ q{Ro}$>SfՋ }0f &"}? ZiGܪ8Tn<39!rA.V)Uj-%>Jڈ`?}a'| 5'Jb|jƧOsX@:z;UCw&@+A[A<Iׁ^S]X(uC1QLڜpW_G*3.D~G}?^>q#ؑ_q,f 3`̀0f iju}^ScMqX].ɢ(j="Owqpe>; sקDZBt+~"޳q]ek_14o[3`̀ 7|q$d=%_:c[ǮħLkdJ|)ck`k\J:>ml,Ǧ~˺_%;tgPMF6 (? dp(vm1&=:M>'fG>N؁A9}9hkQ~CR_ZZ5f 3`̀0f L~q>-.^1D0CH,x73S{R 3 C>sƥ.j3`̀0bDg|{$JErQj㥮.򯊍-d{TqS/RHQ,%?%!5WRjLXN_ cf0|œ_U!&pgBU=@]N'>BlїT옟>̡ׄ1 C'?%TmOOt'FyׄO\^[̀0f 3`̀0OdLrYqɥ3/;5mĺ2N65 ,<59>8]Ԛ`h& )}1x-3`̀0;@CΓ/TɃtŎ:=9R@]_N9QW%)! \(ARʆ> c\ԅOL1 ?T3`̀0f 3`$f9=>O O$yPdž}\pe+]Xo;FO̭wqB<$xDW3$^7kŢUqX̀0f ;|)OS}m/ r)V9XGEH6+WլǞ- W:%}1B_J>6(&8k@ACIė~f" u$|ӯS tdtX zeEW,͏vRGG,HO$Ĥad3`̀0f 3`&7q ŖGqtG;^LV zEC!CvQv@C;җ7!1qCՖħMl,%>8SWǟC$.m/aȎė]3)ԜQ[`Y#R :"[|i*_-"J ŠJt'0RX| /ㅺC,K|puB`>mħXc$./q(|03`̀0f 3` NǼ%O(ē )j}JԇRe\q-2 Vy}d|zo!ʴ:,-$UBWr3Jhϊs"#Ҙ@!KcXKs"1@]c3`̀hf@9Q)DO= Џo+/VuUNUOW):~8K|ʎ|ƢQx*J⇠Gh^ԅC%~4bu.*| H G=1$ɆKP>YCWq4iJ)eC:c0T&|šι)gn|4oCX̀0f 3`̀0Sޱpn\"ő/ESzRױQW{7D:0bqen˷\)Zm}xĶP]t~y:b̀0fegg͗l'/`TU}*K _!;GW/N#7d0| RW%"|ٶzUAP&l&Nvȏ~SL5 ť4f U V1 N P5?83 }j3^YA_1J| 30f 3`̀0f 1$.ĥ՗[V,V˾'|&[+bGļgM1f I9P?mrnq C>=UJG9W~%>z\|K|{&c_1d_cQ\5>_T)<ڈڊG>őENcR! 6QW} $iJTOĿS^1(Ic>jW2}j._ _%>1h*3>5 3`̀0f 3`c`Y\-Kq–D?2C#tN@Yc9vc6%>vG/2g`>Y+N| e{ic,腏 @PQjQi"$83Yl#Ӈ[bzTNq ?:Gl)u(~_P?~騣QĞR'b̀0f 3`̀0Cf`]\][WeW҂/})oh AJF|ѝѦd!am'G̀0f g;%_uDȁÀ:B*W)$A?}J|a@VeXjħ_@4Nb#|T?\5忉#(>bO1'uzy6F'TIp'|-b ω |EE8(3`̀0f 3`̀ǥ% r0ύ˳x setoL=J^6=#ӣ<̙:Wt\R F}UFnMĉG'J/ 93`̀ ]'7&%_J+/ f-Hd6FEJ|"/ǣ>K'>})Dd/|͕𰧎(b_+r@Q?H bre`!b4uDe+mRCJDWE,uaǁFKPW_uT\j3Dv)b-zDu|Ss_13苁XZXM]5 C> lF5Ut+nQ1SL~I Ỳ0f 3`̀0f 3`̀02\&;:;roxF(w\C @ɡ>u-&P<`JZ .uM _M\ׄeOALldÖ6bLƌj@IDATR|Qښ3`̀0f 3`̀0f 3`&>0%HɗߕK+BR~ !{$ꩣ#0v/upGĨM tk!@:i _}'#utƃ~yrS/m#MmObjܚm+lb̀0f 3`̀0f 3`̀p (IS92<-:)o:qȿ**IOIN|oUĮ>l5aĖE/|ӵ3`̀0f 3`̀0f 3`&U^߱+)>N+:kβ%iOD?D }G𠘔6!B|V{d?U 3`̀0f 3`̀0f 30!ZU>mwr3ór`S߱'$ PKu 6 @:J舩lPbFw]:Sm|cSć:Ivbꚿƈbcӄ/-f 3`̀0f 3`̀0f OsU>?|ёWϪN,WtItD 9ۖؔؑ%. Cc,8#zbHS/%>j5|JaiFl !HNp mM8G[FO,|\g$MFq.|—=[G["'* 3`̀0f 3`̀0f 30pw3<>?2 JS" 6z3 Dʊ5X(NJOy7Si8Oti+]9JbLOzťByI]GqGI@ZL$&I H=ȾuE$)UF7ELb̀0f 3`̀0f 3`̀ (Y7PJs:ʁ7~)˵&~T dA`l5@ltD5>G^Ť@Y :5)O=%DxA}210f 3`̀0f 3`̀0f`1\9OJ]92h(z:@@J+OliS@Og Dv,ħg̴'#~Lb̀0f 3`̀0f 3`̀ 8-7>o$ʯ.o˧_)~}  %Q(ɯ'Ϡja<ӏ ?Ŧ:䣚E6VN>q!V:|㇪Qј?GX̛v9`c1f 3`̀0f 3`̀0f L$/Ey*J[_lJ{rNrrՏ^2MO|WXJ#̋}'|ZxҕO]+w毹=~ : 6TzsQ4&"_)_'&ud*ff 3`̀0f 3`̀0f Àc&箼Jr{lR" $YPul׀B_%D҂:SK|/W&hf,f 3`̀0f 3`̀0f N[paSW{$p5їh6eL˺CuRgд˃x)CUŢ㇮ _a~/C}I_EUǟ~@S,ǂ C_Ge1f 3`̀0f 3`̀0f LHJɝ_qء'WP'G-hs *ӕNzJ%V!||8ݪ_efb >D%ui<'|ԱCT`}}¤_uZ2&C0DR^M )_`_Rp>3`̀0f 3`̀0f 3`&M[ysr)7ľ+я)jԀ(AdlXAG\:6`Ulb͈Rq+VT0'>s3`̀0f 3`̀0f 3`&">2ύt#VU'`Ig!>GDHG%zMp00|eϢEO6¢.t]8 S ?l1f 3`̀0f 3`̀0f L4rn{+>o$CͿ˾ ;Cd&ABA@uvD.բu>o,f 3`̀0f 3`̀0f @+*o++>F+^#`MO[} K+K8D}〬M\-DSaO)ձ 1M0f 3`̀0f 3`̀0f`0@Om*:%vE[  W,`#='ۨf^ܡA{VKH>QNE64Z̀0f 3`̀0f 3`̀0󿭏m4Y%{kt  0P $j=v!W<&ȟZ|VÏ>l j֫_1e(3`̀0f 3`̀0f 3`&9I3+'/(N~Q^=YO(8 &S^ina!8c'-hѕ;Viq#>O66Ȍ8h3`̀0f 3`̀0f 3`Db߱kQAKT)/Lu5HޗA([W{lzbɖbbb7 3`̀0f 3`̀0f 30!pߜ<ʵSs"?hŢ0@pIs(O@JZd#KubO J|4~_6ɯ> _ܲctqs.|J/~i*50l 3`b`#>4k֬8vI3fά~He~Hcw/ڌ*BZ87̀0f 3`F]!9ժbˢ-vj˨hRݼiSZn}Roo){#Or'YC|-m>R+BT*⋎ Q*ߪ>%vUF5ٙ;زO?xpU xCW)5Q"—mK['\%e Nli /U<^P\!IcVPe_l_PO_>~1_Ч6U:zėN03`̀0f``aayj˖-iӦ-0~ "DU|W0f 3`&$q3K'SȆ;qusy榕+W兆ՑgONEJ;&?._W?(O]xc8˷ė>:a2C%>ť_'lÎ~E _£xSفQd@0&E*h/|@hIDudOL>%;6᣾f+Чe__Ư=îO[YFW(lQ$ԁE.]˻կX劼eY˟yxè0c;j]wM7ni-q?ʟ@?u{wS>]a[1@f 3`̀0!JaZTkjp)twOK̙36;43Z4q}~PNX@26S'[0ǦNQ&ї}  §8K|)>c?7>'.41GO[` (RBp(5V u𡏤{10PUlQ:A&Pgʯ^/L. <+T G&|p 1u5tMoOS=u1bOzT` I$ ,D]m$A(>T,_Ӯ1 ~ħS I~Ť^ u U _s"_c>q1N1 ?+g|2uӆ=s|wSWv§O=%ikw4s b!^:v6/]Gl` DO ( u%5 Jl9ʘ";_+O)_ヨlZm8]_J|q^QmJh Sx|]X,荏+>=YphyD *K]6(lŃr<@wH┴3`da޹E'0Y>Y 3`̀0f` [yww~p=%?OʋF5'eK&|*,,>t3/9//0(am$ m#_ȥNuGڼ%ObqYqٯcu3`̀ 무5.6nm&'I3`̀0f eOl?"|p5*W6>%"VՖtJ _G0@  JD-0LJAHV?}H'6(ǣj@x!jk=1~TB\q6ck{e0֓ni(M߹MI}('PaAWxqGڲamG߈88 <\3`Dg`]vI[lM[Sb̀0f 3`Yh`$/0@,&D.Jh+_-URHEJm+G,n;Dc XH>~JfHr%>>/ࣸ=lZ̓zhP$r"~DC_vtAH':>m᫏\q K/|!R/mǏ8, @?Mqk>8.˸b "߲vM?9{ZNkz[vc]V }ݭb91n0f L*f̜˲5??,f 3`̀0f ۘcv2#π_MV~ģ=OԱ}Vl|5? i!9@6|H[6)g.<&o ~y16X4!` E7 jPo!`!`twƿI^?Ah(\-rGVg!baٖx {[E?:/x d1[6>(_ycbS]aA񣃜lR}5ĂLRpL#ԖNdA PS $]ub>$+;qɿ4a$S?Ȥ {qO,l(nQJ93~dT1o\P\?ݻ7^o5rP1*+dlB퓩H,6+Ր>\GR:xۊ(ј(+]_M+~)Zd]yִiA `hԒ2!`!`!`!`!P* 9Sb%m䁑N 'G RqI{O %c9(_o>2d+VKmWuWx؄?J}H1!HAyԹD0(50l $}F+:aSmc—i ?t.:z"٤?2j=F&˿t7js ŒVVF38a'S>gMC0 C0 C0 C0 C Hxz"Qƥ+lC_EV ur+_:^c._ :ث9؏#zF!`!`!`!`!` HlV'I$pʧJ1ɐG%o\y`zFؒ8?U r/6$ ?D)ԹB7u>RG+H.(P!c41K CԠ4hԑtAo )ۆ/`8 C0 Ch[X3+!`!`!`!Ҝ}7f[` @ɿfSl$K` ±=|]+@dtj>W^ ٤N, H5(yJK i,32 C0 C0 C0 C0 C!`ߚ\9O/%z{/8*p#%e SԵ,|Z`P?m\6,3_?,#C0 C0 C0 C0 C0Z,ɍ7u[Du1Y*%\bAW;vl2JmIC, HON|M?vA-{iE.߳?J]CT3~(Ÿi#C0 C0 C0 C0 C0Z?ɗB</u2<9WRTUUJrՏF&O||#+S"1.c \+gE|xxS]#k$OH )e8F\}8rJ屧xdY(K4?ύ> C0 C0 C0 C0 Ch9Xwɹ+ﮒ\z:nMJ {T8 ȳK_>zL$-8Î|?ؿuW~eG0 C0 C0 C0 C0 NrwævcXʿÏڔDR.(  >ANд! {Ɏ=+E!!G^$+q,8叱ɿO*~lJW>XCۇed!`!`!`!`@D +I)U|rurb6R9]ħTbUvi+<\WڿrWE>6>H%u)ٓԑ] ƪ~у/J}J(N $Pv-?آ66ŧt>lx!`!P>| i!`!`@Xw俕7/>.ڔjM=42>H1u%Ź*V: +>K#%=E);eWÊ|3&#C0 C0 C0 C0 C0Z"ݵ8 xVUn]{DpJ,pgQA}§_2E]|dw%qDž-7~F!`!`!`!`!`4Nr–{+oMR,dfg*YAP£fCZ(Of~dQ:}>]?032 C0 C0 C0 C0 C KO{- ʿs_=eX'^2#c'/ C8J>JqVƮ"S!O)1#??42 C0 C0 C0 C0 Ddc.ٔ^9yň~ k@&G?Ȉc蓬PG^҃ۤ{?l`42 C0 C0 C0 C0 C%!|X7wۚ2-U1/EbA9$U tO_Z@_#}lkYz!_-}_ |+_?d 12 C0 C0 C0 C0 C%!}-oppr!iO2ʫjSfJuƔעz BmB,|䤣!*M<#ɖFZ_m!`!`!`!`!`$,;oZTrʿg%WƉN JCV<%G@!"Z/-ҖM?G!`!`!`!`!`-tɣ\;x~1ҊEzaR:)EJkq@IF:/ձ=-@(N}zke-yyђgMRô15@c `ɸ@ 򟂖g%ɝ|_EEW9W|RV '[1_ٙ)EO.~t嫘d ˿RW%$qt; U ̅Q!!dI ` PzؓM5٥zVl2 O_eSx &32 C0 C`DNvK/T٧y@ޭ [`Xbn #B]:* h1͋yv]|ݷws3ynԏݤq^|~6jA`a`̀!`ȁ*"?EF B^_=+q?uIXIy'ĂlH>,|Hv _z_>6ѦOv$d}!1(`P6"QJW}@c$"<6я JȠ|5^l>U.˿cDWU'>ik<!`!`%O`]`4|rn~y;чgl7Dֽ[bSNI],nk[_oesG}X79CnZL!`iIn/uh+LyJD“| "*L9/q"~܇N}S*ؿʉ?1RƏ#=š>b/kQ] F0NIB> {ly˅ 1x@n6|dxG [Y=;@&_a~JMOWGOc?|H<#|"O"ed!`@"Xgy1r#6clO6:I=$AF*-bD"؅G1`؇!`!`nreJ5ݝZ#C0 `{N>+OypC]n!KVa C0 9Py81Ŀ |.|q^9Ud(_Vࣰg}اo?+Yt>bXKݗ_~|-Oj,ܲ||WGnC\Ϟ==ƺX̸n ?'v}<նa;-siڴwLsܦ?]tqˍu\pA4?&xoH|o)M ߡSNnsN9s|JqsZS;eIfo&Ԓ)w5սgO셳. RKw.,'|,HW7lP}nܸ7BK̍R#9tزǂa(W3Uu/|Xi@3F\'rF'uE}5$%KI2!=yTlC<ئlɎrY@VgIœ|ѧ>$ݴũ1`KEvڿlQBk>Z`P2cXARrhJ48Xd ؔ9fF%o]Cl/{Z$U>й$UϮdoC0 C0H7xv.>Owߟ\Xly!+;M}nvwvWu!Kɢvo>'rk>{}ɧI?c?-?ݭh~7q[mEʫ丠}lėtlcƜ1z+)?swi焤s,BruwrÔ;oN=ZO{D#}{EX/<gvK^;"8.  gz0~b 6rCpX ;炰HRbsw/h{R?q?gFӑ$=C;qW^vMcÍw;}h>^P>ԯ nǾ ; ׽gw7'wwbc>]qWLu^p ?TqPCqǞ{6dZps=.$y=7gɧĪʱ'QmY;0+N۰!`f 4Vɷ£MTo.jG?6W@A>_+GNZՖOJ.H_OUw?#/_ٕ %O I7ʵ+^.+9(p udRm_Mn&x"iPƃx)t66eW?}ȍZ25j*_%.||,!Z!`|@J'_yUO%ċ N<*sqmڶq﹫[щ|;W.7cL<]\@~ɷ44; !1D~#kq9wdx<֫~qGCc]zgڶm[1>1iplnm]IfZIKZV_s5w Ge.. [[y{vA ^q>*ݢm*\*\K*7_8Ѭ?ٽމOaCS6w~o6[^}zc?䨒l(o[Fh|pcl6 5 wycqFeu xl쟘4}?Wr2>{cOf-t6b3שs0#>dNm-s zg|c0޿ .?x ݵW_n~ ǽڸjkxĉܽ?j[ [Go<#g_o|[n-ur D;="b!&N$VزoJG¥rTe9uv>i^e!=Aj6,Y<&q͞5۝rnߛevH-o1@R~vh`C 'i9s-؋\D7T^uŵ>]s 6;wv$A%  f;5,q>l00^@A]E[#!& 5~[umgwlE5ԟl4xű~ۡs0~Rxsޢϩ`-g#?q>|n;7.f~3Sb.v%ip&N 6\/Y`hvF{(o)ޮyc>W gg0oY`;!vhD6)yA {Jy3=J~8^ x\tݵ' qoL%U`p'+^\c[yfZp3:]2F50GTBBA>ئf<ؿ`Ku,$S?'Oc;1^4S?\G]93~d#6T1oԜNڻ,xr+ݭup{8˽a܋ !`p>lᓶ 7Y? Jg^ulexJX oÜ[Æ fq=O7a7p|6ƿ7!a soc'EWsm+HAڵOrvF/ċ2+ NrQS`︵J]Rn^zRPZoĒݘÎq_ëYwC. b!WodƼ̍|GJvvM0 _$2f/d(%gE\e;!x >|5_'QVO)%!؅! ؠX6?%<⣌el:<_%OcY/QEu-0 %+hC $} Lz D_d><@6fNl!/ȿd!ΩOl+vJLGGcĮt}5ӿߨ"d~qa3g+h4x ϶Wlz;,?;R6 ChJ8g@-6b5Y !U.D'FT:ejX`o b]~c\W3[ IWx 7N(r[v_gʢ'lIGTӧw*^]VC*.YEB['@ D~rw”"5o_t9I~k.w$[Lz|~jX`h3`@!GS|jrxE+|ZE}/c}xʟ"ppꭧ|8[2QNِq9J)g5ŽN;f6t;ł;m.L`oRa/~А1`@m}#b=*]&j/IЈș%{'"%6UrRңLlс('P_2??vCR.#Ib{i?4NlFN}5Q~|eMHs'#R$GK Ræ@/Y'mdb/yJ6Ֆ>/|OHB'Mڱٍ#ƯѓmdK~fO&U[wvMfh!uZҝDz vs!`- /w%7XjpAt9 V8mڶq}֭$9)sh_@?K=yJ=街_ʗ^|%$ٯK.A?uA\=Eϓib~ipr?o.!ƛ`Ǜ!uQ=qεiSs_[)xbt+B8ara3/Gsgq^үl.D\k~:-5 0cX̶6cX:|M8꒣?EPue`{,uSH9Zp7zg!` F >clS6>O `ޞ={O?,-VryJiq<'JE#p6C< 5yAo0?V>A8qXuDCa3O=뷍}4,gwZ.T4)t^E~}6Y2z !˶+}<1~[..7ʱ[v;lS{οKEPSDWv\}NtpST~[>jXyY =.%xbbnPV;^h8rs̋A}i܈{=Hb C iASK2ji7_]ꔐt#-yc|(M[>|J/$.}WГO-G#;?PCHi[ڿbF6vB`HUt\2(.qet+ ʮlQ%ml[ыݿ*Ǩ"?.PPe+WѧXimWbsux?2ۻOnLc81!`@@kD>)cybv!y _~Uoa Mv[c|3-rizm۶21[_t?[\_ȗtT*lթsc+t5`` XeՕHw^sC(M_?6!___o]W|i0?IϢW͝EAx ԟKV` ~ 0-UHopv o@.Rˬq1~];6PibؔrQStlu3Vm)'<h,]ջv~5hwD#C07woz1/gNoę]yL uȇ-dKF, L^5[с(-¿ UA7qaW1zkWy腮ŏʓov*w;UnP/+݊m5f!FS/DO^s6JUW_m&y,z oz\aK#:4yD}}"ě(Wp3Œ1f7^ӎ꼅=wb1&޾?irI4f*imt˯8&S5G|/7b E*u~lM6i=~<Σ׊rɥpeDFJWPG71-Ϟ_zk5)Fc|G5m0Zn0wc_;&ꆀ!`(Y\M%:_.6D9@-H200ӬSȢC+/ԑѯXR)c[ؠ B>2,^>kLt/=UDKcg?l?65~)5iͮZ^|-*~Xm#5UKպXvpsUU}A EVﲠ{dZ!`M@=ғ?Uk} =5u|F7]uöK셿#]"i_Mw7pK냳&v+$U#b 7= 3F>Ox " 'JE*ZF,>!Ǿr{MPsRc{+^ j Zb9Z 3bnZo&ƛ$^o9L>M %> ,ޠ5=n!z8[nq:u eik~uow@"}'RspR\}߳w7a.y uZkCb@sߑ?"ن9]3=pD,|_,̜w΅ iʆ!`4!8+VRyQ]UEGS_MváN}ل.H=?$c 1ɮcG-Kc_ UX=!'0R38t@l:DI i7G@#K׳~& 1GY(Kuc2˿b#E,Y=;>ЇO+!,.Ўqʥ]6rmgCϧ:U͞:*ڵwnb{a0QfbM܇nrU^w׳6aVT/_#C0 Ci` $}2\M']`υ-쟆6|hx DoHt|ϿLwW}B6^`Az›'IrחxO?O)x.?7\-Ϲ㰰[f+ |æ5n+Uo[?5ʻ*Z~\GMUusʍO[z| C0 E`ml7xM6NO1cN[/:!w'%̮އ=&{C2;msϾoUZΫ?Xb/8?t_[ M?Ox@⧧cz<>|ko-}9]31.5/gzáxx#nuE{?/G/~4wϤ.7{6?/N߻}1)#>' |!%yTBe2 =6K,Zl.5'|M#s~ռ â#PT={l߻ n_O0 &F&M"?#H_vw)k_jinM[r.q 4a[|d4)D-Ա#?*c] }H6#^.O,AGC!݈udb; :uP]Rylη{6G0.͑:m~>E|U\j]6"[Wnk*/O[.uUjH+ݢ4vU,F}UX\33]_O!`,3w@yh|b{̨XtE‚O~n{qy_IƦCEy*,lR[}gȲ{ҩDžJu߅q%b W_MR fz扡 PL'1SqXoj3}r1vzE"ǾZY5ON"~ϙ}+^|Ɋg^J&dS?̒^۹B.ު)?tXܰ_vyZ*}5l).d(S/꣄C)/&vlz6v %_>|"ddW%|ɈY!d1C^/ Ct(_~p&QR 2*GhR?t5`JBzd&"?&@eO~b=6rO]}hOLxJLJb}Kn}˨Y!i(LfgUlOqU/>U+V5WuAb5]^+~rc i^/U6 C0Z,$ho 4gS&*jy?*7 I  RʊۼQ_jC 3g W}㙗ϧ5IX|W]ʝGMu늷rἘƸb10 rJLΓ.'ɟ"_D?Pz+JvdC+bBW}/%;`im俕_NXCG/S a.!!MGJx!C#IQd_%;1jnt>fpW-&s?z/.h.9l޶rҍ[W~V!`!`!`!`};Y˕ɟ.H#2Z@N0Թے-&=|J:>JQ Dbr1IUo,2*mbg X2jZ+vrkZ ߱큑!K#н{7wڙ'k gvt}n~#C%#_Z,vC0 C0 ChXw俕+'E,%k(V0, YP6P=d_ }/?/>:O\ئĎYa|M|'ub=ƅ28}3Q7jK]7{87CIpu[,!ss^a|Йw6Y 8a]wp[۟u ]^I [PƷ|=snF{4+XdѮnW#b!Pn†,cko}a8j~-}}0C ?$ \j C0 C0JF'N)o)J.%I6g%r Q"+jtiC!/%93R3%D-\3mC8dBs>/НA`14|,>|].<ձ!}c^C9!٤̈́v$ؗKRzї4~^|ч! ;$=1jTv*w߹.svOliAzjuu3| ;.; 6l6ܪ)̲CYyɕ[Ȕ3fpW!}F~!b'sc7{p p1}b!Є<Уe[g4,!`!`(=T2/UErJQ" KENexG6οʯJ)?y^k0fXK=\GW bYj k|YųpȂ!xȢ'b8%r2#I>k:/9k%wquF> X솀!`!`!`@"K(4*cȍ@T&9^b 9eȵJ'8mx%C)}_ |и쩌 ɖ=ڱ!}ߝ+nTLaGQvjBIGL=$%O\c_` $*pJU8؅+.T^l}S,Dc#YeǮl /`*ܭziW|4U?^yU,F.8iѷoŅ.tíy1\y5p#F 6\*wZʣ]rEgaI-17!`!`!`!`Gdj5# :y8N.TQCȓ#N_G]FV޴䕯ENsc@Թ A>]_+.O[-W:|lȟdh$n@1¡D.D&@ӇH9<$VX'>}YC?ã 6b_~E?z\ґdU*f%sUa[u%p]a 1]ys_=0d&9s/rW]{Y8aVr ,4[?ץK_&)V@=zt{8:uױcGn%믻͝[s39'b#чW^~5O.E]čX~k'ec[pݐe[E-*++)7rӧMRoG(}#˼/ KbFJƤp[<>bXrg3E8_G|94u%Z-=d2}t7a$Ƹ73mBYz~9oOr#rِ)fK'r͊Ŋo(!ab/V01Ȩ 4C?1W`SG_SbC>ŧmJ(G&_?xK"m|u^ƃdcB+#.MƯ߳ DZ"kgDkXXW{FbJu[,Gjl_/[UsP1ddت^6"LrY7ߨZx'Cvڵsւ'zUKE\'y+?p߼åod`Рd'}Ⱨ >IEn-7 "q?wu-Mql};#mf͚.{oƳO?ijwoV^ӝtia{,ǟ|tH_{u !1s֭݉Fs^5k{q;o/LQn{=kJ>V;y(Y0 C0 C0 Cy"$R*'D?aMI^Y.v[(C=}5sɯXSG2:k ٌcS iQ>%md(/!ZO:" 2Ŀ`㶏憀ʊJ"zoQS@._U*ٵWy9cZ]ul{-_z 1TGbK7ᕷz712'7ٴ͎<\tu͝vIIL:?-.Blt'[ '~Bx=֣;"3[n~]jyN=$[n;k-.[XŅ3EC0 C0 C0 CA ][ O.RyJn\1bQ ^ mJ-P$K]H6Vҥ:|eCIUɀe~)ɞd!* <;IGGKX%P쟺l dցm|(rZr|ʿ*4ŭn-rL7C#Ķ@kgIJAŭhÏ{BcN?Cv{ͷ}VOxC︍cжD'$7{v-6mڸSN?!47x4i0amlt'9(ybu[<oTV_}֛X=Y}v˝ak8ފ_nrvgwe:PmQCx |;ɔ{G=鱞_ax /u[/;wg{çX9:L [uGJAԓϸ}ll[6mT=c–HswslɴFyF40"S7 C0 C0 C0~1%1|-%|F'7 -d闼&ueUNUaOWUMbv6?qȦ*_E'{Fv7 >P&mʴϪM  A L!K[7\6$CnKu}ǡxЕ b_z# w])Ye4OI?c"vqQD@(#~2Q.QY9R^\TU?UvC-R1KE>!^}\(4n[ǜ}a,l'ky豐4N?MsM1G7qawg'yc`l84:Mo;cܬfu K idH`C$zy:3 d ֢FwR3Ï?w&6x o;f̉nΜ9Avʔk9,t4`書]v11_5-̄Ņ3N;'isLuGrp?q-1G>w}bSK_viO60v?4zkuXUW]RI^Lq,K6P <ƽKəv<"k2GfNĬrg~_ +/UW^yMZ\@`Iೈ-.7PsfDCB[B`/*$i~ԯ,.Gқ3Ȣ] n9$O宻ܝ  aK{}zg}`qs.LlAW_w ]]}ŵxN>%5Qu۵kzɝOJ؞!T2?W8RJC0 C0 C0 &EtQ##1o_&5ɩ,H Ŏoj8}8:QJ>D0,( OMHNI }ĎlP :z.v.G|G uᩭO|Fx\k©o=SL6q uX(Q%SBiIѪ&?E\gcqE,2}_P( n!K=ߥKwjxfO>2|s[qw u 맱Š'<-0 rJwE:7 (DFϾ[ MR+ [!bOfHx,#{o?f\IM_|osd7t z1?4e$oL?,"zVLYu{q"n#C0 C0 C0 &B 5ljIF/u@2S)?ޑ'9_M2[Eԑ%B۱L×Ǣ?`R{O>D{r'JłcOqOFk4r O!A9.dx *͆`x{vLڿAš~jlGbD6WzœߺYF-jBNmSӑQ[_V)m͉cEcwO["|mg2 Yfiκkm{x+s;`:|?PHV=\Ũ_>;7saz iݟ $rl?vK"1M;SՕA_9U tv)MYy@&zƝ]/:7,2IJi6 C0 C0 Ch8$Qʗ(<CɯBUȿO<%F.W 哅OYYl@_!EF)~N+.;')mNj&QyJ}6J1™C8eu AFȖ@؀GF6e>i FӇ qR_6Ǘd=;hBlD"#N?0죙"*#[XTit;9l}a\>;gZńϙ :81)~Á̩}+4?~bp/4~qy%q.]w7q g[T AYBql $oPi۶=jΫPX˶n͏l*֗0D#32znr!`!`!`! J"'ƕW^υO7ˢ#" 2\vaW/ٖ |ROGNC<%q̴5~q7R?C)9n?/] R] 8!%~J 8<2r/[ :$6RglJX!цK}PؿdK _ڔ"-=S,joR]MMRD&eڶ3brw^:o%:ń_x٭*Aᑸӷwt=O7&,X`~ݒ̳ :85m9W+O-he`VH |jpNϝ=>q &%}OʀӬ&mp89N8:k-ttc C0 C0 C0 G\'rt'uE}5$%KC|F *?= ]QrbRUwߔ>Yߺ^'ePW.IIbzGQλwȣu-Nji^zRɛ,LBU2J [r=z%\ 9wcW̲CtN8<5 C0 C0 C0 @W}-xop(w#}(H$JN(ЧJ}5LI_`:r.Ack&25ِ>$R47Xlf0 \US> rg^{yn9SN\U oDzkv'D]:iB♾Ph(Nxsƛl#|'G.?-RIv٭yhsߗk l4N 'r\)VfzW]PXf^yK~* JN:.Ce oyUp(\Iȿ[n֨5_;/;aJRx`*m63|ςϻ?(:ɍ90pgK/&}C0 C0 C0 F9kC\&_+#W4 HRq Q,|!xJMʏQ9x ?-T\LF-;GFؿOnl]x[y"t&xh˿Hw8>ȿ_ǹEq˯JFm7%z>WuEe:.*z˱=nڵk9ǟpiW^4_J'f} YXmM_uͱϢ`믿q̔TrP?Z{MWZ bY=< ]W 8좋b+ Y!l/s/pgpgpgf PQMV5Ӵ:-˒>̩n>Sꪪ_sӂ tbCyQZ0e1:l _clNO~1:+`+~jOԒ)™"DFI$ǘX#%&6)#zЇlG;" R|3V>&U¦XZf@j/Ƨ7uom7.yn)TS _5˿a,k2k_ Ru[.y Yȱ2suQJ~ig7་a#M60ќP?Kgzv,`oݷjx7ESqv'طsn{^ |K6?)3 8΀3 8΀3 8΀3P-KPT]WsTU[U=ۼcz?U4bSqЩpg|SNTEWZ?1XQޡ"˜~#D&@bJB "=&~$9 WsuIE0>[b )>Ə :n+GƊI SC$L+'~|4&1.݉ƻo_m`kնX&ab|~lA u;}2ܺZzi a8͐wyT7W姌Gܜr]v)~[FgQz~v%f4·Brh";}x S߾}3SZx˯'x*ӫ߰!rG<53+M6|*yzGD)~{}.67M#|p\)ۭnGL ?<] w&y/O}]s-X/l wHظ?O]r~!fХ" >N[–MϑGK}whcƎw6u;1{I7ugpgpgpjIqjRnJ]XzlUV!DS|[ҩ.L"<>)}D:l[bk 脅}9|t1Y#_Wݘ4>63N1shs!~I!XK" "؋h+ZV+|`+?tIT1(৸ C"Ѝ eBh9[uU}bTA-&vRץOP>ޚИ^Cxqa- §]CP\>5+ Cw0K^D] vY}]n^zzx.23ܕ/f/un w"PU+>ßoWSxQ#?̺7x3+] b~(\I̥mS >(B{kks9ػOxp̈#fw[Ej)|F6n"a%wGg*qŲBg/Ҵo+[9[kn \gpgpeP0۔)u : `PﯬozD7oA,|P5QJ;)LCTgIbG+Ѝx3FG_9#_5f1~i D*~آ|5W6_1q)~Oh 3Ht_4ԏBUSV1. BKE2ٌlȅ"RlX-xGE}J {N:Z+R)!!r%?<>>ZxoʸJ?.ݑPXnVS0_5/}=%62NTHmƌo_|IѿxL_jxA{7v0qqaqsw08΀3 8΀3 8΀3 8݀_YL&QZWcKlNȏ=6?>-OR?Z-|crEB<I ?t3}G)|tD_~HK@J$sHajaIeEOS|MWW)66üK-2Њ47k]™rf;kV;]vVm A7UM+.Gs d{V"}퐯Wr\.QG_4_I/h #3 8΀3 8΀3 8΀3й P jUUSjVa*+SSe^Z!N1iAGF5mSlTG>0CPEat3/<>l Sv%">%:R⤓ceX'tc_;N9W,l(гxV,1ȇ1x[ M6Z K̶l؊fCXIgZm0m7 k (޵_\*̻ 8atSm˭6_VYu;^|E{>䑇qjW 8΀3 8΀3 8΀3=Pa5dZ'V=U&EeCՊj[utsH#.-'ug^B[C[-:0rV?|;+lZ9섏^qj]cWVT.k4` #hO"iH8ͭ>£P[Ŏ>XgC";ƊGp3~0C7cy|8$cL< 'D$W o)3gYÉ[4<*iӬ:oͅVrY{In74eƘЍd՟+W ԟeOlf§/*>eS?DA/[b(-˗CŲ0@I𓳲Em2m:<)C698΀3 8΀3 8΀3 8΀3 8]ȀʩwVzz%w{Jp@~AdB `l3Ӎ0Oyl"|6 =KəqOrUf54,kK] M~yexpKSw. ^d`E4mmm΀3 8w>pgpgXF{g׿uJ˧qDBw$h^\R6*kvaHZ|!#l O'->tF38Gt//_1@QN4f~ظ|@sl`Nʒkjk9?: mg + Zs !ZH!oxb?Z% gpgpgpgpZc'RꯌU/&[TYM7DR|Ջ5>X􅏭tOج? 30*#hU.G?}K56Z֊NŴ& -"P٠c!184FQ |J8iffȒo)|o _`!Z8O#KM?1:Zb6FӼmQo; [Q9΀3 8΀3 8΀3 8΀3 8F'Q'n/SUGE}jGU5m\QUcti\Z i.~SElok+b`>-sHOZiu1@Gk @"0Es`i_ CS̋PZ‚C7b2a](Z>sIF!S|O1r̓YrO|!0r1E >6fGKZO>©F6 x嗭7 Xݨf&XؕQy5_\ҡ:V8!\ 8΀3 8΀3 8΀3 8΀3 TȀj ='`?2~ b)>֑_>Ė0r_küC7Ƨahe ,FOcZ#1ƈWK<GE Q|Q _Q&ny\<ڄ>3FZgf 6k)~!>*Zc.|.uEPo̚c?9/V~§m@Yí]i<0kuqgpgpgpgp@'Rj(=R>5R=1?I~^E-e̡r_g jW?1攏 1}쐶㫜RiN̫"(sI274dQRP}L 0 sZb:j^$k^ |-_<|/[+3O$F>9C<>c0J(gaʥ~EU.l4~-V񏇗>oouod֧#/''Y=wXmA)oBNKgpgpgpgpg I܍ʡZ1}jSiU{V ƲUjl" a0ZS<>ǎ C_||b _xgNx k+nt([% } E(cC71OJ_Eza,TN_SN&Oǧ->-q/ЍCOlc^}R?6:0wQ^wuСTA_aˇh8ٰfBϝg6=kٔ)?oR9p=.΀3 8=G. 8΀3 8΀3 85O$Wꦩjԑ`ЪK*dZӪY5էq#K?sa'|%'+ךhEL;3QZr)_-S2$A_I㛒A!yEׁ^S_$UF@K'ĎyD1s _si+ŢD~GsZ?_1>q#'OHό8.5@]]54ϵF ~l5Mᴬktqg {KrgpgpjG?U?K"8RSU[hTǕ[x 90S_I'+g^=j3QZ?v谕Fa,W ɼtC2- JBɡBZc١sl9D~ZWc-gM2#4sM w2y23lOYpgpgpgpg(TO{[Ϊ1X=m)ԧY  SK74 $%-8 +Wy+*5<H0}t6t?~JZ[=ҘvYˆFe k~cHrf[61r3ZvZ3 8΀3 8΀3 8΀3 8΀3P ͼ[3Y-HۋrBV%~%!@hYv-;ԞE@/bk!t2 Glnk%.O#t{|ZMl^O[P}aC\?,U> 8΀3 8΀3 8΀3 8΀3P1߮3S+6PGGϘ9lTWݨ-)8&i^WmE 8c&μ(رK慎x,V)2h5ʀ6^ gg^a?:^Y M:ꂅv;lpQ yyf΀3 8΀3 8΀3 8΀3 tC۵om*Tj鴪˾YK%x>v0}D|Te>zm(-s,V-yi)lZa!!4^Pg>y||<>s!&bI X'y7v |Y e^M޶v^6nzQYptkhxaqHhg/.- '9;q,gpgpgpgpTUSD7t"̣ʡ.ݬ^;HCy4.q-䢘b#\AѺ 36S0̾EC\|d8q-0%`X'Z|S,:ΑgWqhRIeücwk`*v)|a.ϹigmtZZ!tt@ccc(7>/M)Pg?z/^Y93 8΀3 8΀3 8΀3 8@fg,UPY>WƔR=5Tda8T^S[|qP[e,ꭚS`X^ a\?Le)<8j xCZ׼i~"—mA%e Nli Y<^P\J\7/XOmgN+y3 s/w$ у)t3 4ҔBJ1c;΀3 8΀3 8΀3 8΀3 ,3 PRMJT;>h+z꫊qTs?/>up&8΀3 8΀3 8΀3 8΀3 8Y46J>UWv=-sl=~z }lU&N腑ѧs §U)ﴪ $WZG%>"QEM "!A1's0F=z[%O akI:_Y3 8΀3 8΀3 8΀3 8΀3 8݌Q({yͽo䐬jUzj̩EҳKI 6$AqfNѢ-Nrçb>bPV 8΀3 8΀3 8΀3 8΀3 8:TBz@,umo:wҢҳK6 P_TOupGhL tk#@:iqXK9c|KOn-}?J0ՋCX?>Sy+c+lÔ3 8΀3 8=ޚճqgpgG쐿;,RT4N-sOꯪ_sR_:[E/k[A'e1:l _clNO~zs9|+i:7vrnL%I0/is`xY,-:h3F QBL|hSl[Nq vOqqgpgpgpgpfKuWտ9U_C׆@9SOwageNؒ$:ZtĤE%c鐞]G1%\e/ʿkمOgpgpgpgpgpjBM[lկͩ:5ڴ%(mQ(|W c Ž>:6ty``.&EP8c/,pgpgpgpgp@VoֿUO/W'T/:~dYShyO蔐Z_In*`x/[!gG >kwqgpgpgpgpg[0m1U=Kj%ժ3ój`߱) -H (੎>B?:b$A/[ma:.TyJ1ƃ+Ǽ?>h!&>~dc֯SllJˇygpgpĀ.s8΀3 8΀3 8΀3P AT?UUWTĆ*6l勎i}V}bq'SLZrY)hccGS_a(01>C8ҡ'8)>Ac+֏B!,?aWRkI*\X#%N:1(Q艅|B7y\8bK{V,1ȇ1x 8΀3 8΀3 8΀3 8΀3 8@wc]W9 Jؖ`Pa% pꥧOBΊG.c'6K'S>A?ť/|bqȟy3V?LXj3w8%_uqgpgz p 8΀3 8΀3 8@g2MY:CzL0 uTꦪnї1aWgIk؁ |:ڷ#Zt⏟7vWrV?|;+lZ9섏^qj]cWVd\֠iBP6GhEieꯪUVӥylilR|Ջ5V:ŧa]̧30*#hU.G?}K56Z֊NDA&6Usъ4"ک'yaӖ A pW2g-}f0OqTY,`(>~J+';6\<ڄ>3FZgf 6k )~!>*gpgpgpgpgp%JR_COOTb́UMW:iUXU\ƪb/vy4S!jkN(i+>)ōɼ!+r?eSgCŇ$`>PqM-bט"P)_`_y)Fp| 8΀3 8΀3 8΀3 8΀3 8@w`]SVݜ;ckx;e~P0}1\c(3fÀgLJ&}k`ˇ1B|mIS|i >r8 ?Nإ~ ;lua 8΀3 8΀3 py9x C[mjcƯjGAY]}zӦ766Y9sۯnO?=zڔbK`gpgpeꟈjђi"QUȌu0FhUۥά/:;ꯩ>ŧe ́c|XZC7Ɯ֬\@u2^1.)* J} &"eDG ^zN}bȟ~1l>QLƜp\[C+?y|r>sɸw8O^̡|l'CGgpgpgM P5f}ߴ}VKa,0p` M;|H֚|~s'NugpgXz dכ)m^<#*b ;S?=y|R|O|-&ŗ? cy~lc@>v`09>zOqC+O| ݖ98w_f(ta6ٷ ZՇOO]oi;_h3>ٴ yslifO'Nvb3 8@d?(鞋K1Οþvl e׵qMy%{筷l̟g-úp+8 Çۘql󭷱veۍ c>΀3 8΀3 t6ye\P{PjQ 贚 ݬK_]b*6|U{F=ҸeIO Rc+^k.r.wWJ O~Y[I`-T8Փ9@/2l8- 2O|OS{酩1>صRI:)jvD#=Y+O\^ػt3~bc׶G>ؒh g QuVe3? ^z6տ9 {Hgpg~]|6w:a`3qiz5~;䈣l}>c#ǎ3:æ3 8΀3 8@a S}ai!ZjUV]g!i0Oz-zP;՟TEv9t֟S֯OaL,F}ĠMm$J@AHA9$E?dQZ(s^Gf~ORLGcY.5~ ?f;Wmeǯ-pqgpﻏbX εv^mþmgpgp>άg 5v֪̕Y[6(CGsV+ibj1'6>c'C75z2~X2t5ֵ\ߖH@_il&CȷP):΀3 8]cǭl}Poÿk43/h#wӶ&C{tgpgpjXoί3S+.Z`]'քWE|mC'!UY@<ŗy0ֆ}tv#|k4e7 Q4_D!FGG,rdNn~Y:`?t5}Wx3Gg\kŋc6kL3{v^~9׿}rᨃ:Kf>pgpg ʣT+I=BtS]Vv`G D>R W_b|X|\/l!;Zk.#Ou_Şqp: ^Y%YASKtpI/6RDdX1iOa \Zg?0nZ6}Ǯb[Yѿ9X!pgpjAnlU|Eg,I^Du¨OLgE}ip| #ؠc!JR-:4I/|#~QYM-3Б1]'F{  ޵ٽ+6 kb/m ]e^΀3 8@0|a6^{f΅T~s=SRs{c丆V"Kq`gpgp@v]%s^Zr:EDy-6ޥM>->Α A?p;c{M{Ryw I)3 8΀3 8ݔNOQ(: #"1P{N5湬D /S>ud+VPg 0F_xdCN):!RoO)'fGQ'?Z`LB A\ Y{%HA(>0 "=6Xg>gσ<3~AWL!۠6:Rk%|N+x&GᇮK2oҷᬩBi0a}xͬ[h 1̈́Q#mő#ϰ'Bsn S9#1c_n/q3K ],r͙3'wp 93 8΀3 8@g21O'ڴCUW]8bAJ\Ӻ6%6py|hRlxy|iEd/|i𰧏(b_ؕUJB:ZD"A6$¸Ŗ#Qd|г6i|Qay+^>E/ЍcZ x–Ʋ@V;t:qMnc ~ZW -w&̈́V>fB̈́>aSOa3%4zȉ`)f] 8΀3h w$6ߟӀꭾH!W~gpc* k8vyg;clWk{1>vz?ys_KkgpgpN ?On,˖Vu4F,_9ͧ?$Xl``KYi\C~_:c˜b1N1.ŧ/| (q_h)r?[`P y0EˁicIN1McLL}Ab=ڙϺk^XF'rLM|o*~jt!7ვ#OU¥&P?Ҟƶ[ڀ^fB?iƄ% | 8΀3 @LH_wn?6Vߋ˶KCx᜷_OaS-;3l4; ;a-^}=K7lv}߰7޸UJ\gpgpz$?uGSBj\ЪJTw.n,2O UC70Tfb >zT_car §/O/бUQQkJW6>-"¨0tE,hR8Eci^cŀpEǼh!"Y!y|b2#G1ƁC4V=1K+Э_W\˘J?=Muޥᔪi$Wij\ <sMӴm>=z 7Z-Zd|͟??.yJc C6G U"aرќ;jAXK8 >mS&OSNӶM<ǏQ+;ƬIѧW9LJj(|CAF%~*SQE0#i"yDɡOupGIjbc'Nxy|-!Xy+R|l4_ _v;|ڈ=N'.TeV-!vcÏm4;T FK t|[ 4~qŖ'EK>߮]yݦ/7Ywm;kyiqgpg$RT4rA:B1ġګ5'=-`|0haDsj >vVctآh#jW;cuW4WjoㇿB7갓yOE|a[b )>5N6Q+&-8OS 3t?EҘgJf$U*q̴ůe /O^1[ygB*ƔʚϟS#N;b"wUhkVs΍[N"lU?Ey`wj41QlJ^pO`>"6؂-J_b =I|ЫnLn +.h~|SLZ\_R\&Y'MDHP`)kC/> *sF:&)%b5Lx%'5 1Zy%S7񹭿ey]؄5#%'GEgs6bl0}G;'C^t_gUX.;#7dcWXή{bE1r˰!ˎ+-oeq9V&P,uc?i.΀3 8΀3 ,=꟪TQsI1Bz--:߹PgL G:'}%t>8ĠMCW>> f}ͤ e" pD`JyH@Sbc<$(6:m(Jщ>l)O,;(WjJ]ClNK~y||&Z7t3R|ϼK-3 w 4La i_bgnzn3'Z8na*^1b_:яo_)Ɗ+7(|T|oOI>n- *C[رc'"Nz k7 Xc6^|%{.7-Cg3x*!Q]u,#vjkMsSKSSP8.bͿjHcxAu5gkVk`[1J.ma#?w῱W^yy%׀jWJτ/L W#&(<>MR sTn`lWw}^~{;:8iRH5pg xr'KƄ:*B˘*u`l?bp-?ZبC >-|K;h "[=3>XZ'1c't~aI"-)AZ@!EHGS$-HS}eO+!'>E/|y|FGAG?b2N7__9ؔ—.@ |{o?يuB6S4, Rż 'ËweMJuӻNyEdm?6pfft0!lq)QuӍ7yg_MS=k_3G{͜0zlmcƎ7x+n^H+Qh&y2K.kM8V[mK|w6 6߷UW]%51KrϻQW^ y|se 2 t"˷&I! `ֹel0|z}K )z}6If6<U^ ţ3N=+nzɩsZ6~kv/<OoqcNqujP.:*;ɥޒX5^wװ~GkymH~׭jqW˝ݵ`UlG~ʙSpz믫+嫇|9\+|(/t;dlRG3O?h󸳮qoۋW"5pg(7|meؽi2V Wu'zjVa*+SSe{D}bq'SLZrчxM)Gc=qO+ TQ<#z 'C7[?>-T]I%!HOp c-8XD' ׎za jcXc,c_Й6qa(jBGbƐ'ol` ^g" v5Wl|c%).BCSҁE!xQQa~o~նoz 1r&.Ğ1c 8o{Y󞶄-i;p@<6FȬȥnOcd!SvƉE Շo~tc$}?l~η+xO,@ |9%]{3vv%7gG9aE/>O<_/Ҙ⹑3P3 c^ʛ,I+ɧ7;8:*ܥPhJJbk ap]dɞK|sǟtui|,r'wΫ[s}TjsG;Êbt5NPxt-Rnwgpz8^]RW~SE -)[.c@gB^,G."9DKFx)vvGR|>8ϼt+b,H>6I#r]gw ,\0ߖVjũˁyG?]?lE({L׳qǷoy_gshr|a 4(8!^k{A'w(ǖ~w]/m!=o^ߕ_6 wNط=wdK0; }["£:{IҽzwAi:{g믽{tRGY?= Z;5S ߽OLzʆ}뻇/3QVYͯr{<>p}>mu/YZ#:'{jZ$>;΀3 8=V:UdɨUO ͲJ]/8^lG\Zb):|:"#챡?~OArV?|;[ ;3Ch.++*f5hP0 Qёt” :9| F:in}|h(Z}7Dv>z2~0Q<7\k'VAK3PB~8j6V^@!n0@Cؾn0|s{~?忺2a÷Oscx=$b+w2 lK@ʵ) E'==%̞#ofGmU6[pB3m ZGu#駜eμ٘ ;9{13!dse}qVxa{lK=ƍ~<>k堶Zxr-uy 8΀3 ,+ PMDuRZ>eƘЍdJŠ֊*1iU WexK,1N_6CKDȯ_*+W"_GvM@- AvS`:%ltnF}nyA(}ǂdPbO˼|i<{~\, sa8۫qh8ǟwxa*ug_Ng} @^n.藗^BKטnm yհ #?6r?A__i7\`"S$qOmO^ƈ[P$h3EC;n1*%|S<#vu7e\-뤟=IK##:ۃ- R6x޷oR<*uٖo~z8ٲqqnxo]}mE6=v2LK˶w,mG9}"Ūoj-~ut#]Nnus{޳Tyl ٘Δ);"QQEJp}~J$G89D~)ǖ1v 8V ͣA)>1lŤU}>//n3s IyEg0 G2BR2-k@,^=6xTFsϽ`ls})Iw[n9Ti.վixQZ)^kgpe.*>$PMkv)>k!+/D /hRhkLpqjD3ORJ""ȉ>HOOإF"|/["-/K7/]?76яnx:QMrl0 |onڊ⦟tIx ]]%#Rs;/Mpyۄ 㣊hm[Ѡ&Nf\@K.6.=p͚`M{ij6khɅGuOUxsWN#:=\,Xw+ӦXxd95N9髁W+"Z΀3 82OUT=iiE 6=5WҧejtU@g[.n gi,| [Oتg~aTG\a[(?d?E5}|kdOHXLkB\-}" :Cct؉Hm}|YnEG#*.sS;|S|=tΥ(m[xՊӑKq,w00| c;l_Nǧ w/{?o)D|ajlxyt)Yk5ⷮ_~al̐ w(̝2衶n庇f.3f̌E~OwWj6`Bxǀ =_7;!|x<:-Ek\ne!zacs!>-sQ8&eߥի>>RJ5bozx7xFlQ,>Sޟ&'rJk<%%r`P\ E^|>c&RA&ġ0HKf&2 GZ[G{r}OθijytE>vgp!QI}e)>k6.A14k-_C4a _5b?m C eI _k>>hmAHDf8B_ EE!_a#2D(raNO>t#&c}O9|F`a#)ϜpR|xS >QoFJCaR ,S;.7!ƩjZ `@x#\>Vq-{װ_cD / _^iyuvX6w/,@ʵY.< +jgs b-u /mˎyN^cl dyoxb;/%Tʆϥw-<܋2\i)=ܸ.l 7 ydĐK-yG}.(㤯q~x;K6&&w9z m=ވM#vaG mBh{j3Z{P{Zk5F{9uθii«ks΀3 8@g@PC:2OꡌbÁ0ۘǗIk{X#b//l|K|_)iKq!6Gt!=o՗Z vP1>-e ,F×1m~cQj m/a!#I>~=_gu?|ugAQ' P=K|[6#kl ߲}da6v16*<_$nm:{|2r_wً/mI6_igڴ&9jZ޽omch2n_W:Ď8EǷ=n̛G+l`xɓ4@cP@oe_ t}l&}B yow&Yᅞ ;52O"- l8qo鹰Qpw%<%y7= 8q|ԇ ÏyivߏJ 4(9n4zuNhѪ}쀭4W¿$N~3ώˎt~Z[GG1:Gkr+&^\[gpev};=>S?g\V0hI^SUP_0$ĖpE_c9Ѝ̶৘i>t5SՁXE 퍣<典7K-)S/*pU+~Ke/%ʩ浪V^?~f>뮷ϧ*FطH?6]P^ ?SV[nvypG^T35A{z|߿x8'zgK! H㒟/wX zlǧ ]5b,DA1ֱ֥#c:aCwrg>a/|JУW+"E'`¸wg|>Q5os0n},QTAa_9$k+V4<p0+׭ )&g[ɗشQ_JCOl:c.Ò~H3Պ!ĩ6/4sϭ6%lO ߲~z֫A|_l]vPqůWx% k;jokiӧ3Uҕ$UWe+4q]#"{?mJ=%ȿĎ~<ņl;~&Ol|VzSɨsoHRQ8p??9'#/}W0aB4]8#EbD6 Tsk&mp}͇0h[j>.(ױ{1h ;y/xniS7W "H E0 @10r鄵aMs=:V#3Um:39!~D]ins||#:W}Lg'l^oXV/֫{s6}seKJw ~#px/efm˖Tn*A s6M mذh Fg`. 8'l2߽i\=Hul9."qG.gL+A9Zxt.5@0 @\;~J[^/HMUzZlb+Ƕy0S_'a3O./WW w0jE*?If(J>L-: 83';4}iS\W l2>/G%ί?j l?юyWzm~]q'0q$A} _^ˍ(fwu>;Կ쒋/%=2h7;k63$# |GKn),t S,CEN/JRw/?5t7m֢8-R/^R–]߱A_owڊ+4wd|Cip$Bfڊvȣ/W;G4ۤpvх\,xĥxnl+kJ+w-qv;gZG6.1N\<зgA?wo\xE pJ^p//mJ-Qῳ_t>8@?ȥ&ǂ 4R]t?+<~, k]{|r9c6%с_͡c?:#lOe-(\MJ{-EZӊY>07Zr# ~`q;rռ6?bu~}KR5^xʑ6ϵn F/Xצeˬej#hsfc0sL?x}?c~$ ꟢.}i;vTxiSU\M?[n]ջFk[c|O:!KFh$.3WS,>_١Þ1W ]U ;pMXʉC0>X#"PEz L G/Ѽ$X|[XZ0z xOz*KrZ/s0_sk#>b|0Cy0!̀>mq}CmM ֹl۬ kUbmb3LsIB7rcIyޣez!ģ(oH-yʖ|o/^۝.m)FGG3 绻,Y]\:|e,!-s\jsslF/` ؓHuM~OLz*}j>9]aZ/B *g{xڀ@ρ]rVy.jS,K\ǎ+GۊBbC  F U JB Q2k9أ@$`0gcZ$/j|Z v§U Ҳ/_i=ʆ9}cxMdQBp e{mR𲶩`3?&M;&צ<ߚ'Z:ߔ?IϔI6e7ܿpѐq}0 @0716daWǎH[1pGbɷ!dWힰZ8 ` ``3K)vJZTe[vieGh?ժ[lK}ZlB`VG'|l+|b(&zH%|bjx9#/|lЃ) g``/a`5{ i΢5;~FֺsfuwSс:/c4Ve>z`ȇVĨC1ǟ&P@dsur|}}.L/uO_wUZD AR :ea^r:w X_o^2bр1wkU3ag @2/_oY胁` _߳zn5>=sg{Fc m]XI8輀sZxV@#` 5TU?St׉+(q˼[꣗jǎx:Zٖq8gh79! Ws{tE_8`cSm >qӖjx(!¸Ie\ -9Ns|U-\r|0/j"=>] _q+\̳6Zr_:[&!50?g#q/,_lx0Z>sux:t'|;~5緆Q``4_fí[ξl8@0- F+,zoxln޶z9_r5h[^^B<";@8!` E j^8ꯪzjWPSE_\`,N56Nr¸O%,a3O p5>6 yHklgy;J]APlNvȏySLeeIYq]|BUr܆~5|O$Z Orf>aNc>z#ǗnR ~ճ4}FzR5 6 X^Ű_OYWt8yf>bu)*#Po$` FMߓBzש6.6-ZO_x[7Ǘ>:a ![2W6aǼ;_chxSفQU`, T01f^=:ZjDYhNV>s'&AKќwqX}bp񉉯DEO~ؕ3z'1BM-s[]`v)']cu;f_V`(J[o(P@0 @2Pǩ?*.{h}w; 1C./ރPBe֯Yk;f̘a-\XyKZ[. ֮ςS` ` =R4E>UWȆ=-slh%U;F^G᳭`P>UUM\$o|O\)͑ z$JxDAHP~̉4DGOabI8Ni/{> e=?u-[^XQwk퟇3Ύ̯ڵLz @0 .lpwAC}Chhjlfkjھ#$h??8^xi#@Jۖ-ŏZKkXxYZSGn` ` gI(Gw`}4!~9 K6ܙczw E \ J}I_HVIiU%| 7σ|Gr|לb@}6![W'--R [ŪF|.Zg`Qw<57Xgo::k>dOo yh~kݾ'Kqn"` ]@N?=+L 2}.xBt (-.n;~j0\fϝk&L= ϩ\?ȯE=Cvg1` ` % pL 6-PmsZiEdh=}D1-Ů ( hӊِ$ʗCzrUiŗt9K =kVjS%b#/}eC_r|8/-z{7i%g,?)eq[OW4LV?i*Cݟrj!``bѐ/v)[lGx^KXo0L4ɚeʼchzы"߹K^Rl[yY>Q*@0 @0 cN9T;˘'}E 겥U6!?3 C򘊥8٢g[]^ ߧKk`>K'|#6:ҡG[<C1SIQp JXrwka-zN JڌЦh}X’/6es|S_囷_?7**)tu]mD7W_# XS)#` ` (ct͟S{gJFa0wvg{?ܰ7=v2` `X6PP|*VZ'M" ՁJ\aTGk,LZDis|`KViGű|g";Ch9dž>6$AwKaE 9Z!ЇO_|l+gC?w_yHM1@!~?ᷝB^9}aξ>H&`  ;coLz'Xnzk_rmURzj>H0` ``[o1-hֿ(^VTݺaZζrn'"E|*3` `V|y:NO4V^~Z"@X8Q0D6I1-GCKLlrˇ9Z:"ǎ1b)>x3VM\ЉGa _lb,n&(^_g] '̨+vgOg2W @0 @0= E"rͶf8) '|[zul겎Rۃ:2&kjnN/6mr6o|wC{g\z_@q-#V@0 @0 cꪜ| qA*csk; Vy RL!.g$EX_9)6!Z?1Xіc"˜~# &#) HNtnjλN:-ɕ>y~䇝l ߻I+&-8s ٺIϵN+'~|<&L Ӑdxmv]CLJVG#}}mUa)@0 @2@a޷Y<`_l$;䐃쨣IG-eb [ӝ ;ӵ%` R]\UU-ت<"[]~Ŗt I>8'ǢH ``KlՍGO,OH^ucrӜwN1shM(J%#4OlR"d_}rU8tyjj>x~!7X$X+kÉT` d ]( t@gG=x# G#O^98sC` ` JbL12J[~`sB~Q-Ah'|Z$Ǘ W'.v _oD+z0g,|8Nb0N-??*#&"' % i H>!FL^#9&cl8rL=˟7r|qt(&_qs|l#~M%|02dO7B]e}lZ;Nio54\RL]c߹ 쫳v'Ίzmvq"z0 @0}^wԂl '[߽hg 99m`w-L#v0 @0 @''<5Te+_lUPʼC>y5>$$; &}ZO+փyW;_a ?WQ` U`bd$$ 0FW\(CrGC,5pT/l%|02V(^W}:c'^Qזr?# x"KEp @0 #a@b⦂Հn tb9c$_@0 @01;TTPZ-cn񔖎|cmyݚ8ic'=|>:ٗ>ɼϊő?>#;?8[9r6ϋqIHf(Ia EvS #?m,0mx~GԒ_*ǗNr|ƊC+;ZDr|}&M-񈏟y7d,1Px_1BNǕcc\` 1@*i&l2uIqz;S` ` FER:qEzL0 uTꦪz73*1~ĥ%VW'|!Ծ/1d @u_s|tAh>rV?|;lrls UO(CD(iD|Dǂ4H8M\]WxՇjE3!cţ^F'Ll'O-gn+m@jdt?.]-WO2` >:/+@0 @0 (f:)-Tj2cLUwS=(:D1TJ l,{Zg[>|CqU7Fx/j!_Wl<{l+/cCr& PB%D -="Py$/Ed0\VZb#{lDˣz䑈!`:?14>)|&Cڐ@ycݰ+w @0 @0 @0 5N)jwK%D.DĒ|>9~ ?IqO[e_<1V _uszs]> }ȃk)ŸqhlWZcмcr|b" _3zhŏvq@nOJ%ǎ\*:I _6==`XJ,!``3MH0 @0 @0 f .(iR/VS}Sk"SW5gP}bRȶ8#'50O\-[T+|#W}$Zʫw\Et.B4m`}IJICD9g))6<[c7Or|+W["?UX!Q|UW>O96\<ڄ>3Fgf 6k 9~UH0 @0 @0 @0 @0 cJOjNr8}S+ES##s jUӕNzZV!||8UռZ7MsZS>'|C_`sd^}Ge?eT. Y@%ZmhJLs[b{ oog!@0 @0 @0 @0 @0 kߪSs`LwZxO ?( Z%DeÎ:ⒼpO>WW,'a )$` ` ` ` @Z^FC [ѨNw8ݤPxJ(#"K2$T1FXN|sb~o%@0 @0 @0 @0 @00PyTֺ]֗dwWldIWXEТFQo0_vxλM!Gs: p @0 1ы` ` =8WϱHez/\d{g}5s60VpAg.X/ Qh%WV}a0F'w|4]„@0 @0g1:uZP&` ` HuM~Ol"z>5QD|SŠw6#SE^Vau2]rVy.GO,:r;Zx䥱rD[YPbpl%Y(zҢׁ_nO<OlbAO|lhaIVǞ˄` ` ` ` ` ]Sg׆_߇c[Q:PW^i@m"lz ldNG9 6w%4;~@0 @0 @0 @0 @00ﮭkSAN{y!D} VzՁN~'QD`KKS$;@0 @03 v3` ``,2;So|ĪsWD|Ue__l,)Y6[Z6(Ԣފ@0 @0 @0 @0 @00&jwFz}^W]/js_.Vtbi_X!/ QkNqkϿlڬpG"ނZC` ` ` ` ` k PÍo>;|_?y>DuOLAGACZt!y9^3<-6{~Zt` ` ` ` ``l1@8I!dwտR*WJ*y?YC߈ %&ڑHxy|uBohM @0 @0 @0 @0 c@GM|ădUwW,*W NzA+}+d; `6yӢV/ o@0 @0 @0 @0 @00&Ϋޮ;\ut:TK|~`4,\`N5MFƴᣖ<+';5Xfoe@0 @0 @0 @0 @00&zoo!9X  ;!chEwKdjD+[cb⿗C` ` ` ` ` ; uѭ3B[^G,NSA6h5&bPf:ǘd%.}j &6ـ-cCEiq3` ` ` ` 1@umc4uUK/W]+ E굂S۹>!11ᯍWXU^|#v5|l—^~nKqk֏O%|TZc+l}*$` ` ` ` 1ǀS24d,1P(l}fSd;ԓٳlʔ)VW_o--6nܸ$7nܔbmi5kڒKg.[; @0 w+s~Y"6؂-U7)>tK b>G#9փzՍMsMV;Ťϡ{7B*N+X>y$B{HAO_:|tMv`Vq6Ai,iNy`9[P/vѳ~l#79؄k !l>Xw)(}رm}]?Y7ߵZ0 @0 ,RQz4{BWjsh ~0 @0 {$꟪R(4FUE;uS@xQQ`N:_1z9GaL\W8~Zo%|0A֯}>'|e}d e" pD`JyH@Sbc<$(6:m(щ>lũO,;wu+GSSBBlNK~hMĕwK\ʟ1W7.IR 'o]OXO_ِ` `  Pgc[%[c>ǦϘlƷ)KeC촎koou۪:::r)D` ` ΀ P3'jLTeL}:0ihcZ*:OH/𙯄O\—?6l+W` CX`qh`['~~TGGLDQOJH r} 6,B:Z-" lGrLp3/{Z 15?Y/z/'o"'QL9+GJa>H"{_[nM~77+@0 c웛ozo^loM-G6~;?O&r$#` ` Qaꧪ/*SSe^Z!N1iAwKmMS_a(J!k=8'g^8s|lAh9:* `C AE:}KkuI'?%=wS_;N9W,l(гxV,1ȇ1D[ ΅7cjsA>휧G.N ` g@?~o[h%Ać_|W؆`rd//l؏=;C` ` X:66ѮO5s0菤mELB=lphq$bOO/"{U,ZţeL Hn^G㱌O!΀ێ:hgt=I'|k^aO>7b@0 g@ O}]ivHiH{wBqЯzu4+`666& 90>-+E ō>Fe΅>77owͯ$J >cʣ9b@0 b>wt _t:ᮁTԧ?E ~AѪ9q_{u-]l&&OrrLl9\="(GO iCjyF%tWBGXOwulhu~e-ߐ>)uЋy\565\p̱G۱~{^[ @0 @#7ZƵe$)sRT(/tuYc ᇬ㞻{ѣֻb6Bo(53~lk;8đ6V7n\,nQ9'9*K.;9hޥ994@0 @0 jIjØ%D.DĒ\I0he>JNq8!˾ZybWr>9G?Iip> #)ŸqhlWZcмB|OL+rF-ю;ɍ>C[ _qK%|W'~+F[ Owph w.tw[iַvE ߼qv,Mgg6،Mzo0X|C` `Gz W?0=z6/&Y6vmc͜gu[͚Z̦MŐx?ި76V?wk:,ڔ'm Sm3jx??Uxj>` `  P'^,k^+UU69W5gP}b \wUSvٖG''50OVb>+j>UEZ*9-<}-ƀKs8X$Byi!"(D'|d#ll>q_SOlhu`('8|/bםG1 O:NMcͅ\[r_9\|XIkm}6y&CwϧO:)= n`Hn0 @0P q~RBFtC`6鏶;_{nZa~ꧨ~Ge=C8*Nʺ V7g}ݶWWӞbS_gw6Kv׾w1t] ӌ@0 @0 1 PT[OS9TV &8RMWtՁ &WXJ'|ZV-8ǧ>sb< HCďhZ+:$m0@\-}" :Cct؉1cAazl;,VǏ#="<ycO? Ugcz;w,t.MX|/_`8.z ůw]ol\{εֽ6ί2l )6 yp@2nDw9xe/#Ȳ7` v"~㏱7Ub/xV}_XGXѧ=w;Ƀ7TX4z8݉Swj[7ئf6ÊgќJ;n%:Xñmr{l0T"*t@0 @0 ]QY<,>YQAP:t=Qaث<WltCb"j.<2\ GEG/ }5G|3C6j}8X`PчCJCÞD}ʅ9|Hݴ8[8! ĭ%6J '_KkTQH 2P,Ϛ=kr(\ xBo=vMX{xn(%LSXl}Icm96n^ޮOۜ[h3?f---VߨjgG P8^eB @0 #:"mPא&u7\{[}ŇPd]~uvg.t)`tckN9:nޞol?gS ww,<%~G E?` ``,3)%5R)D͓.C3N=G//-D5eUcO,0(僭|9>b)>֑_>Ė0r9ւ>:y{7ŧahU |@'1}-H̑cD%~#" Q|Q _K܅M__y _}g <1+<lךs<<>Zg` ە" <;6y=k\zխ[ |n=ZxƓnGŠǬѿm|5~j۴ϋel[w7m_Y3ǕEx21*J'>a?o9$` j.s@f_"ԗN~lkVa~>৪] )?KE/dK"J)J)K3^[WYi3,ֲϜٹ:@0 @0 {ꟜHR;J>v評"C9I9E1!|k1j^O%b 9D-})>c!ۊrqS9a2>~2*S, *т4.oE X5&T`ʗ>ۂKx{> %ۛ@Ɯw#l\rMx+Bne}ho¦Ǭԟx4u+iu6oٖewيWo`&{qݚwBT26xfq'JH0 @z?pi. ~S[[Ug蟤 zKIUԗ~g&R)~?+m.~iS/ڊN3` ``3߭#poqZv{~p*/`!j-,V6$>6pUlbq+W,'a )hl62H\lb&?r=zuVcz pSu߉~2+toogGoYtu4ش o`-I3Rظve`?.!W~}߾ώoz}X5z]ZyJe0:mkx;u'KOy(&#\G"1Bզ_I|O֩N2ROYSD_9>§_b_Ϊ{71a,W ?W1\-F1$h? dp(vc@-%9>1+1}b GW ;05h+_&?aׯ\\xzoA{ŗ9|\~s0 `  PteewmgYX}7}]־ryz[v|5{8TڼOEziر>}#ǯ>zr>N~Uv}YO\^؇ m}6eUfKG"mz=|M<ߵǺ=ZfolؔXkd-k]g i=ڞn.Pg=֧-?kiɯt w0nY>s륦;e|]tŜwݓݒd@00H ^0 B[=m/X/ vˬ}K6W1Ԟ'mÃ;Z ʅ.Iٮl>0i_a }~Ja;+@68AMl@Ŷ-ֻ`aMs>ߜhIV?m(Gwf?*M9Wgf䳻L4x4/첅 ukv =6؝weW;gl͙3/|.X`XrHZ}$c2` 1Zj:= jXP Um;)/_}Zr}mTpcp6g>wOBহ_K~q\Aq! ^w.L>L;.&Y\7wm|FkMnMFw_m ?n[N{MlN7 4p~ƂaP`wn&cQV/OƗ?ǻwԹ:`uM7<+u3zx8VC?8]yr]ŝNw7/=מt)}ڵC =آ|jL'Lh5neWo0p'gcߵ!B7nh>?PRs/%` ` R]y8? UQO1 >5]lQbѫhLbO^y)DŽ_1d/<3#wҺ*RC+W챣Eġ+nr(AbC  *9b2Ϙ6'89- "d”1~i![t&>bM|G s>6'ބOL֠*@*#zmV[cWۤeX?;} /qszZag]Y{i% zk y=x{swݯ.,t[VwY^gy7XFkkz7ftHw25b|ylG.͝wy 7k? 9#zY翴7N`֯\e􍊕ǎ9}U?vy/JV ӫq}_ܪ7֯_6֬Y['^w 6ڿ}A_qIIs^XG'mH-O=R.={Ǩ_rNy'Cyֿ}f[1g? ~'C؁Bs- b&w)H"43cyy7|CvkwwSMuFf^P.ox˪bM>N?cz P6&FRl$>U.8'%ο7B @0 c?9\w(cZ樕V6P޻6U+n=iX"\,q'7}>O DOOL/ ֐V z0!|WHJ$˼ZlCǁ`G9 :zx(KNz# /)HGbi}s|r|t"G政w?q1>#H1๲e"ky{ָVX/gG pkzg{ෆ7Kxͺ'lNo_e=GZ[`piwu`MnoL#fG~G[ӭg4ybo$3}Ko~CS⳥˯~L5C ҕ7?M_Otub^|] ;{U\;`߾%tϗ"j;r%GT[lödR ۦBAT|F%@03 '-Cλv1l>.^:s 묩ymI'̰YaӦظl!z/f?hs`-\m7JwJ͟Sg7ѦLk(Yګnn?ϩMv`Hw3bHҭ }V[goT.+[η켒+ F~Ow5}_>c?-DgӠ՚go'Z[\En=_lnw{Ze6~<~WĜR?ʏjٟW߅ͅK~zw9|9+uFL$1J_Î8Բ98_^:m]o)QpY[p3f}K0?j˟oO`gyXM?\_;ߴ.1Q-Bz/.5KW 5oJ9uC` ؛qƩNc;.n?e;Ѯ{1~SoYd#-\ݸ~4պ 6g!nk4=߹зAk~ѫm[i-ۏn/sߌg0O.88uf6;é=6W~\?f;p*}[~yM.f.A^ #O'/-3ʯ~OG"/{y˫(hGx?6~N:yށq/0>#G)qŅ"!@0 @0G0(gҢ讓qq![eRlUS?vY(W+r<.q-䢘MxjZ⇠Gh]C9K+&:l'.(t[pη mBȷ/*ſdrz{O.z9 {޷XyյHZg ~WwgɆa8H!{[~sRi l%.KmĘIAMMߨ8W+>c7jI8SlS;ﶿoi}+mAMg2IǟV9~^pEg~PNi]z V囎7e߿ľUʜG1}!5' =hJ hm fi % 1[yJc^6$D~pBV2u+k$0ғ[Ϸ"Za~>jh?oK{?`]˗Z{۴wO.\~}4RS3<Ifgf/߻:vxqoz s!>(U N,fBZU 3NtbS϶s?{mg0e+_ ovI+&W'.Lg!\wr 9.v~͝]Za¿|;t>΍D~p6շ-lqjLʗ&;?|^qK<'zsUQزЃS>bUp1M7ޜp&j=)> &ω;-7L⫗;NJCwmw'ފZem{!첳m9}q*1& @ ج?a~48AGм^G!z|PTV) '[9_5BJr-| @_j#:RH%[fbe. '!OYxKي=D^]j,J\|-?W=.IeS4{1ƿl%ł#i$_?E%ŏ>;^޻Sm yurs>~IcE5guc_x׬F˟h?mԹ ^|Ģ-~Hc븩ŌmշVjL?wyQerA`3~򋋭_2yߨk*e2Ld,q .)^(O.H my/(\@mX˯@ uDߌ?)EцԎ s2,ԏY?ȼUYY ߞW5z0&fX`-dc}2{l1'حNmYSS>]=z6vnK[3hk}S>2u}(mh3A )~-^m3D645;S鳟=5ݰ N9#b҉ۯz+a@i'e܏ϜV+}_׾v?x>~'N:rܧj[w>v1ƵkKM6`3/~+vՕW1OjŤGNLx\+_*}^W Nc"VLc{O@  Z2d$?EJD|3ɢDd=>Eǜ'ydeGygUbKNz^/W%K .?vu!CrKϳ}˧QT=ɎQ a 1A#ԋG*]P:C蟲34 hP_qjk[n3۷֙h-{ iC + 9Ph?EѷWkfkVZH?z 0hjºV?.c-0̜5˶e7@;F]ZW_V5a/?|@~[-[iӦtV$L[ /v͞meT_O2āVR1_g>P`@  ?ul 9=n~g40]վY5]徥;laS/5~-~2^tY]tMMc ,|AMa%v3:inn>P%>R f1셝w)]EPo}ۛYQd|G˖-K/dJtWTWW jNZ/L8WC0ITW"O>ąfN=Dı[*G$'e+>. 3?}yU,grM4I>iS>iqUW 8S+.%c#iɂ?Sv£H?m{C?:< 6QhԑӞM R0 Og 4T<@ IO7>@Wy&V5N>)WM=/W;TxᩍS*_<uVpA/!&F+moΞ 0λ|vArUK햶6_S_}_m;iyQu~ O,>폏qA}uW\hu3 @%/F1PCNPǟ';co~EA^bS)߮po.l~Mj뮽|os_QPz#+\&~1`B#wu`biK~[ܐ/yAoo|} iVE 4be/%_C+*կ?j]|;75[ 哾 +o[&uڛ|{Wg\7&_KY ;-G$eV̚5ULhr-L,c  < #* l L/}i 'ך``r _7vmI>1D [cAo?-voT,4dU^lίƦ>wL@ 0(ɼH1+J1Pxh M*~؈O<)TF.5ܿ|2`%+$/[JW?~ڔW+}W-h{j?y6"Og$3HIW M6 䑑M٢<vFS $Ol/:;Y٪A`h'F~0g445[Q%qszQ㋾zV3/*m5 :tꘄzz|kQ4!о%4,3:5i~#<^+>¿V_Ux߇Y¿xMb53=J/yQ6`(3U 3@:BjnG.}FW/\lU8ַH*q{ F \|5>cl#Fq?)@^Xg=oQ"UέeCd)p&LP1v*uAPg9zgCW2>;'/D?gvwb (>OTǕA2LL~#^7tKʆ>r?a( %폪Myw~ա/K)8 _[C}A_g{wλNN*pÍY_퍘@9+lmEL@;6 =N8q 7f§+f(4[bߚVzj!}0~-uv]w?Lf}glE1ۋ^i/5Y>3":U;NLYُ4@YE!O!iFzJ)2Q`Ȇ#'ԩ'?#?C)[%n>/]j#2FG T Hti<2r/[Ϊt<$2o'RxUXC I4hA.cEϭ>py)v>9:푥=6mʇ(ZT-J{%-pݎŷG~+Ō۷ 2bU2+ BlO.H;ߺȷyEZB1CT#Ç)`(C N.%6e _p7v {Ym~&c# lUĂ50ea})|ܯߙ47C`̧?'7Rf {j?zrZRO }oWL|T##rEqY1 EDRfhE^qQϦ:Jx6z"=()[ R%,Q[)CrYdx)ݢr_6X.-RHvJ&`1L=3:EI+l6nu!Hy)^T1O _S/G\֡m/~ڠa@d7V …>I[՛|?}=YiyBtDOTvhs˨4ɠ&+Dz0^D/ײ|-%fce_xqFѶ2;A'>#Z|OlpP~H~9\;C7f?zPDC&zӚ'mU[5W/9[,y=i0|v(Yu >l۽zK)IG^r 8'W5kTr2.Y„OZj7El¸_neβuD`W룫upć }>6ɼyǝk5@0^O:%ڛd+Hg;D.mCTGzO=(S ~ٳR 2˃L0+UUIs1EZ}^򲃍viJCsS@-F_F6&\jEPZp?ӌgs+]`5ϱ` sf]6XK>@` PS|0[QnK+<[azJ=6ɇ%î|!/*')$/y' Y~Wyˮd/tKRYK~e (.Bje9SʲC#x dSc:Fy{dYMrj3+& i[#6 /J f=4Aej8#!56?L.0,U,;sd_{ܕ̳O+_]}_+`I>ՊjmW [;N;t`gp.[*7t0f~&|iHL{O}G{^뽡 @ 6O83pۧۖmŚnO OTpQv\O[#=@iSC}ѕm+ %n/SxѮ|l{5Tg[>vӢ3FA87y`wj20V!4hcCf-W+ar>hUW^>8`A-bK|%[.ۋ ,/JgϞlJ:|W[m=c j cR>Y*nW)fM}m3 )enlZrb>|k 11T\nGaFN>Rʒ #2@ FJBB)jWSd@=lS=)fO:drjzjゔJrip?jc{{FGvK|.K NTU*0q(oR7J#2/#K>̿'L@؀h&S(˿H֫= iuoJJSmjm{z(r;'N鳶uN1wiBu5齞 f$_̪^yş!rOrKtZ^KPe ο}_q]Ӟ˫ >5x_fwP^%y p)5ɕdO<8y[ T^ 5߫w&37-}QDzH{XhtZliP w\.Г-}_ζ;/|߶ }變+1R.sy~fMo5-p&i&XOci57Al!_E;gmʔɦ Q 5+)G؊ݎy:|>D(_>z x}jy>XlY:~(`k}ymCVgleeb;&.R a!|?c(*s*pNƢU+ʂ=|`ԔQqvZ~VZQ7ӱW!}G9@`!@0D#ML/1Aŧ1*Tʑ4/We%}R]YH>HHu_>9$2 6 gXҽ^{ew4O<䭈XK++<?رt0_-} !ocx经J=o;osc-CW-FCIj{ײ@ 6W8VV7bᘗOQ[Y.-LBHʂ꞉4 gmv˚E6^-hlGft-OkY/I# qM[|zåGG]8Wdz U$Q҇<Dz=Z<{~|]dɭב H* 0a yl<#K)Fo2#@ \F<ɟ~C=4;l1>wv#mnz`ew`Ov+i/h#Yc;?9}mP݇m}7?u->~Mhoޯm͕,hdGwn%&FQАjIҢW_oC{u7TUVVY{*7 F+貏?5q~±pMG>*)e۟~@D?Gj-H{Ɵ27NŪ鿿m)M`;drƓٶf?`Wؔ'jOq_8 l(o3~a]wVf>jλTYUlǺp@+MU ^ p@@J\X\o#ĿyX3X'$n`DC*K:5Ey.<2#' y=Pe\I!=⡫|6`OuAE_zENoh|\ʻ4`;N>le2 /? fxW~U>4p"n1(@ R^SĂd!Smi}x.bGD&|<L~mD8k/&QIjnaiOqlm+l ?aly4ph?:{쬗sm'm⟻o_F1.t?e=#} &HQϷy_7|}9ܳ7ighەÃKxyey5}-GP{/G{?/?dyk9vM}Y򕝻s3W1P}?Os1g;^lkC#jи?fǻoGmVe/翪7v XɱZAzıo:ݷwl>Yڗxߔ>މ2p/LM?1>F_c|rll LS~Yo{arǏ5W ۧN1>G{}F@ 0,`,HOJz@+Br%GA$elp=P>6ȫԉң:syHq\z\_ ȇ~^'KƳ}`  DJ!9S:5Lzن  E_rr7[vwv"WGOm?ߘj;)#-GG}"Ůt=[*S #iMsA]ntGaWz||$x. 0[%dVOct6q(44ђ԰ˤc>ɵyٲC>lJ2(@ G b [[I0u{i:Ǔ| ˿n1a{}8:M{%G}+/S;aSƵcxd+>}1_ّa}by-brMs EmKna{{8yfn㎻ |`gV/TD\CLwR|};R~wHͪEJ52&8Os~\C6M_}u<wIF"@`G žz[I̔*D=2-B\Ȓנ=l09!=R6K{O %.r?:%}d W[|/.Vx䱟_&$R/ dH $ yD H1l $}RF+<'aSec—E~Q颃]yDI9/dUFdjC}&U5iL[ )Sp=۲%+Ju;v|:s?M%}bҾM@ @YG]KMI?v-u35>q0':P[=vC}~f^g¹m-Gǻe=-EuMhm{<~;JоDeJѽݭgL4:{cZlt ο0<#h|AVC/yA*-Xeln9-j{~2lsop}GC9jl$"m>}b)eeď@6g!T;Hj7dSB:\qM_l-9W^gN}><;O(xիKQy[UC:*Y<3 5mxV9DVElq%b/ c.X3A &rϜʶ^y'x·& "1Ȏr 0D< dE* GyO*1SP+3KR@rCK^{'4MgMN>2.>ɓʇDQ;G< S/?sʿg+G"/?!W`+2DX萈2/@dKNtC/E}'YUUI?A)/9R|'{UpEs & >`gVM67)m JjCwoMnطKb?[`;D4ѠJH@ , W_c1; 3 )mF1(c-9?e?Bjz:lz$ނe ;jgQv~ؑom{:m쭿v')?䈌`]YwyvgV`mzM[zmve&/{8Xv] ӗ9_yVOy9w #WŕÝG>2bK`%GI2%qe |nHه3γ~_/[,m_rU;X~8{[0/bN;?]v v< NP/K$_;C:6׷iҥ>t]\|I;XS m+P>mx` c[o~wRVpV٫|[c~ӧS?V12&z[ R[['t C*<7*Zt?"7{NEB~;XJ.@ aOEg8J%V3Pѥ#>b;iOJz'bUIO>'OdKȎg~)pXAZ96#)WH0 #tjd2y@^&ϕ0îx$dKvvb?RlQ}QFNI<>yd<[tx3ÃAyw_#m>piٳҞ+l2|m[ۯ&fmlͲEv ;FnOaST9ry- =Ef9CL0+;yq6Yl-c'YQT2)g֌U_Ceom@ -&mQڶebNi|O+/F1}l>5XҘW1=^ 7=P̟Zvְ7Gv-;G+_t)h\_nH'=ƹ #ߘ, )~>~sRg.i{gȴb]c؃v}bm섃|;z"Qn 1+V2? -#[|SK67_9kߪej rC~(3١[L<)hm\Tlm:'_vEկw/2_W?]+I'ڞdҵHsrh)62bm0S&IO=*xCX[;m Y_#:.(cOZ5FH`埔}I}2C.K&=ѤRW]xLiUE@ (V-{lܬAݍO[z;?kW.J>7q>t[g}fy_mFmmK旇ha@*\&J-Ѽe OKg&y ]˟.at8k?޶M[u`4јSy?@g:d2UwDqs]F%(.;8r2+<ʶ8{ҡj˼nRv˗ &+6h#67&f&ֆXOVymS>Zwt2\A@ {!)Q,.bLUg)ˆ"iW<zٗ.)>Hē]ō)$ȑǾdjwDҥ {/˶RP;eyd+.er%N'F 3!50oRCA"=Nv >2<<\/?qR[s<;$Z<$}O>i$ tM 賌ǿfk)?j}kCA2CAe^yǣVH F;|`l{,L*URP @ \@:cAk4GoL>Kcni¡'ms/nXMiikPP9afAc >w-.?,[mdkUٝl$?Gߥ1 bLt/ZϴYOv|Vёno!ā{ߟH|1*L믟g$@ @ <Th⻊2(DF)65`WؑMx钊ЕMѓ}Cs?}'ydY#[x`%_+N=6W*<|I9Z2y'R )7G@#KQ ;+m& ->zȢG:Tqi+#i5jK9.RͿU-tG`Ŋ~ڄ:`aN,hjzu^zFnTu?z0~4B]]O^_0iX;y.?ɷTiz]<($"zgöPa |?]Z ;G**&$FwV4Gm&޵>.^Y$G{lV\9q7@ @ m/fGUyϦk+U̕">K]Ŝ2\ 5)k)J)9dH>POߒMOh+ZB?}]<=Ɇn e.%jE`7GX)#/}exA,u}ݻz +qX˞ngK~ؕ@j`qڶgoS "Ϯ;{[TvtuFڄqSmw1L0xj` @ 6K-mǦ'nkh=:=h~M~t ko|6v}O ,3n3ז$Cߕ k$[͖2'Fvv3]fp_G3~xC}^`W,hܣToۑg(Pۧ" FFו/^b߸[z@  w*~L jbYROyQ#')qY\='~/b*}򃍼-)E>d?Usdɿ*ryl#? T㩃pՠT+=5zJW>ydlIY7:]f)2j# ʹ@jW|G Gb]s^iV7f 9 ~ ??YS?5P{HV}z*MXI.?}^;_߱i#fJ1gP e kzadOPt0f) ' J% Xz|ozM(EC˖V3@ @  wĴ?Ō$R&PFԗq(:ŔW>_nUԫ>6v UȞSV_[M_lA3ϣ_T?WNaHtCrBH!I.X dK~~K].٠_:/YW{Hsy~ ؑMl P{W?e|ڢ6+M^{q۫U( \N+XY-J zN&wFV2IE/e" DC} )i-yi^V@@ %JPl! }k&694{ES-rJU0F.SosvI'LSȮ]rLqͶW0P"m%-:}8UdpL, wZ[־ՙV?aW YHּ*;NLT@ @ ?yV#@⟊gUB $3Z%-uX ;?]#aok.뙰Սb\#y;ȫ y?Y[[{j?A@ @  jp⦊㥊HLzR ++#Q=yUJ>'^u&/~r}/&[|ѿzt5@ ?r𐕾gQ_,^=Qzmɩ:# @=tC{[SGrS& H#}&W{=d:(ډ$UMRXql-5ی|]y"=}maTG  6peW駞^uEQ@ MAvug[l 4pt]]6uavN/-ԝvl?Ga";~n4buyNuc-gu7MѻZWfXE'!J^S$jl4u~'t" @ @ ^a*bu)J 1x%N:Vb% 8}yɐJ߳qSAB-%zsj"v/ڄvDj?<Ž2ѕV:鈩eO2t q384 Ry L$Y5R} .T?zc[>'jB|0@=r=pi%7eN:sܔm @ /=s|.mJВv1(m-яien9c'{S!m=r-_괥'Gl= -;?gηCb\꒖JS^v&&G4ۊ+윳γJ_ @ @ SȌ',A@:PF#!O3S炰G^TFV^x/乊v \x Ev?!+[J>6Hs5I% ૎ʱ4<[ѓ,٤{K@)#V%c ~}}h/=:eySoOtFCSlo)@ # 7xS}*W'Pǥ>Ջ]aUj4?r8u?kw=yoڼՍZVIu][!UcA#6S @ @ ؜HqMh@YS~7TRe(B؃$K^Rِ_RC@ܿKP uGFlQ!#;؇~N<^̙m3fLFٸq3}UOC-sn+tz RK9M@ 5Fʿ؇?x}cl]vJ>>>PIq44.Kinn#G-7b[MƄHP @ @Gex*?bFLJ\ GJӠ=dWس|EH~O=RJ g5Y%ؐM?cs ŕ<|TO}U`:1(NH@"EzP yDhtS/ؓ x`xy[?6YYǿlsAEES=CAϪ6q͙u~(b}]-~z]rOG?1XzOpw7&G3h0  KlK@ ਷ێ|_>n=ztCF+6DYqGiأV\>x7/O|†>l@ @ i#xĈ?/!9&9l@!qVs_E.'L}2:&M?6%|H4/9g'_x[|R {5i 0($K .e|J:<,$RA=dO27D=gW.MsP˖JP!vu` '0Pz.̳mɅ>=+7F՗'y #\@ FG V tvt#;εI&mFihnnڨ㑶5>e'|nۭmM{0Xp@ b:%O]Q2,l%_J9BVJ?ʹJ4dJ~_Ã/ ʹdj:a2iѿ" sԀ9<u9ɐR٩vP{Еur^z# Sꊘ?)􍔋_<["A Dfp3 BH_'u" @ *mmvݵUءt㐁>vu}I$1cGX- o?n @ @  Yyi !?!٥^Ǯ.dCzy?ʐʲG:ّ>j0F0A#ԋG*]P:F"+R:cNA{ % zγI_r;ԩt`%&"_)|ڇ\?eg /h3A@ @ i!zz/~0W3d@!@ @ 6C*< A*_L PV\)ԉ'9GV|dr?|:t?>?ڙWT1qM"5c{jh |ߏ~eF!@]Q'(sQ>vY5R܎V=)6&s? @ @ @ @ @`!@X8o7qr@V]qw|.b)`^4 Zj kA"^6Ȓ%]:sտw=(@ @ @ @ a@KqV]wu_t$" 'ê/=ML:JA^6'SY<{7@ @ @ @ @ U7⿥[(<7-5I7, 2̄ Ϊ"U!)v!]R]E&9$@ @ @ @ @ y\9vFwƿ"-}@ @ @ @ @ #PkFV<[g*ͣ:?ibu'K8c:8Wѣ!G zv .I g';m?ݠ@ @ @ @ @ f@~" F''@4y@=uemxANMl tlj;;RGOm aOj;l>_R5j?Av;mڡ@ l,^xhH"@ @ ! @4#C )ea%O >^)|dK%}]I'P_2"#j@<:/.e߳yW5 &ѐH@R ҀGK Tæ@/Y'edrS/yR6U>/|'_Oё.:؅GO$sG]_mDO_:@ @ @ @ @ 0l bC**dTWVylqI'“MRڠ˳oI6G&OCmDQ;G< S/?sʿg+G"/?!Wp6aTǸ:A^Wxң,RC)lk PG^e tv˖(CpD@ @ @ @ pC ⿽~ű7t{PVV0g)(ijfERdѣ-:f|_=dz~CٗlqIz/,;W%[J=m}A@ @ @ @ @  X+Ia+*⢒!JTRh\"K-cN"-aCȷRx#~?{UſWrj|B!'er5I ( 5<< )yY!(m]#/)TE%FRϦxϔeCZ@]ϒ'U|ֳ]'.]ō2ؗL-A%$Ql+ _G"RxP.Wd14aD Bc2 _4]A%/Jἱ/GP'EzBzxonSgXP @ @ @ @  ʉo7Sc5)B8WОH/q{'!4,|ʚ`P}>1Օz ق}r߻@ @ @ @ E ⿽C:jK%ܯЊի;W@Y?.CL HO'O;y&|L=_\2P#ԦGC-M9@ @ @ @ @ 0 I"+eɋ剹RF<⯊:%zdu42 ŋUŋYdy~Q_?uJ%{ؕ/RūÃh?yɋ52 O`TeUG*舀~. ~Q{aKrA%@ @ @ @ @ }wWJ,/k>M  {&8@ g%B@y|Hy4;)s*? vG&(@ @ @ @ Έbw&W-?{1IAL`8ϫ1ԑ,yM>HY[B}U?羝t_}?oG_y zlJW> !ۇ@ @ @ @ DZX)S|6:H)yթ='#tզo2VOGRWBvHIr C*Ӽ-*cC.y|K6\@ g<쪪B-ދ PTOQ}{Sذ"bX$@BHBBz2~̾wIHBfZz{'@ @ @ D٩nN͝13iW= |PW`*Jp*!ZtЧ/V|GCv->~_ݴ⤘C|@ @ @ @ @D Ypa!ߪ WiL{" !Q _%V,*H.|FqQ,'.*@Ɓ/ϵ>@ @ @ @ @ *5vսUo]$k[~6򨞂 :sJF\*q@ @ @ @ TjttJ Q촒yҧN=_ũ*]^cډG"Tǥ=$_B與cW+{*'j?c+9"rC'mEk s>IhB9| }%)]%NK[}y\_|NǮ^|Vc#wqϱhN?lyW -O3XgGq]ͽ=ɒ-F]'.>Yp]Q{.3[NN_ncklO ] @ @ @ @ TCb9cdDS-OMZ):;;#U|U#C_y<|H_kݗ7K_-$ 5V_~A&WdXA HF\:߀ aIVߧ $A}-.{ojwjj-?*VΩC:(;#D+xO~Q$ >\@JܻKS㣋s(> ; %J?y/46,8֛yz*ɇrZؐ ݲ=[O1h@ @ @ @ 6(Qo覨YvZj>)DXڅU+ϤC jPb G?-@(zݒA(Fх8'_! \i37ZlOV9JP_E_wttkT=jN V@ͿJKן0(+D#sA']J)  Tp*b\*mB ~ #E"(@ 6 Fmo#FÇշGH+mdӶxѢϘD @ :!@J&y=T|RJ}P!ޑP)-|lEA Ѫ*zw90VޭX(>r(D2ŧ65?ǎ<yLce['>|G_z`]-#j[݁ef(;zYr=Ņ%oՂF=vjI+'8,re +1S\X!Yb4 Y@ @ 6|Cضyg=n 2Z f~\ެt7ۋ?{5m劕h{{ͦl^<8@ @`3AB,hLS1at9 tej#?Ts9}]QYeXy|}Ň䂽|H?.|H~kՁ2λUSCcd#=b4$)Ir@ЇG+[H )ItE#>>e*-wtF2&{ X}٢OK?OlE_-|C6>cͧԇ`!c/?SC9g%_Yg{uX2i-^na]3/ Q/V755Aůo Y03b)@ @Bcw>2y߽vgylֶzGk1ޥsKjց6l?am3u!vؑGݷbOm?)@ @ 9"PDR5UnFcՕA]x҃Z"誀N=Ňy!~.f]*<ﴪ+>9?6>d_w#p :92ZMڻ DZdELr Q>$;l[xcǁzbS_9.<$Ns#>|H<#9jDV:+"VNڵ*}TAL[$UuiH{۹|/.LJQZCG{_-~dWIgOWR[Fwդ]zEt@ @"b?8{{k˖-ߩ e=^jf=p rm7c׿jαvn@ @ 6?}%J'D  !1D9DU|Zi1ب갊OY嬺+>> S_rgsWS|T>DӪ'"%+)Z}HZᑟ_F{B6<9I'E.M}%ǞCs!O3#>tlJ=E}^6E_IqںV<" 祢1JE:/l:) ?[~*9zi1slŢG2lbBZOX m#G @ Yjo:]|OhO/ZTk[E-[VCUj|HGGZ}XUuAzEHsr C8 @ @ P6 .nJU'FFJփ)$;9߻e Ձc_PLJbr@O_<գ6`a!gk_~#{ŧg1*Ʋ[ * A8bc\cp d{,926>>7tpxA+g>>VVY8+,4&z󯍏>+~PC|ʀSt9\[ 1C~R8"Cz+zA#vh~ V{ߖ UO hXH8, F#C v~IβŋU@`:! @ AN{f?iW%>``6v ෣c;li«GA=tk|e ۚU+K[rY?~R@ @  @S-W:FZxmm~K,fe[/>1уqvzJ15毚s̗\_t[΃~]RRuΡ< Rrs6C{7dS_2Z`MFQ>+.\xb֋/[a#]e: iyb#iuu}eg?i]K}^n{i[m+F.yx@f0qpUf-8M@쮅/&O.XdIK^=w\5k}v뭷VL6s5jT,0tChc@ 5lOmUz<0j¤t[Q]f~o2jZw&v4s959i"緝i6}E@ @ #'w:m^OI2J-CC⩥Ɗ.PL+⣇t5.|9Ӣ]mQ|&e|S|t?Ǧ')T/G.%! L$9ƴBb ->A/d>5x<"]߇i$_y|Xþ6>cDu*#V'/ ~ G[ Oە<]eBj] Xtp%Ey`M=4wiۏXb߾ ˕XPp`H>}[MWmfܸq#F}uX4yx]qv"nfoy[tD}&L/\mrJ)@ ذ m|~ڜGgYx>ntkJ l`m튆VTc54{ɝ>DE7䠜}'grssY %A@ @ ,@IDATU@QjUϭc,zv7cb+>r +[씓|;[|ƇiT|6>:ۻ?-$@6AލdMPaHNb$c}n<-<|B1tO5챑Ot)WFLk㳏""!QStTnsD:OzSKn;hB)q8xhy)ox>Lr;m ]tGؑG製1cV[m-1vmSn<@ y"0zh9jzxfچHEm@>fCll#JC ~y]*\ȉDlD|#}.A@ @  X&~@\oԿ9 z>@ Ӈߐ07'{j,+dJE}OZG_dG_:.}xB\`|'>hs?{xϸJej#BL@R0% dJLv|#|*-LJ'Ѓl8S/>З<)rT ;P/~e|+wZ򣭍$d6O,c߻%>^|xאH'#%HCy9D0 h51| $E1ÑTiEXg~m|_kl/>v"dǗ<>:j;F^| pE4 ߹f{ɶݷd~mKm3X|E[7MBJa^́?P\q$SOd>{zەW^lwW_]>8qb/Wv}5/K.$?Ct?yg]mST"?򑏔N83i$>Wģs@`D!ub,-Jߗ1KxҮM z/d8+t\n^ƞ;Ɩ\ښ 3FzN/k@ @ ܎:~+lUTDҕʂ:E0ת/WxIK:[+)'G|>b(g%bPk>'\qj㣫- DˡXC.S%+rd!d"EƄD!W|"_'<1OE>+C/'lyN@s7GK|~;F?]ԧl5)䗊KitmhgyMec1[#G7,Ҹ/5ѽ>KW l?9Z/yzOC\) EtJt6+{  "O#x_e_mѢEI{OǏoS do~m݊Ė_ro;M<}uqǥf̘Q߂yz1n/gնGlޝW,p#< 0O0%nozlSG7on Ț_3 @ @ ~YURRDKZԍԧdCM76cieZxүm]%(&rA4ǧx7}h}cOH[m|nT/5Q9( ,>}}hÇɇˎ?tr;ćԒ˿6xg,?ң^_I?eRa3G{>+;UN-ZFN래c/vޱ{ٞ[&#mq?9/oYc?|q韷~~6_=j5(0&(SE^mZZO_8[Q|oO)ș?̙cW\q=裶[e?r!N9唴qe5\ct})O~wFh!}-͛gW]uUvhܸq^O=P_pa2 ԖEO~i 6,|&Lseqcɓ''l^җZ,04B.@ П໕=i-jb-{ܷ|ZQ;-6w_w{ڲkMѐXCϦے_-!?jÎfˮE p7e/|ڿ&]`7Jp@ @ Ѕʭja*ԤM!uSx*Rϸ{ءG/'M>˗rQS|?)rQ x}N1\h>rV?O C衣j%CO/} ZbvC@!揯TYهG@/ Ω3^ZHQe[|Y+ϼվ6M ; FL[4G)#._Ϙexu h#& }_]\A#rqoE1PaR-3<+V\P*n\7S1j{"=dfmVGֈł {x9.( u駧:0M7ݔ v}T?Cy-[MAA %7 ؼ;Nzr!/o{,`e@A_ΏP5_ۚ 2'ވ`B Swg쩻6t];7VI @ @V}J`PRz*Osֻ £8tV_ֻ'aGOБ_:J"2R|Ũ|Y_G"2V|xPWpOD8QBL LJ΄de_ U|Zi˖Cs&Ă0<+"lIŢx6=A/eljn-&[mċV/n\<_mŅoo~u}L ZM!# oCu=΃k~ږz {%vj<<pa!~ikooظqcmԨ|[[ō{ @ @`3@˜nG8릛Ȩ@pXlTO%bZ\B?|˘HMc> '3chŎvmA|r-^ԋ\qKND !YP@|JRj08 ٟ̓>{=wwktm-\f󖬴woe@k/ES_> "(pk?~ ?رc ٖ-zh|+݊2 =P럧 Dx=3gU]tiz'(>୏m@0yE=)iӦ%ޏQO/x@ Gx[[7 b~ޛl?[3OXCG߲~g>emăb4鮴;R6O'?rba0p埯JOJdo&ELmbOu]]J F @ @pE7{7_XVJYFO7gSj?i|X7ңI6>~)>/F5ԋ!_-]TGwQI<>s+0h9/ h4&H8A$!IJIÃ9G 9}@/G:uW_cS"ţՁ>4c_򭓟;>@P@o;m!׭]ɋBr?0X<^|vw0j3fηs-jk-c&Z@Kxy 2(W]urYƿ,Yb^xaVa=yb'k_Z;J폈-8z"mwԓN.z?^k;< [mUښi}lk͘_t֣gE/gY:@aM`oMj޾ti<=*o'Y,iD9؏m2ѷE*0{o⦠⡹ӚFF7 yl Zbe-/_aI[)}N7ΰ/tJ7`@ @ ?UVӫXT*ˌӲ N_U[uVYU]u`u)=?>,yO+ <Ӣth+>}WNy|xP.ޭ+rF-0`\-}T:5\$GtCrG|H5WѸ}a`h_!=T3BQ/2uVֹz,X`ܯaw]M+O8zïZjr ? 7=v/7V.yիY|&ޙ0k֬ja/# 4hPzwy˦Ea}l+@ GnJ/c;"Dym<)c)&7w.?H[5zs}sJ`8ӿj"|lw0KhNϚ=1 6>{ҤO̳[oh6n?`k֬[\'{qu:zC=ӷ/^lwu{}e.zl?r]OZON~;ݶؼkf>|vӦСCyOma>XtϾ{%~{6iDs=lʔ=V >,@ @CO?e^?fR)EN}Q᳠QCP>?~4U~K|Gm|(>(綒J^c&_EQk?ݝ?K&O"ZQ8PleD PZd8r#wSL:8 lkVuQG*d!dy|@GN_q+}QPEr;_'gj_C]Uvjg1Ͷ_:x{F;gTP|IF_SF789!L2Mg`yf5jTtg[m$O<ٳ&@ +io٫Ec7 ;MVt !Aƍݭuw0tmd~젻L)Bd?HDJ'Cɿ/%ͯACi}Q^͂ϥ8|x1}?[߳䃾U~ι٧>nq~ˎ~k 9>~.vtK}R<"i_]uU@ @ F)꤂Uz(cd9|li2~Ax@lU|#[CJ&#/qVͣ^||N1Ч"am|=ltrd=]\p[ƴrtH 9ڜO_/0cɿZgm; pV(xѳ_J VZA@ s= #ZӜmv#KZ5\# p?)=?6{Ûɜ~ݫ:ljU-.4R>խ؎^;{߰|׿47@= >+_ux=q^J;cھ xw-.B@ }*NQ-V;CAç&ㆤUF LHN裯 NroE~7tD@ $b vwY|ly"ݿ;7xo 'vH>K^Ń~@HÏ?vMY!?g.C6{bt@fL]xi>^X'Od<3^oo{1?lzmW ojG z~ӶHg8ev;v Ogx: io|mԨƖT?Ua  @  }d hXVM>}n&=ByY5o48ŨShK_G|_cbD.+8ƚ }pODС!"j -yI\b\@8?ӣ ⣓c.!}n||r2tyĕS>ʳ) yb3~vV]b}(-hlC~]w080c*{V~xiUd%e^VEy4#FZD /87߬aÖ?`yq4~ओN:!:蠆sM>8kCw}@ ' 7Pk?۝wi_͙3'-(`s@ =P[I=X.oboc0D,FyA@_{G2Hqj;9OP?AEzy|Ĕ}56d`MjypvՃ_>o"G5gnUl@'|׿R>B?>sb럳:?2e>xW^isε?>VƔ?ȿCD{/؁O?ݎ8[*Qx gqF .1ZZ[|O./zыl뭷NOu S"]:@ ЯSf`lj'VUe>ˏ| wUN˓|nlp?v6Cr0 Ӷ.`P&;xq=һF?D5]O=Jۻ~/^'jiҥ%둇Ʊ̯B!lM"ưlˉŅ(r!i'γI&&x'&rbbe9u=] @qE)N @ E(tnIj::TcM3Q 贒yҧЃ=M:6٤`>']&]k6>=D\*K<ID+oťUN(Gv{$d^|NVWJvU&COd/0t8- "?o~K15gzxd\UD#XU Ede >[#u$k8jMexo%wϵ;zڴ-ۀ&ˇ^fM]-WWOrژG>rG#댃>~w5425);6X odVHg}{iq]/ƎE;. tXlቅZ8c/ X)z@# ?:vMjKWv~3fΫ^F;4{]ikY+?k)'j51޷j[ouO׎{[/Rk #GO};O5Y< rmwHG|a+S$G@ }T KղN-TQTB)7A9 ї?U^}k+>}?}H<|@CW9UX\[Cø!qz" @D4i$02E  ".l[ c_ǁ\`ɟ}`O 1|K( =c!pVO1Aɼ>9mIE*YoU:.O3.yVNQyʽ+|o/@#u/J _R_BDI\“ y>R{+7 k$moYx&>{<\&~@JޯsDZ\ϋɾDHn|ʿ{7mԱBԼ7V mb$^{χe/0C>)rTَ/<Ė-[fw382=%;ԃm.m%@ @ ؔ{c>Tq-3FFMUЧKБo*n#C_y7z@//W|%>}쥫(>->S|1+>:y|J_iS\ڮdx#42.(x#E_#|ʺbEۧh': / M~:yt۴2-. ^/ӂM-3կVk W/ 2\uT& \"3b@;Ӻg/[{W[ĐG;fu>s$@ l ~iiF,2|['/|ܖUK:"lO<[Vc+Fm=ArokF:JK^OT{g=w+V\e|6ß'Փ?OúiC3+On$?p}S ^ @ 9RWR7*{7DSE.jd#Tn⣏-lWOi# f_sU||'҇׋O-Ɨ>|ViCKLP|]ԝF8x8R b8zJ@! h bd&F?l<@y L;/"A)spC TBEVGu\x4x_پՂ/P/X?ky5w Z\Uک.)v+|~t@ @#@)^[ 0mүj)D, ;yi]TVI($[r#_e,M]CnPG/UJ\HW`_&_)?:韄ߖy@ %~9xvUU(p}nQ| B U?H lدH푘q"Gz;Yxy?;3˗B:?d`{Ӿ;GVoVQKk}'<r o<:g1s'}= f|6|RαcOɻ[S @ 5]n(:E83MT:/|R8 88ɮY/%6>rdZPjۻ%Pv q2{7/?bș?⋧y\%O#m8Ps4S:)BS"GՍ ˠA O-x;gH#'! \,,Pl`Q.>6^@ @`3F@_]S\[}׳>Awl5繆&#_?[$sH- ȵ {ؿoV,_ncƎR_RmX#_u1oJNyb[ٳMN]wžS>8lǝvXd o'=xgɒv/~פI?ptwvcR_js2ŧ65?ǎ<yLcǗ.XW '8+f_ܯDloU@/)gX;z_t=4o錛mQ2z5~I wvs7U^/s"6&[B3@ |he+{‹l9{q}ɮU{3}_|^ɍG},-4k~${Gt/9՟џU*1@ >@qEaS׿"dWm^WEHޡ1Qd4~tE Crs?>,Wk࣋lnIN<|Ջi $inćGrĞ1GӢ/nP_D@mĮ=DBxP()L8Z/"CyW.]tu[Zː~+Gڏ8ʼnyT%갖O"EB|@ lw`I[i_svڟZGNvl@).{ I]~7g?t`}Ɇ@kx|xmuz] loE crAozш}M ǽ^U)?1ѷ*jiiNZ`aR/|gHntRgڏ~d}1b|z'jq  9-_ݻ@ @yˍ3EJq*@@ OMBO7ܬO>-4U{|)bviOYɗR6>r$g~y|ɝ]^7~OS)9OwSo H6O$$t"K'E |@#><>ȓ>ruv(1zυE ߼.g:ácT|Z#='|#>6>zL>-rZH?Ws#>lE%!Cb ɖ>ҡ/y|Y8--|{7iEXv\4F.[eiqe.I Ÿ*@W\N8[|On?i-u,\侺".1:ň^ @ #l^g4Go Bmٽdil)栿6M5@ @` @CcjyMuҪn1򼎊/b`> RtkE⋧GOxŃɶ6r!JT;/_ϢX̆JI9D0д!CCZ::A ZϼK^/>~Uy|Q@Y<[+6"NY. sPF~uVu˘' x2elJu?hO^ڞnm&Y@Bute"zag A @g^ݸ[ܚ̈!'ӯ7gFmm\@ @ 6;PQ>WUުZ+R=9>8TWn"P97UFᣇ>X1i9 ŧ/q+>mt?ĭǗ_^i!b,[J # -+]x%x dɑA >G|CX9Y/>ݵ_a1՛m|]$ l]4xB2Z[>>]^JZ#cڭ@IDAT?% q0.v~o&qsЭ펧bF`\NnۂE [u{;h6d,k]wt@ 9"wdsRk k=>En6u-=|9aL< E/x 3Cy9}T7G?Ui9d/T/>!t!ŠEET-dj.Jv1Ѕy"_}Z77ݤ/t#D8aȥt26 ZM }1:D'lq@LZ󁧗[S|ZH}m|ݻUXIG8*6 1 `a96ߟ2.tӘfz?>.^ S_yB+dE/T|ʢDsapH~kb+ElĒ{G*(@ๅ@z k.5t5o5_Ϻ[GLVYǒ}buwH-[;{}6P_zMzaﰡ[<{RmnKW }E^@ @ U@7E1OMUUtUϭc,YխVb+J>/g(%cIϸ6>oيGK?l<}Ht`|2?Ň16U7&7ɼl zICnuIVJF`i,<!A颃>IH<&=bHW~q(䴍c+yXLs?T]P޲-z"bg #79w߸P<0`Mz +v] t\iO}SzMdB\-/ٰ%?ˊU 2=y,`Q}`[SQ5@ ł߈uEfK はg> κ]o+`|jۭmQ…ɈE '{x\zam4RHNy.<~xlaFimr @ @  @YTRJO=|kiM,c|p?*Ó|Wn/;'_!G{ţKn r8֋O |~CW ?)>nUq#BL@R0% dJLv|ӂbb/_y|x=-? };;+GSoŷr%?hNwKyPF %c'>c߻%>zQ/><|B_j>I'#%HCy9D0 h51|b/tCyLT?NOeǧ_Shnc'Oy|[_9b'_6ȃ8*%mmݧkd+GemC㭣uu^CfYˠAeS˳~ y3f=scA@ @ 7¦ҕ-*SSEzM^{O:lWI9㣇g<*?mM`jHB BB !!=!!B66ݖ~}Z=I'Y%{&yٙ~𽛽qO[,v(H^8)~>O׏/:ұ~lî@% "ID4IEW\Ebi0/z;R.Z: Wm }v|R|)~1G9 G_vsu')@~͆mŻ_krkq9WXr\<3 8΀3 ?+V3=:``˒-"ب~[G5m$G'|0WYუ8{de>cr+glӼgB2͉@m#&ed΀3 8΀3 8"j@V=U&Ɇ:*uSJC73*1~ĥ%⤵֠W9C} _:b<*.}~vؤ>zťB+|+)*f4P0 Qщ`/%iqbuCEb`G,3!cţ^F'Ll'O37s ե1/*SgCA j;zeC =3 8΀3 #[lfGy=sr$Ssj0amvl.Wpgpg2@a4Iirp*3\Նn'SE(W ԟeOlf§/`(>eS ?D #~V3xcC\| 4_@(!i1锄L y%/"y"?עic# o-D|j_ Oj &X)~#~9\b ?]?}֥1~YKdUm./ͣ9ugp@6w\l8em3g9slEl2 # ?{ElʪJnطO0p 2ztn3f̴{fL *xpgpgp:8UԦꢪ֊ꧡm-jKzՒѩX"|lS;/WQ,i _Cjɑ>zXp> #)Ÿ^|"[J\ cx۠6>W.?Zl-;6ȍ>C[ _qK1RlW.3/]|y3 8΀3 t"8xl{p#6t`p4h 5ʺuyNa ')ІK7jV%K{LgM:\c;΀3 8΀3йNJXOɒkZ+U͕a+93,_5]3i*);l>m)|ybo*Z/2P%W^cS]i D 8aE"')%DyD'|d#lY|/["bS 9DQ!qEKlpK!Gҋkl~76p  h A1:DbS±M󠏏0C7M與E||K{\ez}j_oϼѹ8΀3 8΀3 8f+jB8̖ڿ=*dc9΀3 8΀3eF''O$ P-X_ثԎjۨ+-ګҸʯ|1\)|ՈSԷ%|19$'|U:R G@NZ 1I"jα`h![qB "}o?}#aZv`pgpgpgpgpN6wwD~63n$[jg%+3Ȗ>I3FOOqTY,l" #bʇ>+|?c'|)<&|g1>ĔosZC_pgpgpgpgp.|Z)Sx>vES#-s jUӕNzZV!||8]ji'0CלQ<3Z||S yCW(Ά,I-H|.Z8Ģ11DSl R pgpgpgpgpWM[usjiZ 8(¾Џ)UBO_6#.ɫ >qXA8+t㎓0| 8΀3 8΀3 8΀3 8΀3 8@Wd뿅W-s-]{ԿU'oPxoN(#"K2$T1FXNTsb~mKvqgpgpgpgpg1pVoC[MRn]E[%Yt2(U"hb#(H7e٦}9ߘ8sqgpgpgpgpg0P(ǿzPUO#`QSF60VpOAg.X )hY阸ڈÞV)c6e8΀3 8΀3 8΀3 8΀3 8΀3ux[/^-ԹWU[uuZD_ ,ߜ fs%g\(s%q0'=s^k-'&v9c.<Ra*I|`Oh.t 2 ԕqw_gpgS00dDN' 8΀3 8΀3 8@y ĺ&'?z(}j>bb>t6#SE^VaoyЩ&v)b^xYrCl]Zb Xʵ9|=vKcهnqQBg dXEdff^~=t?!9=e.%# cNgx:dL(qqgpgpgpgpg+1^]5oxpmHFQ?E- Ez)6Tf-F>qN>FI—x?%Aqኂ_ugpgpgpgpgp^]om*=iU/V}6-7POJ/:Щد?$luzbɖbbb\gpgpgpgpgp$^-Ww7jz]+¨9Q_Ul٫E9 l,)Y6[Z6(Ԣڊ(pqgpgpgpgpgK2߆j;M>ΛGu)~7 Nbi_X! QkNqkOlڬf1E8΀3 8΀3 8΀3 8΀3 8΀3B}|uԿyjQ*ʗΊzAGACZt!i9^3<)6k~\qgpgpgpgpgk1@8ߥW_LE A<ցs,`̡wDZ[ $4Z!TNN74&|qqgpgpgpgpg1z GMădUwWkڒ|=΀3 8΀3 8΀3Дj?@i UoZ-%6p .W||4}s`)bh=y|iEd/|i𰧏(b_ؕ-e$1F4O,C/?N>W|}P7LG(~Ik>|8΀3 8΀3 8΀3 8΀3 8΀3BM\ur8QD.%+HZ@`J1_EbGA@_3{ŠEG˼vXrL񹚀8i])nPG_>yZ>)>Xy*̺tIR\Rf3vgpgpgpgX vJmIcTT'fzjJusc>꺡E3O*)>zT_car §/OO[֏\r^X)F§E[IL+CDR"J4b@8xآc^sy<>1COl|j@x!+g~薅B\q1[{+Kb Yo o7jЮpgpgpgpg`-`+5BW[Ri9VVڪ-Xg\ LӶM<ǏQ+;ƬIѧW9LJj(|CAFE*"SQE0#i"yDɡOupGIjbc'Nxy|-!Xy+R|l4_ _/cQWE(lZW^`EȮTMض(8xLn(:Gؠ!lIN_9s^ɏ_o/=V]YxDgpgpgpgpꪪKSu]QSUmUX1VWSMHc0(1g$EX_⧜;O_ykbIGFynb0Ȇ&xz@bJB "=&~$9 WsuIE0>[b )> :n+GƊI SC$Lz+'~|4&1.6rzHt%ƕ ns&Ľϵ&|\?vMy^΀3 8΀3 8΀3 8΀3 t&M?VKZ*uaUyDm(|S]8Dly|R,t` Vݸ\|bd}G0FR|B7 Z>1*:Ya O 8ԃ* 6#6qO|^еtfM~m.t "+:lBWc| 8΀3 8΀3 8΀3 8΀3 @Vo{G׿UO/U/V/bܜRB9$*3^b w6iL#)~_;8gWgmÇ#@xr5PW3pBW:zo|V=pd ]gpgpgpgp3p}sP{OUggy R495WLj4sCM"eS[eX סM%ї 1-]R1|Xqk ~XKge[ mهl ]YZƍPX3{^΀3 8΀3 8΀3 8΀3 8]7|ɷ Hڲ > 1X$+B7BU(eMlt 2&6OnfYYK'd@Eͷs1&셑[ZAָ  ?Cg 8΀3 8΀3 8΀3 8΀3eꟴ?9}ʌ15W@R_ڱnlf§/(ƊS ˦~0(Ucϯ_*+W"_GvM J@Z -zD:%H^sɋa^Oh"F؈(cÛGO f`:◃DS飧.n!ȝ'fL#\3׷b"gpgpgpgp:%8UԦФꢪ֊ꧡm-jKzՒс`?|:>q lCZ—};Z*|r/~"{+\AOip-sJgƖ+(ħDAr!gbmqOnOb%ǎ\ubr|* ZNo@ ޭޟdu<\gpgpgpgp61@zkZ+U͕a+93,_5]3VQKAĔ}Dy|‏x˶)>kI+sL;ZxK"Z:h!6qH>*ӢCD9gQHOOإF&+^_'Dx:G>X31Pg}Gƌn6 mmx3WoU>@W-9΀3 8@2GoVF gVr΀3 8΀3  PT[O(>NbSse-}Z_U[ l INc^,|l>bS 9DQ!qEKlpK!Gҋkl~76p  h A1:DbS±M󠏏0C7M與E||K{\ez}j_oϼѹt&ߙ=h[$ev6[VY- 7jSzGgypgpTVUk;nnÖu6|ֳِ[L3 7 |X:+׷~CQ-7]Y͙gϜc<_oؒ N]gpgpQI}:Ie)>k6.A14k)_C4a \h5qXc*|Z} ^L%XWBq2("V`|H^tsH3'tZ#)>}])|-gʕ>`ȥS1_W ê#z+l2-WҫzzPGtgpva.Wk_8>OUm8d㠥lkà&. %KEu61Y^}ZbF֯hkαgɾvl;oB<3 8΀3 8d BRb]CSÆa>-1gimV5eUcO,0(VGG՜ VbhsDH/bOSV}9`a^ӂ0_Ra@'1}-H̑cD%~#"(>~(|Q&ny\<ڄ>3Fgf 6k)~!>*@>+0r*֧~ +}+!Rz7m 8΀3 868>oqlEX(zj@ҧlhO[7H6ȶKv󿏴 ;;|D3 8΀3 8]bOjĨRxzj}jcDmZTϫ9__WVjT'0CלQ<3Z||S yCW5',@ $yaN]ZL'Y͋d}"\^b0Ǘe+|CbH'b(_֕g &\)6LtVx&CT cLZk{91uβua7p?mշ͚9+kKOvgZUU=ܪ0rm.Qn}c6uTی Ӎg PU4 k 1R-]!QQ>|e6aM(\|~kyl!|qFbZ2P"Zt"18!At$([%.F>!+cbH'=:~VMtVRݖΎ>\\fdm˯xfm^_z}o:5: )˧}Ww3{GHMM7gWqugoA6;AF+)}]v)bpVJ\Kec~ JD7wgs2PW-_7Ȓ8]V!B[8#xIsb9֮ !O}%(f:gpgp.΀N8LO$[#eeRso9ah/W2$D_ٳ^ɣg;D8GG_>q[r]:+ž<޶m~}Ỿ_z7Z*s.g<|p0 6s2dH4h=b7ͷq3k֜ ~޼y"~;΀3 t+pB9"lNu0D_;6lT4}D:vѹyugpgXS [xIUOg{{׿).m f\z--:,ű d l>cq/yMwztQ+m1|'8Zb (^)|`>G/6sa Z)|7t0(oR VA;-ܚ/pnMn5JbAYW+lq۝\kSJ/pg39Ð'h5= 񄳾mQe%N u9VtZhgsty}:&IctKY_>hSYNuڬ̽ugpg pV I("vZͅnV;Oba*'`aNm1M` .}|Ïq<3{-\Zq$}qX_>Gz+lBuKI}L$[%NK!"R{%L-G[xOU]^R'FbW w RN߷O?r~mݺLzu23oF)TVVھMj̃FhxW%ק'?c.΀3 p GZQjXRIEE8 ~]j`ueu cV WD]aCM8C.\ym:`3H;AIIX:΀3 8΀3 tRc͓9Ƈ8}D:b`8-տS>xy|ZZKq|jC4'@DD& ,C4\({ D0y|"x3W _: G=clѥc>`S ?L8)>~ 1>㣹%Kgd@_mJz^-4꿢=^avK66h-]^ {כ}0mFe9s>ؓg6n "of|(0x[oU=aj]z 0xŗ3媆Xy"=DnmÒ+mEܳϣdȐQ%ӘgϞ:^~{՜v[NM{M%܀C̙c/jx]^76ʑ!׭6-_~{Gmɒmu ַp'kl͖֣G?3:3Z}l3e7=g $-_mr>tg`zUږ;  | cS+)/ o{WY9Ժ`A[on!&RMhL]r޽{lFio)t9spgpg3+?YOz(sDSEOu [l#h :$H}9앗kd.W O9\.~'rm_bun!zD9+n4!愀@Az93M =EAlC3Ə1-"2dNؤi§U Ҳ/˜)>փlQ<&|b(Bps ȶ2^NWsZ Кd-9SmI7ˊ2h ϶#ד*k ϻ^yLG_͋6$<YBѤ |`_ !#96lp1Fڐ_;&ڟqc?CAʫ/a䰏?죇%i{s.gƆ_f\lϓ8lmbF+cwqwT#?}d(L6o|oC-~ncmyvhW\}9s^Oj6v1Gǿwg]X&x悊|R眂96q /f5CGȰpE36q8`Sa2~\^.qcy泓8SOln!l7Ȓ΀3 8΀3 8]xbG>9AY_U9l98aT+|O'ajšا[l 'X4?3,dW`G'|lKPLbR|٣Gh_ؠSG|QQТJEÏ>" cA`\Pe4tXz1^G<+|R|A(֯%!t2bܚc-M[Z5yk\~s"w~𭢛 Ee'}Fi+KF GP_|7å7KF \~ٮ_`/b>\ǿe )ݭGngme5>s2#cr-E= );\oҞۅh~I?΀3 8΀3 9 P磌kSAro 8axZV:U|;A[@إ(l+cZr]:+B\N_dYaS^jBM Ge^n:cO~{MQϟ톪_K^r7lup~RBԷy~NaPйD7iQ^{O*e{*³T[ÃnY19ɞPn؇'n_־r9xb9"bͣ][y¿W]K;ٮw쩧=q!Oٕ]m|4 ,Azm]z~UQAZa@Ogp%ǂ?uG}긨xvyr.w|ڦM)ogh<xcs.@!?#uoآE&Q_pakn<؏ߊnV{;Q \c*?} 1X ,6;!@!C5‰`pgpg3Bm|u׿yNE9_K8GX4 %&XxڀPjUtIR|0'R"֯E?`\̳6_Ӻj ĥ32К1nnunyw-iÎ _v=ԩo/q](- U 9m6FΚ5۾vƹ6}b?e6 p@me$|le¦l*T+I RAW5y|V+j[~rdy+)d6$ꯐ` ]ŐnXl<+nv %Fx>pgpg PTBMZERe aW[dQmPE[|qX:Z[56Ѝ"|C|ƥqPlٔg< \Bސ5/|Z_se[&d*v 3AI^#ؠ-:-ĒB7kX U V1 R ~9Z Orf>aNcO_1R| \:+|a.W^^6o fÝwu˭;e~yCWgd \"M}K*\l7~g~]wޛ]qmm6 Cz{$$l4ipmwf 7Bnmj/ʒ~3ffӄB)oN-j%We 2_x$tlZj}z*aX=n(1Ē%K)-yg| _ׅ$_9+Z) +BὲȭS}eU-ӫ 4Nzcmc+XU]nnle-.8&Ff:W4-s'H9;᣹Ѝb0ܖe_W3z'?N_˕۠<}cnnykSO66)>VG/ 9~:1A<*_wZ ' k"^9GKyhN>&BB A|3GEnVPgN r`C,@:HO/6zluǏX:b`W~)z(~_yhZS3ȯ9{FQܠr pn9wخ}{ߒĪ͉|? qRG?~ݯ_y H[Gs2*-V6]]l&;&@ j0`p:xԮ(a=3 8΀3 8ky'-c:}v*Q,{Zi/>3/& aOYTg^@Rb#|rJ5ZGxWN8SrĞVm*-m0&!6M$"B(^0 "=6Xg>gσ<3G )yg^1駇l: \ _Fr>qё'B 9h3Z-|)?AvU3m=Z8P4k4h`2lNm{l5nă%C Oqm6aS;&Xt2 *=u/l/M6F(<<n l(Zp$6 ;ԍO=F:=g0y?|[w&<7%AܥqhRlxy|Ϝ){Zi =9Dk/<#Xma>ŗʮQҷ@IHGhV$ȆX|Il9WMQ|JǘCzг6iV>a*#>B~q _6/WҢ}1D'<1.|^ û?;~Yŝ2R~8>yr)6!x^dv(坷ߵ_x gq#=$W)lV_zx@0rUّ,O=n6t\ocEϗhM'U<5iw{_oj5b Wx\zڊ}f]vEN71z̨XPW^~-?Ƿ=LkL ;ŹU:@p[Nw{QVtQv/)-O]so Lii1?t^)b/f'lc)/6LJƋ/*Bqذqq\fgc矕m.oƇf 13\YԷMe7^ 8΀3 8@3@1H$=:1s[UkV+9TW ( şyNKQ)>zT_car §/OO[֏_y{a+ZOȷ0*+]V 6WX@b,0k-BHiSQ,吟훚K}VSpŸF!!DwR nt6emO9at\?#7qlA1l0EfSL ο !~ӟ8| 8΀3 8jb+5̲>Ы^-UN0i9VOfwr`W=%|lS\偟0NR|ag1g1%|=zلnE`G$D,C8LŤb^5G .zVF/|酯[mK<-|*cWL0>~l #(TӦ9sWz˕,2~:%v B^ګa&6mW/_m /Utyw('­?cqgEI=h_zd~OAxҞ}=ǡ6TQF+;J] rek"_?=8=ͩf&QB9}>r_kⰱ|맇b׌c7c{zlGy>DG"a&ǹrTW{\sE [Dk[o@P\iMnu_]v)^$W*Mr$gB D`^?$>;6 /צ70-`F8JI(s^oTet*sZm S Agv;Vսpv|~x0b089ڎcde~sI{?q=ڏ<)Gd+<;hL_`~UImED~Z?T4~ksƎcGo>{ڛoNW_},&[ :jUùLeUm|䶈5܊w~;+­<1,+n!xb{;q>ϣI W8΀3 8@g`b'{ f9TT:8TPON&i99iW:`0?6Tsk ~e1Wcu*O~1:J+`+~jO9!(R 3/%El-81&AcLK,3Ɔؤ$*zM ? 3|b(<{Fa^mģiϷP0`ҙЫZfO?&ѷLf[o8fά _ko,鯱acP\޽+l@*6Fo&lƌny󖅥w ?삋x1j1XymV>Njʕ)lBm}}WpM G­̙%'^6va +)]R ok• ӺVQ(kZFmP e?lzm`S6C'5K޳ ^}7`[ GG q(G8$g>zcbbjxNiy.Lz&r:?l.ϋʉ6/>-Iʥ> v7 ˮ9 _bkۭwصvl3j"6dx<.΀3 8΀3iI4Su]CE+Qiѩc9N&USl:̧6ALA_ #"_b3gGhbIGFynSxaL? MDM&'1 0i!>zمn\:cE-:b"Ē1tHϷ1G1%\e/ ײ XrmVv5\Ѱ۹_fg?zk]4N:qrHm4UKFW̟]pF{Ѣ|PiYK4=N_ѩ,],;w}:|1X>#6$xXiKmoȀ`gݶ3\!l&;iT=j㴯oėw }npĂEW8dSY+hvjN}}Uȧϡ?k.=2ވ i@frL|L[8[Q0bGїC8 l_Lnm8p' 6zrrI&O2``68lR… s/2\r7ζCE7gW0^}YQCz4-:>uD9pk~s[unx>ŠApgpV+BH뿅٪_7^ )*$ݒP'S*e $H MclmiB6Đ_/Х`?̢=P ˏ|; Y?6̑z؄w Wo. Ww?rGXknMrl)d+fVG|1\.qfΎJ\,&B0Gַ_Pn[ogb~ WĿ"b^.ϼy?l'X4x`ö{H8Se&z!z J?-6S;F¯1FSO>gTǼDZIKZ,*(%\!{6j dap*:Ml{?JO̮cf͊=z>t|*O1 ؕ/1+ X_~3GA-",;:Ʃ__-1h8բO焏lBQq#ii@DhYHg攘3 N  _R|t";x[q {N:Z)bˉiɏ6DK\nUw %̳NZv2e=PyK6(sF(Ph_[SNuO{ma#M.׏7'OR?9ss@>e?aTC>\Z#'x;R>O|:{J١mn /3~p?U܆?WJB.x~Kn0qÏV _ E\у=cšu1vLGQ 1F$Ҝ(% :lXtZ1Ez2H1g^bj,^§'oy|qt(&_qS|l#~M1|0 .'g]j?vڡO9fC78/pZ@U*WO BCX[gj sOJA#sq,s@f} tρ0*)\mvgpV3g:VB'6I/[NMyЩO,WtIK:B7֧mSlTG>0CPEat3/<>l SvE *YV HaNḆ cD-}ŕ^( CrGC,5pՆ _8gK`'ŧ9y~O=/;Z𹺁~Ô˚3VWJO~U8@Wfg(Tx0ޙ 8]P -EKu2;m!/3oaM:JCq_i?-/[ z]S (]YYآif:j!\Px(s8l,ѧ yLag&Å_j EvJ.^)*}M}gqZ'1>hʔeyk+FJ2mlo} G+ L8(&hUÆ ~<,zԷlal4 Ͽm0lY;΀3 8@'aɵꟴ`Ƙ1':B7=|W6=Ǘ8i'`G'|0K>8ѿp2^Y|AϘ4/ƙTMIa EvѦG"F~X`,?bIOPQKW|+/y|ƊC+;ZDR|&৷M-񈏟ⅮKgd-Jc|w -/wC-m^߱8XYB=wPXh>3<0p:? \Qٺق%vߵצ7<^}\ xk)eZ )/^8ގb> U lh#SxRbUV6_ ٓm_#>h} (rBpD.-恝6$ӑ-E`n^B5yOuVO$ܒч|YD~k]-R<_93noniϧ=;|җOVI۷z'n0`mxFC* .΀3 8΀3yrV?|;lVs 9ťB+|+)*f4P0 Qщ SJ1#M:'C>b`G,E3!cţ^F'Ll'O3"{黬9 N}P0 };c<}=33-x3 a v޲:n.J|aMwp5pk!6 SU'~b\5qt4^ƓQNfƒںg9Ȇ@ +آnjU}FY꭬0;n(, CZOʰɑǁ07Rqˮ(mƍ( 5R|R8HY7o^9mxF;!'z]u5+վt}9\Aʼm6pF\/.n%UJZsmT:ϕ&spgp:bIi9gc djC7sUebG}/l+{ZgC7>|'PNI_6CKDȯ_*+W"_GvM J@Z -zD:%H^sɋa^Oh"F؈(cÛGO f`:◃pGCL맏6Ӻt>jy]x.txpl=jfEx\.=vr]|pg r7bx.fZշO9_udKZ9%Q׾},3c\[[aݾ0.%K]~yεndÓ๯3 8΀3skS4~T g:ľ&J}w*֍~7җ*n˥td΅ y!Cp8L_R&#օLYړN>!s!S4L8hƣ$o{KiF 4sY֧OpO~vv!Y޺p`2ydi me^{aGsd#9LW|9x0q,[gN>qgp O~:-K̹V/=W]~4H8# sF.(v\̕91 ̆N9ȇk.ue[ ?e{#>FIM=\e@Ot%ֵ+nME׿-n;ncu#>eZ{s*{3+l\'Hk'+W.kˣgpf@`k\utMo_ igῡD 3oO^P zt1g8`Gsp#՞KUokӭG&.Gyhh/\ߐi牶VcO?;l!' .kxvsδ ~?O~~v͖|=۔g ߌ8㏍x}ZsG3N3f^yǮb2 8R2tPҞ~ꙸA6ɷ+n /;Rwgp WU^Y>Ht3Ȉ)꣢?tNz!?yFU}- pȑ'Eʟ({OascN.hğ+˦B͉P~*" UP4?8)WJ>s|Ӹ9#p!j >b؄_%:g.+o5GʜKaRf T峬WQ^hxx)y~ᐡW [ޖ,+|YbvH~ gugp64Kg6otȐ{ڳ3iȯɛ?P|Åpj |8\ #(ߵoLBx?o٠A"q'*~kmC>9X.qquM| #&F~a\5 maw`=·8 8N =⨑!G4~㕚y¥?fϞܹsmȐ!F]nx|3 8΀3 -5#EXk)q!?*rc :EMY/>* Z-SǏ8΀3 87MkisV%wͶ ]yG#=&˲~Cvn=h_2|bn@7mfm3pJpm|W?˾X͐j£mWMZH5T㷚oМ.{,oٲe»L͗/[} ~ar^Bhޟw"o[Qvvů/4L8Ï8Ԫocf5g8֏?=3y?g.W&=^<'΀3 8΀3Pn rȭ%UCOauFYs!jj)/k.'Kp|neL1hd.Q>fV|bUS5؄]sV?5'lDJ(1lH MS]$ˮX |mXlyQ]ʁ=W1LG9ɑkN P+LR-Yc0#4~.΀3 8F@mE5 n~qWaL][Z_jd*Xٶ&pDk,U]K~3aW{f>*<w g. ./$&>|X%KŋK_NBCbI|S]yksdɄo+qWC㿋7|pCݺo\Y=Y-]~vU#U>wS3 oKúufs̵aMH8΀3 8@0'0 GY+fN~j{VtTԍ;s,Vk^90Gek.|ZڂO$/<8ڇvDÜKZ 0t)ԮbŮz.a-TN#OjO'->#ya.0COnî9~i{ÆOϰt]ʔ ^QBݻ/ W֊sm‡3 8΀32 Z4Ӭߨf7yǔwJ^|ϞގG#-]b{}yOGr:{jkk흷s 8΀3 8He]1TNUȬu1Ψ.}frpCh3W/VK_S}O5QLx`?3OS!FWN&v; ?LcmشgՂ"j EG@E3WcbMׅ^Ӝge'_G E5 _+QqG6ğw/C/ "S;"N<.èQִl]ƕ߼>zNs3z3 8΀3P~ p smSa yƵ6R%X]q gpgp:j6ʘEf=#waN|>Np5bS)>}^_:pR|g =vbuV?t*>L^+^|hmF R2%N¦qFb|GB#%)>DB/B#|Ɂ])|ȍ=Gdc#{'ѫ0qʃ I:A/,[hyNַve[g[ [h͞u{ؙ>mv_gpgs1PYQe]xjGiMߑXܺL\2w+65 \8n#۳v_^%U93'@G0*?#k|sD*'- ɵ HAIb$F|Xs$S1+gѬW޴Vt' /h-Q\ÇuQ(ETfyIɚᇩʈzL!C6婯۰Gf[o={mVZl|왷jФ{)몽8gpgcpP]ڕV߬?&{賷KBje|sU]~[uGK͇'=`'[:?0VU_li磏A}͛g+q+Wo㎳ϕzu/ɓmcM6L9c׸wn_[om~Sv$n;sB ؿ/,~+wMj\ 8΀3 8΀3PU zE?E/!0~@?.ҧ腯|Ҩ=gMne ()>8#F t_c/Xڧt[zF|%UIQ 2GGe *TؘgbEF0;O>遂Di뇘rIxړ^06y~\9'$}omi={nj]*nuKu5Vz AZ|-]-\|#+ qqgp> qIÆcn)Cf/Av? u6m[Z?/V[me[o Tm='ze;KaO>dN7̙33ߞ={U+]B̂8իW?|s 6]O>VW[&?ܓoR_t~W΀3 8΀3 8DaU=MwtcG#|?L9zxȧ _ko?K)>kb>(gf7#\%AѾ ޳r?60'/Vco"$nN(+FS)8: HlyPGuubG/_>W _[S+ߢ/f /͎ CGwgp@;<=へM( C3 LBEWs&496f2;jglҫWYimÅ[ݻۧ>}vۭvaĉo=Ə^|#|]v/lÇwax@xX7nFe'{}_^|!8T-^zF됯& pgpg`=0@S7y=~)kS==TG\#z|EkoMcrz0u)`S ;8jxCZ—]ڿl[&Dh`%dg>$" &T,b.||ɥ2cGA<ԧzTv*hEbto |\f5Gq|^|%-G ߈_ZvQP?dEGWͧ-G .΀3 8A Y2˨-Et]ݓ[u|#9 n3;k'~㚥}Afֵo/|#m 5wgpdGB+mղ54R)շ_Hȷ'lzo Θfs;lcaE8?Hǚ61ޱz̵{9ka;r4teEzC\׀Dɭ=a8Kk|'|_P56*m ji :\@'|a#>%]@wWۑd Bg4y΀3 8 ˳lGv_ޙy~d^3 8΀3 8dVq]S&}s7=ЦAN[4qC p  z|AauEbA1>rcgJu 3ŗ.~ %0mO]È7L]gpgp pKOc&F~6wާi@ ߀&J,n6?nPLpsl n ;M}jޅwYex{qqgpgؘ)IfEӍ$WUTkbQU}UnL }d+W0s;Ɂh+.zSS/{Pg{AWM _5O3GFôtDRrƴ ZS8BA.r QN{Xmv$Fٔ:cޘ|:|R:hUɋ:3 8΀3 8΀3PCvO n~tCSwߴ~m8W'_m'7H~,Untgpg33@VgW_}a|W?=ߴ/K*>\pK$OZKLy|XDӚYkwXf{e.<#)삶`O_IQR (-B NvrA~Xs"6WIcqZvb%> 8΀3 8΀3 d|Ee-[𸤛/)s/לk~ [>|s28΀3 8΀3 l p~/=^Dko~RaI3W?d>pC'E0iʋM#Ey|}3_~J 48΀3 8΀3 8M MpХKx\ҏSGff?~;pEwYU|,R8\G#5pgpޜWaէcp~@͉@i+=05>H~9 LM#^ ՆĆ^6ժz_C$|U#|X)OiBkj+<>`7W('zgpgpg$|2| 4_:Tգ}_lw0`= pڂYV̱jW mG^1jj#BFOgpgؘ_2g>kkJ/ I{諦cKay|0!gl Wu')OQ~uOsO>=ņz)d$G"D,vDšOup GEjLX'<>k.zV#_oK=!B=WN~X _yL.΀3 8΀3 8@ ompPY_iCC?s]{Cp͖͌#;8wcJsn]gpgp> MziW}Z97Eǜ#ל?R=d'||DjNHaĢCD&RkFrᏰ&$W 6.:"Ǐ5rh->xY0Z?Uf~ qC|\gpgp61|ݠުtkTbǴ 6|΀3 8΀3 7UE$+==UVѩc-~qiֺ` _٠94=kS|դY!?9\1Qapl ' `1Oc!Р  (LEDGRҤ#bdQNye ӨO:m𩕹 b%8!?~Fjd?O1\b-T_59aIn8΀3 8΀3 8cpЀ/(ĄwW<pgpg325Uz_#o2\$F+%b |\؉I#/[}K '>z5k?ĠWߘd +.bSN\Z_TT\QcRň,@!(_|̵ #Aǜ|uЀeg,O)>XMi ˥[qO6z6jCϚK.΀3 8΀3 8@0Mѳ8΀3 8΀3 @TSRvNկeD;}SB|QS򓃹 &]_adžx.1Ge_DXU;:.i\_-9<5Om'^>ahκܜ+(D1ڸr2G(:5ӜrnicÇgpgpgpgpgp:tQ?3ózRi"-}A`dTǜKHH9!A/_sbj]rjMu ||1O :FbȉcND9Y*paFb 8΀3 8΀3 8΀3 8΀3 8@agOsUWr_RL{EԠ+LoC7'Ǐ/yTW!&FuG:䐐'.)>l \S~E愤"9% RSk e\*&L㜯h3ʃXSr)D1wV|Ggpgpgpgpgp:]oz9aޚ?Eo0ψ0R8k3 DdEڀF|Fq6#&S=A(>e/|rq)tY+L1FrKx_0uqgpgpgpgpg0jXNG~\ʇ>*}SJô/%!RbU:޷#Ft%>Okj<|?+lF~G̉C/+)r.`P2$ԥ#9)m|W#9#1b _d$WF/b>]gpgpgpgpg({,n(ڜ-}wd\p @1~4I1C2`^+Lc!\9Q(A<6AgĮXFD76Y6 8΀3 8΀3 8΀3 8΀3 8@'cM?5w{ɏNմ׷P$6\gY3Bdg D~ǖS3?lpgpgpgpgpNˀu[Y+(&i^\ }H}WiSaq Sn̩C(GA0[/TQjj->Isogpgpgpgpgp:?"<e/s >?=W2gTUՠzeW/'dž_/>)?#ž/ ~aUG^a1_-]~^k|7%ibHQkecilD)>S?O>m xR>|4/6䣄_ر 8΀3 8΀3 8΀3 8΀3 8@aM]}wIRи @ss* 4%KI#LNn ?=E{`pgpgpgpgp NzwfuGX+G2 M8ḻ!eNѬE>GAbaO: _{>ESxM5>vr*Vi-=xą\:;S6qՆ 83 8΀3 8΀3 8΀3 85?;/3=R9=R=b5Q=]gTcUyYs)?\'Kvw+`6D#sT 5sRܘ&L9aS$RR@aCalڴf:.eW,6D,.W+|ØbɣH5r^g &\լ1\:#Bjav?mV_Q>pQcqT΀3 8΀3 8΀3 8΀3 89M+[~*}V\ȅr/.2SOa΁"j͟+̅a.|)m'DkCƀb@XsBa%Pkeyf:njWb=BN~GW1?$||R{/=1m'sizrSvK6|}ec҉(,0b05zs6bzc][7,~lf^ʖ.[b ,̵߲Y3gOp (΀3 8˖.>qgpgpz$l DhBiG*}dֺgTo>3!4iM)>}\#kDm0u <a8A*|퉑\4ֆM{V-p! _ Pa*ؔ B E*# եqSrQN|/[GJS8y|#OX!#)~fqD Tqm=wA݁PԸ]VW^=mMظql`O<=Kmf:(S:΀3 8΀3 8΀3 8΀3 |P{QSS|*B =U5E襪+MՈ LŧkfR|I2JS7_vbuVr!CNņ )fNDJGni="y؃8)^iSlHI "|ag3÷Q6<|s `0vO2˫rgpgpgpg0PhƖ( 2&ceD@g-L/swUe4ƕO#6D}\#ʥ?>:W_{.j/w'Vvb%ԆOKʧlL3eng*BJ44: ~Eڈ04\"LI?ʗ. :N=6|hL^vհ1-CK92PxByֽ{NR߬ڵk#I?r^3 8΀3 8΀3 8΀3 8פugWN_Q5vlQXTjIHSn `4i]GS.eˑ?"!(3x< ;cJ2;nv柽2y9΀3 8΀3 8΀3 8΀3 tF~:Tg3*ꦒ6Z i2@F饣ѯ )Q,z|/kT>rg.Ȁ~'OeHnDn{)ˑpgpgpgpgS0BSCΨtE?L4XHi.5(Q_*L>I4't:4<馉R"?!/Ϲ7XӾ5jť1b 8`{Pla6{rkrgpgpgpg< TgЯU5ի*"Kz`QV48)W>Ou)`jm֥ސeWڿl[&݉)NI@0Iل3GA'_t,:@ŅiO9& >$f  rS yk?EԌ=5X'9z#ŗ>ͥBoFw 7;kN1uqgpgpgpgp%?9$КDcr!+zGys%>39e$^0zŊM8NBr?=ص~)<ֈf}))PtBhJW96Ԧr0|LJ8bd |ilZkXm _)>9~<>k9|s)C OnȐ􀡪ʎ8[s֦Onw}ե t,QC  8΀3 8@3лg9vپ6Z.MTZzֵ"\Fz=a[YUo5*l:$9snM~ޖ,pgpg`c K(ijekψM/:Ws 9j'|HS1mo3 $5'FOuF-腏t@R$0شi,>@AsDqaǗjQ';q\*<#7vF}x?:d'N{="stx\ LAQޠr)?oaƕ  ƍWfӦMKM:ӯ_G3 8΀3 3ۍ3_U 9={pj6vW`/OpZ{yksgpg d_٣Es9U?Wg$n, §, ؅OD#_1|p R|ك: boO)|9B3jM$$31ךل~ʥj0Eȁr*6S|:Q8ؑ9dSd]9|:\ _Aj>yQ'B hIu ._%7>rH{7J#tfCᓻ =pgp:~>t8S8UZնpI܋W 8΀3 8΀3Pz?tU_}᠊=T|e?|ȍE#ქC )F쌈>㣽2T`O_IQR (-kDvrAzQtFi̕Cy`Q.%> R e{ʊ.AAEsÈEcE\Eϥuץ31gpg(WlZiuN}疃4z3 8΀3 8΀30͝ =qŇ7DX7-}ujOZf4GI#~?:Fv>6cC#vL1'KjL霒uU<>SqmO1)>Xu*X]ʊCϞ=?m{ٔgu A(!luV3|p**l'ؾm]v{n_l%24 8΀3 8 7^^meu]C;4M-nSsT~{45.}.ҦVj6Va7]NJ.΀3 8΀3 8n," }PoU^MqؙsQ>0v\ͭOZ&#"|ҁ\)~ن/G./_yx3"- kJdTM Q@b-0ٵV_tectċdٱ!y|rbCOnbɑ֣@x!Zfɇ~ Bkj+<>`7W('z2cGի}3eݺu߶vÅ]v)2z=3V>r  C O8n϶l2}t10bp;kook#/gpʏtE]po#&fK)8mvWZdgpg '~FkkJ/ I{諦#q 4C>>) NR 8.DX$G_==ņz)d$G"D,vDšOup GEjLX'<>k.zV#_oK=!B=WN~X _yL.@}Qm}u| =\H# f;nk\oGt?]k+W$(d"E?=4uS<?g˭znO=} 8΀3P. ~c^+{icYgpg`g@OzꙦ_i!BzbSsWUUIȍ3ҩߊ/" ͧneW{Anj_;|#>Oi"5'$EqQڄ#2)X3K2`M #9IIFaUܴ^/Hc >#\)0Zy|֪=LA}ģ~Bܐ2f=`;vlͅ5K/>ڄ1vp?d0bE}jSչ=lk-|ef8΀3 ۍب޹ki܎8΀3 8΀3 tR諪ϫ;FSBUtX˦|ĥW|XJs?+ +0[qa7rum67n :u<-=Gt HgpgX0z\}ݚAA^3 8΀3 8@+4NZFR Kn|#oc:Ir+V:K>b1G_0%ƭ'.bAN'|k$~A11V\0jড়̹0(QQc5 4: d8.P(p#<>q g.^ͳ)Sm:sxҖ?(JttX ,_t+~k*D/!rEr|:A9-?rN;(WX#~eGq'F{R _cw)C C$fmgϞn}Ȓ%Kl?z{.ֿ8u{VC oЯ__[tҫEAx{B|w_/}] _h;Ox>Ц1dK.6q㋹ߜ͛mf.o߾o3O?kL$;NocƎ >6cƛjh<`rR MgH{gOQ3mi8F-Fmn XmN{gl钥MQ"VgZ`q)x~f:O/bՊ99V?)9Q"HB@Rs. >lB:FmL._HZSLS$Zg)><>uNjXFb7'QN)C80U#qʍO1|`w)C[˗}?`Gw˙g̘a .QFņmKwx@sK1~aeP׆ߠ?ԓ¡V6z(3fM6=k}׆ =D6x [`S/g|XgdC823׿lG}˴7n%Eo6|p0χO_МϾBuv]e6"s-1bxӄ2W=Uv[ }Ri4‚wYM79oo kݻ5l2W߽F3gpgpgpghDS\Q3GćU,jSŎ?9^)'#5 ӬPrMI`2gj(Q‘CyR|A+0O %,?WTvT*9>IiM0Os7>n``r)Ggж d/c?a<4r޽}l}mwyȘ,M1Iա޽{^{sMlÆ ( ~;C0# ?߂_;G7vJVc‹zEͯ]>Ko1;(ϟpWhKåᐤ2|prSCXNtCaŗ\h]L808˧ے3lŸXx7zvGo t3 tzV-+g~cgpgpg(ƀq|dR^-k 8gDo}k򨧍x.|tϏㄉ]g~(|Cim"|ԖWu΄bxƔK\H`cze!b֊c,4NuQ-|>Ig}Ͻ9c)ӧ[^2`=>~~70x!ΈMt+ZM =riV+O8N?z[Y{ ·>sZf 𭈯qH\|3=K?|+mȘ×Ϟ.4ڛlwmVcāP(G|Xs[hڳWOYF}w/GSwe`{g9[ExdR)yvsoF=vK})~ӄ?SsX*΀3 8΀3 8΀3 8΀3 $U]V?U.ɇ>*}SJ4nH*5qe$֠!4/9B}_S|tA9j<|?|Rl6^y0 _zJ% JR !*9:ѱ!وX)WPg 91(F|y|l\V>ZintGsT<>O]Td- omӌmY`/3s35q1t&{7k{9,{wwfG-1В>&r=;<1>v'CJ}H0̚nG[+.˯%]!Pi;}|'[ :<_&a9VzEw]}͕quxTT*/1x.J#> @tu7!4nѣ{|Cks-6ݬp2IG+N#?ɐ_ &̙#-G΀3 8΀3 8΀3 8΀30@c4Irkz4iCCZXgĮe$b4ꔗ\~Z9|J(#|aCu?وU>tWXtHW$9! dJ^0͊gW^ʜͱXE E3bW,# ?n<5ױm_8w np>4iR=顇tKnjcq; Gܥr~Eg_=plc֧b^wMޚyt1>mFv 3OOx,?}<[3y{#?'y.D/~fq 7 N!.(7kO.V--;\`%/F X}8΀3 8΀3 8΀3 8@ggz[vt|鿫EW"o,1J*A=keb4Cv<6(''Gͬcė8>2'/᫖?.j)Q Ol.e@k0~VG-[3hbzVa0~{_nip:xFzGj<1Az*,IMx zئb+eju_Wj<2I[o7ڷR2 aCݫWGGK-7ɢw7AxСC⣟o<sYVDKq^,u΀3 8΀3 8΀3 8΀3ЩBXWMWz36{p_U_3JN1G'|Kk#UNW})2g/i]u555+@րKSl\AMPqʍ9u#bW }_i<~Hu['>+~龳|R> j\Cy/5 /nk~tĘ0|:`x@IDAT|xP*|￟~]ynj23y~dѣd0QF)~sLl һ#yvJoB>mgpgpgpg&ꧪO~+Uo5bϙX5I{Nʭ^pOL)|l B> ܪE'0+FD\3'N̛iIH@Bkd.0FDArpi?SM`N04MȇEb%|Ճ?bCpG֛cW<5:2d`hSuE!-NnE;NoÆ <++*l2cubEMݵŅɊb:r _Woρw,Kĝ&5m޺u/סXPn2uM3OgX Q6W*|ybYԜpEr8΀3 8΀3 8zfGzHz@@_aN QU5L͉|`D{]WT/!GZ0O-P4V6FS|Ә |!i>kgԕ?Z:`P ScCK*(U .BU Jga1Y)~HcN|S V$U+s.K1F=/AaI}hNV'm! /ANݺw {*<lsRp")ҿG 5J[6Leû2E2rldapC}-y_̙lٲeֳgO;nL|@>?yg,8`0q Df~wرuQ܂Ǣv})<0fۭm`x/֛oGCw3 8@uVppd8$[pQWՅ0wTYz[m΀3 8΀3 8΀3,sN#;ssÎ0GJs]=U@cz#NzF5V5%||):c6D#sCOㇴXՔd 6abל8DxUO9ْMAΜ69&馅C.ZCP0̶.G uSݚOn&{7[?_ϣ5 `>v 7f|:R Mj;3K3!_/_}쑚Ƒ|FbU;iOԅׄדN9vc!=h IU*gzil軙nj Lgp֒]ljl_jlVBRǼ<gpgpgp~ =w.֌B>NJAc_p5Ϛ#!aćMj.ዾ%|R|} y||R{/=1m^.r3GsUzrS4}Oϰt]ʔ־,yZ2eJJ^3'P[ ͚5+/'~WlSNo9%֓o"Hm Z#o<2O:ކ b/7aad|3mϽwW^j?.ga𘦫Ҟ~rr|6#$_{Z駞3|z̿vإ_l~ֺDq[m= U>08;eq͞=': P%^s}Twg M_[eu;q~$fZcS*U[ܶ8΀3 8΀3 8@0{xR}TNUȬuFa'/}fF.\bQ0>'5Q[c?3G)NP _{b$9,0شgՂR `-'@͉~D0Wħd0G80z]豓"!2R]ɁvD9Yli+bN]>>6w/<bI3#K3G -Yy%xÆ xBI=}76K#XZ h;S,#<R%W֬[s.6`@7kvmgJ}<*s3 8m`6~c>[IEp7^^_̋ @,.jƺU 8΀3 8΀3 ̀ڣ2HOUzF|zJ/|>pAFl\`*>_k3)tas؅L^+^|hQh3ʀM/qqbÇ|A.Dq1[K$"D1g͜ѕǏzD66qR  <ؐtD*hϲe lC;?k=&|Y?nܸ&)֦b9ۣ 7{v^SScƷv;K3+~kgf#my8l<6z̖@ㅞ X8?S?Ji[:1=JrЄwt5589λо/աfkWM#?yՅwMM g9=g#E/}!bWŸ̳WѦM.U6._^ժaD>?.΀3f"QߤA-*J^FTwTͅk^][۲rڪ*ӭZS?8΀3 8΀3 8@q|q;]걪 7 tF´ѝO ^1,14LOcxDK:XCn2&#Q蘋;+0bT)'EFMdWQ TQ6((ݴ: k^D66#/?'?|GXmF+^_Al+Zq%l apLJr-G %`IJ?ӱOӾΎ?vm?olW|k}eopҞ3rR}9l+6h ۷m&p8oVWNƗ\WÀVԤfϩ:eқ ?]xR;z``аLeԖ["^Z;g WJwMEV1#]˝vj~3Eb/^n--+!អѮvgpgD 𴃾ᝃoxSTͿyGoew>;nOfFeKfMQ5mJ\4?G.|K عbWs)C?:r`W*˃?pȡa=:bU9M+ƠZ`)Y͉QaI.0mDvmLE.(aA)؊K=5i/yXOl/v${ɮ޻{l&iJBIHD?~&~x`=#gMm` 圵IW93ggv9NUvIC㐏0U_y0!#;Zt=ϲo绳Ή?R9M{c6mۏ8CYT`ȶC 1F1>ll Sh^/~ˣc(I8c~opT,+7k}?t` ` ` ` 5Xc樉ҧ&C/ |F1r{W';lyj1ЫJ9앗i=v9b^xYCu5[?R+W챣Eġ+nr5& 6%Y(zڢׁ_nO<OlRr2|r?cţq22$d62GN3[sH7Ofla @00# dͅk}_bͅ:ẍ́M!@0 @0 @0p kQ=oxpmH!N=6&|*kS?m[cl6=vц$ر煎x?R\ Qp18܂=k֬173Sիca` 3EO e;)/K2ܩ޹@ ?@0 @0 a 3[V}Z:̫%8[ȃJ/@: ؓ("_l!v%%[Ɗ)g!1?S;w#̔uםv+$` 8 ?0d u!_ߴZYa׉|]n)z? 3@0 @0 D8'͹/bK\=؟}QF: r|r,AU}y?{g3!guLu \)'` ` W.zcj^Wޑ> chECsjs+ _r`<O%,a3 k:|l 9#֯9ZDjl*u 3AYGA'[tZ,:%AyO1">$)guU V1sS3'| ?$gs4&_}|eBf+GZؽk}#M yddľw=+&{{ߕ6ll{+{o@جG>ak` ``}ͨ_k €o۬GqO_xc8껪V+o/;a/|t$C9>e^'lÎyyw0X38cJQ~*(PĘy h9e9IJ[ɞs*+-wlGsMaNc=×}OL|%_-zî3>bFG`̚/`Ϟ=vyن &68\{mrʪ-[lpvdd̮*k{ :d#$` 8V T֏NʱHlt[z e2:֯mm0wc{ۢ4Q@ٻǶlbl1pTs0 @0 1fV>^uSZ.)cՕbO|:_U9>G/>p+|ZV5qᓓC#|OyhN< }IP>բHe(qбXlȁHj~ݤ#~曧t~``l\pP `  ޅ5{[cw 悿3N.~Yo:D_>z]P./ڞ[% ߱P*mႅ @0 @0 Xjפp]Lӧ`Wi%D/& OYTg^*j}k.zSyWւY8rCI??dd*'x*|mbLω |"|#Oc1'w>{S4t(XkH0 @0p lc븼,϶Xkt£suYl8 FhI[gV}w$LһM{]zcmEvwbl2X@0 @0 S1@u'jCUW]U$z.zy]Vl8CU?9KCNȞyZDG׿Z {b*VU[ݔS,< t ˏdv2A_1'/OX"TY@S; q-حZ{/4t~q:X[$? ` # TVl7,@-΅ /|<}ڹˆ&΢Ѻ/m[2M6_}.ᔻBx ` ` SQU'?oFrRvIkע>c-RW=;E1()8lc=sbТe^;SgfW9zsZWO.>' <|6dV0+J e+Gld!@0 20&dUYftW+BR-x'X5ZUe:-\޽lm~!@0 @0  c:(YdBZ*VZ՝ ޭ1O xPXW #'cz- >}/|Z ~.؂\rGr|ŕOȷ:+]C{0ّB86! Z[p JIj1!Z "{ZO-c')(*(ܱE NDk_aꤋd` c TWl\M`@֭;x'\-vW6;lxN9g]3bhB @0 @00 D9umXֿ먯^696( J@.O1_I(6`b).vGG\S C,_Ds~us1г׋"'&kڷ @0  WzHӒ~wXFG7 2Y]{Zk훼RŶ<=ddd$=.)6fч@0 @0pj2ߙsJ])L2$/3__G]& Vc|CCEЂ8N`-2';/1d62P(T=xC17$񨂅s0 @Gʯl<({zQ InZZѐm6.g,X q={2L` ` NA;Q;7K?iI}:zdžo\:Ia0IGG͏SV wMwd` x G{`  ]Kl/$;Z&&QQ`|{e-m xD҉;` SZm)3[V=};8A~ &eKў>ϲ-y蔐ZpUmA1iMlOeB$<)zŐ^ >𝦐@ڿw]_7%X$rg +cy@0 ~ʗZ tRV ˹[xBk3.Jize\2B#` ` Cd`J48t-Q/j¿5V&[QuhS@ɢ ƴ—~O/|/Vb΀6glw~ocsad`0 sq8YJ h]K9A` ` `f 3W9CcT(O'*"$ΘQۢb n=oO'p'V=t3VZ"UN9>7aɖxė޻!sH3M[m񒅶6<,YTC̬\.[Zxmväb @0 @0 e2T뤏ZTP _&!Rϼ|:ڷ#ZtQ?>UW|~vWشs պnJQ1{J c%M HG}>,Hs F:yn}Շj/vRz|8$cx 1s ӻ>v[Dv'I92w]{V|>N \J\i[!^|?`de7^Y4,](` ` So )MR)3|iPTOR_%6R=ޭ ṼN_6Sȗ#|aԯ_*WK$moK^\n [۾˿l` ` `Ʃ6_ TU]TuWZa/" Z3 ?)bG>:`<1V G苟(5*+/A2gL<_d V}r2B: Tq.?_ @0 /n˴W:w6,n`@.@0 @0 )@'jǻ,r߬/V  *y^\E|mP S'ilȇ{'-޻Id#l':>؊W/_]UQNZ?gHu3?lB` `  ,eD\\tclLhUiϴWe}=vםwOks(:&/͛g+O_QMRGYф ` ` ``&I_A_$WcKVWV]UR<9Db}c+b㣟 9DQ5qEKlwkȞe=a1j lбN(Fpl<#Lr-:a'|%f10|=2h=>C/7g^B` `  TɵC/v~8s[L6Z/\UMz[hGn߾>u @0 @0 \0 ejHAZOW=W\;+-ګ`/bS" }5Gb=sHOZiu)@o;-Da*yJCS̋PZ‚9rc#w&c}-|ҷMl$N~xs n*|m$Q'>~G!@0 @0 vvҩn0߸*[=oթw>ۼi˔Mv1{Ґ.O'xdtx!@0 @0 b@PnB h$PeL}`~₺: ՔW><[+Ws/|ZR|H~[~Z vPL)E'd1JL}@ׂ41FZ8--RǏN.l>ESxMvu1Mm=b1V5]餧e̡r1~WϜ0Cv 1}W9)0W?DxQOթkCIhA׷C,C*Q0KW^5S@0 @00+hlaV&I lAs@0 @0 Q9i]' i}WS5VhAblAG\W`Ulbq+W,'a )$` 9@Il;< ` ` g s-ݱN`ro6G-z-d8H _ٳ9l3/bK/T-8K ` ``n0Ph&H5%d)5yld@0 @0 *jQ{~:.l:JZt|3}pG/;63YH0 @0 sc;yepYJ޶w` ` `O{[U?Nj{fϤ~7Iia\1^AR1q!L3=s"Te@0 @007(nmTrvulЅ"` ` 8ȾDZ瞩rqM~)ʇ70O`Ѯ z1f6LsnJZV~1#ˆƐ` ` ; VnYyr^ -w0ֶ/`gk @0 @0pH*dz-ՂX__ͮOL'-}ZCGsC+ bOlm>`+|6Ol{75v0}D|Se>zm(-s,V-yi)lZa!!(6vi#3Fr|%=6SLb 8=:$` @F$U[ͻ_,qys|n6iZx](` ` SDU @mQZ+j޻>z٪JLttW!z<.q&O_1b#\E_ԞwUW}㻪QPLU&{5'p|rűFǁ--hs?` ` 3 nQ]U cq7!c!Bh =w0q @0 @0 P;oy]u}N' NR6屘Cן6cN_qkO6mVY#Tg!@0 @0 ]M\~auCPAx\Oِ;ZiP 3N\8p@0 @0  PÍo>~"|<:}u4ov 59ߎ~#8(IC>c?1 S5@0 @00(~>vŘU_>&C_D!Ypq #ݣ_v݉Tw#$` ` yjNNT[_ TW}SC y?ס39GjGF'8nV{Ro(J @0 bC럜rbr^޳{d94VnEisi.pVp ⣈$` ` 8EP=;~P?oA請eQ=5<6N@gfA+F:ӻKb%_K ` ``10XҗR~UW] s`49j[HC+ŠúDnqqѹ ` ` N QŏG[v:3֑YFڌhz)E姍iG->yC~“ƚG!|p9}!@0 @0 =Z>bk<}4_] <[ƗlLH/ gON @0 @0 L[D-DԿ9=O)66ٔuA} #|i9uV~֒=: Z"sgQhS3<bC\1N0u7oo'gCqV^m]|-_,Z`VT2Vޞ}c{l i. ` fŽb2\~ͨ|d{w^۴iYf:*U}b,vkoП(mkKk<"D} @0 @0 RE(vJN/R+9O1OCu]&O\o=vׂ0i9ӗ\§W'#\a/_qe#"򭎪cJ*q`H ZDI15V [tkR/5ROL_b(8v|'f3||Cg!Иܚ{ >$H wvXۼ.{{Rd]]`B[|͆1khδuc-V۵sqv߽͈3` 8 |7~/2$~װJ*- ɊŢZ*3}JKuZ%f5G޽ʵ&-[$NK%$4k>H$` Sdu1+XZlRK@Z-:y%o=> Wy'iVvY?ׯsc՞Q\2|iũTRM`G$D,CاRc'Nx9ZpCVh>xlw _1Na,|őOU*بܱ݆lttF]UkδgҒ6 a?vӞfrt qWCX@0 Ǖ®] W9{~'rȯ|za鱳>uל4zVսڃj p={ZKJRܽpv@0 @0 KT橚i^ULc˜*-5'=-`S[DʋM |a5C[5FdQ cc*|5|>6i"@X8QZˆ#2I {1>&'|媴h=R}kM PÿrnUF< V8؄q玝 )i:l9}D:l[bnNXOϜ|!&>z1 žܴ~)&}߻)7%tr\dDP{HAO_:|tMv`Vq6TbS4'<y0R-b(o; Y?6z{l‡Eg_h-|Ͽ?i>Gtf ]*e?qHa֏mME7ؼ}6o`˗*ͳ$w4- ?7/Y ` G%Ox o2o_q;6t0q<~/J.KCp;1 @IDATCc т` ` #aVSSRbzkiѩ΅8cbpW<(NA_ 0'wSU~g=£0&.N_żrG8zA <0~9/N3$C ",@L3 sJL~F aX9>:B?3ɰ_z_Oj3Ѯw-( @0 @0p WdI͔/TTĄ:*B˘*u`l ~`sB~c?>-Of?ZcrEB<I ߻5}G3|tD_$D -I `"ˆ)˖y$d G>󲧕Sc>z|F_|!.>~d+n ZrOi/C$Ai˞GʙȢV{V;hK/,8D8˜GFy@ig0] #>O>֮wmח&kZFE_%˖سy5Ǟtx7o<?F}aO\06!k='q㋭|,,k*` U`bd$"$"j+"D>ė=z`) l֧>/|`C/)~D ?}:ˎ|n} -VYiR|na#p?[z0_gUHD7OA3a@n^HbnLI-71xsoB Ngh7Y{LxsgV-˟t #FÈ{vO~<>1``3chv>]6艣6c iüɾ'Lzx7/@GO,p3&z|m㚐t"%O`  "F~X`,?fڃ%o+U/VvpiL$[?nlgp%_} ڊOoؾaNIkA٘%;oaF]?E~ߣH'~ߔSvd#C-2ϫ.wsK+TOɂuZ{{gB Cr@_?Jw7~'py贁d3(,` cϬ6Au.-'BRo2|J_Х_=j#h(K{~@0 @0  PVԪ3P@ny{P5ÎXNb2V, ~2E-z?~GXkU\Y Za'|>~nJ9od F D(iD|)At,Hs@t5||GZ}([;`)n=>s1Ve7-KϟgQJhARO>/{/Gv6mӦm6;c*[xekWOdB` c~F7l姧KA?}uZInMv4ܲ#*빬k.m0|>@0 @0 @00jYTK[|c.z7Փ)FCCZ_ϲU}ֻ5_bVM:՗>e3>~|k=F[b(z|JXܮ~2D HEH$`ɋxny#+X%l8y_<>5-rrLr\GCn\S飧M!kt7 t~m/ۧ7H9aө6*vV.60ROJzOt~7 ~[ر7Z}3Z} mvVw;NU5kβڶUg㝶if+6nzV^c֝k- #)ŸqhlWZcмcr|b" _3zhŏP`#8K4wufrm GcqCȼYvV"?8gT1.~½F퀿ygޠfTTw-m{*n/qHϵxs_|Go:I/ow=d-Yb]b;w5,Wڽ7[Wٲ5e7Cm}.۷agvn\87C` k;o+ȟKOk&FE+hKҾ$ ߟӹvȟet1 @0 @0 0E5b h_nrUOy Nc_ѩ̜?$9(1V1NJĔ}D>TZԋ߬-[T+|#WSTM>9>kW1h.h4\h0"̓Fr<"RSlvy>nրOz|ūW[" u'1;(vv?i@CE҇]f7&N6Lmd/]Ӗ^}m*2hQO҂]e6BD7x9sh7J= c.{}A(/lcdKG%۵h2{T?W@0 ǘm鰲o \GM@|Sw4T_R @0 @0 *bSfz*6=5Wҧe_偘ryl@!ġU/>)>b|)C_UWXZt9>:^zq֯=9Q -"P٠c!184FQ |N8ywkȒo3|?WqCpdۡG3/ct!`(,[^\)G#tUO?5gݿ rԲ<^|Ø~+זc}{s~oyKVo<`[v1%wQ9 7*-vڲ5_-+|G-큾]9_g.˞z\6{.;L˟S@!@0 qd w _/oإM:[7o*Z< ` ` c5j K\dSEto2UGE}nGU5WN.A8* p"Lᓋ0?s|5n>!y܋N3$&ey_0G9DI1|~?őjH{DC _>خNڤsk3ybWy.؀5xy|T!T` $/>7ά#l{FVn^hE;>qp۰Z|b?OZ`>[lI[v7gd`gRy~v3phkkAi_lږX7 kXK{ԟ};>= lL=ḃa֞X0 '-vߝe,z V]/RH0 @0 @0 ?SIѨR<;JH#B~^Ue̡r_g jWSɟ`QK_sGϘ>v㫜rl|N̫"'t¢@aAa\/iѲˋ$yy?$ |G1tQLb1/g &\)֧Bf5ܺ7u#Fܙ 򝀮bῇ.ib6hw<=lʇ?|OyP'<;TGX< _\૸7N69$jU0nM< +Y|E1DtIk}I9>OKp ߻i}M~̫]ǺFkn4v63wT?Ɋ̧Gw) v>lqgZ}Wa߹[ 6y{]?G.on Jk{(8n{OMoz> uvv[_ݶmˊ v[tFRBR(w.Qh~?-Xiخl-nvsImO)呑S~a٧3U:ŧU߻)t2FhS[K/sa'|$K7DK,||DjDXQ5pkhND#"ɠ=, ""U5r]>y(&v#ɘNȖX9}BAG3y_a;xN=>#9>3!>m}|rm*c^7xW]f[zGm; {v ׭ﰋؕѰk?ɓn7|Vܶ qj{ؖ3/̓3e˗,6C-ZȊÓJw0q|rkoL,UΰK{K^iz٦C3'hkkN[rD/^)N~4( o=+ ` ` *JXMU?#5UiUqҩ_=* y0S_'a3/|`+/f-v=>cl$f*?Ub:j1"D[$X!G,ztZ(sHOf"$93O D |a3G֠O^z7)sH~"|aUs3>I6RGP7 ]ɇ`n.3?wz_wLvu%v[zϔ Zuvk/@:!w+My6c0ce-VlRuvͷ}~6+B7!8i Io,[Ԗ._SY,'?hwM7"h0vu7 O^u1lu֙i044lwqg*](` ` ` ``2@gE匡XUP@Vsޭx髾K,TW4&jQZ/Z{7Vq|: bt1栏^]UW¥UNGwWޭ sR'Z{  B-^z@4G"S$VӒ G!.^fs|b 9)WM{b#kO\^؇r[)o0󋶽?NtBܹw7zXTrq76*{[IڸԞu^[5b_X<;,m۶ZGG{z=m'X\அ.9n/íV!Q/vZ<RGO~{!W+_2{Hggd7 [ ؟?mHͤ%`` ` ` ` [ LTj}T?tj蘣(F)_-U }[z;ͫ^s[M1gNUN9>z· '㋀W.aBf5#Jz\w傍'>?rQ?e ]w/Hd6 \t7ۿo%}?)+[7\8GSJ7cOXbwl]km谊o"[t*B.N?e4Q__cMY4{eʕ+һ"oat|<$86mΧkgi|=!*^sle!ꏴڽ{=pvם4u]{[ᶼf_kq, ;[n͛4ϕ{'~hwf;X+qu 7%b-6ۻ:a?¼ u@}W^GL5YCSsnZ[W+r_u_G#zWdPĦ U JB Q2cK=z%E/[-DcLU>69ւiCl"0;gz sPFě]2#; :Jez6˞ {s8!.8^w~ww/O6k[oIq׮Oԏ{ܥ4;7=OKvWQ>A:;oE kwۮ]'E}/Iۍgv LCI|pG{*s66s:wN?}e?ܿa6 ?2w,`{D1!@0 @0 @0 @*QlE'4-5QƴaA MlQ<Z^j&=ħ/cp/|2ǝ>:cK^'b7'6@=B=>z曊6W,~ )%<SC҇:#|r|A(cs 2kgHP">ɣm[ v`pzw>/3캽29 JvOgo7 |spk PwWڽ˶u?V(/pn,z?=6*'UUȟߦ;1ȁŗ\4)_+_bzW/|x?ww B㮁zQQO|M55ū9N7/mYwY){asceٺ>ѮE/bB}1/n.c^߰oL9o'?YOh]="˗n` ` ` N5>EwT)IZ:W(<}D'ts1cZ@h9GGe JPG,|hcjP ExCjņx3G1W,!bC!q!*Y2VQ6Z+?짟o6bwL'J_^^ٝ6XbS7.5^Vs|w奍vg?=mY<7nk<7ڬw-mNZe4.M,?Q?xLҖM[lXiSD u0 @0 @0 @0p3,z]QH5owk}UMUOW+z<.q-䢘MxjZ⇠Gh]C9k8[D>8U7 NH0rptldCoyy(|G._6o*|l>> _q\̳6Zr_:[&!镤#9깣lxKb9+pQí}i80T_ۮ].kLRoA}v~Oo>kU~Z(F|j~K w Cc5iEx6;^WŖ?kRtk_tW~i3 $VrxoR|oNzE/.E?:o٣vFGG;nSWez7B*67z\.׬Jy=}?Ioz/M篨m0Tk}]5}ac]|;W7B` ` ` S`ꞌi)ITe\׺*E[tf,-:Dsjc녯S yp%\Bސk^Zh˶~0SM*y`'O_c1_TesJ{).|MSTÇY@16G!UwTL; CK cmk}>[/<>r]}^{ +Z4V~KVݑ -?Jiλw/Ŋ<dP)|F*)4jO-[%0o޼ԧ{gs Ey筗:1^7-#M~pְׁg^?`uͷȕ5};_W&m.0otzԒXgÆ3@uq>]U{y:WSG[ R)_mk??6` ` ` 8L"'%y ţ4l>8sig_h!6g/]9'[ŠOᇏ{X%y _9>y}h1:/d.0pWӣ(໪OJ>(lw<,K^`{yg٧oﳍ{l.? [&IOx<G6<{{9G E:3{k* jcu;ݳ{m߶"4|=f㶉;&r?QBhd?^ߔZzsww;f8֗#77^[o.@0 @0 @0 6W9FQRK;{V;3ƽ-:١A/N;lA igzZąONOCY?>ǞxCsZ76Z4v,O җ;lȁH y^[b૾wNq6/'U'+Wk="}tg.?։ NW>M>.çG.tVLTÃջmIgѮ8ÞxFwv{ΚyÿߡPQ:zooUV]g~u ~BSX$9jrxJ)ڜEu 鏜\xId5 o0Kz@_HGyܴqs>5qxݺ 6ҩ%6 wLPα. 6z$5sMV+f_=vgi6x1^ox|>rH#` ` ` 8P*d3jGSQCc%=-4e؇uXSGV~媦 F/׼kkAK _9q֬\O? e:LB D6O$"A(S b c}s|ypGr|戡9 O+&NUjl"#Oc3c|ȡIj3;z`' *v6{]nˡ"` }6Tcz7K9a_hBZ}V7  R1w`"I~Eב wT".3Z[hm?WOC3aszhCz l6o7.Qn_@8۳UYr\)6B BSBBWShƸ7m,clYK[yooVNr3gf9k9{yyh7;+y(^)F8 X!խdT"|6Vy\Sc|+ּ#ժ][^[''x\02 =8{_]*uf 3`̀0f 3`v(QD(DQJ13 GO1Ӵ;l]}|45| 9C>sZiEd/h=}D1m̗Ůlm`P" !!ƕ-\eV";_+ CZA6F1v\勮_J|ʗ[i% [~S.Nn'(YÌΞEsS;- =pf|`Y~ؘo]ɧ2;[!\yE6;c=[wCsERҼgDg[pFM7Ro+R.8Ł܇(o1<቏)μx %i3`̀0f 3`-.O?2@(RmѦ*˖Vu24X" Cʘ8٢g[x+|)—N`1GltCȷ_yj +bVb`'iW,ZDqJI] C ~M[& [lUVc"]یMTL,|ҫyZt6y\MjO?zZ͚`獹A͌횴./ވi/7GWGgY$`^ȿ_a066HYv_ܴ Ym H[[y3:'AYP9|Gηg̜!a{~[}D_򟜟F瑒?f>x怷(t5i h%'=a#}RmSv93gvSKzsagYr'=-aw#c|#?hoKT0wylko]{ܕ3`̀0f 3`̀0)T\[P{{gZ"TG ;߳;RHp4(@ ʼ cŠ|vEß٭6C/Q 6:ew>cx$ƂW\d!Vo!|Ö πBZ $Op157pJѳs^}M|y 1eOr>\vBH]9%^Xv}tyh"~ʟ=9K_\zά{7獎}Ol<|) v׬YSEO~dwPUrğKêasЈyc͛xk4,yjײH^+Ax֟7۵;NK=1(l?Γ8_ԐlY;[l~_[MiڼIE~kEU`;f 3`̀0f 3`v3CW[mRiUy+kb-~[x vkؔ?I+|c}~՜K||ʗ9\K(Nc⧒j1UFpLB2(9.(IsJ$ 5G .zVފ#׍ǖ~i54y!|[+&i=8)˄f 0(eo0MkbS`.oܘG@t@M툞(DonLl?.aN{>ħ6_^<7:ӿ:Ǵ7.`C\z:}c̚39=#ZWat71N9?Mk)5^}esAuByo-|K g>OLwL|K.,秔8SǞR^jX^L 㟖ú?xuοs5L#ϟ]~# Ξ8꘣j瞥WhX|/9Obܹ4_:󿆹  +.*ySOo'̌ޞj3 53R#uε^Wt3a̩n>Ui9i)/1la(/*e Vk#l5F-zk<|iĦ$|jZz<?|IJ|a<+VX7hHUوGa |B$`O!!P wcтqi_w}PQLB)0ՙɝ}vݫ 4;W56wVu(,T |5yHqwnxC 8+n`s'#әd@W2_7ŽH>gէj6_gJUݧ|-$Ͻ/;Wƣ |w>{SvZYɦ?GGt@fK)~(xǝ:vz?/ysrwq]wak׬/Kw#?jff 3`̀0f 3`v'Rpک+=0VѩlXs! U،u1>ALAUk_bj>x +tQa,QlrF/}d㈉!I$L@Z^v٭Nزx@cL<]S/|[!Uw|-x P~9`7;n]gaOf@pݵ?re㶥"%\IJ5q݃btWn,t[ + ><86L?_Oi@ƥ71쮸oZ~yoNYg' vڠ_¿m 8nnW\_֬^֗OϫU':ۃǽY4j8z+/w|#e<#U < s.)u{S޿_pqƶQx|_|߯ϰ(/xۛ7ts;=Wo~۫͢f o}ÈnH 3`̀0f 3`̀0$u]FM[lկ7O7CVO 7o4PY*1zHDt)6ziEs\7oCfC'_K=qO+4VdO83Ha2G8rl[s>/>Uf@IDATgO6j ' =$(׼QfkܶZS큁|e?t轋1o%ܙoj|=b<cSyWұ9z{󯷻byЛ^gSf΋s/88NX8Kot@^3o|bc=+ZɯZD3Ϟo+h'̞=+'(NͿ`_߼ߙ3{VXS _|GHȿcW<Ϟ[q0oq)Jwz1{̙"{ 7:7u&XdG|*7Uks>Zov̬,1Yxpssb̀0f 3`@3f6)(1sF[nuB{̱GWy}_ _Ֆ5QE [橥6X?dWTWخ1/!GkLVcKK#ZE/;jS W.cLbq4{CXү2أæ\}%)0%0W.Vd#ؐ 1E-iZ񈁏̵']⧺+GW˃iɏI<7jJ|ϼe"3 ^=-t?`\}xgޮ8p^W/{<!j(Wl/\&<`V7;:{7(fW+E'%wvƟ?7zEO߸rS~0cތc~r/v؜OY2Ur|&Kwժuoј"n. v{@wЉ;튲#֛3`̀0f 3`̀ P*RPQ{bBeL}:0ihcSD/|i_63 G+|տV^cV m>X֯)1V᧪ǯZ#&^[aQ)AZ@%JG˖yd%=~bj,~3>yoShm'QL%jWW)6qZˇyf`0YԗTo6wK6Ł{t|NR㲁xcgww܃;]&}pm+oWȍ'r0k֮u]{Dǔ}ǧ_4/._67,]1hs<`DN  3`̀0f 3`̀0f ('P|UA*תO,.{rЕʇxC)WKlJ|'.i[!k=/qЗVW9֏tصFt0"*N~%J1zb+V}8W _+BK~%a~k=n'066_ГX$K9ޜo /<%??vz}M~w>u<|f%kaE6Dϵ__\̓ ௰W Sbƞ8xCȸݱz}G|kGψg%h 5-76'yq^D%4帡O3`̀0f 3`̀0f 3# ۨgUj*΍wx v,:G Hٲ ő 5T}Ӵ^Yiذ㣸` ?*2_tb<2` W}>w.,lнQ6Ź׮fu㏚O>zz|g1xr)DuߋM\jckn_~:ijtK7ǑL8;t\zg G~3HW9Wca 'T;^01tVf L`#[gì0f 3`vw`Tuf PέSm([B5lfK[j Q`Ӗ)Q`j1.so+#~3>~+Eb+Lel-"|9.OFJ}ct; ƛyAVo =/鏃wǚ ғfK>$fNۖ:s$vƚ ~6D;33`̀0f 3`̀0f ƀ[jwVzX睊EZ>* Vr$AeImPGϘB?}lc'Vc<,'& GΌɛ[hNJ*_KOu%``'L sI঵3qwꋽfv5R47ga}qASNϜŻt8bS]FЙo/ts–V{=(ubaj,{fL9=Ww^VxaS箊NW'ކsfWܽ*W`߆*o0f 3`̀0f 3`̀0OݪZJUsU,W|UsfnS^U>});l>"|/vZԋ7V1 };EQW^cK]i D ѸaE"')%DyD'|d#ll>q_ӌOlhua('kbcb20~EtNۻzPogsWOGyp[VunΩ 8ڍu7W+:bθ/GX64 |:i ´9bi瞳{X2?˴r 7:?뿺415?K㒛|9+"#*.sK;|K|=r2XLjG_߰1N>rju;Gۜ0޸1\ݚI MqEGsCa:{37 8!yM6굛{bfdž|{"w:M|tms^Gn_ȯ% aBO9rZ*s4M\ Z̀0f`f[3`̀0f 30HNv\CZOO S^uTؗv_Us*a+-ګʸok/bSp /}5G[{xZOOHQ"#gP209$s,?ZAGf|-Hc?~ bZv2` e7R'}0sx13bLOmuA <{zc56Ғs#7cݛ6Ă<Czbݗ[>ě \7oηmy_DR̳:\ڿ6;N87]!16|bQfmNƼ8׫cE1;/j00f 3`̀0f 3`̀1\*;i?hB @ nhBlG 3ϥELs[K̲a{Wg @G8ϕݗ7|C?\~-՛G7rypƸ4fأkYL]|9ع \ bC_~iQё l.BtaN:-.KsG/n@#j~ˆ~#y{O.\W4r-K7eb_O0f 3`̀0f 3`̀!\%c5M.[z\iM?(q >pբxϘCcU C*{r_7rėm'_.bG}᧪Z?zbIҏuaM: @G^5pDǩ;,W~cy,[_Z'_~9KX ]]yBn4¦Ot,wNL׌ϊ)3▥qSP0&yv;ͅٻc]3č0f 3`̀0f 3`̀1"UOJU̕ztRGfKIUm:31!FJZӪݺZK||#շT9>r}%NN _k%1,V15+lv6?Uo3]4% ^zN}a&ԕ~bbnks,(-_QȳVS]  9 ]{> n]]k :zܼn0UwnPͳ 8b>=O'7#x- <=$7}':8:xHj֭0f 3`̀0f 3`̀VړMMU?m2HMUzZlbaǼt7Cm3>v%>`ʿħXSKN*VϼOlvov+?a)'>fz!o]gn2]oSQa`|lUvx`Vg7 ,6I?6ty ;i$^셵/94cX`A̛7/fϞSNʾy|sbinhܾts\O?x쇻.>gCx#9f 3`̀0f 3`̀0f` PʝU.Xijh5ݺK_]ՒUW*#ˆV٭;sՖqGGK5Ïq<ӵ.rŽ8収c >n-GJ[+lvk j1zB4G"S$VӒ G!.^y Sc||Z/-<\R5v'FykO\^[&8CSG'}et;|sWřM#@__v-9PPm0 m.4^ؘ1ꏕ+*?x{77$zW "L,B<16cz-v§ՌOl|B#}.感Jʩo/ҌX'֒ݺ“ 28@LM3D";b+a!x\̋,œVa`>:cW;4qGت/ 䎏KDfuRM?oYtsj_n:{a_G.'x5kFΌ) #- ua#~ϋ~v\0|]s#i&O3 P0f l_]ry^0f 3`̀nP;1P?'?QOM>5]lQ>Օ6#S,C^D9앗sL蹰+CSΪ??j~\GWcGC#zWʡ&$ @dǖ${J {V >-1)=c"%~C\ V:Ra v<`+xеFȋS`j @,y9cEGc螒fyšUџ1NIiSذ>^< 7~])|2a':/{c/޸<1&0WIBL~7?̀0f 3`̀0f 3`vD'- j*g2eN>-~`Zuv+=eʟy.rUoR# _k>1'6@eU|ZЂ/ oME D@NUE-̫ņn(}t\v#Og^1ȍb▹>1_x9]"%E*}^ڰh&̗ [I"|T:˘_-e VNxjMk+[`YDAiB-}t$._`iD}"1jP Ex)7/,nxnM,K|p@`>cħXc$.ɗ8C>X&璺?ؙ{ ɐ \lc`fag.dűdɟsH1{n7y6v? dž&Cu 蜏a#*0f 3`̀0f 3`̀0ɀjTEOѽQ$R+%[eݺ^.lVe\0.kM`1bfSpʶNz|S{Ʒ?UuMY1aȇ>qV8),l4!¸(IeΘŷR?yy(|GƮ_6˯>yo Ln85s.|ZY-/֭VkHdb*#X}/Mjw[4l< lKf͎}X7 ۜ9fCgcߞl*T~l(h3`̀0f 3`̀0f mb=zʘY Ԣ_t\`0VVͩ-qSR/|m0nS5l3Fސk^Zh˶-~6K"> :٢b1x!I9+n*_l9 '|ȓ/mӘ|y#|(e6,ƛ> ]bA@ \XjU[1|× b̀0f 3`̀0f 3`%(F&?%SƖ ꫊qTsſ/1(f|ZhgE C :|y\6F`[à0f 3`̀0f 3`vu6J>UWv=-sl=~z }lUǦNQїsl+>wO9IkxCs^G64!8sZtv:s0GѽraC$*tJy ?6ʏQf|y6#Ggռ~჉=MeWeo3P=&jΛ ̀0f 3`̀0f 3+3@ꯪNuWt"IGJO-4WUWedc:,ȲW,>1xE/|r*5z-Zտ#<+8rĞV&Gt)\Ir!#b:de/=| Qt't$o.wk4f 3`̀0f 3`.ˀ뿍Ψޮ;\utƺ`#_mek 8 )# FƴnW Ovk=fweZ̀0f 3`̀0f 3`̀0긪۸㾮 ݰvSR_/l ?^٭m[-q+l#>o<7/)0f 3`̀0f 3`̀0f`0P֕b[g/9=R(&>cm&jLlŠ_nFZ$M\Ղ zm6`\ 3`̀0f 3`̀0f 30pqvfz;z_lG N!>:#nRpHK֛I^3>ckaTފ#/nӼAx _1Na,|őmNỲ0f 3`̀0f 3`̀0?yfZUeaǖ9M'uU^9i/"P^k[A'e1:l _clNjt\onz%Sl~i/a)A ƂE<-c.1E'p$J-xh).-Xhgy. x^v٭ d-IrEGLZX1&.u/|[!Uw|-x4f 3`̀0f 3`̀0f L|꺦뿍٪_[oԹi[K1ڢQ!"=:>}tl6)*\MTWq Y 3`̀0f 3`̀0f 30뚮޻o߹*#?bYShy/蔐Z_I[n*`x/[!gG >k3`̀0f 3`̀0f 3`&C#[s]R{.7h!&>~d\c֯SllZˇy0f 3`̀0f 3`̀0f`0@3ES\UeNO_Ad+_tU-K>ebҒߡ+;jS_a(1>C8ҡ'8%>A_c+_k)& .aEU'K(cW>٭|.IQ _@=y+Kdzܚ3`̀0f 3`̀0f 3`&{ojઙKۖ7TاEhI1zd"{-@- )_S)T;;/q#% X\g^:+SU,(uOKX'Ϯ 3`̀0f 3`̀0f 30yPa53VQ*ꢲJTutsI#.-g^B[C[-:__9_>6-vGCh.k+2nk04` K #Ԣha ٗ|!%HZb"&i1f 3`0" 3`̀0f V X<4PoN{<Ăc-ZZ , u^ b+U"!(&}eS@6ߌE |Zi/- ~Xf1f 3`̀0f 3`̀0f L2\R+>RWM}0 C) =cm0h1}.DK|>Ȏ̓Җ̕xWeỲ0f 3`̀0f 3`̀0wv[K[3!e^\}(mTW S'i[#l OOZ|v+5fRqOt}_5㧪QNcǟ#e,͸X̀0f 3`̀0f 3`̀0Kj2VҞ+clӪj.5`cC)CT/X [{u1_o"ƨ¢UZBe/jhZ+:! 2bP`C`Znc9ZBDK;퉧Ax3[;7Vf 3`̀QoU;f 3`̀0f 3W殺ZjqۻB|;@OL6'8ST |)KǏI#L3.5~Sdb̀0f 3`̀0f 3`̀\ PtQ W6}ǣ  ^;jLR(oM˾a>sl4c"~#TձC"=k|XKTWڤ/sk3 ybWe.؀5xe|Ty=Ղ?W00f 3`̀0f 30QpngݙVOdNCOOTb̅UMW:iuG1%||4O|n56*b 9D-})>c!ۊr*q`Csd^}5FM?A "¸YӢeWIV"YE_$ |k7G-1hQLb1/jg &\)6,sPu5oVG[G v Wi7Y 3`̀0f 3`̀047%]%?YMC kWT=:j;SiS Ʋ%_` 8ٰ@m)b0CQ}P>|i8Z[9a>@բ ٭u̓ 弒ŗy\1X]))K||b 8~v'61>vkcr9u-` CEa=YwFtNWW4i] lZkE;~o$^nFýzO f̀0f 3`̀0f ݄HYeiG*uFѪ0N.ufsC(W-V_K}O1BQ}Lmx0v§͎kM"&ԝ*7f R1ʍqKQd*uLDXI<=, ."Đ??U<ق/<|'.ym7㓋#Oƽy C/ "'wDq,0wgŴG$: ``c#f]/zzlc_}Kn}>6^䍋]o64 3`̀0f 3`̀0f`wbOOi륪HMUzZlbaǼt7Cm3>v%>`ʿħ SKN*V1[1}b G;0/f8Ao^fSrDjhs3o.t=0f#1ULD]@ׂCbSX?TO7&s^f 3`̀0f 3`̀ 4 UI(]"F)j.ujUUll2VV٭O-sGKlÏq<ӵ*\Z]3K _+!7lxұV6u8^IhAB8F'NHP F4<"WSc+c2Po.{T~GÞL%rcMb̀0f 3`̀0f 3`άOh\ߩK_+7o4a c^(N?dJG?%-uev~ʘvweCe2c1=bڜF"$}K>k9(9?sysbf 3`̀0f 3`̀4 T5&~۸g;-ՂoWod'F > =v!+[m 0x,D?sؠ>Jy+璡26xG"΋|sa_1eVCK&D|䜓0f`\jB3`̀0f 3U&u_w~!Z9uyDw3٭- Tצ~Jz "lz %S`.M:#|Kqcp8n/ucMOܹ_~i._gM3`̀0f 3`̀0f`22[֦NKU?#,䏜ml AhB.t*&QD[ e>%[Ɗx.n7gȀ LOD93`̀0f 3`̀0f L\m)ޗoiu_c~PM(s5o QT~JT9JL6)'<8aN~E{kZV9@f΅O chUo՜;*<`0nS'8 xCyHklgs,B#ؠ-:-Ē[SLeąIYqSU` v X>E|iÜgW_vz`2B݋Q0ќ_) 3`̀0f 3`̀0>dNTd@cՎc˅`j詯*~Qr--g˷ė>:a ![2W6aǼ; _chxSفVTog@0E*3/|5G,$IIb+Q9rNE{Š ~h.qX}be__WkgO~ @?6y:. _~,f 3`̀0f 3`̀0f`[YIYEIm/}6L{Z$آzتM ,/V|0 VyӪ&.|r?7>'.яm0iBphua{†HT:SUfc-1U?Nq~+AE>EG9 Rc`NuLwMmbxSyhڝW1}~@ 'Wݚ3`&2iu하3`̀0f fwcbv"=Z X*. uYm<.b#% He>~4|x 9S">J_xGS|/_6V%!-yZ D<6rIXMQ|JǘKaYU+v\cD }+ ^٭rȎ0\4Vu;iy]G^~9gVޙ֙3`̀0f 3`̀0f majt'EA]Ö1(i|YG%a18V9]R/1="f|5Y_hi~nmA +PN`- 0BѴ)2YC`9lŠEG v.ƚ|!.cX◛b/߲Uf|'wȇ܄VsTeB10;kBōG)B?4V13`̀0f 3`̀0f b'YNQ̢*VZͅ~KuV" ՁJ\aGk,LZ.DiK|`KVi6F|*C @b,0k-_$k9̡'6(Q lq <3COV ?cWE_j؃=ߡ-H]<1?;]q#5Rߗhi'#uRG?Xt[Dkx^S͜{q~j^dz̸XZxۡdl̀0f 3`̀0f`r1@j^ZlRKBZ-:`e:2`ɟq+|rz-q~Vq!je|G__90Vŕ/sȗQƨOGb_GPCG -yŘ>vGGk bvG\eB2^lu9/ꌏ=+7~MhDfhiHWףO~8E9a<0ڼ*sEo/أZk_qWӧŋ1s挸Q'-X%0Ɗ{v+aLKt¦~y+gZfz#`8合 ^RK.c7>Cc!3sƆ _-۴isY6̙]W,_1l~[o[mò0f 3`̀0f`3@]Uu^z{9j˲֫xWl*c-|gSU!!g%rRl|7C~brX6cӼTU >a~ >bbHA0AIhNgWs٭7#" \H>)~䇝laAϥ5V@IDATb҂CĐmT1˹)Sؕ ?q!eLcN7I:lA {wģ쌇z0\b&NQ?A>11//[>L.?|Q^[E/8Ǟwߵ$~rϋmwmf 3`̀0f 3`&u]\UU-ت<"[궛"&>-_JO<.)#a-[uK &>z1Rk=Wݘ4W\j`V(CJ%#4Ol R"d_}rU渄tyvjx~׾9LAsmO•W\\%or3`̀0f 3`̀0j?vFUE;uSBxi:'}%t٭>8W?Օ_9rh_LlNK~hMA9UsU+-~roO#t^B5^+ŦU읡{P~9]wCy n;?r[uh>>o}cyo}z N88r}sbŊ!×\|Y D;"9挅˗Ǎ7W]yui2Uk@y.m0η6o̙3[_9kyޘ=gNs1xׯP4o%pMC-7-77tKkι'}82:L^u5[}3sʕo+.*m3f 3`̀0f 3p_2@jlTd -c (ԁ"~ЧdR%|Z )e>~W[-z x ,.bG ?>v 17FM?Id4Q %HK@(u$ذh0by2TOBLO|[o'/` }$ɸW\_j?&N+|0o 1iܔ;]qێ%^;Ŧ]?቏CuW/z~/zH,Zthyo~}[Nl"3A)Kyܸ(eGX3߈?8#sƑaF=bW f: MoXFo ՠFʇfen</g_񪿋=)#-ӟLmk^D:~c7^W#[~w?O2uyJۗuU< )M65ɧmS]<#z{%\?*hBNZ}]%qɏD2FO,|ݪ'}+6TEbɏD>OV|DdomtisZbmZ+ۓ|r5qo/ClY#ט炪`ydGV=-7/T;p7we Zx&o_ 쑛?)?S/y!öxհͅ0]-2?:,2۫sc[8}1O{k ;NK 9zsaf`_,f 3`̀0f {l cC;OhRO2zG4R0& p,Tz$(YȞŏ\Do|[_GJ|>ϼt73V?j\J|&ɖxWٵLTk[',VM]Ŧ]ԟGA.8էo=x^Z9?>OlM~q͉Kqz3bΜy} {qvԟL{*L|qs~ngᡇ8yկj؎U%c֬KUM9it9Wo`j>sI{©|^ /|Xzf/׿]=0x/o^W>o~ua~iS90ߴiyẁ0f 3`̀0f LTX|U$uԪʄ lR7U4=<ƏRt b)t(KG c#l?~/ߩ_9_>6-vGCh.k+*f5P0 QёtؗcȂ4bc\ _W\V|63%cţ^N٭)n3>s\1&Be20O$>eb,ci 0~~yU˖/ͫ\4*o/(8 ?j?䐃[~n  <>;|Q5\xw߽_K.Ŝj;kk(./5԰: O[ '?õ\˳JX6XXTWDZ"^xΨso;OGN =dXSzcᆛqD3`̀0f 3`̀ P,DOZ\>eƘWݪLa R_%6R=٭}O+_P\)6%>}˦>~|k=F[b(f|JXҮ)~h4!Ž` ^$(Q,ʿ[BO=-EŲ0cCev͛V>AFj=;_ٌmcZ'/ȑ{Y%?y>iӦW]?Ȣ#;Q2yHb6Pա` s;q::'¯~z)z+qg#Ɏ0Yxp~w\n.(>|iPry_7Fl.hnG5k}CbtY׿V,_1lnko{lyWhę`^{gx?䊷~oX̀0f 3`̀0f Lb\R+PBj^WNjXp- +9Za#A3O[7X14ouDAVș1~G;V|& y _qK+TWv_0g mh'Oe,څXlېz6jOH7>I0cX[eGno;wMSvZe~X10X/s9![ha=VrņGk.0cs9J8Y!7ϟEG>&T>ħ~z+ݡv}jWeg z+k/l[*g}zSK.+.2a;o 0oZgOdY<7fyiOhV{l̀0f 3`̀0f`21@„eT5WNjdW*cTyKĔ}D>m;|yHlSɏ\ѷϩZ>%>k)ʫv,;Zx+"Z:h!6qH>*ӢCD9gQHOOؕF|4+^3rǧQNZ/`/]c`:ZV{0M1k5߱شX<=y@S4tooo {?԰`h' _kvGI;M:ѦGm/g $ `I3 83SOW$">?$m4o輅X/6n,ֿ1?;O|&q~yCsȅp&g~MmCo*43|ܳd#%w7> 5b 3`̀0f 3`̀0*bz*OilJ{jOjk*̦184>X􅏭t§UlՂK||31j#h\~n͏iqM_$$B л^DDTDA қ"(ґ.EwzK oNwLɝ̷;^{3#Y}VtAXL=! h @1:Db 6σ>>Œ"7٢#vY򭆏#="<ycO?4!<00p>HZQmĦVS^xᅊP+rcjm0z111Zܦ}'x)1/-Aһ|\:W<_k> CKkIgs\}5i>$ɦ2L_n$Xo5p3ok,L;[mE\Fqd"E_uM"P|,vy;jEfgh/,Ju3`̀0f 3hr73F'Q'oK%a~cQU5v|ŠET{]W|#E o=|X؃O*|Z]} PA>|qB_ %E!_a#2D(raN>v&c}O9|vxFR 9Z#9>}Z-|#YrO|%84 <Dbz`@AmĦ&@'fvAvcftt /i|/O`7ftRO-{)۵σ{0rd^6gO>tznj]Us}}/`'oxI&op{"r!?>Pæk_~i|85w.xbwS|ku젳 3`̀0f 3`@?b@PI͓:2OꡌbÅ0^ik^<+O6᫏?'| Zs_Ҭ N\1njXonWHZmĦV 6d"̷Gȿ]LΏ_б@_ {69x7vbu]u 3`̀0f 3`@2PI)U COOQjJ'= w˘K>>\jW?1攏 1}쐮㫜rmN̫"ʨSF%u1dQ$`>Pq-bט"P)_`v_y)Ft/>4 uSoX9!Ve|l_z|O\Ħ%>&?>v|qX[>΅pέaכ Fl:Ø9D\{VmvF9 +Rx)1w9{g jM菍*I.0a|oku_|1)A] Cm\?5y57ȵӶ'8;3ѿ:<̲K?{j{  KHA<`e95b7GM !q'Y<6b08#@:E9VgW&~>:\vEgnޘ +,8>{<5>曄u?Uyb'7nNcuww,:j]vJ<ʟɛy#o3`̀0f 3`̀0ꟈjѴix*udƺMCZv)0Ƌ*~k2Fh#o >́c|XZc735+l|Wn @>V3O"#bB E>1O?ǏO 쉋(&cn8k+2>9d̻O^̡|l'CGK3_o [15|vAsˇB-aYKƫوo#6bwWq8kz Ù)b~K> Q? -`*~ʷpOzwˁm0M6(UQ愍1ᣫf8A믅^nS|DªhyhZ?X+ /luk*IXiᖰJ¦K.7K#6׃-Ei$B(x7r5W]Wl0,²;+wK9]{C\`LGm`#M\L s$za;m{0$>1xM^th)Zrׇ=ȣkl0[oxx7v~'\򷿧RcN>?0yғ}vtӤI&C J{aNc.᫕Cy V9aGI>`>/zO;@JJ9%$O&$EqPh+>s$ 2#OZA|eTd=!#8~Np씀c^HC3)tGcPfm8ZW28|hj)ař&l3foxqOx8K,nsԼb̀0f 3`̀0f2@M߾S(n.i/v-\gB216#*|1˗9U -slc0?-O6`Wؐ=-BDe||2>s!&bI X'~m>-@ۋj;^ON .[}mk QiB!{5,@Gͷ}v}?||^Jwg{3^s}fo7FQl|#v|qO}<<𣍄hWb̀0f 3`̀#TUXM2*].9l_-|lbWWʜ yFEKuc~MKfX"o CėTrSÙj/{"Qv4͉+/vO}@# km5bo3`̀0f 3`̀0꟪sRҳꯪzQSE_\`,ꭚS`X^ a\ ?N%,a3O p>6׼r5G_mS]W[-X!뼪܆x)q寸0o6}`Qȟ ,3)9NtEV^Tj.5|Zצ]293`̀0M`̀0f 3`@1@q;c|"mz/zdP9y976̑;~A˱AGs%|>'/-B~5F>Y CGt;îi -S)ú̈́_h҅&^f 3`̀0f 3`̀035E46Se(B1V])yJW>\bKq =s8, {:Ղk\ Vyg`UM=W^cMk?%\KyhN>;L)uc#̉x|q24O rn85Y 'HfOx~=|彩g2M'yb1f 3`̀0f 3`̀0@oԿ)(wժZ;z.j̩DfH0V2+d"Y 6iA1=-zlH*~\Н:KW>Qo~>}0Z߬Bj]0f 3`̀0f 3`̀{zpR=R)|3D)i%Y'1*1%-bE"qV:\¥7XzC>Qh%aȗ>+^>Opȗc7i%g,?)4--BKH>n ^:%]=AZ})FV3`̀0f 3`̀0f``0b\|T;V1S]4v9U6!?T&~HSG5[K30ҝ9G!^a5 3`̀0f 3`̀0f r^oտ-U/sD- @q즄i)k3C(|I˞͆-cŘIEie< Pxv=/-@K o C;VNC835Z̀0f 3`̀0f 3`@7px)w&P$@eNز1ibIǘxHG1%\e?vLetK1,O\@6^Ys\~};̀0f0f 3`@16y&2P=W[մ{G WcSUH(y>X1 r -zc,BOIa0IU)[Z <'6k ^_\S^ fC3`̀0f 3`̀0f 2E]߾^9>_S2SF(ӧЯM-6G/{m O1Ԃ|c/_lOCw<0VZr/٣_o2ͅ+֒kd̀0f 3`̀0f 3o(*[ᡷ Vr?r"m P-\*ޣc1.dH2&s^1J (!?Gðv ^} <%E"!?:˳f 3`̀0f 3`̀0 }Q/j\ͪ LT}g\k9 ~洡^~j#b1E=I =]~.biM?D-}y);Zk~>:b0b S)Ȏm'iK33 a_ ,snuś9aȃgYrǍo.4G̀0f 3`̀0f 3)5Oꟴ\VX_VW=5)׭6$`G'rj̫XZQ=|AϘ'V_:VvpiLgcܟxė>v-m2 jm Ck|Q_”k)|*Lg:f7dxh2O2AZ{! zs\ҭa~B~&3f 3`̀0f 3`̀ qu\ԪʄlR7U]5v=c.Iyqi8y5>!Ծ/1dբ/9>: KK9>6U9ϜkM(CD(iD|)At,Hs@t+ ||GZ}([;`)n9.+}e,<6:ab>y*nz'7Dӷ4?bȄ*wB+ImnO0f 3`̀0f 3`̀9(f:)-T.j2cLUmz2,tbPBG}KYn+|Z2O>Q^bU YW0he>Sf~S\$b-|tH=|ת3O U'G')*j}$2H#e^kaCQ `#|B?Fb#8KTÏ$/|"[ϝd1f 3`̀0f 3`̀0ԋUTR\i橿⫚3sKa⫦zƪ ?RȶO|^PVb>+ZqEZr|U8-<}-ƀKsX$ByRANGD ~<}.G68u'N_2>%£Յ=z7bם 3`̀0f 3`̀0f 3Џ"꟪/ܞ+clӪjT5]UfSHګ4_:*j9>>C_UWXG"~d/FZɞ~Q%1暣ea!"8v*Y2$2++O3`̀0f 3`̀0f 3`@a߾S׆Zj{y\nR! P\J~q: %KKˍ twf2|n1f 3`@2A2 3`̀0f : /+pջa׸'pUqƴU~=!(J9!OҌx)E]5|dcGu&}XǟyHgSs|!/Ge1f 3`@3aB 3`̀0f  TR+vJ}/=R>5R1Ę Qt_=˘K>>\jW?1攏 1}쐮㫜rmN̫"ʨSF%u1dQ$`>Pq-bט"P)_`v_y)Ft/>뵘3`̀0f 3`̀0f 3`M[usj\i]Ӡ B?kV j>~ٰノ$>6\O=|*6fWM;NœY 3`̀0f 3`̀0f 3pun4԰>qMj ΄< B^>jk$EbʞMay#lE_z/Ny0E,~\ 3`̀0f 3`̀0f 3Py ;{O׿u4Z}VIVJZt|؈6 拻#ab"?pt130f 3`̀0f 3`̀0f0P)[ުX=m5ܧ] 9%$%- +W`+|6Olnk^yq.[̀0f 3`̀0f 3`̀O &u_{ N<;cFuMzڪS=! OI[|B|!TإBG<|b).clY5@ECZ3`̀0f 3`̀0f 3[ ̺۷om*賄j\QQB~ʦ@ Vz(Bb? )iE[=XeG[>˵3`̀0f 3`̀0f 3`%V3ͣZ;>\u&*૊-{5}|r6x:|ǖt CjknG ,f 3`̀9P빨d̀0f 3`̀ʀjܪ{7:js NFbi_X! Q_qko6mVD8$"@g3`̀0f 3`̀0f 3`@c뿕sj|Q*ט.AGAc!JR-:<\I/|{s}/f 3`̀0f 3`̀0f $NCȌתW>*?U2T,aO[~i1 %&ڑ $<zZ!TN74>X̀0f 3Ѓ ?z0C3`̀0f 3`:0z뿑!oAǞZsQssbi$㌰%xX讐,BHV ~hPLeO[G,n1f 3`̀0f 3`̀0f K\u^GgK|n"lgQ@IDATh{t ym64$A_1'ftlȥpPg'ʁWQ =bGQ ٣S p %/|cvP-z4}k>qU9>:9|qƊ_|C|r|y*ZX%sqHi[}F&cl3`̀0f 3`̀0f SjH^R:1s[UkV9O1OKuM" Ձǟ*9>zT_car!§/Ok-Q[֏\r-^X9F§E[UC;L)CDR\"J4b@8xآc^sy2>1COl|j@x!+g~6B\q1U[{[1ZBj69OR ?.D~$G_5ƪ=㣸e="SUL%IX%>%%qJ⏝; _sࢇ`8K/|}mG[ _1Na,|őm* 2j0Y]1 w2b0h2l>]NGZ3Ly0G'n <o6Bv7310f 3`̀0f 3`RT4N ǖ9M'uU^9i/"P^lbh[Aǎز腯16Z'Zէrr. t:"@X8Q0D6I$ǘX#%&69BLac5)V/FÿXnTF< V8X zRuPKv S5rqc#njX ^h0tCb0g‡7>FCdCf 3`̀0f 3`̀0ꪪKSu]QSUmUX*1VW0֕[+1g$EX_⧜;:WGhbIG[Fynb0Ȇx6@bJB "=&~$9 WstIE0>[b 9> G~c7#cŤ9~!hbsâN~Z?HL SK32O. gT񉅕S}Er?&0l/v&C0f 3`̀0f 3`@IqSOjҪJ]XzlUf-uۏEL|[ҩ.M<>9}D:l[bn(>tK b>G#9փzՍMs|֏vIKݔ UlSb0DB_FG㧄Yzѕ QvQ8F:}Zbsa0^ C7f,r cF g1f 3`̀0f 3`@cċ]V=VZqg"ld}%QџyME1'|ce|]Gfd ޱ-Ʌ9uz Wu0bày,vNf 3`̀0f 3`̀-T0nRs]V{=Uggrt9uhSLj<sK&2I)zŐ^ /d4HH/GKmas JO24# 3`̀0f 3`̀0IKO1Ʀ뿕x_Կ W,xzjiV8Jh5v \Н[24$5]̀0f 3`̀0f 3ЯpU&H۪B΄B=lpO+ QO>N6 %#2&6OnaG6-M@]! ]H$=P*@LZ̀0f 3`̀0f 3`@P8u[ճ{8>}.z?A0է́S<::6c&ߨN>jG[-vR2>s\1V<˘9E;-3%cIH)>)6Gͷd`hɠyGZ_ 3`̀0f 3`̀0f`1@3?irQcjjc::D1T//cOZ-|O+_Q\Ս>ėM-hD3P<^^bU YWcy/c@ie^n3~Z, 4!4xgD%׋+ԯXsql̀0f 3`̀0f 3` NS7F*7EFp- +9Z mPGϘB?}lc'Mc<`(&3chŏQ|& yQ _qK5Nv_0giBR=~JF^݃F֖Ư 3`̀0f 3`̀033KzI5)_U͙p^_=1 ?RȶO|^ԪV1 }-8Uև"g-y]yyG uy_DK1-D܆9.g^uZt ', ))6<[h8e|++w|ĖVr?_wfb Zl`hkHnfݹ3`̀0f 3`̀0f #TՒ2Ɩ>ꯪFUQU]u`6AdU/> VU "ʨ¢%68#{56Z֊N;H @@/0ZDABbpi;"pl<#-r-:a'|%j1__`!Z8#?1:K31vg ]L9v3f 3`̀0f 3`̀0jԪRE}"A[-~cQU5v|ŠET{]W|#EW;}|=!y20f 3`̀0f 3`Lˀj)D͓Oꡌb%KYbjyD1WƗW>%>_8C#ZG~[~e|;|t)LBz Q bL[džĘGZtaCKXuSO&,l L:"&rԵ6X/ :7g`5 3`@_bf 3`̀0f Np¿UGߩ3)Ue@ZAc'%aȎ̈́دyt5/_ڤ$hNL!—OSž,K1㫏ʗuɇ\8eiZz@3;$pV  lmVi >oְ9" aȅÅ9\Kz.x/G5j0f x2|1_23l TfK3`̀0f 30s0'BAYU+OuLU2)c٪^X5WռiD8ѴҗP}P>|~W "|i9၃hu_\0ɡL6$jU0B< +Y|b s=:= B|O9 |>Ǘ[c7=ɏyXsBGҤ NoM/zw%cÊ+MCyd4h kfdy 65[n۷.0y7)URvۄoœsQ̒7{o¤I }W;80\sW_y5]u0f 3`̀0f 30 iztRaKZv3 BjO~\Se2F.-́铗0sn_k%1|Dj.~eOkLDXN}%oN}I ^zN}U.<;d '|qdK>l/i##|GxNܑ_q,M@B{~A{vW}S~_n%K/d@|z` VyO)LQўvxͷ ;LJʗw!CƛlZx\h0l@kw_|q?Lw,;3`̀0f 3`@-Y^T\/UG_*ӪE,옗Nr<ؖS9>Z_:pr8Lr|\e|j3[ncS b]㧊5#-B+ ZO91!1ᣫf8A믅^nS|DªhiPѿ/֞k 1Jl4t;E6=6[ ky'L|sb;8XfEZr?fmW^ge; ;S]W^8p=sHOc;.13?3:0rS; f@E3`̀0f Ȁ*w&s#Kъ.Xiji5EN0jI`qlhIϼ4xj8##?X8WӅ.rŽ8収c >B7W<-zI }L$[%NK!"r{%Lnz񫆏>'𘓟rvOOZ?1 jU 셏IPѿ/V \64/]oI4)To}Ѓ??{G9/B>}g_`HL0f 3`̀0f m/*AB:PFU;T^G_4VUէUs 1B e|)n^:["`ر[6_S 3Pb'E " ;D"E"!pG@[ _Ƈ,q1OL.œja`>:c.:~4Iqr|# lc|rGsKK320=E׋Gŧ)G8ޙ=5P zX+hsN;% TCsjZZZ³<^{tʫ[log~6<#.t KFY"3,70a+gV\q0/EK^<Ǎ{*,Ĩ0t> u]+=C>lƪi?bxeBxc5WKOCPɧ7:y5K.5&10O|O=y0et:7(t8IIvBIO>$waʫ_~o69fٯG-XxcER^[xBaUV ,22|31q]U9竮Jڌ{f0yƒ{eyI'; Rދ /n^Z<xi/ﱅ[9-Kޛs9ó>{R=[*{Yk|Nwsbg1vE K-d񞕧BE/>Jg1f 3`̀0f 6R]yX2GM(5Q*F1rNv0O!zX3R<DŽ _1d/<匭ÍGb]֯Xʵ3|Jn"Pc^79XgB@J 'ADj3 z.Dj!§%0gcZDd|O¥e_1s|/A_0 >6'ބOL֠Unfd`z{#6p8VݖWZ|3N6[o_NE%(=ż:+Ă@ihpPd{>8ju֌xiXnzjZ8~jͶK1qCkW=Q{{vg߲<_ޚ8]p O19saǤ iٜ:ujzZa 73 isG: [:z.qظ2|3mvġG6~Z5O{9 }A*ɩ+vy˗w.p鬿E?ϑcɦ1N.8Oo70f 3`̀0f 1/K)|JS~iÖK]ZP#ajű[l9p/|2Ǔ>:cK^'bW'6@=B=>z櫊VlS,~  ;%䢪@E:,}IѦ#>~9 hHfc@wfjG]Or~ĈaMQ}tlεW__D*|(;~6T\l5VKEK8ȃj R=_w}c%-ϷB%nKmNc 7k _ܱCX|SOH/nM7 }Hl)yw?cE̿6[~ﭸSeɜu];nJ=1S0n}c;lasy4p̯/>/~f:t7=23P:?U7X\& 3`̀0f 3`:ewom*CV˾]י)o ˜VW2e JPGMl2bղ``Zt'0VXlH>ՅOƮ|Ur|%}TxI |P[.ԃ.^M 6l0,!tan ߋAǻN?AZq8/Cw ~zdvi!0=GZ;Oyl wı>ŧY'5Y\x~tGȡ0pŗG~4`|f /DF~?ڥzk wuo4fonK.r{֏~l 9oNM|Fa{{ o9HXq~tq\w|*GS959_W-<ao1f 3`̀0f @TU FJr˼c裗jDOW+2~8s|ZXb#\E_Ԟ-GUEC8ie+(,3!ra\ L $2Θ7R?yy(|G._6˯>yS+se΅Ogsx=?פ"v#T l:Hԥ؎|K/t G+ f?XSN:=yJ/yرic>EC>2FlK3 |Pyc7_j;{t?⸚~~h8S¢.br/8X7 ,:2PFO`;&ye%=H l ?e>q ϑ*-MM6郭_I@3-}_: 5m?u 8gqfl}X쩉c84ϹDNk4/4ݑXw˅A^;/m)a ٖ[3`̀0f 3`̀:'cN1U\O  UԢ_t\`0VpӜ;fZqM-|VgR_§5G_mS]W[-# \9ErS_q `l:ő?>X{ի2?Zwh`Aʥ;z|3%^s1I|;\c^]8;Ӟt kMŠ/~텸c*#9G'5zBxS#|_CX6 ~p޹ ^QħLaU&E~$/Ǧh GyțpRQ,m9G yI2#≑13ŚyixZ&a Մo7dr]+bYKۮ};l.`3+6YyT~i_~M${+cظP{RqYi#-Z޹Mp٭WR~Eo=SWD) ɺ¹g]PM)D~Vfй3`̀0f Āc7 Θ7灨N_Y6},PL;#JG+_I=s64F>so2l#wl3UᇏbK%y×}OJ!EO~qI4z:'?K3V jt;W[7~O GH *g:>ow>b}ԍgwlK/S%>RE[ybBĩ&r|eM"t=pP9w7 |G\jLq4ق{pdS1|klUˋ Z~_̥7s߃l,IyU ?6e|嗓GCGHGrğ1Wy7;7,M@O~Bb|o0_R̷sѻx!<8ʷj ~gZv]sDѴ i$#OGt%v51K)Լ8+r\|㎈GGNn{ygߒ[dF#±0X |kx)O>)w)/8OŷyBzysq䟁(۲jBq]BQ>K[{+㑖גQǵjũ臭ArSw'=t. >sϾđ[3`̀0f 3`̀>PKj0Ч`BӪR:$ טY>҇A@F19ypGr|戡9 O+&mT'P9UZW.!.:DQI@n'X/Vow{ XeՕ /TXk%TkO-v7)r*6h cOkY(~37p1Pij{J>x"lZ{Y}Uc,8s~W tD|OuC v9InzȪ%X.}^Zy0qBf)_'6wywj-b|a˅?Л 33`̀0f 3`:0@ 'B3/06ثuY|$T† 8H_UV>b+ 9|2XG׿X+}aOQLaW_Քz +q >B~q!_6/W_O0(.znʙV";rX9m1ГPX=n͖P8,.8^;r'ͺ)_a𻓎ӰK-gh GxkGQy"+ / a/ޞ59ص >xcxxI G蚩O-{⩚v&#j^H~o&O=L;{:& 3>G$3cԒN~|3mƜds:?]sǧ~^wc|Ulq]eẀ0f 3`̀0fN.ՎOn*˖Vu<2+gC:<|s-8U\/11="2rC Q<.+-8Q'$t&Jɞ1`$O˅icQ ٣S >TȦՇÜ>l}Axڙ?cW'.177%_/>yU| ,pt<7pBˏ"i_c{ﹿ;_|wHhwaLlۤ8xj Cq7%!E4LjV4|ၣx>7\qPT|3c`/,f 3`̀0f >a ?FNL"[UkV'+}3O>Gk,LZ.Dis|`Ke|t +W\_|+XuhȎZ. IjŇ@:lC(|!G>:D\高-c3)~s+4 zJ^qz{^8r=X;mÀym#n\pD@?vt5s3:^ _z77ܖ>ByqDB>TWLS{މr'w|oi<) nMO^1r1cFO9>ǜߝ ˥Mgz?Z}iw]{ps6U¦>`07 ԋ=jq8re#8knfV!acs; 1H3smo1Nمz+ƍ5\F )`xg/qX8C7)P %G÷[Yvt};KߝpR#~^VZNo/>Ï:$Ϗif7KZx7O1G7g>K^]bg?>qEy6#>p7ä1OM~DzDqO;\kOE sϾ lFw{65n6uyisr`3`̀0f 3`@g*-]SSUme;ƚ/bXWl3Ttԟ=c募rR^S%m)>- >aȱDB$F& -G/MC'{lYEGLZX1&.)ӗ?>-*{V3 ]Y6|<]j-Aqz{^8w=K׹_.3Cc"K4x!eJK+ƊX-0U *ި h-ZP A 00 \`ntLO9}Og=o?osg]nNw^^/Ccf0|?_ >}}OOYˈw+WcN.Gow ?#|/7 2\~~`6PW͍8k}_/e/Ch衭\\zJmw0]Ƞj_u/r܍=wgg~cM&O}wKFOwsko"D @"DnD!F敁z.QvNN/6fc|6cn`a!u@,1O8võoրc>YDl֠e<ɅocGw17߲~O;{e_yG5__=/u\23?SnOeo۽/|ɸ8Wo3d_LgOB?m>QJlq+ >O{*PR s>>Mo?ӏ?18h,|IzDcO;_\|/_o`|> Gu>u.1Wqɽ/>/4K'Tou.n.."^>׼q] O@"D N(wqd/p[9RtOFz0q#Ḻ=&8ZC~Ƙy蛏:%1zcB~uZ8zCN?qL ~a׭@>1Iq(c8|X<cİr 27~|ÑQgnW}ropf}hk}enggk>g>;^'ryս6G_1xϷ>og /xlbs}c%uW^S#~֧$c?>䟟]ff&r$d>F G)$253״ϼ9og׳Ƶ!/>4s2wO k={dU5gǑ ?CnT7 'p/_%o_OW9u"D @"D !'VSTok!ʼz}rf^񙓖=5v^O\8ꓗpcj5ƬqчY˃#?cjτXڎu@3T7]l/_?VO"D @"D w@.;rƁ?iyNjgt>-ĨnQ&Nc=~Z0o}|ocj]gM՟yOk>f}m{&v}c3E< 1q}hg q ]G+1:3;㩏ٲoڮOv}5.- M2|gF7;^G$sGg[|w=/N>>o߽_y "D @"D HpB1j3Kkc֑\晵^3ZC>rO G=t_1Fk^<\:Lj#fMc87/}aO܁}`ބk7 @α0{3pg賆ܶbѧy3ǣ|[I}iZ/sO/򎯺 D|w;x{^gY?'}lv;sE @"D @"pҢ}vtW=ifZUrZgigGwi]<5̫n|ߘs-}c}klܶpG<1\9n~0#"d& 37 Fs8yj",޸8q ?c4cz\ ¬偹𓟹>{f|ceǕgKeF??Jsl"D @"D @@ n߾"g|ĞHp\\AW"%coش?ka?<Ǹ@p>>7^51߳3-f[&&1$5phA9N;| e>b9/sY9q?O}xn#=ݨQD @"D @"N hhBAA">y}bpZ~x ?hy#yဏ<ִ>㹾՟oNY}21#pn9ćٞn;lr)@"D @"D@Lh݈n cȷK?cxhB2rkynϳM31|`᧯Ԙs)q'Yg~: qB`\-D=eKG{=z#^D @"D @"3B [ \7 8PPGx3rc/3!GMҧ ?ל#ei9c@1_g#E #N:P"D @"p [A1ҍv^Zwlho Wl>1}0C.Zq88wr_㸽hOֳq,@"D @"D /&{CV?jۗNwwnrp*{Z|ؘeѧKas>տ?̲cJ+Kc{8Ⱦ@"D @"D 86S7wnZU^sSv BE37_ 8]9Zk׋k:֟A',̎+?o7-zn.;6"[@"D @"D 7@ݳߍ}ouuZ\KM䎟x$&! ~29cGwn:|؜9ў`̎-OÅ'>\ mnc,vnD @"D @"J`5t7o9Rq[?HnOf$-¾cÿx8q=| cs>]?yq#$;(!~Yz9w׭uyz>@TMD @"D @"1UDM7V.3f9bGw4)b1[{~|ye-^ǃGz>?scc^>9=\zcz5g-F}s#1Gw8s9YCr7k90'U\?Xos;pݤ?3?gLnr87Yz>9XσGw57k-ZϩOwypP1&}XظkS˱u-ssX59g=->3_(XZ {v}_9~O]P16קo.P3ɒvo !I %_.{lyMb/Z.˃Yx￷GzܳW#˹qN}rd5ބϏg\I"D @"D  &ʩ?#bjh<`>~cTɉ#Ϭcyؚ=>}sZۖ;OA{f-kG,dLkuV`=\F,c_p4s-ýαy67yTX}߮>5ͽyfn}Z9-{>mFHvR ^=/)6os9ކY"D @"D @ ύAg=T?_~ūk{sYch1bY抶Xzss5ecjigng:>1pnWyz~h1N?ij풀<$&X|:gN=yq;\ZjaĚcCY|gd19fFF?177N"#`l8K>k֛>12C s1ѽ5sl>̙8jhC&c5Ѻ96AgHnX>sƓ󜢽9h߉aku<9Z{ZgmgyS@`_睔E @"D @"DLؗOg'(/}.+O˜F,>ï'V>qoY?ϱ֧i\_VMI߬>y>c/s÷L9H 91a1'x=~XX7F[o3<-9ܓ}s,@"D @"D  #,By͚Q@VWwU~4wnS={8<"݌ h lVdxǨZZ=@"D GG?\m?z9@"D Y%П+ޮYGgC}ODgÌo<>;:/[֘wt׾9\g=;%,"D @"D @"N52;jxov^4_G0?B$Fз1܄:?JõKu`N-y1X"Yzd?#"D gH3,_XΟ?B-@"D  !y]ZNO6g]y;V3F6YӷѢYb Eu9o Zb9; DZD @"p4| wyZD @"L\,gc~?+gGN `]o^;|AE{?>o=Fwv?5h}{M[`Ƶ'~YcNb۽zu) D @"pD|ɅJd8"@"D 89T?g( m[=[^߼j_x6};C&]ǜUЧż(ryw>!/> cẹS|yg]iO,9:ZciaIE @"'zk^<˅ sqyg5ܾ[q@/jMD @"|&^~dkw5G! ~dէZ{MHZ)ߦM~ZZ|A19x<1|'}7]WY:Ůc9}L>>b1k10mSskx~[Yog|D$h9(Y397:P-5'05U\ě{?ܫY=g{U&ngg%kGw\3}^Ƈ?-@"^>Ը`x_\|iy 7?u7{xk{GzMD @"Or'}Ĵv?x3Mnf K/=~sjGf{2F_E&>-:rp9:Z95-O>ꓗ8Z_`ZԠO>S|`LGwG>>rb;af"7HKB60hp}Bo,\11=smyS_w.l9y\q?GCokUPGSe^>x\3ks}كkȷ5>qēiw{8ָy'\?cky~bnw@7kb1f14AL#o^1a1܋ggumԺ~uύ隹>'r0џqu^ '^D @'\sND @"Ɓ%?iyQjgt>-ĨnQ&Nc=~Z0o}|ocj]gM՟yOk>f}m{&v}c3E< 1q}hg q ]G+1:3;㩏ٲoڮOv}5.- M2|gF7@"D @"D @"p2 ljlz!nV:ku0o#П|Ce\c`C}8b1戳>~gFk}h7a2$2yC7y pkasm{HZr͹"D @"D @"{{e̙ѾR&K; OGwIvEa 3FLsc\ = ֧%yb^^?mr,@"D @"D  #{~o;gAwbW,o6ɜe{gVyr֚^gnǕE @"D @"D 8vhhqG+裑Cy0[5]}iVXxX5:ܮ>yMK5}szdP3e@"D @"D @N"ͫ6xаNor!f1D{lx.#?1֢_>!g8rD @"D @"DPׄoG&S99-/6E|c<~FwR>sÜOo,@"D @"D !ןWwK#B=` "~.bb_4hyL^/"1G ~w82HD @"D @"D 'k}G+G31ꣻiwng$SRunBu1!?qB Ոf>3y]/@"D @"D @"p[K#GKUߥS; 7M dh#b?ls-1[ 5s؜B \XP}E @"D @"D 87KQkuѷEam-~(D5vs kXXXg /(l;wVY"D @"D @"I ƭm{7oYWWͣo;2pp*Sعenzb!s"e?ټak YYD @"D @"D 'nF)o/G7~*0/"Yx8|D @"D @"D 'zvxďZ&m{ݦؿ{Fq6Kc7xYy`N捧OYZZ=@"D @"D @"p" =[]ֳG=712-Fab xsA<>;%~y8زy:yc/h^o>lZl<-9eY@"D @"D @"pn^0tm(orm]-}X3k#GbLu{>XG0Ɂ^v}w}f'k}֟X̑/Y9ݷal};D @"D @"D ''r0eN}򠿪9hקJ,f 5%v ְ>9mc|ⷾcb<'-ǺmWs7ǻ79I$<-cǘ3ƱgQ#'khgbOy˥GCT'@"D @"D @"p" Ю~'o,/ pq~8/<oB>>eϭ }׳ƜĒsƻXן{s~F @"D @"D 8uߍ~}o*jh?m)0PDxOA? ">>.| >ss}/){c}?h>qD @"D @"D '{oow^xC[`bc#h^@0ُ [ 13amg>g"D @"D @"W ݪҿ z8ScO['C?,fAZR|шah7 ~ck5qsMO1kŃٮ׳-kȉ:͜Ě:u&fW}0E @"D @"D 81<2J fk!}rf^񙓖=5v^O\84_RSaLƘ5:C#\yk??k0ZkyGNaFR'Csc͍2O.ֺft>a}뛋ijosOj}SD @"D @"DHw79wn -(^?}6YoVIJкo~W>p~Kq=ogl]?\Pxz_f@"D @"D @NձcN6[=tQcQMJGyu%yfu̻=оcm[|Ce\ֿ~}wԠ>F6-qo^h=~4 ؛0Cx{f|ce@"D @"D @N,ֿ}3]sKk-{. f\\As"93ϦGw~xguf>𣻚1v̤yO|}jokۮ?\f=Z1tY9f<d@"D @"D @NOR S/}9͕1i_Vk_EvXd>szc[X}%\T9&yE^ocׯvM䖉A iM=&9Zq~Sߎ'v=rXlvoN|~>ē˥KxD @"D @"Dn@tr+wQOw7-Z=>uV?("\'9OXЯg-o$/ǚgy2.87Zz76}Gcڝf{Nr0kynϳM31|`᧯Ԙs)q'Yg~: q=qp}R;~;lG@"D @"D 8=X@;w`x$,\7 8PP?0#121<|t-}js͹?_f?<}]^.\b@"D @"D  % {0}X?xZc[7D}Tec"|eqknW&ץКFwdkXp_~XZm.@"D @"D$R -MX慚un<?F @"D @"s|3ZC m\0ŝ SׄoGָSG= 1bs8.܌ڿs3163׷OT8j0?7;s ٗd f}km'#_lWǿ^W.ƓE @"D @"D /_|q:1ɵK5^WcxgK=g<ַuóZ=]\%;2q~9 s}@ {PC&x0_B2֍ӲAɏ>ߚYC܋Ϻ]'z\^k&_Obݮ5'ؕW\7xOh"D @"D @O} Ow.SL}tP5UBBFcF^9 G|_ۮO8a/q#7F>ygywө=קyE.s~s  HܼaA6/$0uǨ ~><9y̧]OR1ØX|s}{9ZYkk1w8799ƿx99n@"D @"D XuM~'%?1PD飉bK,1{#9+gxekç&qs}so=|Gs:Z{={" cͻlp~$$$l/?4>>1an1\|>8&HD @"D @"D 'k}B7bwIog ^ Kmb\8Ĺ b\?k7;XK>X`^3R_D @"D @"D$H GNK7vo (G>~(Zbk29@>ĝ7@"D @"D @"p" nZͣNo盋L_x[Q>kf#.<95C,>Y [ΝAD @"D @"DDHq{ ~yw 99$v.ks1~2 cǜwμ=7#lz?+9kD @"D @"D@M/Eh:ftOEE_99k Ljܤ->a>0{-1gz~D @"D @"D 8YWKҿ7Ҏ ;VY"D @"D @"F@=;wphGcjy罌nS={8㌈%\]aYAجF h0''ܬq-fY?E @"D @"D 8ޮYGg죞ϛz#0c pLKklps<~r.1g8fD @"D @"DD@UMݼxov^4_G0<-@ 7!İ5o1Fmk,qcO=sԟSg@"D @"D @NYWߣտn`Y8 XLA m/7c6ZZw6XƬqJts~F @"D @"D 8a7/6Fs7z϶]_,m#1=gkx#z/Yo>c;Gx3G~~ons,}vW}]SUߜ0ySY"D @"D @"G@Sti9~2n>y_^]~Z4]So%szkX61>b[11y̖icݶ|P}ΫY|չ]wS⛜Ӏlc 1qcai؃3ƀ53'<֣!i?E @"D @"D 8ohJF{W788?P7ntW!IJI|IKcօYcNbɇW]cOI}9|?#D @"D @"fFVVW7o5u^ 4Y6_ `k(WǧuYG }oXý汾C4 Y"D @"D @"D`_LzA;s-oYؿerky4/ o{`NRX0aRg@"D @"D @N+nsohhQp=1󧿭s!f -E)>hİ}^5}捧阵lwY5gfNs}bab{~:sk"D @"D @"O\_ST%3ֵ_g}>x\3kI|Fw_;'fO/yk 0Vca}ɡgϼu5-<#nQ0#\c!F'k]3kK0aZzٷ\Xs c'積@"D @"D @"p; \͜ ;i }ZM`gP>ܬ7+{[bY^h]Ǎ]+kugov\?ץo}ry}36Sk.[r}My~?cj}gs}jP#Zc87/}aK?qM! }L`%aB:}ck`!}I}/ ?c0DZD @"D @"D '@ mn߾Iw5=xx.o9FGg a?<3q:s3O}x]k;f<'>>ͷ]ߵm}{؜s3_bD @"D @"D ''z)XFʘXjõ;O_2M\9LرՋO>q.fh֢U7LJW&{V|ӿLr Ĵ&8\?)oǓσ|G,6e;K7'g"D @"D @"C hhwBAA">y}bpZ~x ?hy#yဏ<ִ>㹾՟oNY}2Y"D @"D @"E3wwaw|7 .:i3r@ff,}6c~~:~s"Fýg?zYca/>k\wM7gMfӠ-C"YT@!}kSwnvo@IDAT_띅7@"D @"D @"p[͝1:a c[7DM}~cG^6o׹vknr]ykt'k)@"D @"D @"p n^Yƃ(ourx' 0C(!ڻe3`czaM#3ğY"D @"D @"Cv7:ҿi1s}/7;~f̓ oM;<0}|;/pӮ'އX%lhbql0f@"D @"D @NUGemov' 3Za[ _t1~@`|ø^>k}9b[ܮ8ǑAE @"D @"D 8IV]7p8Z9<ΘyQWOXt;#sیŸ` qgk!ZXws,@"D @"D $_j7Z;o.3|Ulm;G!X.cXZ:kxAa߹Z "D @"D @"H74nuoۻ:ou۹7;'Sż`-ss.0s 7/c`e[^8J}ΚE @"D @"D 8ip7K~?<NSQ}yNZ.(1bG07iuؼ<鷾c<ƃsꯇG"D @"D @"Ex9@^* *`cjuq^m1ec.k&ޜ\=Y{8KE @"D @"D 8aԳ &~7n<֎}6ݳ7?ΈXEjİ΋s2o<-~br`֟Y"D @"D @"I ﰞut>qo13[ y/΋ǴıƖoY8!>scf@"D @"D @N$t\K(Zf7@;(yDzãApB X#Za~,1ց9cޖX߼G^>3'H8uD @"D @"DuYL=ZgLjE-0_0l{l'afKz``]/e{u?>Ac]C}ޮ%swR+ mỎ }ۖ~g-˅ +W,Ǘ-{O-e@"D @"D KLտdVmJ˃Z->tչey]5ìO{u]Yǃǘcs}՜a9<3f㧛1HFrlބ`Icj,ӈv}wc>1Yo}'{#|]c?Uߜ'0ySg.]|W}5k/_ _K~3 |'}#(@"D @"D W?Vb1k/.1X9Gg#cԷOKsT߽>1֟YsQ0#)g7!#L6Ӓx1khI k5\=~\us1\L[NS]/Оki?lC̙/_w_hy"H7"^ʗ/+|wZ駮D @"D @"D bΡX|Wb9g19̓gZֹ'6\g?ݷ'ͥv>1{toҔ1y1b L9HF6&," M،s#s![kXr`s}>Nu8cYtW{dlNZ?0v9owO本3eg|g]/~C?7p\}'Ezjyzr[~_/,Wz_~z?WD @"D @"Dn57?\jiRх8jkxE>`;O{P}:?ק}0]XsoѺ89?1̱7yܤ?[Hcߺ_W8sח_}= ǖ}ly'yzy~]\x/[^-o׽fy+_۾ۖl@"D @"D uMORO; 굴Mȣ(7\:Su3<֣G,f-;>wW}jvc o>ݛo]0-1y恀1\ǘy  ֠e@F>r<꓋xs{5G[s_LnNh3ђ׵j?q-ۗU_|`'>|GG}t'?\ ׯ?\e//+׼g B-˿ַ7~[>gxg g!#D @"D @^J2E:*F}Xhhg}Zlo Uıw=1+~j``LGwG>>rbߌ~Dnl`шh=9X汹&cbx7V#cד>zָ5gfNs}aw371껆So=u].qHG܇>|>|/<\IE/]Z~+7|g/7ˈO,/x~߾͟sҲ뉙G}RSaLƘ5:{gϼu5-<#nn&bbil91lW@xa'kycgL-k]1obse}jfO?cy;qu:~o[˯+#}{}?x}h'>&sO/POO?\}[[oo_-ĨnQ&Nc=~Z0o}|ocj]gM՟yOk>f}m{&v}c3E< 1q}hg q ]G+1:3;㩏ٲoڮOv}5.- M2|gFt۾omy}WxHZ>,_Xz<]?ϗw5\{ /_^_+jKwD @"D @"D lj,ĭjaƠx/?YG^ZrgZ{ϼk> a<1}Y?ǘ:ygs}P#6}9⬏߼YZ_?qb{&chB|O !79c<\ý_֣Z_sGZݮfco]֟s&1٧y3wkis-o-sc~3㋜?|?{gyr}|?e/_~M{ro^~{`~'nE"D @"D @"p IiSy> cj;43z-*9a-곣e%|oAY籾5on[s#jel}|L?It G2Oݛ^ ~s97b>-̻C_Np_>Ǘ>3}{ї w_~~n^'޼Eַ,_3j#D @"D @"'{C+G[7zj~NН,޸py@,~^08?_Lys}>6>O~i?u}-Yo\yuW\Y>?\<đO.l^.2ﺯտjyeID @"D @"D0YAKm۷F]sK~ ޚ89g y_/v~K?я-W^]tv o{-8|;T"D @"D #^$WCz׾fy_<ŋg]??<,ǗGyt:;yݮz%~q)2~J:*~8tY5ѽ:RߜnԷ9Xję^{w? ~[Y߳Zg'-ywb·#ba?\:b!PZœtך}Y>sP}mW}3[<4m>gtOXvN굯"-#/izr2cy.\X>>k0@"D @"p nl::Igh nCx?X~,o}[.-<̵k\:.B /,=Wr/=ז!pϝ;^63}4gƃ扎̋ʘ9bW5b#^>>0z]Cݏkɏ9[\a 9]CnYxo8y>0`LPø1lcL9cq[<7>YG]ݏonm?zYǖgϿGzZqsE @"D @baH/˖W˃=\tgkxO}s}{]Ϝ5l;Gf>3OB=ud{sd>0mF[?y3E"Y1硍t6뼐w-Eb-sv}XXڹ?>yI}@˹3&܋{S._ ˅K /;%|aƘd| ]0)"D @"D4rec[>޸79]3^(zXtK[zMx}?d>w|:[/^R.$xЮ^-7ˏ?< PAz*:Z1}4T𲬺M,OKAM򏌯Gg(-yl|l>G7b#?i6Coۏelw,^>{ϔ- @ @ @ -KmS_n}s `!ףE{:-[G/SDK]>kIi]95b{ L/.N3+}<)n[UP㤊g><>ybeTyWRH)CoS?<2{P'Rlaĝ߲?uڂ ɿj@ u8yK= Pꐅ/p-[AF#'{:.|R޻ɿuwr?\"}T9c䋣(@ @ @ ̎>nqiW 6E*g.0=$~lW^ 6; M6>ϣ)qﱊv 6.T4 #շB~0ayP_r2C$=NWo&52]?Ĥ8@ @ @ @`ζHg϶X:A.a>ˊ+K.ضݞڀX>OBG_-U7⿵P|('u 0 ,8B;Q>b6RC$+{,_Ʈ"S!O*| Qz6IcE'pCOl(?cI @ @ @ x!`CvBP 03g9GPGrX| ϰŎ Kh"|}RDXe[quRHe_ ԑoJ[2 3@|kEt<$ٖ/6&O]Wr(# Wv#*/!vtTgYLNnڢkg/pXM S%P~{NWꞞ59X*@ @ @`"sf^tL,ϙ3'ݳMwh)'?~ȃ l0 K .[8p _Nzgꫯvtvue*n;GσjXKsA#&J(yb"#^Y٦" yKtv|.ِ͊?ֿ/UcKmm_mE9RH6—} hX+ p j 5BQcG<|5 yKVRlȧѣL y5ax' " QƷO= +@k٣]_mcAN֗fJGӯw(x^~忤0f)=6hg 'C[:t(zuv j)鐭pd@ @ @ 08fЍC P 0sƌkbpaڴiժUC\u1b5ׯ⭷A{pP d8M[ҡC?2ㆹ}K @esL2QU V)uґ=ISWjce>>vM"Vugx?6dcS e>)edH/!Jq<a\d'r$ KRd(높!G"xS:Mom#X//VIe=d({VT[:߲Dؤ[V 3wkH_ujj&-ZeQs١SSww B/?on5{݉юUiϥݻ[nL3z3z g @ @ @ L:u0.Yö;^*Q=HMOz]iqA8jb|kSE6öDguNb`k[9PZOA 2І3<=Kv|[$GK)T}^SjFdTNC%K2TVCKytBQ٠}Rx'|ȯRH3vS )C?y;uD&-q(]t%iJ"fYŨAlKk"gg\vd/4 WȶEc\YnQݛ\JZgi;ӣbVA@ @ @ 0YUwhs),MSmwͲ&:Rb{'mO@wqs|VdmꅙLsy +9uWHٖH̷AU>f:&K|_L肶ЦΎ T :qI;,yyULUaO>%Q;$K$.|?et%G""#J%$/C޿lCyRmkE4!YʺdH7vjN# kYv]_v+c.7R.>4~/vNt޼} #ƶ+GUQG$˒nd#͙#a{3;;=6oٗrR;pQso߁i&[k;}[novzOW\yYҋӶ]wܛ 03:z<߷7U[Z}Z#dx}vB2@ @`T2Κ5+|P 04#z,2Xb@B41 :`/ V/@ 4{$_N٩60@SZb#.ba}093փ6eHLyH ⃲JzVU:A~"pz'UUG aKn1Q/!dI: `i,[-ȫ+dƚӖ鸧NwLl<堿gXJF*vZ>TP:p+gGڳ@:ح['csy048[nKWv ~Kjb3lU l+W^uyjkoK'|R:}N:D[<>#6phgUnhLgwhw!@ @ ኱'9@/ #1@`>lDC!ر>ʐʲI:ّ>@ SeE#h( ȊND)h/ߑAY6˞CKyz5.'EW+O+^_MҖ@]=\`@ﰩdٍHA]ȗ5X@Uo}E Uטf}FwI{,ᦏssv1>,שALԗٍ"~4ovvW|B&W,K^xţٜ@ @ nv!w [.DXDΡкm톼EZ,?hCmm{E7jK|;%b)+ a:Rz$g٬G _, _>| P>?F7>h'D&6LHv}:c2)D~?hA P'drG' q *>8*JzȒGWy6 [U!%Їq= !S 6/)$i/Lxa,]8=]|iiק4XCh@q`@z N-LK_~:=iv>t9g:m_Nv[%-]zrZrBTПoK{;'sY*cOz~O>jȌ yhܖmՠ@ @ @ Z!V+?f;+]*AgGm?b= F]vmGGgX7s\w{l cl;bydWEء}Lnܷo_ޕUPM;mw\fl&yL_ھmПcQ̘93a&ZVUl3\>+8G?lā͢;v6mޏ E/DJSȋ/%՗+qWNU:\+({?OV#K^e>6 "#WjlD CyR-۟`CJI}ȫLN+luS.6O6T'g'ʿyw^2n@ @ @  lsJ{8Cj0 t6n"ϡ+V.}{7^_]I'XY۶m'6ل*w+xs[*f"P/_,oO\W m`׷od[/ZJoimBXe>>X">i l ,򙠝+B[1^d8dy%crGFAv k&ґH> ;Y5[l͇{qm~l9kE[u`ov5gqoyT.SN3vܕ6{A@ @ 5]xf9fs3yZ@|ٲSʳ$+h=&1qWSxݵU+-஁*ۼ_~*|+O[Ql܌ff~͆F?VTt;^}T.-RHvjҿ(Rj0,9R5eu g4 >@"Ȇ䭪M#F)u_ˠ/. 3!//ڲ}^wu~B?怞WXxQmJfq:~Ͻ- h|+_HW~p{Svn-x2 Og2[HJ9.oe͝ߝ3VСCGHO8I};$m6]x&r ^<\sAY}~- G>v}J;r7^N`ꪫAƇm~|>\{]T5Y)}2y!, 1]ta>MCuUȍ{fh|˳kp%퓟1/m%u˶d`*(@ @ V}{${ݹcWȵ&Y-U*lyxkk'LRŞL}ރ Wqy~M8)gpA`bpں&e5aqs{i&1pj}Ş;m{cLd[=KNXWAOP0ڙ Lb0f=򎵶+$~p&1مIcYomb}1;HlE>p(|E@Q޲<öH__5q?ki[ Y*%6 #*VK˲OY :sa+ -ae9H˿$O*'|ɿ ~R/ꋱ2!K!!*]H_I!V+^T WQg )FQ3ի,~p;-7bgpp~z//̴om_R/^e`p2 H૽@jlM' e?pL+9pg bB es }Vo{Urgm%ӕf=='/fsqoϧnuݚXZ܌rr*}^@<њʫ./A݋aڔ7õif=ᦏf\21^Wf7i 7ٟ Oo~9/ B @ /5sMm9I$l-ĶC:KoY~7|ؾ6`LV kk$tvԷ,⹒hE@dқom8aespтH[j\~k{qV:dlX |=V=08~up0"4h`jԦUg}o x׿KĹP9t:+GDݼ."r<1U^+*YbCZx##teS\ ѓdY)>DHچ.TrO/_6zI^.|HXbO2e|%a o@B<|CK~hÊO)˿HUNd_zk:jGd2yGN*בN >'͙Kvf6 tSBPPz S&cD}Ö-[aI1pX&L{ӱci cL|偦CPl8OO9fi=2/陧7c&"kox0lEl[_mo|c@ն6l㿷[$3 @``np=0Y96-y-Z~ k{(u-K?[?=tz y[ssN1KcH<@늫.SKzV\οT VʌfDO<ܳ/t/`OiAygg+/3Qf`?}[u \x~1_6b2~cg&j_@ @ ,U03_D+"p34h_G=_EeMgPm45?|lwW@h[(5".;.mQ__n}?I~ ]-[vgP8\'}e`YAcW-@āҫ $'bS]F4"E!82TȓzȪ._uA!鉏t !?#*!Me+񯶐,&Z=GF{ytZ:]DKF P':(@G[pPFxKGm?No¿@6}7Xd!DG|׻-@IDAT+/d& ٟS;@϶9mِH(TZ&2v@}6{v{8`Aܞ4Alqtš)1:[[af{C< v 814|~\_\`{?ϰ(Vae!`YyY'KP>dmG,Vhyx׿\BK* =N{ɏ,fW^b>ˀ`ow*^ε~ r–еL\vqfі u^Gy>8|v+&F_(m$=`շk/e l10>l̙3UC'T_wwʲ-%6P'z?4|a0M;4Zs \y<'!}O[}`}b7S (2oV f1$׾|yd%\{Zg"H@ @ 4޽˫yXlZYjǸ)Ͷٹsgގ hCy0wG=·hϓZ˱V SjCȂPD\`&HzZey\ 4[19MDr%`.Պa9V3mRw,:=􎢶$ꉫmE32(~K:[UI=zAa:IdX9+;NSVWd] E2GKN<ҲdnfR:e^G2#U S#Dl8{f'@ Io sւVū> >o^XD@( RIbD[4+wUY~VƖwmϡNPluOry Y^uZywk6`Q_aă _*D$+bdtؤ,>v +v Rԣ Œ x ?rU=]H3vGʿ>: >:ȑ'\e3_@F1"ERd$ \4>yّ=ȗ jH&9~'OdSz .`G=ͦdT~ [}cҌHb~{2@~2'Xsn):g0tپ"GN:] [o.0 H .pÆ_ٻVC'0Og+=ttE 2>ˊ*a}HY`NXx(g}mغØco7w(X\&\X9]gfalTQy3PfO вe3hx'$رQQ?u:f5Nli$iu9 ڱe {@ @  sm3b!VBb{cϙ!R`nF}c@fmDoWG)ճcp4zl G~ 1goKL& gm`V'\F^mh]@_ӗV _R|W$Dʥ RʐSG?syVe^GԩMKOCW;:|3I^tq3HhdN 24 ˖lÇ'(C t/c yRqjd2ECOl S ǦS+. A>pu51[@'h4HBNrLjHա֨u"xgR}阶oDX/*\lݲȏ$KfovYԞⲼP`mRSyA}(.^R[޶eU6o^l}/^rޔ&1X}wGBG8k訽44x<~Ъ/ paC>b6GUm/rU ]eٽ #ş3NhX eΊbm[EöWl@ iӦ5̨?hﻵY:|ڌ{&_ﵙ@ v` 6Il@* O1MW{YKdTR JFlLg b )Ֆ*GɅ!e9אsؖYˏb_apm(Z7Kة⟤eP\de٬ wȣQ[pHK}?uc[~!_ r{$k{Ie?ro&ٔUyQI ^WVS@<\"dhxt6|RyBG*&ecc #$G}HǮڈPuOlىO3 -:ֿf̷}1R]:g'hΏgQ6C rv`ТN}y HԽwgڵmOKo=̶"To,SJvՑ:{򚣛뭷 mId\.sz'󃆫tl>7t7mڒ m_X fj&; B:e5#=7n<83+ [nY [?(^{>d{}9i&WÃ~9{  |DܮPnmt+ u5D:.^Vָۋ/k_qi 0 妆l @ L*8tXDfByb0?+Dh&ƕ3OynmnI:+ൕ)3:־sԼȳ AX&ow;|[ 1KqD`[_V&pƛSm!xdlɹ|e̳?h[6Q| fs$bE rq˖["i-V@[&"-`3@gq0(Id'%n,y!gtVpGm^"8sAw<ȶbrU7M1߬!a<*r0{4uT/E7KG{~݊S^}EZoYf"Oؐkz>3쟟Kײ?m@ @ &uVٶqвgX`qr,-?5-29 Z٦ K|3t+VMJu׮]+ckk%3N?vUW;ٳ'V8{"nZ-xga+7{N]bY^+1}0,6sժ&gl@ଳ̺V53$[BeB 0`wbzOdu!>@cs[ >eqef;qJڔ[gmBvSfɐ'nJW7V$X ­cCdG[lA%ē+$J2o+LJ/=l&RBԩMM Q×]+)v!kJ f7WE l /5R:*e62v]#)zޟ| !G^&Ve > S<45`& uSOν4_&mwDj%Ѭqa@|wz'kmAT؟ֽ&w~6&n N 3n!?477c&O`LĖ,TOݘBW^xehelnyqR~&)=NJf|0Y0Za~bO3O۪f\rUX:Yt}s"Nr}DlSlo˼L [A00K?`31S>{Ksf+_/Ow q~Uy݊ھ 1_-~cua@rWe;#s b} bɽs[n/||XO4@ @ &  gmO|Ww[ mc g2p~V Z 3"YAoc2m ` Y&yVF7y&tڪ@\g.F hPhA;,/b3&; Y J3i t#O3 0LL=|k ƍmdiX4vi|Ӷ=}ߏ`?hmT5@rL#%E<xI:|%VJ !oL޿OK::j?D0C/ˎRِك vEҥL=r5w±,F 3!57ƫH>D"=N6|;GF7V} \ t+'=e|?u\rlBSEZ$Ma'/ŝ6ބiSz9=ŶBjaZ0COlì\eu?نɔebNyk$=Xil͘iSsǞ^~k+3ҿo}!3+D[xYgdxbd[1R q-۞0(umϽ57x(3~Y<m+tÍ xgذ< j ڳ‚> VcȀֿ#%l^~/- .fk0}f6VHiG&@  gL?_6YL";8w}7W.I<`sϵ6O oW31Ɉ.[&(b *X βwq Rxb{%U;rSM_ J-&* _u}c%5YwZclU&7UW[_i}m_=ѡ+'LvK@[,/bUa1ݼoMd"u&,n4OvWbZ[g'g 6vŴ}h5 :ERTqQȐW.#p<1WyńeGȢ/R|@^zG''K޲E^mEN7BW޿AŽ2DA)h+~% sA< C,e> Д4pu:zc@ f"/,\v?:{#-7VWOmЀН6;5MCɓzH귦]';92^w.À [нm7ӕ8r0v]wko.L+!3a灆A*|g̳V`836ۈfjrqwMОoxE+˯jO%OFcn<1} Ij>hۀJbߊӊw̜Fv(qXz 0 3WeO<8PZZvmCטmG#oߑ(??{öE 2q*ZZaj 7oifZxiqX`W tGǮo )p/W ~F3?ي] \ebSf" @ @`"0Њ~Bx"{Ap9tX1LIoUi66ؖ@\~G:vVA<&h@Ԭ#wE_  ɕSȄ9$l L4t [ӬwD@-@&с[lE9Η,ۈ}YvAZCX &ep5krt/q'NJ(^->VR_.cOq[lRF<\lBo>v!U+=ڂ}t)eSH__2޿UgR;(x7%LFȨsK6SOhxM䩇PzM=y'Eʲ #υؕe L[V/̄C6 ۶2RܕCCOqOw&V9CAYOw׾:Ik}`{8+mݶa'"ɪLڬ~};Ƀ&=|zOϤof;bmAj4|_c=}.vU#_#;Q wons *^pg/z ď'/]MN]{^Yo~sy \1G7 H@ @ #w#!ߑ[1?80kypA>xJT7Rb==yϩX0>0r~;TB~oX[LI_jI ^heVJUɣC=l+M /oَ/c}R.l <>сH=xXٗ//G* /y/$o V$'DЊ$O.}‡/?JゔzW2J6dK26NrJ#ÅR)C)/;7c|H&4-ڕ.^ni3E,_ ExB+MXyU2͍vwlJ8-=öp:D_8̙xglͦይÑ`PNv꾕 $34 0_ƊimAɡ<*b Uv 5H[9e[8H-o=Hۤ{ 8g )]d+3>ebO6GfHQ.`<ϖ$=j:4/E"@  赉gKlA @"@gݺuG=sb=ii+I'4ްaM}3.2 н?ӒER'ʅ )|RzzTzm!ׂ5;RY>e[[ylj C>G>GԓM !#_k&yY6v+RrNvS“@tHt_(?ӃeײO`z| CɫHv؄Ozxt*Š#бMT3qnZ R\쇙-T,yD f*؅H!R,f!^I,x5Lfc"rITm2#A߾;hL@e`H{ ֑==#}Krc4ͼbSZgۊ?`@ߕ @ @ F)y~%Ga݀@Xf|+o5pacm۲oS}}~)M#E<.YDW-_G˧2@ԩ\_푮ⶦV6苷C^C _-_}ss?Y4ՠnۂhocͷ|D ,cy@ L*OQAd;C`}'ʹ@ [~cu>]{y/ &鸹s=hE @  {ufE&fE{ 'a;!ٜ>;vNlhm߾=TH'S 'F^]⡵^ފYBGA;AT .e탰ɇev`÷G]GXHWaK^b[6ē D хV*;PNaHFGlsd6Hr>NU.|x޿:$yn eJ_y_ed/r :qIO>W޲^zo2nHy#zm9jV{yM~ҧ<<;h<<ЙL5 -F>@h!?gfrƕ9,Ӟ={Y+9E>@ w $<`[zt9.@ϴmGē]/d%6%նEZmٷy>b>CO(JUbX*uAe>\%c>n}|:?rOR^v-[ɾzHetMA!ȐŇS4JG=|.>_WtO;7,<|b?+>e6#x*Oz4mꔴwo+3ڱ/;$9Cji=S+< 8v/V۶[b S:pѝAx@ M8J^KO=+_`KkPؼy-~{l'v4 ߁@ @ B{nw<@`# f>Gz6z]Q_Ċ7^=5H M~ԕc'P&ƓG/)d /WjUY&EϏãN>}*_I_/*"vE^~3ꟁZȘa1C"@ >WI_7@2VICyi|PO?$S'OpAB|?_zN>'0Z༥i.HS7``38bJGGIFU6e:ҎW_Ov:%Q @ ̸@ @ AQ&ع#-^xB#gYƌbWնkSiٲeO;v*޴qcZfM,>5ot49~t"q??%R)CT"G^u)_ xW=y:.$+Zu`\#/GNO{߱cX_>'% 4uFa\8!y0 dG;($=(c[cs%=lӖfp=j/z?2mL?|W?_򤓊@Woc[ڳ+M~èV KCѱa6$sFC=]w&`E` Ҟ۝T@ @ @ Ld}`Gڽ{wkgY>|9hQd!.e֮[NA%'f̘1uvv͛6 6pv`kv;?E mHp-#z[X+SЎب&~{Y"twUJ@Ⱦxed.r/}ْ.C/G(-oLWT7J~x]iŗtį/{)R FE:0<4odpRH=!l+oK^OI!?r6AS[hK|`'y/˿e'u{Җ{Ғ;s +@L_? 4h\t@ds Ez]ow_:e?my_⒠@ @ @ @`r!4SvYp9s&W7 ٓA=Ǩ,ng2c[t]Vױig0^ I$8`ۉ,۷߾ XLU 5ۙ3!qx?a'rB$X,Hbe|#O5l[#?_G *S_I^eo ;.627a:##|uFQ_zA~#:ľʞVUyqQ>JOx K]?;*|ȣV@ 圅\ζF7˖?9?唩#&ذe3O[bP6f2M} ZtN6Y{{il:dIe;ˑV|W{-mBv?eِ2uS&t]'nL+R1$j 5 Uzʤ<|.HD_)|RlȦEba> 9޿K*,ϧzT-9?RsהxtoN|ٮ,Z´cNVgn k }FG~Eibnii{40\+j@ @ @  8hZ.fWsM>=ϰnx]fO|0ӿ+g?B-?Xҳ(%y C +&9q'/⟤0˭uL@:.`RJ=:(Ϊ3ǎ)6Ӳ9Oml{m,##˾bCK_iJw)GRV]*#U8蜜89š:֫!JA^!htT/ؐ)mSY˗ [Bޗ싏MQ}X}&v|ߪs[dӲT{Fڞ]UpH?@ @ @ %kŸn_F#2Q@~r]AxBvjuxog(Ač8)=A2PC"~<|lP:p3V⫙G?:> .#ё٤ 6W%Kҧ ɑʿxʄ$Ó?\M"PcPR9Q$=x#/PRGnd)S'z O!#/[7:z]HjC#[ha uL@ @ @ @ 0@hX(m]mD4bAS#C>c_L MMS)JJS/+VJꔧN6dS2TbG>E! #.Euybʿ3_G4,T}&6H%+?#:֊0%p0R!KP ِ )ޗȣG;ԡwڊ/eҡ^6"#Vء¼_v!,EOvWlHP @ @ @ f[  ׻K=UThHحSt d d,*J2r3OueL'A)v Q/^2H!t$ n`1Q/!dI:  ҳlne.dd:5\BNv.)l?ȿ5!&zŒ==v"eƒɊHm/ [_rWP @ @ @ Gm}D`' ➊Ll_Е6s D$BES-f)Y}D$\YiLիD@=


I. XJ1/ԑB2uC?˞]¿*P(^V;VQ]4K(C4@7\3cG>LuaoǪh 츋?'Lh'yHW]2:ʊg[?4q9'eBq,$y 4iD7Z6<)|de_ҥ菖>?ʣv/]#O}lP @ @ @ @ (JLU2V!eGU⯊J*.z[U M!& O.}nJ"W]5:ʲȮes^6'SYȌ$|lbTe36 @ @ @ @ @`!@SNbebW+&mGY]btI!'SF;/TVFUW6In)FtcTT`XKFvH ɿ,F'-j[S`I !A%(@ @ @ @  +`'q߁3ƆZeS!_]VZf?n!p,Cɩt3䌛!zw!xSbtAeD>lN_P?.V @ @ @ @ @`!J}*L,BVR#|8५Ĝ!şq+bEUff O^7zنM!4lQ&ȿ|"KхT7!?x㒾neă@ @ @ @ @ ph@qrWRdtEz.P?~DjK?oOk2 4N|О2CN(3bD TeLb)mdH';`@plI>v ?xy|ΪSm @ @ @ @ @`"@x.p2d*r9- b G`cC_ +VlG`%\?$:ҊTO(Nhn<|Y6OH.xe-(cOu>eYAj`4ƿ;|+_rO @ @ @ @ ",K]A}Q"VJWҡ__(,֔E~M#?ӫTؕunm2ND臧aљfr`C ?O* ||/cgLH?@ @ @ @ @ HqMIym(|9ş#;3U>Oo/>OnE^Y .H&@S/]|Ȟ##؃O0_uv`=]c[|lC`K{@ @ @ @ @@zk%~ث2(/RgpؑfWM .CtGf d@IDATV$grD= @ @ @ @ @`">J̵YW,=婃#O$âߊR'[ &kbyo¿?>?|Hm)aI٧ )[UM,yO#NG*?zŲ`cGmy(?:C^%2' @ @ @ @ @`!0ł.I48qTj5";G?UMVr6>O*`#rc]HHNHI# t/5L3 ázSͺe?> R|Kdjet)S%ئT7X-1"c R ;|GoooW&F&(@ @ @ @ 92s✊r?_;%4૩|Ÿ1|>>x }T`ہv`;lہv`;lہe>/w=ԹS;w;_^sOmP|>n|5>pfGtwF5J ҈d` +P_H!~#z:Y 3|en>W|)j _[_'?)>9hi;lہv`;lہv`;|:;YO:*quykg_ّ3_/[g\3V'O[yoe?7ήͥ3gr/_.TG )QEug{#cfhba`P¯>qKN |§S9O?;̗|~?>Kہv`;lہv`;lہv݁2|\;$;tt:;|{gU9#'lU~9Q-oԓ'qr5qs?TĿ9P6?1.GS04yاH|i2}MBي%üa~&9_=rÇK?0k)wӗ><ņg+GQؿqv`;lہv`;lہv`;@g6ps֟OY;%;uj:W<3_#}Q<בӇ73__l|jG [Jr'/nΟfUr $kj%yMb^?~OW8d~|O_]',ww=gv`;lہv`;lہv`;Y&:6t? gB'9 팔۞v>{>|tŷ=u[Ls8a=1| /Yޗ.O ȡx1qzQYl&E_טpO᳅/ /vbͣy__͏,q} ??/K_1\,lہv`;lہv`;l}8|gͿyv>Kv9<un٩R6<}V/?d1h/[!OwOE3c _ܫߛU+PVlp@Zy|ళwM!jzy;ii^sepqx|/9~կ~g>a]lہv`;lہv`;l8WrXy|=Jo0tw{>5_;%;k!;5_yĆvol+ocq|-o~uG)'>K#犒?AٚPE6ۛCP|X CǓ4N~>^/=CpO|Ϳ8K~>[9P>g_W}sfi;lہv`;lہv`;|::~rm‘3?ѓ]vj<ϟѴy*{]WN? bN|ٸsG e+̉/GHQ)b$MFTfILVCLXwC`}d5b6{vx8O_-]?T/__~;O_1v`;lہv`;lہv`;逿^7_ҳǙ7ผqP矝\>;әc9 ;3esK,m}>Η_hg{~wƯ/?J }* .)RdDDȁ;ogW1h?L:\ƍNMCgl|g7WqWds٫ч~?Żۿ~g?z/rv`;lہv`;lہv`;uo?w~_{Q?~^yn<9+rjNGvnv@ZLgُgCx=ċg#ega8S69l&yߋcK5>WkVR 6!1U)/LHl]%>}/i_;L7 _]̦ >6)|y g0?~?_ַsv`;lہv`;lہv`;Iwsܳ_}?g=2v/9?;,to+=*-~ᫍ>r>W;M?QW&~x'O+y rW4eX|u/CuqnlͥtƗ#89'>j&~s}G?(<+o|w7g>gc纴lہv`;lہv`;l~B_{>?;?OQ?:.;:l!xet9Gl;K@|xgauz^ /|9Dn/yo9o1rU7>7*4}:Χ:)>>}2LX\B\*P=ĐQSL|u\c ~Cu0ʉ5e=|?Ͽ9M|s~7[gAgہv`;lہv`;lہ_ir{/o?{Gqผ:gȝγ8rNB+7fG'3l;\ɇ/rx0Fq9YI|g':yxtͩл|\lp}׾[ۿ//s՗_ UH%Vؼ wa'D>8?^q9>]to>8]>rOc |p>7C|??yg~~_&iv`;lہv`;lہv`;u/7ͿտWف 踜:p扜yN.1̴sNL!^d~+|6gߝ˅Ÿ5db =9~rM_0Ap= +}6HVad9ĝ'f3q`r'@7a؞—s!/>_8?<}s/}_WK/><>u.mہv`;lہv`;loԁwyٻ~7˿o/>{??Osr;'w6kܙ*޸3CpJ/'d980cE~!c\bߥNvTl7>qyp?ql*qM6n"ŷ;YX˷ d>/]\aEO{y&>8{>j ^]1k\ n,_{>~<{_>/|/Og?ϟw\/>8~RqO,mہv`;lہv`;l:y1qw.Rq_.?qO?1 ]"]b;.cz2].T' QapP׸:ofz<_'wMj,[S _\؊IዏX\,&s|s ~iX^~L9˯8zpqCÿw;kǪ/'{{W=˫z3}vw?ntݭv~b_|s~Gw_ut䷏9Ys$̡Ņx|RO[|/WyG\I.891NJP{O} 9^s©I|Qg^uOg'zt?㪃+!^OT|͚;Ч'/U<\ⳅ_%?e?׿_~׾ӯ|K_9h__<~ ;>/;ޜ߀y׉\sW}B =|>'Ձ]vzϮ?Iu?D~W_/}~3_qgޅÒpc|pc>@qr0O|1u匶ڊ_|5O821ۙ3;+8W|q*t.Bf,5b؊EMĘ\Hǎ>bÄ99W _MVg5 _Dbԃ~uѓÓ8!<|˃u/>?qqq' yޒЂAW]/Fl{9͏Dž_oU$ߴo?g߼wwgs]vw?޿Δ"勵Oszqv$y\q\r\\9lPNN˥sƜyĈWg^K1OT-r߸Zs kʊ"W*.[w#&_v4:Mg6M=HxeG7ĕ?YGXlQX 9gsALZãCΗ~u-f◫< G8_\]s{5?zQOC;T|MEcs5f+'}pq-CY󕯜lL?pO=׋k?wۚy'ÚֱWyg}w8m,'‹SIo5 /T.P:.erU_1Seo:Xzv?=ș|WýgSVt#n(^ %NB%Kbbg |Aq6\3!\,/kϸÙ9ĸfmb'>[W/\J?u䚾3acd˷l˯j&l3^lr?A+.&P9ϼ|P&O%GV*|jr.=^ wr׿ۻz5Ȯ?/dןk?®`=nk|xNp>~;SlX\b+.z$>_$˅YUs;ߙ/W\|DrV?C?99a'#g&d򗓞ol|X|κq.(aOoC򄅋8vy^J"0_Q8Š9_lud2/#?b#vOCqaOMgwcMr;ݡGws<38cs.]_?T?CxT\88 X וqy~/~ 13~cr>.=*Oc}nQkG 5OO>>Lǀ k|>sN Y]&pv\Sl:=&!._2[6ΆM|r9P. #lt-̑7Ę}/Лeʁ7zz{=T=p>_A':Vїˉ^l1|\0\7WIr;/eb5/[|o'{ݞߞs}z7mן[vb?{Յsۚ]r];ګvvy{I?yxIXP_K/"s@?݌ib]W_W\qziXÑCLjc4r+78!^yWa#8ja{mx@f)U(9&ğ^= /~ٌ]n:ZгӅor-/ǯ9wek/_vP13~t?tgK+\[qyž|ҕuoY ~>| _#/o]Z#wϾw/{ek?v}}8<]V:~A?~gyWOS?qvlUL1t.4_q-y܇s#=|:too,W9c3*9e7>c[=icg_mĐ?{q?ģ{Wզ7~~Fy_-o]nkkîB?s+c^ןL垎s߻ǿ{֧Ο=[{?=_;ToOuqd1ŋAGOW?/g󗣜aSG| 1xP_d?[K5sGb`6&w-|~*bW7r}eWSAx:GJPgn2{ظďŗrOr9; ea??_lhWcCr?5U/UBƍQqj!+cKLìCW OrL.k7 Y =sjh/<H<\pP߼آ)/5iƗ'>Iwg`օ?y`r47|O+P]u_7CN\ĆUm?|l:'/xT ӓgAW><Ӆ዁yoϼ~\'-d}c_+n{߻ɯ5=3<>?K9v$s ?r{ 8Od>{O3\`\ygn>31Ollِ h[We;g㏳0_lqʍ_ ϯCu!cW$ .3kJw/az=tD*ʤj!;|v!ųrF0&.|2űӑeƮ\tbؓ3]%8reO|^kƟ}Õ5\(]~ӟϾI3a̖%g+|͋?MLs.r]~~>/|ç+<_dozZךb}iMiG7}Wj۽hVs?o{{W~\?./={O.?ć==?Ky9ϟ _\}ʗ/\5v9:ėG_NO/Ӟّ81s3~ UYSgW+^-2/+V\0Ͽ3|"._OP](S҅_tƿjgI0 d(ބɮ&ɇps`Ol8}|)&&3q./ P]bμn_G'>jC_OFXrj o~|]/z Ԟ9M_fri,.C|Y.z,&.{R,Wi>2| l?zw{I=S]ϵ6w_ɾc-~HVʾ>@3#w#_vp?1o=٫_ __~r{jEt<Ͽ9Owo?+;'?6qQ7q~9ыAt]|q/:~]C>wJfŐ3+U?-)UL5nF6n:ΕNM5Xy0.We\>@ 8&ylK/V^ޛ9?2g3?R+j>jxiu%&]u`x| gV68= b§ ?/ʟ<+3>7z'3{>}^.O{ k?ta_+7vy{6vOCexfl?yO\ϢgsǞ?&ʯψ_X;%ŇLɘ1ufţtv?X9/ovEa'.5bqqp ?L?U?0+kg~y sF!'Wo2OoF&󶤈33mbӗe5N~rn1.̟o~???p1=㋛Tg|,W*L{'~1՟NO70a~X|07Daϖ> 5M|1_s%{o1/9+Ox3۴긎nC8xoT_^m/zw'ZgCG⒍N6y|A|C`g}1<1s󝵗'<{ Ĕ{kʼW㨸cb5$WMbڋ3 Qc;9=?d/<~ V[mqbG~ ӗ?T!*G5}/{W $g/kkabχ%^\`LGmEؑ~&x>eO/o~xA8o>g֦֍]58y:'׾:r8z?wqލ>Gp2?Wq3'Wwz|Qy5>$vQb>/\Xy|͟G/O7O!?Lw◧YqYmd_\X@.=VU_On\ّ>fm}+YtEU ) g]j=*'naք?KwMcxхo\|k\P?qu.?_~c gG!N[j?z-alL|2tg^l; 5.1~'wC|E|Շ!ctょˎA.̏ 7..lx(\'3OgCl<>>?sW(rpٌqs]Nc21/1xΝ-,7"?r|ϿfKcjGv??r+U~Sop }7L͟O8$SG7돸q4agGS>kg=c3^\y!:tf,?(|v+~#{<,s<ڸ螛 }}>\]oBC|#k_υ\{]K溡/Q?nknl]wc߹vs 6qOػZkftۅZ ^%<2EC'?I|W-|Ϳlxq#'_ TShqⳅM1|gϧ=>9F[}¤ĝp\7}j.:zß 8Iq&(5n\Lkl~QyrYMd7`ӗ.|2IW<//Ǚ|0'WÓWvXHDQLY/?=uL-2 &]~Ϸ9Ϻ S]9ċq>r'W9{aJw.D_^ _Nſi2=w_þ>V]wtOxޫ}}E<;.Kyu& ??ņw֝>Lj Gᆕb|no||r8 +.F-0'>Y̬>̟,^r T9\y̜/OuM|9`?uyʛ1>CxTaoy`W Fՠlqu>7Y{8ْؿM'7Y jL&6|:ķ0?~iW*P]?^39<˜sgb~g?}bGr>ʖ,O:2Jw>>1/^t/|Ÿu ϊp]Ү?^/Cx=fɽkvvu}Ƕ0jI3}vvs{~|w3<pJ1 SvW a{\0N#{X8,/!0/g!fO3'SMtt|CGO7ɃŸ /WHhFޑI%97 _>5rNTL7Qvl5'>/?s#}zTxrb̿h>_.qϿ\p_l1sh.5tS ;P9OWrp^Nӣ~17囜\b'/|?wG\{)߽<\][sX[=/c?:N}?h=\vc {+{W?o~K܌o=7|x6y>b?qCg|5y9Ąyt/pQ|9ňE5󅏣3k)硺`7]W;}ي ˸|q6٦ek\Q NUjj͘cMܘOtz82Aȗ-7.]|D@Cp/G|/^HMg?9擞\ 7Ÿv6=)1;{ez1hbEPM1Y娾8~{:gO#CX]sl{zF^/kן u~˾nɾs߿R?}?}u~_(}ԋ3gts=q.v>b䓫Z1gN(]1W䙓6&.d$u2ZLbqE¯t'9ӉMWWSGs /~_ ?\ˍ eϟ~W 6}6y+!^z&ǤO_.0goNlrVM堫^ 7mhӕ\tW=֯6d'{MuO{`wL<Ѿ? 5h?OkDg6_tH]lg9^Ӈ/?!w>I¸{0"3~_tӉArlßo?^y#U>Jg>xlNb1UqvMn1.~z"_]Sw/$&Ìgc"y[RU^CkL~QMb"7F?o{>TB_lbfn:[f69ç5rlb\:⏦Y]s>/>ƜQ }S_-b^.C P>O ճGV.qC|&:.4?6 >V]+B۾º;v_.n@IDAT?ogW3gmmo~F-|Xﳞ_E糝OoG zq=;p/7p#!i.UW2~oWol8״f \xGA0Ճ/W4tb\B7?Ջ/|^~|]нld80qSEykA 1R fCYc6n<7D~垾b~ uG_Ή_yp 9=Dg OtfK #/YTfQqdT ld܂Hu!gzʋ/optƗ<>j8?z3z>A^{˨{ϾwqzNv^ީk_~?|מo5?rPn[}.1x>g|1Q~r/oM㢊5ԤɊ >lpyhE&>Y曏87qyP?/|ao2N6nN/W8_|9L/K/z\g|Ղ _66}U=/x5}}lku3JP`31t2SzsKgC\b܇^Gg|c~H,o*_6LjzYlWgROqqM??#G=oNn'WI֠xb5Ic4P#f󝸍]>0ʭV>VˬX'>rT9嚸au<_mx܌'~XϯUc1oB=y=Zg{)vwxkE1]tnρgbG6kLM[kTl<߅o X:w\ 6rɗ w/Gm>Ǘ{NFca?Ms0\#zr]vӟXQQy'ވ+md\Mz6I>dxͷx7; nC<~Ɣ+0~‘+=\hƄ^6rNr'U|~rGg|>űS9Rc'7Wq*22_L^15ٍӅ'gc/{SpPכ__?+߮?wƮzg?ޥ}?{?wÚߜWAygRoun5GɭOb5>?щv3^%G3l;rS01c\,By9e>ƈ\ȼPGW.>”9L͟7LJ?7ΆԤ8X,ftOw $g?^j!Hbj. :x8Wmvߴ˿|3՝oȯ!#cd,t3EfA  .:|Rϰp^ِgGG8y6 b`+ħC+G|Pia[{w, ϩʰ5feڧ]{u=v9ߟ_;1?뺆3&qgcpVsƝcq1.>]Ŀ3&7:9¯",=slt͟_yՃM|:ꕇq/XlG?϶pg\NaN?U>͟X3>]yPc>lM-)(ŐoRf5+{k2PyE?[q?|g~%x25.(LdQc§?ˡ_`|N/n ykTMd1?jN6_\].~ØaS_!>7CĜF{w ֏։]>{?;gyz_?vv>aߟu}lC.Ospykނg)C9W߁zk~ugk,_J+8sѫ5y]go|Ͽ>́.y>j\N>(_qᓫO4c|_P8zqx?) Uctur8*w߸g\l9VwD1th○k΅q? хyÆ/9~-|'~_o^O='^?1w{6ez֏]w᭧k߿s߿em\WvӚJ}~8:3N=}qzÜ?]}{sEO᳡rgS=>:Yr{_ܬ#:TCg>88*7^/GXˇ&t|f=rFŰ/Z.>#)j.&;|Q5\ž6״;Wq854Yz~Q5&|á;Es Q1RsS7J_,>?/^_ȗ_xً;Lw3^{/?>gCu-?TutV>cn7wyŅN>!^8{Tz oßyBM?rUx4GN/6ys:?/_|c]< Ӯ|=֮kɓ=ںtiW{ƍ_u7W?CXKxӷX~ŅGܴ˼pFqg+'3~_l̏t9GzrOg^]Ǐp&~|Np_ V0$媎 R)$Hʷ6űr*䳖ƋlBb\儏U|'^`~ĆǷ).s.~?kM|u/:rxѝ|g Cro1PQc8bI/'_tMm5ſmX(FXvϾw8>j};EiJ7W~*Ͽw&.7g 8ŇCp%O?=|$=|3W/10"ӅX|McB DWx9&GpV-_/;W|yoT7Np6AE5Cv51eM2CB&n>8Y=*7_՟'gހӹNCu!Q>x~񉗌#>g|:ͷ;LjťS|2j~#=3zP9>9ʇϡ$_/w0g- N\}n_ˮ?ukztWh߿/vqs v?wӼ^}I:G+g<u _^2O5{> .oEg˯}pbX|Qu\y/fFyOE|g|t?N)7o ߸\%+en!IƮ|OW=4.nz-ޖUsxEGA|fg܍A|bk߅OWxIg{P[o'Q?0cy5oƳsͫآd9Ð'cėϛ~T|6z2xڋ7DW]}6E߸ZfL\{Qt?<~B/76~׍+m oBk!~ߣ g߿!k_rwiw~؝ۚRvq/vu]O?m/*%K=y _-g|1r6\؍= C_q!iӡz&>}uڼ?$<'jFT(T&St cWy^)rÍLJ1Pß5ꇘpه-\g.cj"w>]$7uZ'~9gw|biዩKڊeOoO}cuO2'5k{OǷ%obkO~<(S/U#pb\c?<^ϖ?~d|kөnKGFx8LA.ň[pTAofKdDM kKlT|4o[l|xeOwϜr&9fg7:Wnk!ObGKrtKO/'X<y̼(纇/'q!v$glˉoş}r'1bwſ~zwߠg}\{;:9߫Ⱥv?vuw_3t[{w~;O&|󷗒eL]&+:&.rϜU;5˷D'G8LW/dz~r7ݡ[Na /8xlU"u+o>g=|Mv>tEMJ5Ry\\CtGLt50f'C,/.|a~'˯<Փ?W_x5C?}1|[ЕWLKu8l%&]#^e.^qO<W:/Gy{bmoccg'zjg̾ڹڹx|O 9ى Y:w<[uyV|w~9no;"^!c^!^X5'qN ꪞC/=tˡ/ǬAߢy֗81[_<:;~Sl>jJ6Fx>iGbf?fl-!>EjCI\P]r_l~g)=/Z-jZr<=^ ub;͚k?.7c߿gE/ғs߷b]Y]h>=m=ŧ}޵? =ul{;VGOᗛ{D^-aWkp?gǫyw/~>ŊCO>_yb^ tr//^x--|䀇gMowA~sJGV$R q>]&S =9"?_TC̯3~x2]X˩;|Bcbxlφ¯/4pGB%{oÿW#r&n;6Y?Lf>':Q8ħ~^Ht+|cx!zTݯ|eŏ}aEg 8_j5?_X~H^)+'Y>_9H~D|\; ѣZ碛l|1_Gnln"||7g2@og|ׇy o|lӗ>AfSKO/rU+ǖ/9p}W§/.|:qt/?<2j~l}\owٮ?ޕ]OƮ{a?Fw߿oڑ}^߿9>w#-j=Gq?LoY< C6m,Fk>|/??[9Η.[q.w?ɷ\a~zGo&7 =_5o!>P}#w?ye?Y1cXK[O'S鳰N=Myl?L:??wn#Or OsO|:XqjB .CvE0Q>r4W5#ß{󯎉_bΧ~_|:_zNWr;ċm+RKϧp]#^ݘuLٗIH7J/wDj{&/j|r[nϧ5cA0fo'f_Cѡ˛_1/O|?ԇE'|ՍG|5]WWg×܋{)ԓzx_bm~oן﮿})}kǾožwY?w靰8~|?<{yҝ[\誃*s'ϹʗK. SMãs5>ċ _Jo_aVlc/?f,OP('*6DVT3*\Ԡ9tMt/~6B\7<}84לb̕܃? ZѹowO|c$/7#\tx;gs?޽~pOs8B/4[Ą&j8{l\3,TO蚿7qP !}hY#Ĵ'ۿGg N|>r/N =A$3'r71Y<?o9q|/Sl??y!Lcm_=gOv1?zWxc>]9c#Yop$,|Hb҅VjOwc_g=EOoן]ky;o.?`ٖƮ{'/!i)j*tF'^?quѣugG|мqO^YO;3O<϶yJ&F8`6W?9|!A,9>j~c޿8|٠?cTi~1f@lA*kZ TX9U0 /5^1=~2m0'>yvF>%s%6~~'K#k7CՂ?J6#H?|kMgqe+[m{#}M W%Sng ?L>o~_ƿx[0ԣVP߷pן*sI17Zwvee Kў?g} g?k+}#&AblWs>#.썯6W}j1ywk?_l>g6Wg&We7i[os7?č_[s{#fFkX+j5Γ3o]̛v1\[U-+fFݳngIY7{'?c7?J]=)g'l=D_ 򒡹7SX>g?iQqFx*̉ ʃMy%Ϧ>lOGdCN|rڃC6W)}%>P5~EhTzن*P1ȋO_v+ވ ^\'~Lbߡ? M?{+>h8yFr).w>]q1_t3#6ӑb4.yo@_? _?C| ''r#Cs|1߯uϹϞ{?}co`Nmg|8'a$o;>ÿGoz$ħ\ϟϴ }zq}:ķӎ-o-?><ʝ|/|"[覝`'Gb"K~yUǗ%7ʦ8?פ+LGSwtoE|{ğP=.=?!I ?+!z&_1>S~jggdW<~̓OgӻGd󥛱ا+^6bos;%|6icN6r=D|?IW+yɒ/߮?c9ׄ=ǝy1׆^gk}m?i;ךs 6GyĿ?E%<CiXʕ_SW~0瞯x6hq<0?_/ߎ`vxtx}(lQj#jod&}9N|\33|?$ȠPђbv՗c/|fp ??Dgl,|g?~cҲ׊+NH䏯NVIĜIgXˇ\.٥עͶɞ-xPq'_^ӕO,~v3:ƔNӏ<*Gn g;vszZ =+>V=ٽs{y/us:vX-f}2~z _>6||EXl=x_l٠p&ZM-C.||xs}deWʉ<\1DlcH[L~K?g\|~~7??يUnԒkQ+S?U$Y䵳HM0 i;mL KN; 4ylo8th˶\٥'pYp!&*&^l|3l'(da?;m1_y >6j;Ln1g/$k,ɯ{:N:wk#{kkO!Ml5V^ ]cMd+n͇_?x>slȊ1OLOFm9ѡ7Zsr6"4?lƓ>!S2,|O*m CP7+lƗsO4MGLlwƮvg߽wȽgbz޿[~=ք1ύAOq~sF6g{gaabigb^|/Fgˮ1Oww9;$Vr;nm1Oo"l6Y1rO~M?io{d>|V8O|ӅE6Q0'~vBO ?gI!P-7l+6_6c_;c5bd_|O(n6;-;hFM{r7V #TO_ig,|-߃)b+̏>ْ+>=O ׸a߸%x'>**8YGͶfcN9u;Nj?>C5|\#={tg=k_f3v{޿//͇F~%^83v`L|uu1w_]qf_^#glL|l<dz5bE^56pcE⤟Ȋn-;̃,[?*1}X|#Y ~لW (j0}%g__˷W$K*|1eO?m=3ӱAZ-hùf-;6NJ8a썯e+~ˋ->Ě{ӗ3N-ov|c_~YZ,t_ =/v>g>/í]οd]{sQ ?5c?Qc{z엝!{r|̑G]߿=l7w^`DI._z1zGo|\fb63ahG&a'\'! \һc?bק/o'iS<] d|fl7x>Ҫ(i~I^ "ދPl`鲣ówɼH-YYPhC&>t?-ZXŮ=DxDG钳-&YgEk˭">囌<|8?}эSp҇S>okb%+ƬWl]ˆmiѽdH ľq䞿c{wg9am{9љ/s~#ftb]/vy__ˆ}wJ2/!? __S/Cu#)/V FٔX3^"~}W=ұxŜC}#棏LJGnmc9CV'$ f%W WlL,zqzxK^<>]vxS^9gO8721G%WC~FIص~cd>lI9!<<2M;xWҕ'3g⽇O/U}[^s8۾oww}uǞ?{{xߟsk_{zGG}֟3?} lOrK' ~~ٗGc/dOn-wgSZWy)?XWߘأ9?o/'+v7x4 R 'g@ע71}Я3ʼn?D7S3;[~m~_/. }v<1k.AFqԊA"]8tJFOóAgK0 ݸ(A _o?gA{g{.Ͽ[ǝY?M/3]}s֟{jw|Z[(|m6Θշ$l?w||9xW|8~y _Z6Zƒ=9;~{Gm1Cte3 ~;r7_lbSn@~b[(I4( 蔞G6;}f!%ó/>=&PE'cW<*% }9x0҉->,~c pk?Tj6~D&ʞ |/-^>NMX^?ƫ7Ng=v=]kX.7O]wyk= ?{{ݢqKu~9u֣Tc;smN}fcG+l|r/1K;ϘOcK}raixr6cڊÞXd#9x}9N9}>3?keđ;ce;ِ!6pg,FJR`UL|E%C=Vʞmh7 ==ӢZ|_1ز1~:XT7|YS\Da)M[ wFkh_l+_?Oɋ[_mfͪ;գG;sLzkSvG{3o\h>{;c{g#s"ϳ{׳u/ܩd3ɛq€|?Ll#/FOxtŞd)[-T :ww OHh>x3nPߩ=yOAMsl<՟_-/ obN~;ε]9wBϞ{y:uڽ18gϽ.iuGf/ZOYF~wϜß러O|f[xa7h3gu{Rx|}z<*>ߙgs![(|t`l/ q_}>TwL>_.|>/ ~-qkѽxcG A>unQ~[|k^Gws'qՖ9"$ⓗ.6LqPq_/78-H&̿|ûcGW|<_xC|-jClOl_O8i?uJgݶg- vݯ1QgΘ={Ǟy0/d_I߽[޿lmlS-My}%gwO{=>00-"}xg&>կ>+?y~4CL2v}l,F ]{7|Yxm|GUݻm?? oޡ?[koSϏ)W?ۉhbU?cć_dѕ35~~zK@1؊E MzQb#ӇD(6]_da^|1&>=X֢Ɠ1Ə/fz7~|sW=;tl?%¯61O1)omx鋭Ud{ xh닁˧>]'>jb§Gډ}7x6~vو5sh||սVmPآl;vu*<}cgsU٪.>{8gIsd_{޿s|]Zx>s|[<5Ws}+~wɇ-yuC'c>bƣ_l?S_nZ1Wl-}oGN|e3>}<?cџ?7;6}X%cS_f;?ǯax?p@IDAT$w,VY9e_c< 4F=O?p1eO^_r/G1f_'aO/-.9g]/&Kxa[;Cd_ -ҲK67vjOV0|=r?]|EF[sn;עiai[gνdǼxuXW_Pklgw2kgg=%{xCwΕ=O޿ H{6ywL~nז]_}aO闓.V,“͸G3д 'tɵ ,|UC:6}|jx׆{?TOyP=dl}ŗ]{Ӓ9~kˏ= X銣-Z1?_GwQ B$oZy^ǮbCZ&,4}t/[v?ç qzaȻFI7˓?|Ė_[لW<-\6M|ؒݰdҟ+NE}6~6Iahq3?|3ٱ-8_r9{oL*_쪟_vg>Ϟ{??ܓ;vO< ޿N! cC}>ͳg[/WW|{ _賖mOq]O]!|;:1n_iKww|-g?_r~Mg>L~-|;`,B`'GϞʮdm63>{}:=l-^|v6ʇ8ȲKû#.bӘ >_Ts\/oοΝ=yYA{waOomvf'~s7tټwg_!vH+Mqaoj-= ?~1y_WD(G C_\1kM|&?(0iŽ?[ej5+~6='/KA` PW|TA*bçxq<;~>#q^?[}ԋ#~?_&+>Q6Zޑ?ٚZįXջ?ג7m~?8oL$K8%GObz8D73O=+>?&>O||qն>(;gx\K Qj~>|0?=y{9ac_{<wT`~D5{U&Wf?#G|yl#yx|?Ϗm~/"Ca^EggL~WΚ/yd\"?cFoħZMuY rN6~9d^r>B5UO߼w];g,Q?KsF_wcϟ=́{eu׊ս_Cwv|{W_ߦ[Yț۟Ņ?eOxa4~S즌= t_8$n6?ن_d"ӿ97 1X|Q%a+dk3eLr&GuvmŦ^<;:>,|HL>̘ad%aWW|gNXĿ◗>9|~0,rg[6ᇕ}0->U6eէqΘ?XӶώMӅlь[ 6j{Xŀ;eb7߮枰?{>_{x~c u6dy{{ݿC:g?_7◿XOY1u`|썯gk `o5|?0PĉkLirNVi[a 3ߌ>9{mcHW0ϻd|çGƄġ}cw-6& W+'J_Q㑶#=]EWe>8PxJ^rq'>>9*կ\M=7{M%Zx(Ox|WWCW^=ҏ͝kgv?Nco?/j=˽E8hw~;m =Om_!(N/~d =H1;o;.~13XŌ+y>+?/^)? R*,jb|r!>jC3Oœ9f}y%xxGak-&>x!21+|(=~6^,x|eNls#k<?0 _.`of>[~2O';kaj1Ԥn_]vEFds05Ο{yʱ6tM;?/C}#oV?A8H@stoTd'*F7}6ZϮda~G e/?O&> ԜrO=o;L>x6jf!~&8O?}x2 vb#|6T%zoTv}/̇"C|xv9k9)q=:XjmX;my{F/sa{|2KQxQ{Y{۫~_y.59لb76z6lf^Wx';'ϘGT/[<Ҳ+=߫^?x}O6|:|2<{Ĩ-f|i/JWNIR R.ht^5G_YPSk_XyMhⓉ~qWZ×3C|O&Ga>G,-8 o<' +~տ:~yO|>'~ ]]7gWl-*lww/z)Rya_{޿́g?ͣ?D?ԫOtơkwW`?ÆσfCN]uvG=Ư+V3{:~yg]z7?iï'}gGNˇ7' O`O̊_1V嬈%+ "z;MSlW|yO&|DVZ:qiaq|س[ײa.+\e'9>+|v?rlV/l-҆[ߋ_r_syc;kaiON0Oj ) 0咼(X/bR={͟>~N|\So!Wpoa|'~+}9M}/H o#S}?Ɇu,|hз#+z'~§>s5>ş_ο믝l?s{{v{?so޿F{9y{ŵ>|Cbj?{d ^|~_~9ՊYN{KW>[v軿O­3'zIO?]s}aHh+bD̖\B^qwЉB8؛]&utAvt=/g!SZ2oB;؛-;|H\,|Xዟ6Y~c[|٢{_.~_jZ߁o owzv}??~GӾvwcֿ|7OŚm~?x#'Fl&ObaL|7&š/} =;T>^?£ډj;T؊]⧫/wI];c!X%ه exEGه/l{ҳ^aWIW?}$#N`o>ڈMoa8˜t/Fc}oLJ_<+>Yzh5W#_?/8Wzvhϟ=mdNt'#~?D_Ϟ?{X9}ʻj/_k}c燶̏]cȆ7tboY:_>3˿9\ƅ_n3V'Z'i"mڛSRT^Z9)"|WG ]NX-0O|?C$r᳙×ṡ>JP6p˴_{O~qjg]=>ct|pWϨ̳窘kn͕;{^b}{VeVw_ssuU͞?{} ,?{VeV=;wޞ3q.F3umqg|bOѽE߇|9*Fztw7&ovSߘ&~s\'=p'>]{8Ņ_+>M|?gt=؟/]Ϟ>?{?O?9޸Ͻ~3=W{/eE}̸Տ<EU>Fa쭟|1٦zhQbj]m=WդK6A9؛O6[d }rŋ>9*.^oFc2_ul&~c lWdzl?]Xx6^eFtQ/|}_|>bէ<vb_FVb{&~ka<#.T ՗c/k}6kLRt:Ɠ2).7r _cÏ]=%=_~*/2<o|/fc>||϶|qOV/˹}o|}w@-vk/1>{ξEjnd^sз|7hanοV®?B{ ~ϣNէUfwan|uB߻x/[a٤j[6{?T2uӗxS^-~'݌N1LJ_Y+>v$߉*f/X+E^^a6i|kuL⤯ώ^La~9ό_ȊG?g|q7> ;e qN<{%& /nq4'롟x'˯?ۏwϼ|[^scdzGz{޿G޿ޮLO1O+9Go$[G X?ÿo~s7;|flIl$nv7s|ZȮ澹&:?ƃe#m}v=Ҟ?=f=羹cm_4=Xwx{ϳ:|?6Ś{w}ogˮV,- 狟6?w__|=􍻖<M mg[ $;"W^C(-n8;[|^Wl^ц/vr<:O1mb"V%OY6-'ӎ8izd^ }Vq񳩟3|vyO2|~տ]qӿ?2:nվzW[^;9=R}sԠ}d3b͞?{ij_sc_{rftj9T{VNF#:T{okv9;].Y;=ӣ}*3}/g6룉8k:nli'>OiE+\슃<ȐX4=k>O?HO>\ѝ`S>f=I^–oz:|Ct/A.FK"c$_bMr giMr1go#@^gט'+G!׆?e;NƃS>{X>{3>{?X`Og+ƿ[vÞ`G~;kv!avs5A?O?{?<UMrgumr:}{y~crF_ט8D7A7?ulC]l7{]񓳥gS0##?⇲%=yoC%$4M\aO5gѳ_ .yd+! .,>ƅG-r1==ֆut _,vƩ3ɗʙ?*O|yM*>ݳ}_W9r2F7̿9?vkY??f޿ϽF ?-7zw_-dJ3odK7x;%}?D7bF1oǟbԟ>>z>0bKIjW|'G߂$;"K  rћM:zq~0dg0a0O|<90^.b?F3gs<nq<9{;;/6]7~:OZ_ků }#_|Uyox:wMv9׏cg߽sϽZݹgX_^\*}_}w5"kLJWi^}/ $>Yd'':O5*>sly#=V,w {e6ŕgE^H|cH 66!za{ 9jVD6(wß8Mtag(Vl'/9Gr#N|kV -?Oζ'#\+~>>GWMWGk__οc|[7{c]vnyRj7q޿WK6_?:v'?yO\r=]vFTOZ;Pg{ ݿį>b\-&!\no:h4_| ΋B|/%iCgeSM8Ԫ[gK7'4iOm\9c/(?>cxahٲѦ' ?&?Qd#_?d`wT}Wev=ܞ?{OgkyzoF }j_ Rs4󧽥A1i5Zz!ڟ]pʭ,b|E,~Cus/^NObO0t'>"W;ǟm{SfxUaQŮ%OeeE᳝z{-pk*&_ϷqC{>|WtZ$MӡӖ.de/VmS>/6[ 'տ磆/xOxowg=zr{vPc_?s:c_3Ǻ4ZL?OIV,,m¯+,Wp#>]w-;6j,ltٗkŮ?\gx#åW[ GKM__L&]T5WE E/'2I$_> vOFN^^ Aޤ;ۈvd@A1gO7 4g&~c:C :pki3yy߻$+.bW?c=.} {cg?sc_ s==u\Np>s+}ߏ^_.h\ 9ҡ_?y؊5c\ɧѽQ8bGt_]صMI7R^tR^qyzl8ϗ=?k75OO+<{awarOc&ώYqTr<]?؛=|zƴ'{.g_U=?i#ſtzz;s,~cw3oC ~`oTnOt񔓀aY|? +|dQ1GGP#,1dl{>5yK_;8wbŶjM@ 3t>_'M|?wqkb=;KO;_?{鋉֟lNJxӿ>]EOr‡ϾW|xHv(7N'~3IOK%BE^4M}Zme3'Jy'YO?a"_?\θN&ިx)6ޏɑm9r,VwPzlO~9WWr S,?js}[cԼSsW 1WZW5N{sq}=3wᅬc 3f%\Ӯd#œ_8bk9+ֳ;Wj޵?g]H^Hm/~姍û'r1l!O 슟=b}/8q',>W97g~:x>_d<ۃ}_? }{ha+ſ9"gο{W?SDwŞ?{gT?r`?1v]*Mu1'?ۉr3ϷW|9Fv^C/GMmi=El=hOoWAd/N{l/D~<h{ug_g'~yO5y;Zqk@dZ/ %għϞ1Qꤟ8y6bdN[LIV鵨բer+n㛶~`?ხ}󣟘ͷ鵈F1'g˟1͜?Ux쫭Kk Ѯq }d/{c>篽`{烳s~z+WsVo|kѳ^z'{8Z>lù3r-_8D+>toz[h_y6drE1O7?Os/&֋h^R?k={AM&yvr/` >4Ox C^g{ϗ^GM[.WxpסE%{UGvXîǾg1XRZ!hP/3|ٱy6C|f;k5=׾ϯͮ|<Gh|xVjƎM,߹&Z3j;9?=>w9}5㞱?E8H-?:#?Ήkrɞ`=x~~U෗?oxv/{뿜? _9}V̿r >].jX=,\4jEr&3 zdȿz&_Ò_Śx >ħGM߃?}l xD0__\x4N0/oa-XV6ņ=h:zz۝ZV}޿쮵?yaOߟAz|q?|g<鴟[ ?cAãt}OW.ZZO䝢wAb\ȯAG,=o{1^'53Wli4cפ*F>|m~Ş|kp.FW?:2$~,|{G^#o{|ɲ{ǟ&~Xd[ϮW cGy5ϱ^w~Www_?ͻQ;|p[|'[g{?W׿E{GOy}fe>U'r"{3fs}`_z-}O 9/~Nl[<}qǁMLn-SL6IF&Oxᷨ=r_vb{O3}aŗ>*WėkYϮKZ 2mŭvaس#/v".jmd߹̍]Ϟ9+?޿Oa=?Ww"cߟm?|ϼUgm<{oSN>Ǝ=,{i~$c_L_+3N[2{r^+vrvdPVjxjs^n>~FIB^1c.(6sB?3LxaLP+>/GaSʭ~z#6p+>d_ o6{6|ҋ=?|y{/6/Ƴ^{Ζ|osBv=kY=1{z{^3?5~o?}s=OmoY2Ϧ;w6jAA ɾ>ry8{mvã6iϧ>_?hod_ϖz 0_{oMMlL^<y:>|䩦dQ~MnןN?| CL33| M>{"Xx6_c1͜n9xdaO|\E7M=[ >L<֟_:_~֟/trh3owYIjo; QmvwϟǹluXjI{sP£F{?Bkc=~9eI<}?Lu+d.߹3t۔q&^RI?1'~//нw)rcٔ_M>"7OkM p}6s&-=|bf >[~H v_2$VdaOy3pj*>`/6S~5+߃3'lmXŇ3Q秿gԡzճ9Pz?m?oZ{\wu O{<\hsdݪ}bUݫAԽ>r|[Eg^b~M1}.??=KOxb<+|~|+S{ZS/dAƞdN&H6||e#36> Xwni"ۏ&%>{$|>߃*<_񍅌b;Icߴ lT߮vuz<Ώww>޿ϓ{_\fϺW׹:7G#}nC3W<+|qglvV8cS]gdO ~mbW𫍸JR5IL&:JV-MadҒ^D.'<#-i'/g߿;g~\ŧ᧿Ct!l?_̏O-՜T#w?jZģ(7Rgο|w֢51vyjc=m=k庿'u~s[]Vy|k?F>Jg)9ܚƛ?~R{>aN-ugǧX&&쳭_.[~SO%IO_<K[._Q_9'꥛LMDf&I:h5'IV\u7ňﴕ~8fs4<'Yc~u)8m.7,~c-O&d[}aћ˝ [$VYv9t{xf_?|x=zOCV}7&?_:4vgᏉ_&DL&>JW?M/F6MiCL|rŸ{#CI_o+~ p+gŚ;|U/YqӊU?]Ev?g #tz5rŧSF?+ڰ&Z_{ZKWv9׎1_ɪ9~_Uoϟwϟ=sc{莰am ߟ9ogٍ x7>#kˇ=|gXg^Pɴl6qSxe2гc̉Mڿ'X&ވ kb?mҋO I ~ƣ-Q6?2b7+3maߛ?K#\m?s>7o??im?4vUhᅬ}F=|m=gj]g5?~IDAT9o ~6Wn-/+% ?I'M&ƵOMie& ҪECߤhBv(ݴk_l=ʲWGfcϮso˞?}S-S>?Ͻ?'GU/wl2H}u]{ƀؾ\t8W]7֯c_\jU}MWa[pKEibE׉8'i}R]'Mpm9=ÇW>gr-~vC-cK%&+O*gƿ[vy췻:}g߽k>pQg:xco5'G?Ζ g?'3|=azsh&myO&W|4OVS,jM 5I$-ʾUlAd=bs#ȗS[/c >>#gqwⳙ^s^`W/?H5ãοֿSoW?Ϟoׂ9Ϟ?Ea{gs{g?P7?:#=W?p'}{Ad=lo\u0+m3;5vvk$A?&e:7\i^BʯQ7#ĸ4V%r k! >-{1/V-~=:}zhՏN.uX;F=!?=~_9<+|ً5`~迥_G?]e_kO[]qS)TąozIJ?(~Mk-qu?!G<}u-+ ?>U31{4e:yto1Op jLhY3/i@λ-{k:e>,z_gCr3Gg>Ϟ?W=?|YϹXo{t/{u~0? ss]m*k-[?7?s>SWmh+Vl6f(nld}w#he?ڟgƘн ļo'>:=s3O|cZd< Ͼ_xFkU^VskС>uFݟ fKݴϺcG|C݅+ߘ˱?}^gc`-ʿ1nV: cTU5Oh fg6`No[o}{v?>S?X9rgzZ3{£5nr8D`>ɯ4bx5DYMy y9H8#Xaѽm_=wQu}Կ8gr#?@/zO ώO' ş՟O'l( I3߿h}{~՟9F{~t3 Ұ08.\s&Mygn}1,<[5S{:ǘ5b/|C݅{gA9A?5y9ƞ?l9.𼥟u (/K/P2\ߘagߊ[1gn.`C,I,oG/GPb0:7˿_l17)ij}eڟ},\_3xh{)5>[8Ĝӟx0 9K5N4N㤯y:+?{@8:eM`|TgSπ_))|?S-Pk޿[Ϩ_~7gml'W#qy9\s22~p\4V [`wV5"AZpwn )ǀy]B:qosOqzurc D--kZ?ZY/?^ e_KA, ou?>[߾;aGk2gwvʻʨõ>FC@m1j3ڜXqL+Og=iUى1kt@3 x/^u!{u=_w_?d_g,_o[-?VM_"q hoa?q{Vvo^}yҊ'rꥯ?e'4J[ƈ![Mc͵$ʱ/K>3? -̵B_"/~ xq`g@?x˳l;P[] U,XrVUVocDž޿Vg[`P7ώzX2K =53=s/|my{tNyCruyz ت-NώdDtx&\ә?C,8Wڿ>>tL|H}\sY>-oǿϊ?X2؆-<8d^Ϙ9?ȑEGde 3fN~ꏟW`ت1P>Xs5kٿrƬ?>˞[@x@sy,40^.Zcg^bn켔E?scD?=uCu- Q܇ D&m-v"oǪfq~Wgww/wL$?@/\z޿y~3Ng[PO[bC 9fC_r<+gosq? 9)L9#Þ:cZ]~ ݺcW~X23 m#RlDWwl 2s[x#Ѻg4r;/y><{{KgbI;ȸ֢qnct޹zա<Cf zn[xec=ybG3W* ')S]y Uwm}HIǮ1hmTVA=Eu+}Z⊭r=^b{=JF߀2Y萴8DN'#q}g{/o^[=?g<9/ۧe|qZȰJbgV 5V;dK[y??v47ZS~_g}!}Zc}.$(9XWߔ8#9Hyֻ>XyA͜{(#y=DW:yvz:q;+;E=ގ3rfȠ˹ /~AuIK5/Dv.*.<-|R]>b^caSE;&JVWW ,>!I{bؚM{3[ʲsy}ܷg@| >tv]̹y MYԃ,4d|^^gYkW-Kg[.s3bYo,\}F5?WĜB>c_1/)[qe!Z6j3ڒ|/@|3y&3D3`\+hyʸӿ6@0=Cg<򿅏T7gǛoA)%|{ՖW>vMlΎꏏ_OE|n} p1ϗ'Z?;𔥝6}/)kR'o_sα{(^_ ?;3l?'c*oZC+mS)??_0gښ3Wm)zkYxhCb-1gcX8n;L|\MN:y/Mg_~t|)[cdq~soǎ\Rbdggg'B9^`z՟՟[9㽾>Q `2?[!8Jk L2P#9 ϶ ~>~1埝d؏T-w-'Ȯ PLƨ[61O>KgMHQ| .k? &lO| ZxЇY55X' ߶dS϶0 ->S]8Tl6 mkQm{?viݰ״Ϗן{p?!{PJwkٗ<ȓOK"1К}akgkԥnw+ NJ6gL}Io~?f/Cgz1}X+yy_/~o oW;Ηϲ~#&Zow{mċ־^я~cxKK>YH7~׊ [OwB} 3/g֛ǙǼG_s{beӝ(>| Zˌ>op1!F(h:8] țM:&M)vv9%oL>sy>F}3Τxi^)goOYْA=Ɍ$ӌۋ^ gTқK~˥a\Յk:9ʕlCL|Ȫ-[zc3ñ:(vtצ=@zy=\Q.}pƙ=d1ZZcؤeuǔ5-ԓ̸{/[FWÙզs2riߟՒgW_drEɳ'gOXjicN3}SxdPO2lۋ^ gTқK~˥=sW4ŶK4c&Slԟ8Iƶ;if$fS|缱%k?fgbg/A^k\04{fG7y2='͟Enwʊ|&c,x `́'YĘ991ed1_3.Yu-g3ה3F=f>u>1ٙ[?m{(Q8%˸ʫ߸dūGsB 3>ڔEZti@.̗(|M>`3'YĘ991ehgS7z>}~)5'YĘ991ehgS7{@ AVKnP̸sq8~Rřq:cO5CnC&O7fKy9I`~u itsl?y )mV~zLkdխ i11_guց|Z~YÓZ2-Y13J?{lIK7I/>[}1GzkK3%ogN5.F9QSͫ;\(7dف,Ӹ6w̽#;X/rɼ髅VLdc>sbƘ=[6?Qqh^hB4'dƵiWfVѵ7ǔxcq/1o(}ɮ'Sͺ0s̺glҕX3&ΘũI-9u]W,s]u} |'݃5ی]gl[o=#ߩGfu% ړ|ZI>CoSV0?YMrk$8tXz>ɳ%^;!y}keΓ❱u\ϵ|}ɮ'Sͺ0s̺glҕX3&ΘũI-9u]W,s]u?> V%KYS<Ƶف 3rN\܌?chQh3s-rr[?ETEsMVԵgȆ,LO`c]gG57Y S4G5͘f$'K^,<72m?{'<_̀3(yf0/qaOѳ10N|dPD2N*$8O |/Y{6$hkCb%O)7Ss-={r>ҡxTnƙl~h6lW Ք]9јsgg_?kI2I>zvsSo2k(gq"7NCUW9ȯ:1铼~/K|ꞥ8 fڃ_n6lW}]o2k(gq"7NCUW9ȯ:1铼~/K|ꞥ8 fڃ{my=O MMA^C9#;r1~7/Frv8ڼ(ȲMO~R v8|.е vZukq}k(q۵Xٗ;zzЙ_+C=3W~|f3i׮ov~SnٖnKi%#(ތ?ggG֜>gGmzbů[ uهyNn 3V4o 1Yk3rg[O:k:vQ Q7ٷ3δώ9{}ώ,'`3n_ϷZVfh\cܳv-֬gζtt ("sdw3MUl@ƷM.F}>1=ON}r/xbϘќ7AkM գiS뺝z47g'vٖN_,21fLcAd$VM1yuy1ŚIL}y4{FWx!;>S8/VtFøiE&ƌiL]6Ȗ,d I~7&b /FXs>IW/fʙ]/;dccGsjN(vۯ 6&ӷ&T6=Ϛ˗OvXߜgY)c y#Y(OgܟFs9S!gn5CskǴIUO}3FvkiNָِfr֧qvǔU0G>)o$y>S=s>M?s}>Ufڲ+ǹzh>N$kژ'K?cd=d ]~zm*g}Zg\|L9Q] sS֑x^+N23eSn<3ic]ʼnl-r]'k3$Y(O>>y}3Fvki@ָِfr֧qvǔz5SOgm3va컖t)>9ske Ӷԟ9nkz6W+g`3K[c65Ƶ8n֓mlcr>]߸Xmqӝ1q5->l[ΘObP#ؤ+PٳFMn-Xݨgg_l 3m|9c3c9k<5Q?ȧm})yϺiu1{ȏ>?}>3K'n~ԗ_(F|ULg<ӿx-u4&\VSo\s>9M:Zz6?5tŚ(Ӷ>[Xtg]4yG>o%yuԓbK7.Vz}ק;g>*q#?|/.y| c\pD=qϛ eK^dhQS^Aj.Od1cfka>KfvɌdC&g:}qMO}wd_|zʳ? 3d?!38rXM)1 ha&P䳸I1q#;A?d{Q6;^+!]U[S?~SwڙFxO:j^bMfNcY 5FlZlCd[C~Srq c~׸S>A,<世ǜK嗳=k1]Ko+O=i?̃L}G}StQ~9iTP8lیHǶ>x۸d`n|lΘX~1'{qvyM1{ק|@ٷh{fgLV,L?F8;<ӦkߧxɊo>ݗ֑}}gIVdM:7>l>;ݼn|lΘX~1'{qvyM1=@FS/A͖dHXtC\c13cL;cɳ=ј:-Gs=Y)ӗ6u.$}5ϽvSCjB0f:js4>k;A6LEcqf~&Z拉xbу;eٙW#U31NY+͏ga1s!.=FO1_뻖Ixk&}91gfg }~ɵbOgG:߼sɏt&Koz]o3fVd3k/Tdvڵ55Mmҡ|K>Ԓ<;ognφOqgMטאX`L'WG(~'ͳףkmo^9iGd7Fqj`\󟿶Ş4^uLy=Xc9.v $)f1J.K&_9YnZ4N./jiCcٹsa^t4/m~)ބ<]-^>6]]y8H/wQ,3}V|cv5tg,b7}ӣ/˶xqM6MȵԳ/Q\? :˩C}n';ȵ}fmS yi/g[|m#qv%끡qN@cmG7KdXcb̋3o0seN۷>+1ٸIv3ϼW+F k?}e}6渆̼s drc.}㹖Y+o𛤳5}9k ~t֚GK?㶞zbwBV3>Tq>[zuTޘll\|$:W+F k?}e}540z QW$y7ك7FjP;1h>bij/:\˗ȯq9|H7??"i8fb̞Xk_՛7>{-F2xڌ5&øXS)˶&ԋw r7Go4>}q_r']nK2EӦqd35Ř=6W#fb˯x(nj_=#6dr 9f7YiC9F~tA=Zr/y]ż}؈Ǘa^dO&G5Or:ɳ/ތKg1kH~ؽ#~۷ӮfdȮ||G[Co ӵh}lˡxwq+c#_3yyk>e?H<}Uk$Ͼx3.=շГ7!9矬ӆsڃz>C_< Zy /ü<5Mj>媵~_B0&Lyd6G_L}stϜl'c׋連v1KV,Wy6Sr73i)1ZȌ;}t3aj*5UX'xr;^ .-Nj}gc:njm1c7Qt=+y6S}ϟlS]ؖ3Vbnj/GyO#h^L9?ޞwg}WriϢ㊨im8Li˶:o9GɦvS=q7fy~ cl9#}+; sx$VnWS:Xcj2}~,OZyN+N6ݏS^L$w=7fQI;;mf>ؐq[?o/W$o$d8D}Æ>W)/~lN|n`Pk8] OH>m峱Ջ`׍NAds N&@tlsLy˓O1X7{Lu(gtƚv37\[#+$Ӹڊ51kHbݼ83^M[MlkݴYq"쓙S9>դ#.{$xG9͋C\mGjuGtwsLycN*\Ax,J2knLW䍵3md]f [w2c=N쒵ٳ1zdSZOud+̋EyQĚ:9믎r9s LtNX;F\T-dz>%래=#r4Ozuy(/nŦ99믎rwΜB6}ӕ;ycwfW,s9lPH޺qd{b0fSl}OY.}A҃n!/F58M2'dƙٞ1گ3b՟L1о_sM{ۜxn>3μflRb_0k<3)/o5N]cc31ƞ3dsbƜ6Č33GDb髋|kD1گ3b՟L1о_sM{ۜ||-fyY#٤Q7axgS^j>W>׈b_g,vŪ?>b} 9,=Vg0̾>yE,1fAŠ50/8ycE]Ř(הc;kI^-VFfMe_]-xtq3]ɧ}6b?sygl$=ՌYǜ>l<`K>0VO9'Մz٣<[0'swa|Y'ݜÜ|)Yf_cŴ7,_|گ}w:xtq3]ɧ}6b?s}]tgl7~iӧ޸xH> Vl@+Yqt%#ݔ?f\9ݵ69/l:KWs~6o/} Դo|a>aNޏ&e%W rZNSW֝ 5OWZ?7Ck\^:cut3>f||c,r$kf].?㹯 lZgtSd9׏9ݵ69/_qyqiۘ8n:cAG3Fyc!0m'd|ɋ˧fzZN:2yS^ ޚY)km o!Nٷ?g|Hng^/3=ʃȊt3WK?saW1kH~ƬƠ7}2'NAwtd1c?ךkd|ɋ˧>mf>oقu\8f_O~۵2|63X7lA:CK_)5cɐ- \II9w#e/QT]SVA/Vm/y7$cS|.Nr3N6Ŭ^ykn^Lz-_M^97Oe_>Z śzU9mkՇiO7c'׊A^r'ÌGLMј>"{gb79eb=ٖ&ɛײ/Y-HM~*-_-Z>a\+y1Y 393)6)Gczş9\+{Ћd[;s$o^˾|0.g 7x0骜gd&җX3.瓿6Ɋ~RnmfG^M̛i0%gcql'>jI%@Uq hO9f-d=,0Ϯ3my"YJ~.uGvNYAOkN<ӯqt/NTyt5}u瓟3䌋My⦟Wڌȫ)DtZnǯrZy>kS'Uoc6c>jʦ8-V|k2?YhIQ/ ̞>ۊIn>`ݤX|XK~vސ|cc֤/ll~0YMlyc7dXH'onL1üsրb37yի)}~0&o6o-n{ڏbidZ4|bNj@sG̚7c}1?0ήzqMgꍵglt'oW_)΢&LDi;Xbͼb̟Mvͷym/7Z|@MS}{]צ9'}M2ukDk}6&dӶ\(_<U3f\x#YK3f,}rצ9'}M2ukDk}6&dӶ\(_<U3f\x#YK3f,}rצ9'}M2ukDk}d V-$r*g?{ l޸qndUg1hj55lrzg>e(qڜyPcM Bϟ y몡(^1tŤ'{ol3!Ӫ5|P˃Z&ŝk'ƴk_-bdt>uRbՓ̻7O矞|YO?b:kjc1ۯzYGO1ĩj5u ʄm>h< 2l+9Y791&o~=}ӑkwߌ;s@kh/ƈ͆}{Vyh7O?zo}%q6b4fÿ˿1#]fLk6/N|N{ӷ/<1Nd{<_}cr-ٺݸyvͫd/_hbaz3ǐ7q{s<~l-h̆q˗cGb͘l>g_W6 +w/<1Nre{<_}cr-ٺݸyvͫd/_hbaz3ǐ7q{s<~l-h̆q˗c\|9 Ӹb!;e68ⶨ Nxc?Uz2JΛ'/>ԟm"6kl\.ɾ8r~IkbC~cSb'+nlQ_貣|I߼VGdc&=~݌Y^ͼX k\ kɿq~٢0kց3e?aur ي:OV̜sS>+]go=Fo81c/֬3'f);aֆ[o(xڗ<=7:euW_y[-QXLY3~\|8fs59o\b_q[|l|lWldŭb-џvdsO6nu4/O6lӱi>ЛKޢςy ɞ/OZPdK_e_0n={c䠫UC}6N>_0ҷܺ]jL_.vň:q.г逊o[/8Yxln:k:0kWy_LnÌdQrdAUk6ɳmzԷz $~yk؋UѾ?rךޘxZg9X߳>__/~Ր\[^4>cǿz:1nzkړ?'v~;5 .vA/F||Υދ<}oKdڹOɳmza[Kzkj隧'9!{>//U_|ɠ_&cO9IǮ\Ƶ7?3CkNEz>:_Ctw֣/[>ƭ_OJXp=YWLc|bG\\s_`t]Ōk\Wm3^Eũ'`>/,אc2k?/x}ͮN/?>9C^it toMȫ/$bתzo1WY6dMW#駡qsYAW-:qZ?|CG{_ b_ qQk(q57&ǵ1flr\S=8k/O:kSO?@22 ɬ]ôqȮNr懞doև:,`+yX?f|UĜ|ihl@VbUN9|Bu^õ8x3vtR.-w?}M&IT$^l2G=W#[:e z7?܃/6gNsԵ0oȖl7P8d~ }]K5ŤôWg:WG4ƜCAD˷73WϬ갾rh8?o 櫿~*O"9U%%׋Sd391m'兾[˓.̳3@63q ⲟVSj'Icڷg-uՐ>f5LŤF^濹̑L;wPyƘ^1eL(O:s}TZ͘b {d}vgՂz߻}}´/6[5Ͽ+xFfoMܐ֞W#ji]5$b)^$/&} θ5LgE:odmld<6-08gW.ϻw]FSI}cӯMCևS_39/絠/V/zUjwl'U$3eӘNSL~Z2h(yYMbwEa㾏k9 ;}pk<|{~ݯ u1sdAvb׷dn]ϊyq3?^^7=7c=pXGԐg,G55eR<`>7g;g\Y{7WZcwwFNkk\{`Ȃ.ۃd~Ƥ/^6y_ъ?K&I7="ϸ/&U3.Wo~E]տ/`o|~b{G}3?sCSD/ş=|hpꧏ1Y 6\1|H~ u$d[|zp\gc? c5׎k_(zZ컎^C/o0?z5\jqjC W0Fv?tsm]z%dWSc{)^ڴ ?@rj3G_W+ud,NqXg'lf=gO_N/`׃_DogЋ/?O?Y/w EE܁s :XL?_t!?0W9;}9]w d1㴙V \XzD_g=#}sbں&o0?[uY~J>je^szBb 3^O͹ 2/zw{ \߲vmEM&=h=mz%7.c\?yvd .cl}COܫN;L'O{Wl=|3u}=k~銍㴙ֺ ^+/syzfp¾Qjd/~-^[gj#<~k>009M[q8An4ル=kAV%6kkZ:t6kl*?{"wGA?:m浨ƹ.5f?2Myjr1 _8wPd ѼKb`MA8ܻsw!_ njc۽6Y9Iξxq 슗söjSiZt=I_d eorTvm s 5@ՒZۓC{;1]s :[ CHŽ: κWMYWĶ~|mӼA]}Z[.6lbc5k^'X A_.-Ӹ 0nޅ7fȣ32 pavַl0'O?!k:P?ם\Of\7{dԑ>|!}Nr6vjy/8'<ٚ /B(iE_N%:V_Ũ|(~LmZ/28a ~\ x|c_7d?[Sb_?mܯ_9W:uigbEuϫwRMǝ=?ׅ-WQ}#[:dq6;ZgcGkkǧ&fh% Jkn;ʕdn'ܾM?k]نgݻ>4<):Oߨ\ DL }x.'O?!ruŜ5ۇڲ-qøO-il3ף5=|g5AA[dӎXa%z4aAˆ܃hg~ڊ痪ݯ5uorkIA]>ڴ+^}Λ9 ͧyf|B//2O>P_r | Rx{1 Z>Hn|uN3.Ի;w\śKԓce?aÔ/|Gyk}k;(rל_׼ֵk^Al׭Fm}?gqa:-FG_#{C>)|}yߨd/~>g^{}yXz/ܓ壉;_{4/'O;ꙹKԓce?aÔ? 4KRb\l'>_[Oo{0z0My55t0>ג\{k=;sۧbf|aIox ^=z{u5sTga>k&FqǾ}Qơg ^_]NdOWf_n5#|{HgZ5Moⱙ_{19gOYV=vIs"N=>>; ~ޑ{ =f<ߞӞ5׵;2\Wo"kOϿ ٪m2?HZkԷ/3fkwT1ktN_.u~/fl 0Qp|"q3.9۟zVU12szQM &+L۹Ƙ7yW? y谙-UŴSGݬ;K_v{G_C8Yl|\o}]lf+ތ  |Ag7/)Y.1c\1cn$odoFf'd8\p\_9]7sybG)5UGكga_=|֩z8g\gKwνΖ,['looȬX'+ȶw3By orf] 1QrXn1}~_ :s=q~zk|u0׈MsW hUSySFF'VSkց'=ӧܘNPWikJvPYS]t3{6zZklNEdw/}\(;:=g_ 6/9HO>`W^^}W;eZ3g? >Y6UgSl/&~9A3d}!/6怜7gʣΛ/$=Ew?ٶV諵^،$1sMfn-KW>,lؓ6̃5M뷞ŕ51O+͚^x׽NvyţNZ-S A^B>1mkkR m>"[{1n{,='=lګ5׋gO)姯x>t|ժ|fT_d} oj6 ɺuBf>/S>|m?e'Gk,ٶjeyzs)nus/ GϮ򲣳~ӵ0/vmuX5=[3蛓>\W{\֞mgO{3}6Hζ^hzv9fsj>3oq/];ԍԁ|nhI&]?VqXRqu37W}t rqgևrL}ɣ68'w-OLOÚ']"mދ^7;;Ź!痜E΋K_M|q.}ϊ(&]*&í/բј {L|hޞt}YqT'}hFgG{XCl~|aݏXt=ʡ9AyYȗ5=b 2}=E/:ɌůDD۬ YI9~5^ͦ9WZxW]l<|.f}5#htGOZ>d7>3Frf ,I7y#G(۰ؑёC_|g٠:yw} \''U}d\ϸzw6]zԫ֫O~a؍VSt!CL»bTŶCbסkioXK\2v]{9}\?vgyDzTx}<Ƀo`^>lW916XzΜP^k]K8ՉO3ԝݫ>KvjGS>T]]p>)ZŸ,VRü32$lfq{Z]>G13Z}vҮ _S𯶹jg_#ǵa#P;؞铟{X?@qM޺3c׷tzkgɾ7N>Lݙ8d 56s+{afnmUP~5zo~v˾.1WGPw^ŌZƸ}~r'ϦzWIgw6x_=O|a@;E=|k^Xzq㋦|l׌[ F9ӓ~wl9uo^0n}Qrk|sP׻d}^kEv (Ncxb|]z1\zv}kfgޚO9?{pcjF5Oz'}{Q_Ok"&w}r̵&CvOjyd'bu yߔ;nl˫_6ac\侖CCK+¿/?y9Ccg}U~g{I6%ژςv;1c/7?%to{`]+=1z_5~נOTW2dg=mђk#?]n8O>XsϾq1ߌej>76H>u]ٳ6ok)Ѻጏds?O<^ ֓gxʋM$_wgOֿA^v3δGOs-.Sgl_ϼHL?!]}1Nfbumf^r=ʕ~a3atϺkߏጏds?O<^ ֓gxʋML1?kw3=fMOkͱH6ԋK.͓`| 2q'tS6r>槯yanlij cQLLOn>7.̙os]hd۞ @dTZEM]>OmƍnXL}鐭&h\07ίzm-7>h] +Nd1&?](fkl>CPO9Ǭ}O˓m9ٲ͑] r4gd@6+{ދ͋Qq5C>zƩi͸/^=q1<8ZͶdsܼg dC^"Yyfdr}b=ldZkl^:͍ҁlʋW}>Gkl[K8{6Ѻ@V<5/;-gLf.w'v٣oFjNՏY{jVhQq&2W ؘB\SrkӷИ.y׳I?.B5S6WM񌑏|G񪳜ɵUXGu Y 9d2gݠ9fA_-N}ljr5~c}k~z-No5+My|O+Wqӛ7Kw-OZ'/]㘹aU5i3F>KH<:<9fA_cj\0Z>S߾kFW\-OV7o̗Zl7׵'OF_,1s|jҌg|yo{9,JVLg(F߂\mL/G#\n!=6j|l'C1/fgjlb'sƘ&o7GȮ:,d^}y4uˮ#:ӵ?Zg3TkS<܃b#[ԷVƼ\j~MtϘMd(LWl#[mP,$b~d?3~v39 ұףNSGk._vdՙL~6=C9e=(&Ȋ[]>E}khlL_l˥}DW$Obi_tF1 O/1gM=wkg_BG0SgHL?!]}1NfbMKGO;|Hɑ#j0ic^0=_9f÷&]ȐNOyc=g15e9c-]-?qڞ:LcqP̮{8}П|B~^Ot_7Kwl닳}'Grd7Ȧg}Z8s̆>0}A1OuɑM=Yj=% AiE6Z g`e^ӌ8'T/c)w#vvSx@O 0b5/_I羪̘ ya\9c 8mdCuVlMh|G6ի%i|w[15;hg[?ٹn3DŽ~G0z}k/NzE'ml4֟_̽ce^3N8Ջr}yy1|~KVx@O طclMh|G6ի%i|w[15;hg[?x=|"N`}E^r<._qߒ71P-œoBo>^,B$ȡy[ H/^}cT gӆ/ң835|ҫlxd'yv"맯x={tӦȞtϛ2î|݈s"?Їl(G@j.y몞ֆ3V)qy/FƧͫ/y{&͜]G=瞳5[u1:AVi;Ü\uOhoa:Ȟtwʛg9>0/fy:~|PA'}ת)9|/Yż $y szT\gs-(즯VnGT -?z|x gli`vaEH>'9Y>Ŝa\͚c}tM=y7ӌaL~-WT/y9ԟvdkٔɪ[C>Yk?*aƅƷ9Mͳ!K7_3_ɡ7GS=|=1'즯VnG>iћf}ϦxqamG7{lMfl3WlcrQ,8T9nOf2}"kzѺGOZ~<2%YcA'|:8N}r1}Vvm\lN9N;T)ˣ|ƪX[kY7D-ߙ{5q=MߩY3u<ɐodQ]Q']YY<57ucrg ikQ_vD9_0oyc'o}VvL_6s *h?f?c'o}VvL_6s *h3?32C熐MltZEk_<M]za/V.@+w>g3}IL<4ךs,y闾1ĬE+ٴo\}i,{eP3FcW۔15K싟٧Ϸ=gjjӓv!y:qZw62q6g=5ίak=l\+}.;H_3Ǚ]d⡹ּsdɛLl'f-Zq8Ȧ=}rOdg.sdj1mӏڦ|a&]rd_>}]?>k6fVS{ӉӺ)ϔa9s葮q~gl4u]g㚾Xv4Fb9|0$͵Ŝk'Kd/gc81kъ3A6Cx'^_au6sLWҧdzd3u[ƭΙ.tHt=1}|q?5fN<ݴm}g tZq0ת;y''Ӷ80~h^攙kt|\+6SUS}$c//pɢX3mkφ@}Lyqb$'Ӷ80%bSϴIn=bd3ʼn)S5ƒ =fq%>˙M>ś>+?!2Z7!]65s'Mלn2c p"Y9}riޚ@Vk¦5V{0\AV-Ͼ8?F^=3[_m:O/gu v^W/5V=G}/;cGS|5FE9sNӼ53믁֜U=zf gk^#zϯƶk]b':|OOrfy񦏆O>kȇL?c=AMI|5FE9sNӼ53믁֜U=zf g{v039 a68娎36sl>Uoui3?̫+Yj9bƅW9L <>}~bNS?HWMtdmZ59Ѹ>1ZļMkA uVS10vzɋ? }$Ϸ8ݬ'.}9㌧ d$Oc~[]JVZiǯq:mcd6~+~$/OHoq4Ygq7._5}qO]rTOA6ILՕz>_t.$iSa!bkEImҴVdyz noZ=q-cd)瓝6]lRX3sZ@RSznYͽj.W&|d֐M\:U`dw"^\`gUOzWAۿY(Ό6OTGV=Z!_1nkd83[۴?mS}?Be?wV 6 g t0gҁГG2q,bE屩⦛5L`S?c+QPNM]'vB^ U#ɿAqz9scMْ9EٛS?us~%GѾhޠ8?Ogg]zz[N(|'k֓##KX~jC>!Ⱥg'd_ɑ}y/Z>7(;V- d ^(~L}dg2GzTO /vqU,'!]lfg|`>cҳEh^ůZ2yz(ͫ5]c>Ɛ:&Ssl 3U3yd=QK6}vZvd,aʍqŌo|zLK7l>ydʮ\H)V0csn y3m21gP;X<7noN_ًsŝdogeG"Xg^i~gyʪ'dk}~sf'kOejbu3vq1>Ɛ:&Ssl 3uU %yKT@k5?6IX?kJ7:Ϛe\L_ڔϵOllʯPiS h>g_ƭ\L}-F?Жx g/V>,i[_zs=?k9cMY6'TӜ5gxA/=󞈮w1AjFrYS:r1Ϛe\L_ڔϵOllʯPiS h>g_ƭ\L}-F#Y6'TӜ5|G5#3>\mnM\Ʀ ;!6 K~n:ńrkj/VgE5/pM{|f?/hq76Gs=6k8yX>j쬗Z L_ϣ#lO!Y몦~0ez3ynmiŦ&Gnj;}Ɋ7mD2w鵹WŜvuNř=}2uatO~&o^`[bcۺ#83qϼ^.̚Oل|}wl[ŵ`<tl%x-(٣Kl1g@WKzϋtS[rQ,}`[r3~Z6lǵ\8'6gO7۬-9cgg>ӎMg=5]L)GqOȧktsk{ƙjNh>Xø!HW }xlg}ö(fll4kq6OmΞnGkg3f5`;{.G10geϦy\oBlst/zcm]̊,y5xkq dwA=3_5y S>r4>!07y9/٧W Oܿk\y]fg!y>ɓy49s>iO?}gm֍!\o^^3~ƺF?Oc@1V}lgC~Sh,c'$?c_;md?~M. y]fg!y>ɓy49s>iO?}gm֍!\o^^3~ƺF?ϿZ`|d|Xl3x<<Ϝ9QH_cfa3@=X3YmFW ;-mrԷsW̜h?gcJQз}j=^?kcO~<3dϱ85e 1g|ͷ{ŔX?O~ڵO33X''d7/qi]8t5q0em\^/}'{m>e;\?(הlƬN}7S_c_?~i>XχxӶkhU/Ym>~6ygcqk@6cV'u>{4,m:e7/?o~AFx׳/6xn̍l3md8;jȯ~a߸$Mz]Iw>\#3^sYG9nо"Lm-}D9u#>9ׁL-Y0z)|o;ŧ3tӯ{¸^,6Af\ҷNS<~sH_~ђ%7vK_|}O;yMg?0'"idg}g]~j.<(GqT[*sd|ϵM}lv[#쐼vq-GcgSi3^g j}{=l9~^u}5B6fgW+>Ѳ]A } ~qv8NƳMs®CU[u'c)&sjPH&3a=MQd~.FLu+9nSٺUc6&Ì0}UC6Y_N/^T1uz&cWoXu.}kĔQ}--S^3_> B?~ڤAgOZfzŜqFt?ZK}kɪ!flrײ) 3#y1L71[6Zfl@W}~ƑI'>e績bXq{Ƕ\j̦dqojȷ&ܵl{5 !Y%~gw曾glgKXP-6i%)i#_7syQmZ9G'yy0>7ߩV1_s=8i3ev/!`g?(~T|WKvSF}ig9L)+>?sd_-MNG',,Jf=ɳ}yɐ16b3e&f^3F}MvƩL?s>I_^}yjhiM}F1̓]ɍ]9-ZӞy}6K|4z5d8}Vc5Tٔ3|k";13t(V{DGVmĩΓ}Ś9<;mٗ hS_,6Sj"aE?cdgj3>՗=v=Cתmt<|6ɌgR8l^壱A%taȧfϦk^wחى5C#:jK'Nu䧯/̉isξdHFb2TY 3/W>&;T9/<5tV]ln.Cn>Of,>`ƹk d`>\ϮnhlȦOPX[Hc Şidbj c3cC~0g>m̧zv'}C~0g>m̧ G_ͯD#kL~;uì]v>Oe[,)3N*lbw[S1IBB ceT΍ߘEO(5q̡oﴽ6F+ {W끜9<&3nq3Y?qNlˣu=ڋX;;䛞ÜX|WOc4/7֛f}3f㘹?1glS#30GY|6-9? 1syMf4/f~Tkٝ6'ٖGzvv7=)GY9{NyƘ=mg}#+ZrԫcH7'̸i^|gײ;mN-h/cozSsb:r1.Y=8q6ƨW<۰Xsƚ 4o'o5Ly$.B$=ǭݬ$>ŏy!Y>Ts} yy3*5n颹F_=ٴ.V~ȷ4ҟ5mgl3qv$*?)N=Oi'f_=}us)n=:>CLW_9f7הŠg_ic>g7&6WIq7ou0ol$=ǭݬ$>=u P򡚋O\_*LTd5A-nU/Ǵw:[COV=Ũ?}?Yx'E1;)^c]}1d]0fYC.ן1 {2}Hx@_?NF_I6sk0/ֹ'G-Iv=c͟|A/V1u?ڛdgA짯:0卵zMvГϿ>_2c7:ŃIFuaSl\ȗ}ʅ0~6}e^mɋQdIL1= n.5/y5OLb\y NtǴu5l49{v֙}̜@x^[Zn7Yih a,6}ﯻɋQdIL1= n.5/y5OLb\y6Z#ݼof9Xl~/V)Z f"={/o] yqk`v=wS7H~ڠ\;Iyj#zsIsb'a},kcB"(ٳzszCߔ(:zӞ?dO?Mvg|a'`C+8}c4بͬ%_kir;{ԧ5|lkf>Q mƟ~S~}_O٣>]>Y0"JT˯$ˋJ]yzwF);zF.8}HeG|tZՌdГ瓼yM~٥o=QbXӏ<5ٷz5>s li Yvb;b=,V7 |u7V1c,G_k}Z#O85/>Iv3u"O}0ZMȇ>䓼yckҷ(~1fGEXŌ? | ^gǾEuAߞ]0]if=0ꁸ/GAP{wÓkzCŞ~`c|fk#fbO?1>Az5qaks@?6N;M 傞Lb'φ=_;Y-ycz_?'9ԟߌƓobyD~3V}@b ދ|/Մ1&fAӁ|"wp5>}~z3vc91jէՙ}hwG[|sZ/:C묶r;ug'納9'}5缆Ww<澘OfM]fr6Țg7'+ߔ難O!٧Oo>ߜm|ĨUbVg~)䜶\3gdc}5ݼ^S_|SNor?}ˇ3Vd>?s{ʚV}Yo۞706I 1iy=}ֺӳg;뜭~Ʃ̋ߤ8G5-^1=ȴA?mOF_k@駇1[-񌫙Cߋݬ\Kc1tY}Lj[VkL>Ul$<[җKzC]gjμxMS|TӃLkӦC/Oc}LWgOd步yscWXt\9Gd5US0k$~zՒϸ9?ݘ^]F|ӱ+}:ԕ~Ʃ̋ߤ8G5-^1=4\ZyQA bNW<],jg2.h6_ZoژGGLb̾xqZS1#yՐ_.Ø#]r̻^ڴyv f_Pܙ3;]#{YC߱=>'|9gɞm ,9b:L=҉מ3N_̜]3zlϾ=4ϧ1yLV5kOic鲗 Ӫ/^d?Ci>Ә}&gߴ1tYliT/_7_ GǵMG鵘5mql tsG169aA6ig葾uеgkM#6塟2a~ց(bĩד>M-nGg~~>;{~;Oi>9t7&Cw0>ij~SxրϜ}])cg8ki(Fz=M>}r#oL3a}L09g?ZmkkRܙdgkq[^2͸ykQ}?N|Ʋ,˲,˲n?/˲,˲,VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;V'a[eYeyռ<\koo|`,˲,˲,y[YͿ7_+賈5_/mYeYu]>`pz0kkoK0u_uUËơ7/қy-+F˲,˲,W|W< 'M`,˲,˲,?L-˲,˲,Ӹ `~Gdqf>?:O_yiʮz۸|#s"&֓ gYeYe?zsO;̓ɵc>ͧϥxϯ:[QxOOLY5s~V=1eYeY+=:TCݵyPcևv88}e3r/bƚ9堊k~<ռ4}σ^mkB 5cb> jj:ĩ^CиmyP8aYeYK?:6q`vwkwGpvyدy<(w`'֠Ym+KeYeyxgՏ8zyޙ<:v=W֠v}3˲,˲,cg:>:k9s~Zֶ,˲,;GSyͻ|wZK1u{yYeYOޖYb8@u:bг~fڏSVxm^eY㗗~X/ GT6cEqFO-z1iX /|3p~zp?ǎL$ow=#x㍋=y}=>O~'}'ܼ^~g7Ix>3x-˫_|?2^eYw/?7_q>r9j|!^3e]tl]1.݃{'w]7qwȇ>|3?韺OTn~?vO9rڏȏ?">[9)첼J,˲y)}AGYr "wXwȽb@CñIs>O:^GVd韹x]owgG7Їn~? 7ɟt꯾oƛ?'#~/e7=мxê?=%_%7_E_Ͳ,BwO{{/袿8o]fduSvK|w[yс½G>wU_y?7IV˿ey2}vYeyxU?Oԛ;]y|~28eۘ;޼9L>n_ۣ9+ky?7o|gO'l--޻{X}gO707gwyy.}S,˲y^aW9zii7?c?v3?3C''Oe<Ok}ԓ?M$k6UXo_acu<˺^fb~ȷOIqj6'˪uNn[׳-kqq>?DcRgwYeykϬ^j.^_K1iWFbx/x6?Ȏmh5;hc]8yzPwxZ_OZ?ct^6Oz'ZCݓ4i@6^s/_1;sڳ\peYzXufTW;WxV}H/σΗ^EyY*4ڳ kpX",Uoe~,̋=;9:4gcRW_#5c\ IU^ġD?曃ګUvs:_x'eYWϋCK\0"??zW_4:/X#g5~X߈ɟK7c<W%a, (ʳގws_5t;Z8i?~l/g]ӳޏw2\eY^a>/:w:grt?/p}}/ſxX4W4wVkS}^VAb[᭾N~'v`oC:}| ,Ꝩ9?go~~rh]^ԽXz9!O]lcw 7/ëIu:^$>>k]R]^ڳ'=׮8eYg]ؾaO?v_o٣CjuW,=p_?|I_OOeY^g 'FeYKxg`T?ͷk}P]eYeY^>/z9>~uPS\~w\eYeYI<~AՏeYeYYyUONzy7uYeYeY,/3)ooYeYeY /wG>#7v'ReYeYB>_sޛ韹/\eyS7TeYWKsT˲,˲,ˋ|fuYeYeY^{X}N}eYeYV=.˲,˲\^auOp˲,˲, VeYeYeyeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY; ?ZeYeY^/&,˲,؏,˲,˲,w=.˲,˲,wX~`YeYey1;;^~go|ow6~{o?Gn>=i7@,/|0ZeY^4ٟ`^;~oܼ>>O>^~'ݫnڽ~YeYeyywV_Oy滿.7xinK>=nXYGn>ԿN /?swVWB~}ѥ_eYooV/ɟyV_1Ȭ2?|S?7,V?xioƿo\{n~/ed˲,/=]^auy?'nx0-<-D,e;_^?_eYeykwU;rP܋l˲,˲SkN:_ .eYeYיwxy Kk~ͯ?Geyym,y?vGbw-.Ϗ_b|\eY/^OOa5_//|૿/'U=˿/>Jx,K`uwV_c_f>˻؏إ /;˻]u?OyOO˲,?~Y{gV_S:ќ[x_]ىZ'yeYeY甆0y3hu?yeYeYga)}6ڿV5?'w6ҧ}y|eYeY=ŪNZeYeykH[Yc=.˲,򺲇א>OS=[=_z,˲,{X}?! _r1uk<,˲,˳׌U}C]?u헸<˲,˲,V_#/V=!is/e9djdOP]y|eYeY=F79mϢv7קy,˲,,;;>`|џz+~.^Ϲ˲,??/rwwVeYeY;VeYeY;VeYeY;VeYeY;VeYeY;5eyؿ,ٿpwwVeYeY;˾,7OϖeYջ˾,˲,˲Yeyxמּw<_UeY']eYeYβeYNOS{˧Oe\,ʿ/Fo5Wy@ZK;~/sY>,o+ڛ/___Șz˷3VeyÁ/y!ڡuaw^bozUv^>~,ލ?f@^eYWVeyixƁڻ1dZNeYkauY|H_?b3|lN߻“~d[o[O>7mw'ţ[Zb=?l\yN}5V39-f?r{Vbږ6㱟Z~gbݶ?/{X]1>痩!̡M׻F//>/]h'_C-֬[yXo,41["{z-ĺ֣,~9V[rv,/ukOE_EՎlkXqƈ7{LbfejY^3`Vy,sh>_AIEzCc~3{!.7\Ƀڮ֡i=-I'gԜ=Ϛ;3\b޶7kF5v@k~Ҟb5}z`uwwVeY l`_{Q}\@ol̺}_`8O:ؘ?aE级kyƚ;"Ik{vPٛV“'tX^Vey"N 90< ݥrƓ9ؼ՛;x'f'qu?Xv@:M,2r^V³^ϾxR90/<,rS?Ԍ]~x[Aݚ֮*z=w&^/u=Kz7s={U_neYY; >_WCgyw̻0ؿC+;gu('ٷyh^ub˲w^, :.=wFaT{Ҿ,[~:S S-oox0f[]֗[V³\,auYJ/oN[a~vծAYuhBߧ׋`cơo.^[Ow.{X] [[ܥ_"m7s蛇7.y{rrm7x3iw̵zg\'=7{X]V9l;Lw_Ӌy0q op"kd\Cs=<{a~;onoz^7v[}^+<-mUg]X׈oo/x ug=Ϗ mnJ;x5^}BNǏbQgݓY'f=s?zm\35oF9w۵“rcqmYǗE(|\."  k>uk^5^l_;GꔷZ;vhQY?ZÛ905?wߜgh^ŵrӋs L˲.kjd¼,wV.,/ ޹AuYey겼fN ,]`YauY^3^*_ϯϨ.V.{X]׌ VџwSeKaeyx˲,w=]eYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;VeYeY;˻>|xY׀oo_E_teY[[.|\.˲,˲,wWrXmݼ}{ؾfYeYeYnV>eYeYeWrXﻫ˲,˲,xU//}0oeYeYeK?Ώnn~oF˲,˲,rzXj|ɗ\_~շƘv`~绾נ?}sYeYey|W_~꫾|!|7AW_#={]ز,˲,˫މwUk2?=w5é_ _s[Ps3Ş~(_W˲,/G./#U|wY0_âd<_p07s/˲,˲,/vXp.%G'zhf'<4ƳeYeYK;̃ՌyɵטG|;{p]eYeyCdI?իaz~L>z/d-˲,˲,/rX=1ic `1?X/fV)-ġo)J] G2q)rvsYu,T1c߭CYRZc2gggw}g1zUc1LŪ1 Ϟcaj1cVŪ1Ƙٸ̇~~~nvS3h-c`j9i$lr*1D(p-6[@1;sVU] T׿`  ]'1^__l;$v># Aʜ άc̿ / daDf2%! 1;l)`jY( HY,5y5Ƙ)eˌ1Ucl0WsS>^CnA]Ǥok>lg[٠>vWIE/N :hK] Ekui`j5e[k]su9:~b[IT~h7傾/Qz2mh?=~6XSs ۻ\ضж9"Ḵ_JbcvPZ K+%dZc\Q^ ڠzZB-jS>Q{h%?Mz*"۱]޳}ګm/{*~>~v9E,V1]! )hb!`(xa!GM)r+fj\`~٥Q4'Cvw9{;E89s{hK/3Ū1J*ZO +A`ZP_"%v/Qk0uk s!8o.ݛWSJe|;׵ C9M,V1]AAmo a!Gg2rvSUѵ{@9nhҐZ]=PEzH*M?sYys }kߋSUcL AlvSr5D&#Z "VJ!rm3N>r~1`jQ#x\b|s,8N&Frҗ>2V8!rsr hwC5ǩbӕ2u 4uᶫh[צ]Ԡ""f%k U7z:9\,V1]!)S-4c Ԧ(P,cB2Lе[ڙ 6gs/fXc>C\tP] M> 9![Zٓ ؓsY؟qkl_Ҵl+F郘|Sgs,}/7*_bjΐ 9pŕ1@8J$ͅ2= [#d.,G|f_ΆT+>C}I|SEia=K?rI%bUcLWN, jH5Bf1f"hOv)RSA}hͪ Cݴ=Z2{gu A}V۩3x_2s|K/}}o965*$|VJ:csz~~y5UcL7Z*( dXss܌1Ɯ"~ɚ(\IENDB`bleak-0.22.3/docs/index.rst000066400000000000000000000033521470032643600154550ustar00rootroot00000000000000bleak ===== .. figure:: https://raw.githubusercontent.com/hbldh/bleak/master/Bleak_logo.png :target: https://github.com/hbldh/bleak :alt: Bleak Logo :width: 50% .. 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://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 :alt: Black 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. Features -------- * Supports Windows 10, version 16299 (Fall Creators Update) or greater * Supports Linux distributions with BlueZ >= 5.43 (See :ref:`linux-backend` for more details) * OS X/macOS support via Core Bluetooth API, from at least OS X version 10.11 Bleak supports reading, writing and getting notifications from GATT servers, as well as a function for discovering BLE devices. Contents: .. toctree:: :maxdepth: 2 installation usage api/index backends/index troubleshooting contributing authors history Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` bleak-0.22.3/docs/installation.rst000066400000000000000000000016141470032643600170460ustar00rootroot00000000000000.. highlight:: shell ============ Installation ============ Stable release -------------- To install bleak, run this command in your terminal: .. code-block:: console $ pip install bleak This is the preferred method to install bleak, as it will always install the most recent stable release. If you don't have `pip`_ installed, this `Python installation guide`_ can guide you through the process. .. _pip: https://pip.pypa.io .. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/ Develop branch -------------- The develop branch can also be installed using ``pip``. This is useful for testing the latest changes before they reach the stable release. .. code-block:: console $ pip install https://github.com/hbldh/bleak/archive/refs/heads/develop.zip For checking out a copy of Bleak for developing Bleak itself, see the :doc:`contributing` page. bleak-0.22.3/docs/make.bat000066400000000000000000000144741470032643600152300ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-W -d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\bleak.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\bleak.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end bleak-0.22.3/docs/requirements.txt000066400000000000000000000000631470032643600170740ustar00rootroot00000000000000Sphinx==5.1.1 sphinx-rtd-theme==1.0.0 tomli==2.0.1 bleak-0.22.3/docs/troubleshooting.rst000066400000000000000000000346231470032643600176020ustar00rootroot00000000000000=============== Troubleshooting =============== When things don't seem to be working right, here are some things to try. --------------- Common Mistakes --------------- Calling ``asyncio.run()`` more than once ======================================== Bleak requires the same asyncio run loop to be used for all of its operations. And it requires the loop to always be running because there are background tasks that need to always be running. Therefore, make sure you only call ``asyncio.run()`` once at the start of your program. **Your program will not work correctly if you call it more than once.** Even if it seems like it is working, crashes and other problems will occur eventually. DON'T! .. code-block:: python async def scan(): return await BleakScanner.find_device_by_name("My Device") async def connect(device): async with BleakClient(device) as client: data = await client.read_gatt_char(MY_CHAR_UUID) print("received:" data) # Do not wrap each function call in asyncio.run() like this! device = asyncio.run(scan()) if not device: print("Device not found") else: asyncio.run(connect(device)) DO! .. code-block:: python async def scan(): return await BleakScanner.find_device_by_name("My Device") async def connect(device): async with BleakClient(device) as client: data = await client.read_gatt_char(MY_CHAR_UUID) print("received:" data) # Do have one async main function that does everything. async def main(): device = await scan() if not device: print("Device not found") return await connect(device) asyncio.run(main()) DON'T! .. code-block:: python async def scan_and_connect(): device = await BleakScanner.find_device_by_name("My Device") if not device: print("Device not found") return async with BleakClient(device) as client: data = await client.read_gatt_char(MY_CHAR_UUID) print("received:" data) while True: # Don't call asyncio.run() multiple times like this! asyncio.run(scan_and_connect()) # Never use blocking sleep in an asyncio programs! time.sleep(5) DO! .. code-block:: python async def scan_and_connect(): device = await BleakScanner.find_device_by_name("My Device") if not device: print("Device not found") return async with BleakClient(device) as client: data = await client.read_gatt_char(MY_CHAR_UUID) print("received:" data) # Do have one async main function that does everything. async def main(): while True: await scan_and_connect() # Do use asyncio.sleep() in an asyncio program. await asyncio.sleep(5) asyncio.run(main()) Naming your script ``bleak.py`` =============================== Many people name their first script ``bleak.py``. This causes the script to crash with an ``ImportError`` similar to:: ImportError: cannot import name 'BleakClient' from partially initialized module 'bleak' (most likely due to a circular import) (bleak.py)` To fix the error, change the name of the script to something other than ``bleak.py``. ---------- macOS Bugs ---------- Bleak crashes with SIGABRT on macOS =================================== If you see a crash similar to this:: Crashed Thread: 1 Dispatch queue: com.apple.root.default-qos Exception Type: EXC_CRASH (SIGABRT) Exception Codes: 0x0000000000000000, 0x0000000000000000 Exception Note: EXC_CORPSE_NOTIFY Termination Reason: Namespace TCC, Code 0 This app has crashed because it attempted to access privacy-sensitive data without a usage description. The app's Info.plist must contain an NSBluetoothAlwaysUsageDescription key with a string value explaining to the user how the app uses this data. It is not a problem with Bleak. It is a problem with your terminal application. Ideally, the terminal application should be fixed by adding ``NSBluetoothAlwaysUsageDescription`` to the ``Info.plist`` file (`example `_). It is also possible to manually add the app to the list of Bluetooth apps in the *Privacy* settings in the macOS *System Preferences*. .. image:: images/macos-privacy-bluetooth.png If the app is already in the list but the checkbox for Bluetooth is disabled, you will get the a ``BleakError``: "BLE is not authorized - check macOS privacy settings". instead of crashing with ``SIGABRT``, in which case you need to check the box to allow Bluetooth for the app that is running Python. No devices found when scanning on macOS 12 ========================================== A bug was introduced in macOS 12.0 that causes scanning to not work unless a list of service UUIDs is provided to ``BleakScanner``. This bug was fixed in macOS 12.3. On the affected version, users of bleak will see the following error logged: .. code-block:: none macOS 12.0, 12.1 and 12.2 require non-empty service_uuids kwarg, otherwise no advertisement data will be received See `#635 `_ and `#720 `_ for more information including some partial workarounds if you need to support these macOS versions. ------------ Windows Bugs ------------ Not working when threading model is STA ======================================= Packages like ``pywin32`` and it's subsidiaries have an unfortunate side effect of initializing the threading model to Single Threaded Apartment (STA) when imported. This causes async WinRT functions to never complete if Bleak is being used in a console application (no Windows graphical user interface). This is because there isn't a Windows message loop running to handle async callbacks. Bleak, when used in a console application, needs to run in a Multi Threaded Apartment (MTA) instead (this happens automatically on the first WinRT call). Bleak should detect this and raise an exception with a message similar to:: Thread is configured for Windows GUI but callbacks are not working. You can tell a ``pywin32`` package caused the issue by checking for ``"pythoncom" in sys.modules``. If it is there, then likely it triggered the problem. You can avoid this by setting ``sys.coinit_flags = 0`` before importing any package that indirectly imports ``pythoncom``. This will cause ``pythoncom`` to use the default threading model (MTA) instead of STA. Example:: import sys sys.coinit_flags = 0 # 0 means MTA import win32com # or any other package that causes the issue If the issue was caused by something other than the ``pythoncom`` module, there are a couple of other helper functions you can try. If your program has a graphical user interface and the UI framework *and* it is properly integrated with asyncio *and* Bleak is not running on a background thread then call ``allow_sta()`` before calling any other Bleak APis:: try: from bleak.backends.winrt.util import allow_sta # tell Bleak we are using a graphical user interface that has been properly # configured to work with asyncio allow_sta() except ImportError: # other OSes and older versions of Bleak will raise ImportError which we # can safely ignore pass The more typical case, though, is that some library has imported something similar to ``pythoncom`` with the same unwanted side effect of initializing the main thread of a console application to STA. In this case, you can uninitialize the threading model like this:: import naughty_module # this sets current thread to STA :-( try: from bleak.backends.winrt.util import uninitialize_sta uninitialize_sta() # undo the unwanted side effect except ImportError: # not Windows, so no problem pass -------------- Enable Logging -------------- The easiest way to enable logging is to set the ``BLEAK_LOGGING`` environment variable. Setting the variable depends on what type of terminal you are using. Posix (Linux, macOS, Cygwin, etc.):: export BLEAK_LOGGING=1 Power Shell:: $env:BLEAK_LOGGING=1 Windows Command Prompt:: set BLEAK_LOGGING=1 Then run your Python script in the same terminal. ----------------------------------------------- Connecting to multiple devices at the same time ----------------------------------------------- If you're having difficulty connecting to multiple devices, try to do a scan first and pass the returned ``BLEDevice`` objects to ``BleakClient`` calls. Python:: import asyncio from typing import Sequence from bleak import BleakClient, BleakScanner from bleak.backends.device import BLEDevice async def find_all_devices_services(): devices: Sequence[BLEDevice] = await BleakScanner.discover(timeout=5.0) for d in devices: async with BleakClient(d) as client: print(client.services) asyncio.run(find_all_devices_services()) ------------------------- Capture Bluetooth Traffic ------------------------- Sometimes it can be helpful to see what is actually going over the air between the OS and the Bluetooth device. There are tools available to capture HCI packets and decode them. Windows 10 ========== There is a Windows hardware developer package that includes a tool that supports capturing Bluetooth traffic directly in Wireshark. Install ------- 1. Download and install `Wireshark`_. 2. Download and install `the BTP software package`_. Capture ------- To capture Bluetooth traffic: 1. Open a terminal as Administrator. * Search start menu for ``cmd``. (Powershell and Windows Terminal are fine too.) * Right-click *Command Prompt* and select *Run as Administrator*. .. image:: images/win-10-start-cmd-as-admin.png :height: 200px :alt: Screenshot of Windows Start Menu showing Command Prompt selected and context menu with Run as Administrator selected. 2. Run ``C:\BTP\v1.9.0\x86\btvs.exe``. This should automatically start Wireshark in capture mode. .. tip:: The version needs to match the installed version. ``v1.9.0`` was the current version at the time this was written. Additionally, ``C:`` may not be the root drive on some systems. 3. Run your Python script in a different terminal (not as Administrator) to reproduce the problem. 4. Click the stop button in Wireshark to stop the capture. .. _Wireshark: https://www.wireshark.org/ .. _the BTP software package: https://docs.microsoft.com/windows-hardware/drivers/bluetooth/testing-btp-setup-package macOS ===== On macOS, special software is required to capture and view Bluetooth traffic. You will need to sign up for an Apple Developer account to obtain this software. 1. Starting with macOS 14.5, you will need to download and install the Bluetooth logging profile from the Apple develop `Profiles and Logs`_ page. Follow the instruction provided in the link, then continue with the steps below. .. tip:: After installing the ``Bluetooth_macOS.mobileconfig`` file, the profile can be found in *System Settings* under *Privacy and Security* > *Others* > *Profiles*. You have to go there to actually install the profile. Then reboot your computer. If you have an older version of macOS, you can skip this step. 2. Go to the Apple developer `More Downloads`_ page and download *Additional Tools for Xcode ...* where ... is the Xcode version corresponding to your macOS version (the XCode version is generally one higher than the macOS version, e.g. XCode 15 for macOS Sonoma 14). 3. Open the disk image and in the *Hardware* folder, double-click the *PacketLogger.app* to run it. 4. Click the *Clear* button in the toolbar to clear the old data. 5. Run your Python script to reproduce the problem. 6. Click the *Stop* button in the toolbar to stop the capture. .. tip:: The Bluetooth traffic can be viewed in the *PacketLogger.app* or it can be saved to a file and viewed in `Wireshark`_. .. _Profiles and Logs: https://developer.apple.com/bug-reporting/profiles-and-logs/?name=bluetooth&platform=macos .. _More Downloads: https://developer.apple.com/download/all/?q=additional%20tools%20for%20xcode Linux ===== On Linux, `Wireshark`_ can be used to capture and view Bluetooth traffic. 1. Install Wireshark. Most distributions include a ``wireshark`` package. For example, on Debian/Ubuntu based distributions:: sudo apt update && sudo apt install wireshark 2. Start Wireshark and select your Bluetooth adapter, then start a capture. .. tip:: Visit the `Wireshark Wiki`_ for help with configuring permissions and making sure proper drivers are installed. 3. Run your Python script to reproduce the problem. 4. Click the stop button in Wireshark to stop the capture. .. _Wireshark Wiki: https://gitlab.com/wireshark/wireshark/-/wikis/CaptureSetup ------------------------------------------ Handling OS Caching of BLE Device Services ------------------------------------------ If you develop your own BLE peripherals, and frequently change services, characteristics and/or descriptors, then Bleak might report outdated versions of your peripheral's services due to OS level caching. The caching is done to speed up the connections with peripherals where services do not change and is enabled by default on most operating systems and thus also in Bleak. There are ways to avoid this on different backends though, and if you experience these kinds of problems, the steps below might help you to circumvent the caches. macOS ===== The OS level caching handling on macOS has not been explored yet. Linux ===== When you change the structure of services/characteristics on a device, you have to remove the device from BlueZ so that it will read everything again. Otherwise BlueZ gives the cached values from the first time the device was connected. You can use the ``bluetoothctl`` command line tool to do this: .. code-block:: shell bluetoothctl -- remove XX:XX:XX:XX:XX:XX # prior to BlueZ 5.62 you also need to manually delete the GATT cache sudo rm "/var/lib/bluetooth/YY:YY:YY:YY:YY:YY/cache/XX:XX:XX:XX:XX:XX" ...where ``XX:XX:XX:XX:XX:XX`` is the Bluetooth address of your device and ``YY:YY:YY:YY:YY:YY`` is the Bluetooth address of the Bluetooth adapter on your computer. bleak-0.22.3/docs/usage.rst000066400000000000000000000040001470032643600154410ustar00rootroot00000000000000===== Usage ===== .. note:: A Bluetooth peripheral may have several characteristics with the same UUID, so the means of specifying characteristics by UUID or string representation of it might not always work in bleak version > 0.7.0. One can now also use the characteristic's handle or even the ``BleakGATTCharacteristic`` object itself in ``read_gatt_char``, ``write_gatt_char``, ``start_notify``, and ``stop_notify``. One can use the ``BleakClient`` to connect to a Bluetooth device and read its model number via the asynchronous context manager like this: .. 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("Model Number: {0}".format("".join(map(chr, model_number)))) asyncio.run(main(address)) or one can do it without the context manager like this: .. code-block:: python import asyncio from bleak import BleakClient address = "24:71:89:cc:09:05" MODEL_NBR_UUID = "2A24" async def main(address): client = BleakClient(address) try: await client.connect() model_number = await client.read_gatt_char(MODEL_NBR_UUID) print("Model Number: {0}".format("".join(map(chr, model_number)))) except Exception as e: print(e) finally: await client.disconnect() asyncio.run(main(address)) .. warning:: Do not name your script ``bleak.py``! It will cause a circular import error. Make sure you always get to call the disconnect method for a client before discarding it; the Bluetooth stack on the OS might need to be cleared of residual data which is cached in the ``BleakClient``. See `examples `_ folder for more code, e.g. on how to keep a connection alive over a longer duration of time. bleak-0.22.3/examples/000077500000000000000000000000001470032643600144775ustar00rootroot00000000000000bleak-0.22.3/examples/__init__.py000066400000000000000000000001521470032643600166060ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ __init__.py Created on 2018-01-10 by hbldh """ bleak-0.22.3/examples/async_callback_with_queue.py000066400000000000000000000071771470032643600222550ustar00rootroot00000000000000""" Async callbacks with a queue and external consumer -------------------------------------------------- An example showing how async notification callbacks can be used to send data received through notifications to some external consumer of that data. Created on 2021-02-25 by hbldh """ import argparse import asyncio import logging import time from bleak import BleakClient, BleakScanner logger = logging.getLogger(__name__) class DeviceNotFoundError(Exception): pass async def run_ble_client(args: argparse.Namespace, queue: asyncio.Queue): logger.info("starting scan...") if args.address: device = await BleakScanner.find_device_by_address( args.address, cb=dict(use_bdaddr=args.macos_use_bdaddr) ) if device is None: logger.error("could not find device with address '%s'", args.address) raise DeviceNotFoundError else: device = await BleakScanner.find_device_by_name( args.name, cb=dict(use_bdaddr=args.macos_use_bdaddr) ) if device is None: logger.error("could not find device with name '%s'", args.name) raise DeviceNotFoundError logger.info("connecting to device...") async def callback_handler(_, data): await queue.put((time.time(), data)) async with BleakClient(device) as client: logger.info("connected") await client.start_notify(args.characteristic, callback_handler) await asyncio.sleep(10.0) await client.stop_notify(args.characteristic) # Send an "exit command to the consumer" await queue.put((time.time(), None)) logger.info("disconnected") async def run_queue_consumer(queue: asyncio.Queue): logger.info("Starting queue consumer") while True: # Use await asyncio.wait_for(queue.get(), timeout=1.0) if you want a timeout for getting data. epoch, data = await queue.get() if data is None: logger.info( "Got message from client about disconnection. Exiting consumer loop..." ) break else: logger.info("Received callback data via async queue at %s: %r", epoch, data) async def main(args: argparse.Namespace): queue = asyncio.Queue() client_task = run_ble_client(args, queue) consumer_task = run_queue_consumer(queue) try: await asyncio.gather(client_task, consumer_task) except DeviceNotFoundError: pass logger.info("Main method done.") if __name__ == "__main__": parser = argparse.ArgumentParser() device_group = parser.add_mutually_exclusive_group(required=True) device_group.add_argument( "--name", metavar="", help="the name of the bluetooth device to connect to", ) device_group.add_argument( "--address", metavar="
", help="the address of the bluetooth device to connect to", ) parser.add_argument( "--macos-use-bdaddr", action="store_true", help="when true use Bluetooth address instead of UUID on macOS", ) parser.add_argument( "characteristic", metavar="", help="UUID of a characteristic that supports notifications", ) parser.add_argument( "-d", "--debug", action="store_true", help="sets the logging level to debug", ) args = parser.parse_args() log_level = logging.DEBUG if args.debug else logging.INFO logging.basicConfig( level=log_level, format="%(asctime)-15s %(name)-8s %(levelname)s: %(message)s", ) asyncio.run(main(args)) bleak-0.22.3/examples/detection_callback.py000066400000000000000000000031321470032643600206420ustar00rootroot00000000000000""" Detection callback w/ scanner -------------- Example showing what is returned using the callback upon detection functionality Updated on 2020-10-11 by bernstern """ import argparse import asyncio import logging from bleak import BleakScanner from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData logger = logging.getLogger(__name__) def simple_callback(device: BLEDevice, advertisement_data: AdvertisementData): logger.info("%s: %r", device.address, advertisement_data) async def main(args: argparse.Namespace): scanner = BleakScanner( simple_callback, args.services, cb=dict(use_bdaddr=args.macos_use_bdaddr) ) while True: logger.info("(re)starting scanner") async with scanner: await asyncio.sleep(5.0) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument( "--macos-use-bdaddr", action="store_true", help="when true use Bluetooth address instead of UUID on macOS", ) parser.add_argument( "--services", metavar="", nargs="*", help="UUIDs of one or more services to filter for", ) parser.add_argument( "-d", "--debug", action="store_true", help="sets the logging level to debug", ) args = parser.parse_args() log_level = logging.DEBUG if args.debug else logging.INFO logging.basicConfig( level=log_level, format="%(asctime)-15s %(name)-8s %(levelname)s: %(message)s", ) asyncio.run(main(args)) bleak-0.22.3/examples/disconnect_callback.py000066400000000000000000000045031470032643600210200ustar00rootroot00000000000000""" Disconnect callback ------------------- An example showing how the `set_disconnected_callback` can be used on BlueZ backend. Updated on 2019-09-07 by hbldh """ import argparse import asyncio import logging from bleak import BleakClient, BleakScanner logger = logging.getLogger(__name__) async def main(args: argparse.Namespace): logger.info("scanning...") if args.address: device = await BleakScanner.find_device_by_address( args.address, cb=dict(use_bdaddr=args.macos_use_bdaddr) ) if device is None: logger.error("could not find device with address '%s'", args.address) return else: device = await BleakScanner.find_device_by_name( args.name, cb=dict(use_bdaddr=args.macos_use_bdaddr) ) if device is None: logger.error("could not find device with name '%s'", args.name) return disconnected_event = asyncio.Event() def disconnected_callback(client): logger.info("Disconnected callback called!") disconnected_event.set() async with BleakClient( device, disconnected_callback=disconnected_callback ) as client: logger.info("Sleeping until device disconnects...") await disconnected_event.wait() logger.info("Connected: %r", client.is_connected) if __name__ == "__main__": parser = argparse.ArgumentParser() device_group = parser.add_mutually_exclusive_group(required=True) device_group.add_argument( "--name", metavar="", help="the name of the bluetooth device to connect to", ) device_group.add_argument( "--address", metavar="
", help="the address of the bluetooth device to connect to", ) parser.add_argument( "--macos-use-bdaddr", action="store_true", help="when true use Bluetooth address instead of UUID on macOS", ) parser.add_argument( "-d", "--debug", action="store_true", help="sets the log level to debug", ) args = parser.parse_args() log_level = logging.DEBUG if args.debug else logging.INFO logging.basicConfig( level=log_level, format="%(asctime)-15s %(name)-8s %(levelname)s: %(message)s", ) asyncio.run(main(args)) bleak-0.22.3/examples/discover.py000066400000000000000000000020171470032643600166670ustar00rootroot00000000000000""" Scan/Discovery -------------- Example showing how to scan for BLE devices. Updated on 2019-03-25 by hbldh """ import argparse import asyncio from bleak import BleakScanner async def main(args: argparse.Namespace): print("scanning for 5 seconds, please wait...") devices = await BleakScanner.discover( return_adv=True, service_uuids=args.services, cb=dict(use_bdaddr=args.macos_use_bdaddr), ) for d, a in devices.values(): print() print(d) print("-" * len(str(d))) print(a) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument( "--services", metavar="", nargs="*", help="UUIDs of one or more services to filter for", ) parser.add_argument( "--macos-use-bdaddr", action="store_true", help="when true use Bluetooth address instead of UUID on macOS", ) args = parser.parse_args() asyncio.run(main(args)) bleak-0.22.3/examples/enable_notifications.py000066400000000000000000000052011470032643600212260ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Notifications ------------- Example showing how to add notifications to a characteristic and handle the responses. Updated on 2019-07-03 by hbldh """ import argparse import asyncio import logging from bleak import BleakClient, BleakScanner from bleak.backends.characteristic import BleakGATTCharacteristic logger = logging.getLogger(__name__) def notification_handler(characteristic: BleakGATTCharacteristic, data: bytearray): """Simple notification handler which prints the data received.""" logger.info("%s: %r", characteristic.description, data) async def main(args: argparse.Namespace): logger.info("starting scan...") if args.address: device = await BleakScanner.find_device_by_address( args.address, cb=dict(use_bdaddr=args.macos_use_bdaddr) ) if device is None: logger.error("could not find device with address '%s'", args.address) return else: device = await BleakScanner.find_device_by_name( args.name, cb=dict(use_bdaddr=args.macos_use_bdaddr) ) if device is None: logger.error("could not find device with name '%s'", args.name) return logger.info("connecting to device...") async with BleakClient(device) as client: logger.info("Connected") await client.start_notify(args.characteristic, notification_handler) await asyncio.sleep(5.0) await client.stop_notify(args.characteristic) if __name__ == "__main__": parser = argparse.ArgumentParser() device_group = parser.add_mutually_exclusive_group(required=True) device_group.add_argument( "--name", metavar="", help="the name of the bluetooth device to connect to", ) device_group.add_argument( "--address", metavar="
", help="the address of the bluetooth device to connect to", ) parser.add_argument( "--macos-use-bdaddr", action="store_true", help="when true use Bluetooth address instead of UUID on macOS", ) parser.add_argument( "characteristic", metavar="", help="UUID of a characteristic that supports notifications", ) parser.add_argument( "-d", "--debug", action="store_true", help="sets the log level to debug", ) args = parser.parse_args() log_level = logging.DEBUG if args.debug else logging.INFO logging.basicConfig( level=log_level, format="%(asctime)-15s %(name)-8s %(levelname)s: %(message)s", ) asyncio.run(main(args)) bleak-0.22.3/examples/kivy/000077500000000000000000000000001470032643600154615ustar00rootroot00000000000000bleak-0.22.3/examples/kivy/.gitignore000066400000000000000000000000211470032643600174420ustar00rootroot00000000000000.buildozer/ bin/ bleak-0.22.3/examples/kivy/README.md000066400000000000000000000025411470032643600167420ustar00rootroot00000000000000## This is a kivy application that lists scanned devices in a desktop window - An iOS backend has not been implemented yet. - This kivy example can also be run on desktop. The default target architecture is arm64-v8a. If you have an older device, change it in the buildozer.spec file (android.archs = arch1, arch2, ..). Multiple targets are allowed (will significantly increase build time). It can be run on Android via: pip3 install buildozer cython buildozer android debug # connect phone with USB and enable USB debugging buildozer android deploy run logcat ## To use with local version of bleak source: Local source path can be specified using the P4A_bleak_DIR environment variable: P4A_bleak_DIR="path to bleak source" buildozer android debug Note: changes to `bleak/**` will not be automatically picked up when rebuilding. Instead the recipe build must be cleaned: buildozer android p4a -- clean_recipe_build --local-recipes $(pwd)/../../bleak/backends/p4android/recipes bleak ## To use bleak in your own app: - Copy the bleak folder under bleak/backends/p4android/recipes into the app recipes folder. Make sure that 'local_recipes' in buildozer.spec points to the app recipes folder. The latest version of bleak will be installed automatically. - Add 'bleak' and it's dependencies to the requirements in your buildozer.spec file. bleak-0.22.3/examples/kivy/buildozer.spec000066400000000000000000000271001470032643600203340ustar00rootroot00000000000000[app] # (str) Title of your application title = Bleak Demo # (str) Package name package.name = bleakdemo # (str) Package domain (needed for android/ios packaging) package.domain = com.github.hbldh # (str) Source code where the main.py live source.dir = . #source.dir = recipes/gattlib-py/src/ # (list) Source files to include (let empty to include all the files) source.include_exts = py,png,jpg,kv,atlas # (list) List of inclusions using pattern matching #source.include_patterns = assets/*,images/*.png #source.include_patterns = */*.py # (list) Source files to exclude (let empty to not exclude anything) #source.exclude_exts = spec # (list) List of directory to exclude (let empty to not exclude anything) #source.exclude_dirs = tests, bin, venv # (list) List of exclusions using pattern matching #source.exclude_patterns = license,images/*/*.jpg # (str) Application versioning (method 1) version = 0.1.0 # (str) Application versioning (method 2) # version.regex = __version__ = ['"](.*)['"] # version.filename = %(source.dir)s/main.py # (list) Application requirements # comma separated e.g. requirements = sqlite3,kivy requirements = python3, kivy, bleak, async_to_sync, async-timeout # (str) Custom source folders for requirements # Sets custom source for any requirements with recipes # requirements.source.kivy = ../../kivy # (str) Presplash of the application #presplash.filename = %(source.dir)s/data/presplash.png # (str) Icon of the application #icon.filename = %(source.dir)s/data/icon.png # (str) Supported orientation (one of landscape, sensorLandscape, portrait or all) orientation = portrait # (list) List of service to declare #services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY # # OSX Specific # # # author = © Copyright Info # change the major version of python used by the app osx.python_version = 3 # Kivy version to use osx.kivy_version = 1.9.1 # # Android specific # # (bool) Indicate if the application should be fullscreen or not fullscreen = 0 # (string) Presplash background color (for android toolchain) # Supported formats are: #RRGGBB #AARRGGBB or one of the following names: # red, blue, green, black, white, gray, cyan, magenta, yellow, lightgray, # darkgray, grey, lightgrey, darkgrey, aqua, fuchsia, lime, maroon, navy, # olive, purple, silver, teal. #android.presplash_color = #FFFFFF # (string) Presplash animation using Lottie format. # see https://lottiefiles.com/ for examples and https://airbnb.design/lottie/ # for general documentation. # Lottie files can be created using various tools, like Adobe After Effect or Synfig. #android.presplash_lottie = "path/to/lottie/file.json" # (list) Permissions android.permissions = BLUETOOTH, BLUETOOTH_SCAN, BLUETOOTH_CONNECT, BLUETOOTH_ADMIN, ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, ACCESS_BACKGROUND_LOCATION # (list) features (adds uses-feature -tags to manifest) #android.features = android.hardware.usb.host # (int) Target Android API, should be as high as possible. #android.api = 27 # (int) Minimum API your APK will support. #android.minapi = 21 # (int) Android SDK version to use #android.sdk = 20 # (str) Android NDK version to use #android.ndk = 19b # (int) Android NDK API to use. This is the minimum API your app will support, it should usually match android.minapi. #android.ndk_api = 21 # (bool) Use --private data storage (True) or --dir public storage (False) #android.private_storage = True # (str) Android NDK directory (if empty, it will be automatically downloaded.) #android.ndk_path = # (str) Android SDK directory (if empty, it will be automatically downloaded.) #android.sdk_path = # (str) ANT directory (if empty, it will be automatically downloaded.) #android.ant_path = # (bool) If True, then skip trying to update the Android sdk # This can be useful to avoid excess Internet downloads or save time # when an update is due and you just want to test/build your package # android.skip_update = False # (bool) If True, then automatically accept SDK license # agreements. This is intended for automation only. If set to False, # the default, you will be shown the license when first running # buildozer. android.accept_sdk_license = True # (str) Android entry point, default is ok for Kivy-based app #android.entrypoint = org.renpy.android.PythonActivity # (str) Android app theme, default is ok for Kivy-based app # android.apptheme = "@android:style/Theme.NoTitleBar" # (list) Pattern to whitelist for the whole project #android.whitelist = # (str) Path to a custom whitelist file #android.whitelist_src = # (str) Path to a custom blacklist file #android.blacklist_src = # (list) List of Java .jar files to add to the libs so that pyjnius can access # their classes. Don't add jars that you do not need, since extra jars can slow # down the build process. Allows wildcards matching, for example: # OUYA-ODK/libs/*.jar #android.add_jars = foo.jar,bar.jar,path/to/more/*.jar # (list) List of Java files to add to the android project (can be java or a # directory containing the files) #android.add_src = # (list) Android AAR archives to add #android.add_aars = # (list) Gradle dependencies to add #android.gradle_dependencies = # (list) add java compile options # this can for example be necessary when importing certain java libraries using the 'android.gradle_dependencies' option # see https://developer.android.com/studio/write/java8-support for further information # android.add_compile_options = "sourceCompatibility = 1.8", "targetCompatibility = 1.8" # (list) Gradle repositories to add {can be necessary for some android.gradle_dependencies} # please enclose in double quotes # e.g. android.gradle_repositories = "maven { url 'https://kotlin.bintray.com/ktor' }" #android.add_gradle_repositories = # (list) packaging options to add # see https://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.PackagingOptions.html # can be necessary to solve conflicts in gradle_dependencies # please enclose in double quotes # e.g. android.add_packaging_options = "exclude 'META-INF/common.kotlin_module'", "exclude 'META-INF/*.kotlin_module'" #android.add_packaging_options = # (list) Java classes to add as activities to the manifest. #android.add_activities = com.example.ExampleActivity # (str) OUYA Console category. Should be one of GAME or APP # If you leave this blank, OUYA support will not be enabled #android.ouya.category = GAME # (str) Filename of OUYA Console icon. It must be a 732x412 png image. #android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png # (str) XML file to include as an intent filters in tag #android.manifest.intent_filters = # (str) launchMode to set for the main activity #android.manifest.launch_mode = standard # (list) Android additional libraries to copy into libs/armeabi #android.add_libs_armeabi = libs/android/*.so #android.add_libs_armeabi_v7a = libs/android-v7/*.so #android.add_libs_arm64_v8a = libs/android-v8/*.so #android.add_libs_x86 = libs/android-x86/*.so #android.add_libs_mips = libs/android-mips/*.so # (bool) Indicate whether the screen should stay on # Don't forget to add the WAKE_LOCK permission if you set this to True #android.wakelock = False # (list) Android application meta-data to set (key=value format) #android.meta_data = # (list) Android library project to add (will be added in the # project.properties automatically.) #android.library_references = # (list) Android shared libraries which will be added to AndroidManifest.xml using tag #android.uses_library = # (str) Android logcat filters to use #android.logcat_filters = *:S python:D # (bool) Copy library instead of making a libpymodules.so #android.copy_libs = 1 # (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64 android.archs = arm64-v8a # (int) overrides automatic versionCode computation (used in build.gradle) # this is not the same as app version and should only be edited if you know what you're doing # android.numeric_version = 1 # (bool) enables Android auto backup feature (Android API >=23) android.allow_backup = True # (str) XML file for custom backup rules (see official auto backup documentation) # android.backup_rules = # # Python for android (p4a) specific # # (str) python-for-android fork to use, defaults to upstream (kivy) #p4a.fork = kivy # (str) python-for-android branch to use, defaults to master #p4a.branch = master p4a.branch = develop # (str) python-for-android git clone directory (if empty, it will be automatically cloned from github) #p4a.source_dir = # (str) The directory in which python-for-android should look for your own build recipes (if any) #p4a.local_recipes = recipes p4a.local_recipes = ../../bleak/backends/p4android/recipes # (str) Filename to the hook for p4a #p4a.hook = # (str) Bootstrap to use for android builds # p4a.bootstrap = sdl2 # (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask) #p4a.port = # Control passing the --use-setup-py vs --ignore-setup-py to p4a # "in the future" --use-setup-py is going to be the default behaviour in p4a, right now it is not # Setting this to false will pass --ignore-setup-py, true will pass --use-setup-py # NOTE: this is general setuptools integration, having pyproject.toml is enough, no need to generate # setup.py if you're using Poetry, but you need to add "toml" to source.include_exts. #p4a.setup_py = false # # iOS specific # # (str) Path to a custom kivy-ios folder #ios.kivy_ios_dir = ../kivy-ios # Alternately, specify the URL and branch of a git checkout: ios.kivy_ios_url = https://github.com/kivy/kivy-ios ios.kivy_ios_branch = master # Another platform dependency: ios-deploy # Uncomment to use a custom checkout #ios.ios_deploy_dir = ../ios_deploy # Or specify URL and branch ios.ios_deploy_url = https://github.com/phonegap/ios-deploy ios.ios_deploy_branch = 1.10.0 # (bool) Whether or not to sign the code ios.codesign.allowed = false # (str) Name of the certificate to use for signing the debug version # Get a list of available identities: buildozer ios list_identities #ios.codesign.debug = "iPhone Developer: ()" # (str) Name of the certificate to use for signing the release version #ios.codesign.release = %(ios.codesign.debug)s [buildozer] # (int) Log level (0 = error only, 1 = info, 2 = debug (with command output)) log_level = 2 # (int) Display warning if buildozer is run as root (0 = False, 1 = True) warn_on_root = 1 # (str) Path to build artifact storage, absolute or relative to spec file # build_dir = ./.buildozer # (str) Path to build output (i.e. .apk, .ipa) storage # bin_dir = ./bin # ----------------------------------------------------------------------------- # List as sections # # You can define all the "list" as [section:key]. # Each line will be considered as a option to the list. # Let's take [app] / source.exclude_patterns. # Instead of doing: # #[app] #source.exclude_patterns = license,data/audio/*.wav,data/images/original/* # # This can be translated into: # #[app:source.exclude_patterns] #license #data/audio/*.wav #data/images/original/* # # ----------------------------------------------------------------------------- # Profiles # # You can extend section / key with a profile # For example, you want to deploy a demo version of your application without # HD content. You could first change the title to add "(demo)" in the name # and extend the excluded directories to remove the HD content. # #[app@demo] #title = My Application (demo) # #[app:source.exclude_patterns@demo] #images/hd/* # # Then, invoke the command line with the "demo" profile: # #buildozer --profile demo android debug bleak-0.22.3/examples/kivy/main.py000066400000000000000000000052311470032643600167600ustar00rootroot00000000000000import asyncio import bleak from kivy.app import App from kivy.uix.label import Label from kivy.uix.scrollview import ScrollView # bind bleak's python logger into kivy's logger before importing python module using logging from kivy.logger import Logger # isort: skip import logging # isort: skip logging.Logger.manager.root = Logger class ExampleApp(App): def __init__(self): super().__init__() self.label = None self.running = True def build(self): self.scrollview = ScrollView(do_scroll_x=False, scroll_type=["bars", "content"]) self.label = Label(font_size="10sp") self.scrollview.add_widget(self.label) return self.scrollview def line(self, text, empty=False): Logger.info("example:" + text) if self.label is None: return text += "\n" if empty: self.label.text = text else: self.label.text += text def on_stop(self): self.running = False async def example(self): while self.running: try: self.line("scanning") scanned_devices = await bleak.BleakScanner.discover(1) self.line("scanned", True) if len(scanned_devices) == 0: raise bleak.exc.BleakError("no devices found") for device in scanned_devices: self.line(f"{device.name} ({device.address})") for device in scanned_devices: self.line(f"Connecting to {device.name} ...") try: async with bleak.BleakClient(device) as client: for service in client.services: self.line(f" service {service.uuid}") for characteristic in service.characteristics: self.line( f" characteristic {characteristic.uuid} {hex(characteristic.handle)} ({len(characteristic.descriptors)} descriptors)" ) except bleak.exc.BleakError as e: self.line(f" error {e}") asyncio.sleep(10) except bleak.exc.BleakError as e: self.line(f"ERROR {e}") await asyncio.sleep(1) self.line("example loop terminated", True) async def main(app): await asyncio.gather(app.async_run("asyncio"), app.example()) if __name__ == "__main__": Logger.setLevel(logging.DEBUG) # app running on one thread with two async coroutines app = ExampleApp() asyncio.run(main(app)) bleak-0.22.3/examples/mtu_size.py000066400000000000000000000025671470032643600167220ustar00rootroot00000000000000""" Example showing how to use BleakClient.mtu_size """ import asyncio from bleak import BleakClient, BleakScanner from bleak.backends.scanner import AdvertisementData, BLEDevice # replace with real characteristic UUID CHAR_UUID = "00000000-0000-0000-0000-000000000000" async def main(): queue = asyncio.Queue() def callback(device: BLEDevice, adv: AdvertisementData) -> None: # can use advertising data to filter here queue.put_nowait(device) async with BleakScanner(callback): # get the first matching device device = await queue.get() async with BleakClient(device) as client: # BlueZ doesn't have a proper way to get the MTU, so we have this hack. # If this doesn't work for you, you can set the client._mtu_size attribute # to override the value instead. if client._backend.__class__.__name__ == "BleakClientBlueZDBus": await client._backend._acquire_mtu() print("MTU:", client.mtu_size) # Write without response is limited to MTU - 3 bytes data = bytes(1000) # replace with real data chunk_size = client.mtu_size - 3 for chunk in ( data[i : i + chunk_size] for i in range(0, len(data), chunk_size) ): await client.write_gatt_char(CHAR_UUID, chunk, response=False) if __name__ == "__main__": asyncio.run(main()) bleak-0.22.3/examples/philips_hue.py000066400000000000000000000063551470032643600173730ustar00rootroot00000000000000""" Philips Hue lamp ---------------- Very important: It seems that the device needs to be connected to in the official Philips Hue Bluetooth app and reset from there to be able to use with Bleak-type of BLE software. After that one needs to do a encrypted pairing to enable reading and writing of characteristics. ONLY TESTED IN WINDOWS BACKEND AS OF YET! References: - https://www.reddit.com/r/Hue/comments/eq0y3y/philips_hue_bluetooth_developer_documentation/ - https://gist.github.com/shinyquagsire23/f7907fdf6b470200702e75a30135caf3 - https://github.com/Mic92/hue-ble-ctl/blob/master/hue-ble-ctl.py - https://github.com/npaun/philble/blob/master/philble/client.py - https://github.com/eb3095/hue-sync/blob/main/huelib/HueDevice.py Created on 2020-01-13 by hbldh """ import asyncio import sys from bleak import BleakClient ADDRESS = "EB:F0:49:21:95:4F" LIGHT_CHARACTERISTIC = "932c32bd-0002-47a2-835a-a8d455b859dd" BRIGHTNESS_CHARACTERISTIC = "932c32bd-0003-47a2-835a-a8d455b859dd" TEMPERATURE_CHARACTERISTIC = "932c32bd-0004-47a2-835a-a8d455b859dd" COLOR_CHARACTERISTIC = "932c32bd-0005-47a2-835a-a8d455b859dd" def convert_rgb(rgb): scale = 0xFF adjusted = [max(1, chan) for chan in rgb] total = sum(adjusted) adjusted = [int(round(chan / total * scale)) for chan in adjusted] # Unknown, Red, Blue, Green return bytearray([0x1, adjusted[0], adjusted[2], adjusted[1]]) async def main(address): async with BleakClient(address) as client: print(f"Connected: {client.is_connected}") paired = await client.pair(protection_level=2) print(f"Paired: {paired}") print("Turning Light off...") await client.write_gatt_char(LIGHT_CHARACTERISTIC, b"\x00", response=False) await asyncio.sleep(1.0) print("Turning Light on...") await client.write_gatt_char(LIGHT_CHARACTERISTIC, b"\x01", response=False) await asyncio.sleep(1.0) print("Setting color to RED...") color = convert_rgb([255, 0, 0]) await client.write_gatt_char(COLOR_CHARACTERISTIC, color, response=False) await asyncio.sleep(1.0) print("Setting color to GREEN...") color = convert_rgb([0, 255, 0]) await client.write_gatt_char(COLOR_CHARACTERISTIC, color, response=False) await asyncio.sleep(1.0) print("Setting color to BLUE...") color = convert_rgb([0, 0, 255]) await client.write_gatt_char(COLOR_CHARACTERISTIC, color, response=False) await asyncio.sleep(1.0) for brightness in range(256): print(f"Set Brightness to {brightness}...") await client.write_gatt_char( BRIGHTNESS_CHARACTERISTIC, bytearray( [ brightness, ] ), response=False, ) await asyncio.sleep(0.2) print(f"Set Brightness to {40}...") await client.write_gatt_char( BRIGHTNESS_CHARACTERISTIC, bytearray( [ 40, ] ), response=False, ) if __name__ == "__main__": asyncio.run(main(sys.argv[1] if len(sys.argv) == 2 else ADDRESS)) bleak-0.22.3/examples/scan_iterator.py000066400000000000000000000017161470032643600177130ustar00rootroot00000000000000""" Scan/Discovery Async Iterator -------------- Example showing how to scan for BLE devices using async iterator instead of callback function Created on 2023-07-07 by bojanpotocnik """ import asyncio from bleak import BleakScanner async def main(): async with BleakScanner() as scanner: print("Scanning...") n = 5 print(f"\n{n} advertisement packets:") async for bd, ad in scanner.advertisement_data(): print(f" {n}. {bd!r} with {ad!r}") n -= 1 if n == 0: break n = 10 print(f"\nFind device with name longer than {n} characters...") async for bd, ad in scanner.advertisement_data(): found = len(bd.name or "") > n or len(ad.local_name or "") > n print(f" Found{' it' if found else ''} {bd!r} with {ad!r}") if found: break if __name__ == "__main__": asyncio.run(main()) bleak-0.22.3/examples/sensortag.py000066400000000000000000000127141470032643600170630ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ TI CC2650 SensorTag ------------------- An example connecting to a TI CC2650 SensorTag. Created on 2018-01-10 by hbldh """ import asyncio import platform import sys from bleak import BleakClient from bleak.uuids import normalize_uuid_16, uuid16_dict ADDRESS = ( "24:71:89:cc:09:05" if platform.system() != "Darwin" else "B9EA5233-37EF-4DD6-87A8-2A875E821C46" ) ALL_SENSORTAG_CHARACTERISTIC_UUIDS = """ 00002a00-0000-1000-8000-00805f9b34fb 00002a01-0000-1000-8000-00805f9b34fb 00002a04-0000-1000-8000-00805f9b34fb 00002a23-0000-1000-8000-00805f9b34fb 00002a24-0000-1000-8000-00805f9b34fb 00002a25-0000-1000-8000-00805f9b34fb 00002a26-0000-1000-8000-00805f9b34fb 00002a27-0000-1000-8000-00805f9b34fb 00002a28-0000-1000-8000-00805f9b34fb 00002a29-0000-1000-8000-00805f9b34fb 00002a2a-0000-1000-8000-00805f9b34fb 00002a50-0000-1000-8000-00805f9b34fb 00002a19-0000-1000-8000-00805f9b34fb f000aa01-0451-4000-b000-000000000000 f000aa02-0451-4000-b000-000000000000 f000aa03-0451-4000-b000-000000000000 f000aa21-0451-4000-b000-000000000000 f000aa22-0451-4000-b000-000000000000 f000aa23-0451-4000-b000-000000000000 f000aa41-0451-4000-b000-000000000000 f000aa42-0451-4000-b000-000000000000 f000aa44-0451-4000-b000-000000000000 f000aa81-0451-4000-b000-000000000000 f000aa82-0451-4000-b000-000000000000 f000aa83-0451-4000-b000-000000000000 f000aa71-0451-4000-b000-000000000000 f000aa72-0451-4000-b000-000000000000 f000aa73-0451-4000-b000-000000000000 0000ffe1-0000-1000-8000-00805f9b34fb f000aa65-0451-4000-b000-000000000000 f000aa66-0451-4000-b000-000000000000 f000ac01-0451-4000-b000-000000000000 f000ac02-0451-4000-b000-000000000000 f000ac03-0451-4000-b000-000000000000 f000ccc1-0451-4000-b000-000000000000 f000ccc2-0451-4000-b000-000000000000 f000ccc3-0451-4000-b000-000000000000 f000ffc1-0451-4000-b000-000000000000 f000ffc2-0451-4000-b000-000000000000 f000ffc3-0451-4000-b000-000000000000 f000ffc4-0451-4000-b000-000000000000 """ uuid16_lookup = {v: normalize_uuid_16(k) for k, v in uuid16_dict.items()} SYSTEM_ID_UUID = uuid16_lookup["System ID"] MODEL_NBR_UUID = uuid16_lookup["Model Number String"] DEVICE_NAME_UUID = uuid16_lookup["Device Name"] FIRMWARE_REV_UUID = uuid16_lookup["Firmware Revision String"] HARDWARE_REV_UUID = uuid16_lookup["Hardware Revision String"] SOFTWARE_REV_UUID = uuid16_lookup["Software Revision String"] MANUFACTURER_NAME_UUID = uuid16_lookup["Manufacturer Name String"] BATTERY_LEVEL_UUID = uuid16_lookup["Battery Level"] KEY_PRESS_UUID = normalize_uuid_16(0xFFE1) # I/O test points on SensorTag. IO_DATA_CHAR_UUID = "f000aa65-0451-4000-b000-000000000000" IO_CONFIG_CHAR_UUID = "f000aa66-0451-4000-b000-000000000000" async def main(address): async with BleakClient(address, winrt=dict(use_cached_services=True)) as client: print(f"Connected: {client.is_connected}") system_id = await client.read_gatt_char(SYSTEM_ID_UUID) print( "System ID: {0}".format( ":".join(["{:02x}".format(x) for x in system_id[::-1]]) ) ) model_number = await client.read_gatt_char(MODEL_NBR_UUID) print("Model Number: {0}".format("".join(map(chr, model_number)))) try: device_name = await client.read_gatt_char(DEVICE_NAME_UUID) print("Device Name: {0}".format("".join(map(chr, device_name)))) except Exception: pass manufacturer_name = await client.read_gatt_char(MANUFACTURER_NAME_UUID) print("Manufacturer Name: {0}".format("".join(map(chr, manufacturer_name)))) firmware_revision = await client.read_gatt_char(FIRMWARE_REV_UUID) print("Firmware Revision: {0}".format("".join(map(chr, firmware_revision)))) hardware_revision = await client.read_gatt_char(HARDWARE_REV_UUID) print("Hardware Revision: {0}".format("".join(map(chr, hardware_revision)))) software_revision = await client.read_gatt_char(SOFTWARE_REV_UUID) print("Software Revision: {0}".format("".join(map(chr, software_revision)))) battery_level = await client.read_gatt_char(BATTERY_LEVEL_UUID) print("Battery Level: {0}%".format(int(battery_level[0]))) async def notification_handler(characteristic, data): print(f"{characteristic.description}: {data}") # Turn on the red light on the Sensor Tag by writing to I/O Data and I/O Config. write_value = bytearray([0x01]) value = await client.read_gatt_char(IO_DATA_CHAR_UUID) print("I/O Data Pre-Write Value: {0}".format(value)) await client.write_gatt_char(IO_DATA_CHAR_UUID, write_value, response=True) value = await client.read_gatt_char(IO_DATA_CHAR_UUID) print("I/O Data Post-Write Value: {0}".format(value)) assert value == write_value write_value = bytearray([0x01]) value = await client.read_gatt_char(IO_CONFIG_CHAR_UUID) print("I/O Config Pre-Write Value: {0}".format(value)) await client.write_gatt_char(IO_CONFIG_CHAR_UUID, write_value, response=True) value = await client.read_gatt_char(IO_CONFIG_CHAR_UUID) print("I/O Config Post-Write Value: {0}".format(value)) assert value == write_value # Try notifications with key presses. await client.start_notify(KEY_PRESS_UUID, notification_handler) await asyncio.sleep(5.0) await client.stop_notify(KEY_PRESS_UUID) if __name__ == "__main__": asyncio.run(main(sys.argv[1] if len(sys.argv) == 2 else ADDRESS)) bleak-0.22.3/examples/service_explorer.py000066400000000000000000000070271470032643600204370ustar00rootroot00000000000000""" Service Explorer ---------------- An example showing how to access and print out the services, characteristics and descriptors of a connected GATT server. Created on 2019-03-25 by hbldh """ import argparse import asyncio import logging from bleak import BleakClient, BleakScanner logger = logging.getLogger(__name__) async def main(args: argparse.Namespace): logger.info("starting scan...") if args.address: device = await BleakScanner.find_device_by_address( args.address, cb=dict(use_bdaddr=args.macos_use_bdaddr) ) if device is None: logger.error("could not find device with address '%s'", args.address) return else: device = await BleakScanner.find_device_by_name( args.name, cb=dict(use_bdaddr=args.macos_use_bdaddr) ) if device is None: logger.error("could not find device with name '%s'", args.name) return logger.info("connecting to device...") async with BleakClient( device, services=args.services, ) as client: logger.info("connected") for service in client.services: logger.info("[Service] %s", service) for char in service.characteristics: if "read" in char.properties: try: value = await client.read_gatt_char(char.uuid) extra = f", Value: {value}" except Exception as e: extra = f", Error: {e}" else: extra = "" if "write-without-response" in char.properties: extra += f", Max write w/o rsp size: {char.max_write_without_response_size}" logger.info( " [Characteristic] %s (%s)%s", char, ",".join(char.properties), extra, ) for descriptor in char.descriptors: try: value = await client.read_gatt_descriptor(descriptor.handle) logger.info(" [Descriptor] %s, Value: %r", descriptor, value) except Exception as e: logger.error(" [Descriptor] %s, Error: %s", descriptor, e) logger.info("disconnecting...") logger.info("disconnected") if __name__ == "__main__": parser = argparse.ArgumentParser() device_group = parser.add_mutually_exclusive_group(required=True) device_group.add_argument( "--name", metavar="", help="the name of the bluetooth device to connect to", ) device_group.add_argument( "--address", metavar="
", help="the address of the bluetooth device to connect to", ) parser.add_argument( "--macos-use-bdaddr", action="store_true", help="when true use Bluetooth address instead of UUID on macOS", ) parser.add_argument( "--services", nargs="+", metavar="", help="if provided, only enumerate matching service(s)", ) parser.add_argument( "-d", "--debug", action="store_true", help="sets the log level to debug", ) args = parser.parse_args() log_level = logging.DEBUG if args.debug else logging.INFO logging.basicConfig( level=log_level, format="%(asctime)-15s %(name)-8s %(levelname)s: %(message)s", ) asyncio.run(main(args)) bleak-0.22.3/examples/two_devices.py000066400000000000000000000113311470032643600173630ustar00rootroot00000000000000import argparse import asyncio import contextlib import logging from typing import Iterable from bleak import BleakClient, BleakScanner async def connect_to_device( lock: asyncio.Lock, by_address: bool, macos_use_bdaddr: bool, name_or_address: str, notify_uuid: str, ): """ Scan and connect to a device then print notifications for 10 seconds before disconnecting. Args: lock: The same lock must be passed to all calls to this function. by_address: If true, treat *name_or_address* as an address, otherwise treat it as a name. macos_use_bdaddr: If true, enable hack to allow use of Bluetooth address instead of UUID on macOS. name_or_address: The Bluetooth address/UUID of the device to connect to. notify_uuid: The UUID of a characteristic that supports notifications. """ logging.info("starting %s task", name_or_address) try: async with contextlib.AsyncExitStack() as stack: # Trying to establish a connection to two devices at the same time # can cause errors, so use a lock to avoid this. async with lock: logging.info("scanning for %s", name_or_address) if by_address: device = await BleakScanner.find_device_by_address( name_or_address, macos=dict(use_bdaddr=macos_use_bdaddr) ) else: device = await BleakScanner.find_device_by_name(name_or_address) logging.info("stopped scanning for %s", name_or_address) if device is None: logging.error("%s not found", name_or_address) return client = BleakClient(device) logging.info("connecting to %s", name_or_address) await stack.enter_async_context(client) logging.info("connected to %s", name_or_address) # This will be called immediately before client.__aexit__ when # the stack context manager exits. stack.callback(logging.info, "disconnecting from %s", name_or_address) # The lock is released here. The device is still connected and the # Bluetooth adapter is now free to scan and connect another device # without disconnecting this one. def callback(_, data): logging.info("%s received %r", name_or_address, data) await client.start_notify(notify_uuid, callback) await asyncio.sleep(10.0) await client.stop_notify(notify_uuid) # The stack context manager exits here, triggering disconnection. logging.info("disconnected from %s", name_or_address) except Exception: logging.exception("error with %s", name_or_address) async def main( by_address: bool, macos_use_bdaddr: bool, addresses: Iterable[str], uuids: Iterable[str], ): lock = asyncio.Lock() await asyncio.gather( *( connect_to_device(lock, by_address, macos_use_bdaddr, address, uuid) for address, uuid in zip(addresses, uuids) ) ) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument( "device1", metavar="", help="Bluetooth name or address of first device connect to", ) parser.add_argument( "uuid1", metavar="", help="notification characteristic UUID on first device", ) parser.add_argument( "device2", metavar="", help="Bluetooth name or address of second device to connect to", ) parser.add_argument( "uuid2", metavar="", help="notification characteristic UUID on second device", ) parser.add_argument( "--by-address", action="store_true", help="when true treat args as Bluetooth address instead of name", ) parser.add_argument( "--macos-use-bdaddr", action="store_true", help="when true use Bluetooth address instead of UUID on macOS", ) parser.add_argument( "-d", "--debug", action="store_true", help="sets the log level to debug", ) args = parser.parse_args() log_level = logging.DEBUG if args.debug else logging.INFO logging.basicConfig( level=log_level, format="%(asctime)-15s %(name)-8s %(levelname)s: %(message)s", ) asyncio.run( main( args.by_address, args.macos_use_bdaddr, (args.device1, args.device2), (args.uuid1, args.uuid2), ) ) bleak-0.22.3/examples/uart_service.py000066400000000000000000000071251470032643600175510ustar00rootroot00000000000000""" UART Service ------------- An example showing how to write a simple program using the Nordic Semiconductor (nRF) UART service. """ import asyncio import sys from itertools import count, takewhile from typing import Iterator from bleak import BleakClient, BleakScanner from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData UART_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" UART_RX_CHAR_UUID = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" UART_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" # TIP: you can get this function and more from the ``more-itertools`` package. def sliced(data: bytes, n: int) -> Iterator[bytes]: """ Slices *data* into chunks of size *n*. The last slice may be smaller than *n*. """ return takewhile(len, (data[i : i + n] for i in count(0, n))) async def uart_terminal(): """This is a simple "terminal" program that uses the Nordic Semiconductor (nRF) UART service. It reads from stdin and sends each line of data to the remote device. Any data received from the device is printed to stdout. """ def match_nus_uuid(device: BLEDevice, adv: AdvertisementData): # This assumes that the device includes the UART service UUID in the # advertising data. This test may need to be adjusted depending on the # actual advertising data supplied by the device. if UART_SERVICE_UUID.lower() in adv.service_uuids: return True return False device = await BleakScanner.find_device_by_filter(match_nus_uuid) if device is None: print("no matching device found, you may need to edit match_nus_uuid().") sys.exit(1) def handle_disconnect(_: BleakClient): print("Device was disconnected, goodbye.") # cancelling all tasks effectively ends the program for task in asyncio.all_tasks(): task.cancel() def handle_rx(_: BleakGATTCharacteristic, data: bytearray): print("received:", data) async with BleakClient(device, disconnected_callback=handle_disconnect) as client: await client.start_notify(UART_TX_CHAR_UUID, handle_rx) print("Connected, start typing and press ENTER...") loop = asyncio.get_running_loop() nus = client.services.get_service(UART_SERVICE_UUID) rx_char = nus.get_characteristic(UART_RX_CHAR_UUID) while True: # This waits until you type a line and press ENTER. # A real terminal program might put stdin in raw mode so that things # like CTRL+C get passed to the remote device. data = await loop.run_in_executor(None, sys.stdin.buffer.readline) # data will be empty on EOF (e.g. CTRL+D on *nix) if not data: break # some devices, like devices running MicroPython, expect Windows # line endings (uncomment line below if needed) # data = data.replace(b"\n", b"\r\n") # Writing without response requires that the data can fit in a # single BLE packet. We can use the max_write_without_response_size # property to split the data into chunks that will fit. for s in sliced(data, rx_char.max_write_without_response_size): await client.write_gatt_char(rx_char, s, response=False) print("sent:", data) if __name__ == "__main__": try: asyncio.run(uart_terminal()) except asyncio.CancelledError: # task is cancelled on disconnect, so we ignore this error pass bleak-0.22.3/poetry.lock000066400000000000000000002334471470032643600150720ustar00rootroot00000000000000# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "alabaster" version = "0.7.12" description = "A configurable sidebar-enabled Sphinx theme" optional = false python-versions = "*" files = [ {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, ] [[package]] name = "async-timeout" version = "4.0.2" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.6" files = [ {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, ] [[package]] name = "Babel" version = "2.10.3" description = "Internationalization utilities" optional = false python-versions = ">=3.6" files = [ {file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"}, {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, ] [package.dependencies] pytz = ">=2015.7" [[package]] name = "black" version = "24.4.2" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, ] [package.dependencies] click = ">=8.0.0" mypy-extensions = ">=0.4.3" packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "bleak-winrt" version = "1.2.0" description = "Python WinRT bindings for Bleak" optional = false python-versions = "*" files = [ {file = "bleak-winrt-1.2.0.tar.gz", hash = "sha256:0577d070251b9354fc6c45ffac57e39341ebb08ead014b1bdbd43e211d2ce1d6"}, {file = "bleak_winrt-1.2.0-cp310-cp310-win32.whl", hash = "sha256:a2ae3054d6843ae0cfd3b94c83293a1dfd5804393977dd69bde91cb5099fc47c"}, {file = "bleak_winrt-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:677df51dc825c6657b3ae94f00bd09b8ab88422b40d6a7bdbf7972a63bc44e9a"}, {file = "bleak_winrt-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9449cdb942f22c9892bc1ada99e2ccce9bea8a8af1493e81fefb6de2cb3a7b80"}, {file = "bleak_winrt-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:98c1b5a6a6c431ac7f76aa4285b752fe14a1c626bd8a1dfa56f66173ff120bee"}, {file = "bleak_winrt-1.2.0-cp37-cp37m-win32.whl", hash = "sha256:623ac511696e1f58d83cb9c431e32f613395f2199b3db7f125a3d872cab968a4"}, {file = "bleak_winrt-1.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:13ab06dec55469cf51a2c187be7b630a7a2922e1ea9ac1998135974a7239b1e3"}, {file = "bleak_winrt-1.2.0-cp38-cp38-win32.whl", hash = "sha256:5a36ff8cd53068c01a795a75d2c13054ddc5f99ce6de62c1a97cd343fc4d0727"}, {file = "bleak_winrt-1.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:810c00726653a962256b7acd8edf81ab9e4a3c66e936a342ce4aec7dbd3a7263"}, {file = "bleak_winrt-1.2.0-cp39-cp39-win32.whl", hash = "sha256:dd740047a08925bde54bec357391fcee595d7b8ca0c74c87170a5cbc3f97aa0a"}, {file = "bleak_winrt-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:63130c11acfe75c504a79c01f9919e87f009f5e742bfc7b7a5c2a9c72bf591a7"}, ] [[package]] name = "certifi" version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] [[package]] name = "charset-normalizer" version = "2.1.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.6.0" files = [ {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, ] [package.extras] unicode-backport = ["unicodedata2"] [[package]] name = "click" version = "8.1.3" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "colorama" version = "0.4.5" description = "Cross-platform colored terminal text." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, ] [[package]] name = "coverage" version = "6.4.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.7" files = [ {file = "coverage-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7b4da9bafad21ea45a714d3ea6f3e1679099e420c8741c74905b92ee9bfa7cc"}, {file = "coverage-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fde17bc42e0716c94bf19d92e4c9f5a00c5feb401f5bc01101fdf2a8b7cacf60"}, {file = "coverage-6.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdbb0d89923c80dbd435b9cf8bba0ff55585a3cdb28cbec65f376c041472c60d"}, {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67f9346aeebea54e845d29b487eb38ec95f2ecf3558a3cffb26ee3f0dcc3e760"}, {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42c499c14efd858b98c4e03595bf914089b98400d30789511577aa44607a1b74"}, {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c35cca192ba700979d20ac43024a82b9b32a60da2f983bec6c0f5b84aead635c"}, {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9cc4f107009bca5a81caef2fca843dbec4215c05e917a59dec0c8db5cff1d2aa"}, {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f444627b3664b80d078c05fe6a850dd711beeb90d26731f11d492dcbadb6973"}, {file = "coverage-6.4.4-cp310-cp310-win32.whl", hash = "sha256:66e6df3ac4659a435677d8cd40e8eb1ac7219345d27c41145991ee9bf4b806a0"}, {file = "coverage-6.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:35ef1f8d8a7a275aa7410d2f2c60fa6443f4a64fae9be671ec0696a68525b875"}, {file = "coverage-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c1328d0c2f194ffda30a45f11058c02410e679456276bfa0bbe0b0ee87225fac"}, {file = "coverage-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61b993f3998ee384935ee423c3d40894e93277f12482f6e777642a0141f55782"}, {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5dd4b8e9cd0deb60e6fcc7b0647cbc1da6c33b9e786f9c79721fd303994832f"}, {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7026f5afe0d1a933685d8f2169d7c2d2e624f6255fb584ca99ccca8c0e966fd7"}, {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9c7b9b498eb0c0d48b4c2abc0e10c2d78912203f972e0e63e3c9dc21f15abdaa"}, {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ee2b2fb6eb4ace35805f434e0f6409444e1466a47f620d1d5763a22600f0f892"}, {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ab066f5ab67059d1f1000b5e1aa8bbd75b6ed1fc0014559aea41a9eb66fc2ce0"}, {file = "coverage-6.4.4-cp311-cp311-win32.whl", hash = "sha256:9d6e1f3185cbfd3d91ac77ea065d85d5215d3dfa45b191d14ddfcd952fa53796"}, {file = "coverage-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e3d3c4cc38b2882f9a15bafd30aec079582b819bec1b8afdbde8f7797008108a"}, {file = "coverage-6.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a095aa0a996ea08b10580908e88fbaf81ecf798e923bbe64fb98d1807db3d68a"}, {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef6f44409ab02e202b31a05dd6666797f9de2aa2b4b3534e9d450e42dea5e817"}, {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b7101938584d67e6f45f0015b60e24a95bf8dea19836b1709a80342e01b472f"}, {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a32ec68d721c3d714d9b105c7acf8e0f8a4f4734c811eda75ff3718570b5e3"}, {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6a864733b22d3081749450466ac80698fe39c91cb6849b2ef8752fd7482011f3"}, {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08002f9251f51afdcc5e3adf5d5d66bb490ae893d9e21359b085f0e03390a820"}, {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a3b2752de32c455f2521a51bd3ffb53c5b3ae92736afde67ce83477f5c1dd928"}, {file = "coverage-6.4.4-cp37-cp37m-win32.whl", hash = "sha256:f855b39e4f75abd0dfbcf74a82e84ae3fc260d523fcb3532786bcbbcb158322c"}, {file = "coverage-6.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ee6ae6bbcac0786807295e9687169fba80cb0617852b2fa118a99667e8e6815d"}, {file = "coverage-6.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:564cd0f5b5470094df06fab676c6d77547abfdcb09b6c29c8a97c41ad03b103c"}, {file = "coverage-6.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cbbb0e4cd8ddcd5ef47641cfac97d8473ab6b132dd9a46bacb18872828031685"}, {file = "coverage-6.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6113e4df2fa73b80f77663445be6d567913fb3b82a86ceb64e44ae0e4b695de1"}, {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d032bfc562a52318ae05047a6eb801ff31ccee172dc0d2504614e911d8fa83e"}, {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e431e305a1f3126477abe9a184624a85308da8edf8486a863601d58419d26ffa"}, {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cf2afe83a53f77aec067033199797832617890e15bed42f4a1a93ea24794ae3e"}, {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:783bc7c4ee524039ca13b6d9b4186a67f8e63d91342c713e88c1865a38d0892a"}, {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ff934ced84054b9018665ca3967fc48e1ac99e811f6cc99ea65978e1d384454b"}, {file = "coverage-6.4.4-cp38-cp38-win32.whl", hash = "sha256:e1fabd473566fce2cf18ea41171d92814e4ef1495e04471786cbc943b89a3781"}, {file = "coverage-6.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:4179502f210ebed3ccfe2f78bf8e2d59e50b297b598b100d6c6e3341053066a2"}, {file = "coverage-6.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:98c0b9e9b572893cdb0a00e66cf961a238f8d870d4e1dc8e679eb8bdc2eb1b86"}, {file = "coverage-6.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc600f6ec19b273da1d85817eda339fb46ce9eef3e89f220055d8696e0a06908"}, {file = "coverage-6.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a98d6bf6d4ca5c07a600c7b4e0c5350cd483c85c736c522b786be90ea5bac4f"}, {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01778769097dbd705a24e221f42be885c544bb91251747a8a3efdec6eb4788f2"}, {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfa0b97eb904255e2ab24166071b27408f1f69c8fbda58e9c0972804851e0558"}, {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fcbe3d9a53e013f8ab88734d7e517eb2cd06b7e689bedf22c0eb68db5e4a0a19"}, {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:15e38d853ee224e92ccc9a851457fb1e1f12d7a5df5ae44544ce7863691c7a0d"}, {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6913dddee2deff8ab2512639c5168c3e80b3ebb0f818fed22048ee46f735351a"}, {file = "coverage-6.4.4-cp39-cp39-win32.whl", hash = "sha256:354df19fefd03b9a13132fa6643527ef7905712109d9c1c1903f2133d3a4e145"}, {file = "coverage-6.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:1238b08f3576201ebf41f7c20bf59baa0d05da941b123c6656e42cdb668e9827"}, {file = "coverage-6.4.4-pp36.pp37.pp38-none-any.whl", hash = "sha256:f67cf9f406cf0d2f08a3515ce2db5b82625a7257f88aad87904674def6ddaec1"}, {file = "coverage-6.4.4.tar.gz", hash = "sha256:e16c45b726acb780e1e6f88b286d3c10b3914ab03438f32117c4aa52d7f30d58"}, ] [package.dependencies] tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] toml = ["tomli"] [[package]] name = "dbus-fast" version = "2.0.0" description = "A faster version of dbus-next" optional = false python-versions = ">=3.7,<4.0" files = [ {file = "dbus_fast-2.0.0-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:558748ce71696af21414b44119c4cf9d8aca1f3d7ebb493a9f4723e95dd71da1"}, {file = "dbus_fast-2.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5404cbd9c50075f18aa1e412c354fd4c8ab8d4b824599ded1302231acc30990e"}, {file = "dbus_fast-2.0.0-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:804004c552b271d9dc5709934ebeeeefce763fe3081f793ea163518fd1848092"}, {file = "dbus_fast-2.0.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6030b22985caa030a5f873dfa8aee556506cdcb47ffdae85cf250ce2b360a11f"}, {file = "dbus_fast-2.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cfeddf01c8e57be89cec50d0b978a8307e1ad8402bce162b58467931e9ad6596"}, {file = "dbus_fast-2.0.0-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:ec7ddb797f1c92a3089de444bb422aa749f845affae44527310d322b0c48ea65"}, {file = "dbus_fast-2.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eea7a2730bf7490cf50950ddf0872a5cc61c6e557bf92320f15178e7c9e9aff9"}, {file = "dbus_fast-2.0.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7a72d03edae8269476aa6af11744871281f6e1b0b6d8f065ef98047c03ffa464"}, {file = "dbus_fast-2.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:34c418b322f47be0313535505915df0fd5cff9ffd899d0be87edf2830cc1d339"}, {file = "dbus_fast-2.0.0-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:9a7005db366adf01e871a8071437218fe27fe067554c44ca3d4b389769dbbc76"}, {file = "dbus_fast-2.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef8a64d5e853b0accc379dc4c6bf1b395227834adc705fa78d3417f2e09cdac2"}, {file = "dbus_fast-2.0.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1aa133f0a70ad83ad8dfcde6ac8684e3492404c8ccc0080695855e4c2973f355"}, {file = "dbus_fast-2.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:45057e9f6984ad3298c6ce4526022ce432ca44a78ff05547bac20028e9290129"}, {file = "dbus_fast-2.0.0-cp37-cp37m-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:3dc584fe6c87aa4db3b4d938795f4d70bdadaaffa8ca4967c4a68f1151f36243"}, {file = "dbus_fast-2.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cb7027302584377c97bf6441d4ca3d35300cc3d0b49b407165eee20796d2f04"}, {file = "dbus_fast-2.0.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:855f8c76cb227fc1745bf43f40899898be19ea2dc8b120e86c55f26eac07e541"}, {file = "dbus_fast-2.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a73cf139c7c5014620186fec64d749ea566656ad4a78e4854bceff999acdb916"}, {file = "dbus_fast-2.0.0-cp38-cp38-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:2ebe6975fc24455b672e59c4a26476f3e5e9d25350ffc7941127cf3ce23df79e"}, {file = "dbus_fast-2.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77fe92d8588f18fcfeb5773fb8f541d1ea0064347e95a12cfe7f2d0913f4d3e9"}, {file = "dbus_fast-2.0.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cc21cd1adf3aa3be205f2ee027d0d3d5b73a809de59c48027d87434309dfddde"}, {file = "dbus_fast-2.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a4db0946a8b7129a573b9fc01b70107f51db2fbeb8aba8409d75edf59e525b6e"}, {file = "dbus_fast-2.0.0-cp39-cp39-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:83af9ed1479e733ff1d5bcedde1d689d02ff746ef587bc3e9aa243b7a464a0c3"}, {file = "dbus_fast-2.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f5c93343b580be44caa83271331e45aee0ce329461f132a1be581679cddf6c5"}, {file = "dbus_fast-2.0.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7779a91aa5e1b0811658d672ae36d0a66aa1d2e693a113713ef1f4e769913e67"}, {file = "dbus_fast-2.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3a0d5c6e9b874bf2ab509d9c5116f1d0067fbf48e72689c9eecca2de0939ec67"}, {file = "dbus_fast-2.0.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:4ed0dce64aa6ecf0076524942184cc7ca50166c67930eb0467989ca5d5c4cfa1"}, {file = "dbus_fast-2.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e515ed03adc94a76caf12ce93bf53539ae8b9e2960d0fc329e258249a0767a15"}, {file = "dbus_fast-2.0.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:4efb676f5c577853ea1b45ac88fa6c799504b651138926e8714e7e2b40b848c7"}, {file = "dbus_fast-2.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29d7b559d780fc19b5250b8358e43c557227e36bacb27c3cae40c44d24589b5e"}, {file = "dbus_fast-2.0.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:683fbd87f76099fa767a01c7e6a98f0214a8746f4a83aee6b57adec23ae2948f"}, {file = "dbus_fast-2.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8df17f12c8d32e0cd0f32ff9691b9dcc1fb6024750de62f23c1c571256cbe87d"}, {file = "dbus_fast-2.0.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:1bb1d8c582f53c0051b9f8ffaceb1b01e0e3fb2acd5a80a70c3a4b21488fe4da"}, {file = "dbus_fast-2.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3552250763864f285899c1ef0153a58bf0e1fe5644e7dc43772132890300cbd"}, {file = "dbus_fast-2.0.0.tar.gz", hash = "sha256:bb8bfdc01d50be88598f58bab2e1dea72b0730e026b3c39260dbf8f6c64b88bd"}, ] [[package]] name = "docutils" version = "0.17.1" description = "Docutils -- Python Documentation Utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, ] [[package]] name = "exceptiongroup" version = "1.2.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, ] [package.extras] test = ["pytest (>=6)"] [[package]] name = "flake8" version = "5.0.4" description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.6.1" files = [ {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, ] [package.dependencies] mccabe = ">=0.7.0,<0.8.0" pycodestyle = ">=2.9.0,<2.10.0" pyflakes = ">=2.5.0,<2.6.0" [[package]] name = "idna" version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [[package]] name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] [[package]] name = "importlib-metadata" version = "4.12.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.7" files = [ {file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"}, {file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"] perf = ["ipython"] testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] [[package]] name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" optional = false python-versions = "*" files = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] [[package]] name = "isort" version = "5.13.2" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" files = [ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, ] [package.extras] colors = ["colorama (>=0.4.6)"] [[package]] name = "jinja2" version = "3.1.4" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, ] [package.dependencies] MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] [[package]] name = "MarkupSafe" version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" files = [ {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, ] [[package]] name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] [[package]] name = "mypy-extensions" version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." optional = false python-versions = "*" files = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] [[package]] name = "packaging" version = "24.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] name = "pathspec" version = "0.10.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.7" files = [ {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"}, {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"}, ] [[package]] name = "platformdirs" version = "2.5.2" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.7" files = [ {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, ] [package.extras] docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] [[package]] name = "pluggy" version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] name = "pycodestyle" version = "2.9.1" description = "Python style guide checker" optional = false python-versions = ">=3.6" files = [ {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, ] [[package]] name = "pyflakes" version = "2.5.0" description = "passive checker of Python programs" optional = false python-versions = ">=3.6" files = [ {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, ] [[package]] name = "pygments" version = "2.15.0" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.7" files = [ {file = "Pygments-2.15.0-py3-none-any.whl", hash = "sha256:77a3299119af881904cd5ecd1ac6a66214b6e9bed1f2db16993b54adede64094"}, {file = "Pygments-2.15.0.tar.gz", hash = "sha256:f7e36cffc4c517fbc252861b9a6e4644ca0e5abadf9a113c72d1358ad09b9500"}, ] [package.extras] plugins = ["importlib-metadata"] [[package]] name = "pyobjc-core" version = "10.3.1" description = "Python<->ObjC Interoperability Module" optional = false python-versions = ">=3.8" files = [ {file = "pyobjc_core-10.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ea46d2cda17921e417085ac6286d43ae448113158afcf39e0abe484c58fb3d78"}, {file = "pyobjc_core-10.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:899d3c84d2933d292c808f385dc881a140cf08632907845043a333a9d7c899f9"}, {file = "pyobjc_core-10.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6ff5823d13d0a534cdc17fa4ad47cf5bee4846ce0fd27fc40012e12b46db571b"}, {file = "pyobjc_core-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2581e8e68885bcb0e11ec619e81ef28e08ee3fac4de20d8cc83bc5af5bcf4a90"}, {file = "pyobjc_core-10.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ea98d4c2ec39ca29e62e0327db21418696161fb138ee6278daf2acbedf7ce504"}, {file = "pyobjc_core-10.3.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:4c179c26ee2123d0aabffb9dbc60324b62b6f8614fb2c2328b09386ef59ef6d8"}, {file = "pyobjc_core-10.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cb901fce65c9be420c40d8a6ee6fff5ff27c6945f44fd7191989b982baa66dea"}, {file = "pyobjc_core-10.3.1.tar.gz", hash = "sha256:b204a80ccc070f9ab3f8af423a3a25a6fd787e228508d00c4c30f8ac538ba720"}, ] [[package]] name = "pyobjc-framework-cocoa" version = "10.3.1" description = "Wrappers for the Cocoa frameworks on macOS" optional = false python-versions = ">=3.8" files = [ {file = "pyobjc_framework_Cocoa-10.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4cb4f8491ab4d9b59f5187e42383f819f7a46306a4fa25b84f126776305291d1"}, {file = "pyobjc_framework_Cocoa-10.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5f31021f4f8fdf873b57a97ee1f3c1620dbe285e0b4eaed73dd0005eb72fd773"}, {file = "pyobjc_framework_Cocoa-10.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:11b4e0bad4bbb44a4edda128612f03cdeab38644bbf174de0c13129715497296"}, {file = "pyobjc_framework_Cocoa-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:de5e62e5ccf2871a94acf3bf79646b20ea893cc9db78afa8d1fe1b0d0f7cbdb0"}, {file = "pyobjc_framework_Cocoa-10.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c5af24610ab639bd1f521ce4500484b40787f898f691b7a23da3339e6bc8b90"}, {file = "pyobjc_framework_Cocoa-10.3.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:a7151186bb7805deea434fae9a4423335e6371d105f29e73cc2036c6779a9dbc"}, {file = "pyobjc_framework_Cocoa-10.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:743d2a1ac08027fd09eab65814c79002a1d0421d7c0074ffd1217b6560889744"}, {file = "pyobjc_framework_cocoa-10.3.1.tar.gz", hash = "sha256:1cf20714daaa986b488fb62d69713049f635c9d41a60c8da97d835710445281a"}, ] [package.dependencies] pyobjc-core = ">=10.3.1" [[package]] name = "pyobjc-framework-corebluetooth" version = "10.3.1" description = "Wrappers for the framework CoreBluetooth on macOS" optional = false python-versions = ">=3.8" files = [ {file = "pyobjc_framework_CoreBluetooth-10.3.1-cp36-abi3-macosx_10_13_universal2.whl", hash = "sha256:c89ee6fba0ed359c46b4908a7d01f88f133be025bd534cbbf4fb9c183e62fc97"}, {file = "pyobjc_framework_CoreBluetooth-10.3.1-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:2f261a386aa6906f9d4601d35ff71a13315dbca1a0698bf1f1ecfe3971de4648"}, {file = "pyobjc_framework_CoreBluetooth-10.3.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:5211df0da2e8be511d9a54a48505dd7af0c4d04546fe2027dd723801d633c6ba"}, {file = "pyobjc_framework_CoreBluetooth-10.3.1-cp36-abi3-macosx_11_0_universal2.whl", hash = "sha256:b8becd4e406be289a2d423611d3ad40730532a1f6728effb2200e68c9c04c3e8"}, {file = "pyobjc_framework_corebluetooth-10.3.1.tar.gz", hash = "sha256:dc5d326ab5541b8b68e7e920aa8363851e779cb8c33842f6cfeef4674cc62f94"}, ] [package.dependencies] pyobjc-core = ">=10.3.1" pyobjc-framework-Cocoa = ">=10.3.1" [[package]] name = "pyobjc-framework-libdispatch" version = "10.3.1" description = "Wrappers for libdispatch on macOS" optional = false python-versions = ">=3.8" files = [ {file = "pyobjc_framework_libdispatch-10.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5543aea8acd53fb02bcf962b003a2a9c2bdacf28dc290c31a3d2de7543ef8392"}, {file = "pyobjc_framework_libdispatch-10.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3e0db3138aae333f0b87b42586bc016430a76638af169aab9cef6afee4e5f887"}, {file = "pyobjc_framework_libdispatch-10.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b209dbc9338cd87e053ede4d782b8c445bcc0b9a3d0365a6ffa1f9cd5143c301"}, {file = "pyobjc_framework_libdispatch-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a74e62314376dc2d34bc5d4a86cedaf5795786178ebccd0553c58e8fa73400a3"}, {file = "pyobjc_framework_libdispatch-10.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8e8fb27ac86d48605eb2107ac408ed8de281751df81f5430fe66c8228d7626b8"}, {file = "pyobjc_framework_libdispatch-10.3.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:0a7a19afef70c98b3b527fb2c9adb025444bcb50f65c8d7b949f1efb51bde577"}, {file = "pyobjc_framework_libdispatch-10.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:109044cddecb3332cbb75f14819cd01b98aacfefe91204c776b491eccc58a112"}, {file = "pyobjc_framework_libdispatch-10.3.1.tar.gz", hash = "sha256:f5c3475498cb32f54d75e21952670e4a32c8517fb2db2e90869f634edc942446"}, ] [package.dependencies] pyobjc-core = ">=10.3.1" pyobjc-framework-Cocoa = ">=10.3.1" [[package]] name = "pytest" version = "8.2.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=1.5,<2.0" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" version = "0.23.7" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" files = [ {file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"}, {file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"}, ] [package.dependencies] pytest = ">=7.0.0,<9" [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-cov" version = "3.0.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.6" files = [ {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, ] [package.dependencies] coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] name = "pytz" version = "2022.2.1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ {file = "pytz-2022.2.1-py2.py3-none-any.whl", hash = "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197"}, {file = "pytz-2022.2.1.tar.gz", hash = "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5"}, ] [[package]] name = "requests" version = "2.32.0" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" files = [ {file = "requests-2.32.0-py3-none-any.whl", hash = "sha256:f2c3881dddb70d056c5bd7600a4fae312b2a300e39be6a118d30b90bd27262b5"}, {file = "requests-2.32.0.tar.gz", hash = "sha256:fa5490319474c82ef1d2c9bc459d3652e3ae4ef4c4ebdd18a21145a47ca4b6b8"}, ] [package.dependencies] certifi = ">=2017.4.17" charset-normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." optional = false python-versions = "*" files = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] [[package]] name = "Sphinx" version = "5.1.1" description = "Python documentation generator" optional = false python-versions = ">=3.6" files = [ {file = "Sphinx-5.1.1-py3-none-any.whl", hash = "sha256:309a8da80cb6da9f4713438e5b55861877d5d7976b69d87e336733637ea12693"}, {file = "Sphinx-5.1.1.tar.gz", hash = "sha256:ba3224a4e206e1fbdecf98a4fae4992ef9b24b85ebf7b584bb340156eaf08d89"}, ] [package.dependencies] alabaster = ">=0.7,<0.8" babel = ">=1.3" colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} docutils = ">=0.14,<0.20" imagesize = "*" importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} Jinja2 = ">=2.3" packaging = "*" Pygments = ">=2.0" requests = ">=2.5.0" snowballstemmer = ">=1.1" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] docs = ["sphinxcontrib-websupport"] lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-bugbear", "flake8-comprehensions", "isort", "mypy (>=0.971)", "sphinx-lint", "types-requests", "types-typed-ast"] test = ["cython", "html5lib", "pytest (>=4.6)", "typed-ast"] [[package]] name = "sphinx-rtd-theme" version = "1.0.0" description = "Read the Docs theme for Sphinx" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" files = [ {file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"}, {file = "sphinx_rtd_theme-1.0.0.tar.gz", hash = "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"}, ] [package.dependencies] docutils = "<0.18" sphinx = ">=1.6" [package.extras] dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client"] [[package]] name = "sphinxcontrib-applehelp" version = "1.0.2" description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" optional = false python-versions = ">=3.5" files = [ {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sphinxcontrib-devhelp" version = "1.0.2" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." optional = false python-versions = ">=3.5" files = [ {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" version = "2.0.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = false python-versions = ">=3.6" files = [ {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["html5lib", "pytest"] [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" optional = false python-versions = ">=3.5" files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, ] [package.extras] test = ["flake8", "mypy", "pytest"] [[package]] name = "sphinxcontrib-qthelp" version = "1.0.3" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." optional = false python-versions = ">=3.5" files = [ {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sphinxcontrib-serializinghtml" version = "1.1.5" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." optional = false python-versions = ">=3.5" files = [ {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.7" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] [[package]] name = "typing-extensions" version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" optional = false python-versions = ">=3.7" files = [ {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, ] [[package]] name = "urllib3" version = "1.26.19" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ {file = "urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3"}, {file = "urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"}, ] [package.extras] brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "winrt-runtime" version = "2.2.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" files = [ {file = "winrt_runtime-2.2.0-cp310-cp310-win32.whl", hash = "sha256:ab034330d6b64ce93683bdc14d4f3f83dfafbf1f72b45893505f7d684e5e7fe1"}, {file = "winrt_runtime-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:ad9927a1838dea47ceb2d773c0269242bcee7cb5379ed801547788ab435da502"}, {file = "winrt_runtime-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:87745ae54d054957a99c70875c1ac3c89cca258ed06836ae308fbbb7dda4ef61"}, {file = "winrt_runtime-2.2.0-cp311-cp311-win32.whl", hash = "sha256:7ee2397934c1c4a090f9d889292def90b8f673dc1d320f1f07931ad1cb6e49bf"}, {file = "winrt_runtime-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:f110b0f451b514cf09c4fa0e73bab54d4b598c3092df9dd87940403998e81f30"}, {file = "winrt_runtime-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:27606e7a393a26e484f03db699c4d7c206d180a3736a6cd68fba3b3896e364a4"}, {file = "winrt_runtime-2.2.0-cp312-cp312-win32.whl", hash = "sha256:5a769bfb4e264b7fd306027da90c6e4e615667e9afdd8e5d712bc45bdabaf0d2"}, {file = "winrt_runtime-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ef30ea7446a1e37660265b76e586fcffc0e83a859b7729141cdf68cbedf808a8"}, {file = "winrt_runtime-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:d8f6338fb8433b4df900c8f173959a5ae9ac63b0b20faddb338e76a6e9391bc9"}, {file = "winrt_runtime-2.2.0-cp313-cp313-win32.whl", hash = "sha256:6d8c1122158edc96cac956a5ab62bc06a56e088bdf83d0993a455216b3fd1cac"}, {file = "winrt_runtime-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:76b2dc846e6802375113c9ce9e7fcc4292926bd788445f34d404bae72d2b4f4b"}, {file = "winrt_runtime-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:faacc05577573702cb135e7da4d619f4990c768063dc869362f13d856a0738e3"}, {file = "winrt_runtime-2.2.0-cp39-cp39-win32.whl", hash = "sha256:f00334e3304a43e1742514bed2dc736a9242e831676f605fdfb5d62932714b18"}, {file = "winrt_runtime-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:ef1b2dc31576d686cce088a349b539fc0f47bdf2f66fb8ea63a6964dc069d00d"}, {file = "winrt_runtime-2.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:1c9e8a609cf00acc426eae2ed4ad866991a0f33f196ec9dc69af95ae43b4373b"}, {file = "winrt_runtime-2.2.0.tar.gz", hash = "sha256:37a673b295ebd5f6dc5a3b42fd52c8e4589ca3e605deb54c26d0877d2575ec85"}, ] [[package]] name = "winrt-windows-devices-bluetooth" version = "2.2.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" files = [ {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp310-cp310-win32.whl", hash = "sha256:f3ced50ded44f74ac901d05f99cdd0bdf78e3a939a42d3cd80c33e510b4b8569"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:241a8f0ab06f6178d2e5757e7bc1f6c37e00e65ab6858ae676a1723a6445fa92"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3abefa3d11b4af9d9731d9d1a71083b1ef301fa30f7006a6c1f341426dd6d733"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp311-cp311-win32.whl", hash = "sha256:4215c45595201f5f43f98b1e8911ff5cb0b303fe3298fa4d91a7bdc6d5523853"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5cda69842b30bf56b10ea1a747d01b295abc910d9ccc10e9c97e8f554cd536e0"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7c12a28cd04eb05bacc73d8025ba135a929b9d511d21f20d0072d735853e8a2"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp312-cp312-win32.whl", hash = "sha256:c929ea5215942fb26081b26aae094a2f70551cc0a59499ab2c9ea1f6d6b991f9"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1444e2031f3e69990d412b9edf75413a09280744bbc088a6b0760d94d356d4b"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f2d06ce6c43e37ea09ac073805ac6f9f62ae10ce552c90ae6eca978accd3f434"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp313-cp313-win32.whl", hash = "sha256:b44a45c60f1d9fa288a12119991060ef7998793c6b93baa84308cfb090492788"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:fb698a55d06dc34643437b370c35fa064bd28762561e880715a30463c359fa44"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:cb350bfe21bab3573c9cd84006efad9c46a395a2943ab474105aed8b21bb88a4"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp39-cp39-win32.whl", hash = "sha256:7ee056e4c1a542352bcacbb95f898b7ae2739b3e0a63f7ab1290a7e2569f6393"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:f919cee2a49c3c48d1ef9dd84b419a6438000ef43bc35a7a349291c162cab4f3"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:f223af93675f6f92ab87de08c6d413ecc8ab19014b7438893437c42dcb2b0969"}, {file = "winrt_windows_devices_bluetooth-2.2.0.tar.gz", hash = "sha256:95a5cf9c1e915557a28a4f017ea1ff7357039ee23526258f9cc161cf080b4577"}, ] [package.dependencies] winrt-runtime = "2.2.0" [package.extras] all = ["winrt-Windows.Devices.Bluetooth.GenericAttributeProfile[all] (==2.2.0)", "winrt-Windows.Devices.Bluetooth.Rfcomm[all] (==2.2.0)", "winrt-Windows.Devices.Enumeration[all] (==2.2.0)", "winrt-Windows.Devices.Radios[all] (==2.2.0)", "winrt-Windows.Foundation.Collections[all] (==2.2.0)", "winrt-Windows.Foundation[all] (==2.2.0)", "winrt-Windows.Networking[all] (==2.2.0)", "winrt-Windows.Storage.Streams[all] (==2.2.0)"] [[package]] name = "winrt-windows-devices-bluetooth-advertisement" version = "2.2.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" files = [ {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp310-cp310-win32.whl", hash = "sha256:3d5fddffd5f6eeafebe1bcbaa096b8962c28c9236490f6f887ac2ed3ee4ed62c"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:f1cb5a835dc3574b0c47a613fa49eeeccdd9aa5801d43d7b7606ad5ce3614a54"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:9c2530c4972671ffb8a6e54621490c6c7a8c13b4d57e6474e05b62f211bbaab6"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp311-cp311-win32.whl", hash = "sha256:28b36b3be137bdb6bdaad0d7a620c1a8b156e3c2737d08b9827af02b3c9d52bf"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:52948f17ecfc70c58b07077191985712172b518b5e3f4874e5708d175b7ace72"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:338296b76c01840c1dc10799a405b76460346bf677af11e6ab324311fd58e1a9"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp312-cp312-win32.whl", hash = "sha256:4c14f48ac1886a3d374ee511467f0a61f26d88a321bf97d47429859730ee9248"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:89a658e901de88373e6a17a98273b8555e3f80563f2cc362b7f75817a7f9d915"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:3b2b1b34f37a3329cf72793a089dd13fefd7b582c3e3a53a69a1353fd18940a3"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp313-cp313-win32.whl", hash = "sha256:1b2d42c3d90b3e985954196b9a9e4007e22ff468d3d020c5a4acdee2821018fe"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d964c599670ea21b97afe2435e7638ca26e04936aacc0550474b6ec3fea988f"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:add4f459f0a02d1da38d579c3af887cfc3fe54f7782d779cf4ffe7f24404f1ff"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp39-cp39-win32.whl", hash = "sha256:756aeb2408bd59983a34da7f2552690d9e1071ad75de96aff15b365e1137b157"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:9d19ef4cb00f58e10bdd0a2eb497eabecb3a2a5586fdcacebae6f0009585f3f1"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:1008641262bbbe130b6fcda76b9c890327aa416ef5b240a6a2cbb895d37dd3c7"}, {file = "winrt_windows_devices_bluetooth_advertisement-2.2.0.tar.gz", hash = "sha256:bcbf246994b60e5de4bea9eb3fa01c5d6452200789004d14df70b27be9aa4775"}, ] [package.dependencies] winrt-runtime = "2.2.0" [package.extras] all = ["winrt-Windows.Devices.Bluetooth[all] (==2.2.0)", "winrt-Windows.Foundation.Collections[all] (==2.2.0)", "winrt-Windows.Foundation[all] (==2.2.0)", "winrt-Windows.Storage.Streams[all] (==2.2.0)"] [[package]] name = "winrt-windows-devices-bluetooth-genericattributeprofile" version = "2.2.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" files = [ {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp310-cp310-win32.whl", hash = "sha256:1472f89b9d6527137e1c58dfb46f22faf2753c477a9d4f85f789b3266ad282a9"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:e25702f1aa6d4ecdf335805a50048e70ee2206499cfd7ed4fbe1a92358bdcc16"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:d07d27a6f8f7a1f52aa978724d5a09d43053b428c71563892b70df409049a37a"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp311-cp311-win32.whl", hash = "sha256:5c6c863daaa99b0bb670730296137b7c718d94726c112ff44ec73c8b27a12ded"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbee7c90c0a155477eba09eb09297711b2cb32f6ede4c01d0afe58cb3776f06a"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:655777193fd338e1a8c30ebbb8460c017d08548c54ddec9fc5503f1605c47332"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp312-cp312-win32.whl", hash = "sha256:45a48ab8da94eee1590f22826c084f4b1f8c32107a023f05d6a03437931a6852"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:395cb2fecd0835a402c3c4f274395bc689549b2a6b4155d3ad97b29ec87ee4f2"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:25063b43550c5630f188cfb263ab09acc920db97d1625c48e24baa6e7d445b6e"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp313-cp313-win32.whl", hash = "sha256:d1d26512fe45c3be0dbeb932dbd75abd580cd46ccfc278fcf51042eff302fa9c"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:21786840502a34958dd5fb137381f9144a6437b49ee90a877beb3148ead6cfe9"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d98852458b639e875bb4895a9ad2d5626059bc99c5f745be0560d235502d648"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp39-cp39-win32.whl", hash = "sha256:827b390b1a47c9aa6bfd717b66822f4fc698b0c02c8678924e2bc6ac37093b65"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:727567b725ca94b677bda97a6f725d58fc1a4652d4cc232b44cc57dd7ba9ee87"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:ac901d17d2350785bce18282cd29d002d2c4da8adff5160891c4115ae010a2d0"}, {file = "winrt_windows_devices_bluetooth_genericattributeprofile-2.2.0.tar.gz", hash = "sha256:0de4ee5f57223107f25c20f6bb2739947670a2f8cf09907f3e611efc81e7c6e0"}, ] [package.dependencies] winrt-runtime = "2.2.0" [package.extras] all = ["winrt-Windows.Devices.Bluetooth[all] (==2.2.0)", "winrt-Windows.Devices.Enumeration[all] (==2.2.0)", "winrt-Windows.Foundation.Collections[all] (==2.2.0)", "winrt-Windows.Foundation[all] (==2.2.0)", "winrt-Windows.Storage.Streams[all] (==2.2.0)"] [[package]] name = "winrt-windows-devices-enumeration" version = "2.2.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" files = [ {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp310-cp310-win32.whl", hash = "sha256:69e87ba0ae5c31f60bc07d0558d91af96213d8b8b2b1be0ccf3e5824cab466ef"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6993d5305ff750c5c51f57253935458996fb45c049891f2fb00772cc6ece6b3"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bb54aa94b17052d65fe4fa5777183cf9bfb697574c3461759114d3ec0c802cec"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp311-cp311-win32.whl", hash = "sha256:fef83263e73c2611d223f06735d2c2a16629d723f74e1964dc882f90b6e1cda1"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:cf3cec5a6fba069ecbd4f3efa95e9f197aeebdd05a60bcd52b953888169ab7ee"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:d9ce308c492c1e9f2417f91ad02e366f4269cc1c6d271f0be4092b758df4c9bf"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp312-cp312-win32.whl", hash = "sha256:5bea21988749fad21574ea789b4090cfbfbb982a5f9a42b2d6f05b3ad47f68bd"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:c9718d7033550a029e0c2848ff620bf063a519cb22ab9d880d64ceb302763a48"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:69f67f01aa519304e4af04a1a23261bd8b57136395de2e08d56968f9c6daa18e"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp313-cp313-win32.whl", hash = "sha256:84447916282773d7b7e5a445eae0ab273c21105f1bbcdfb7d8e21cd41403d5c1"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:1bb9d97f8d2518bb5b331f825431814277de4341811a1776e79d51767e79700c"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:2a5408423f680f6b36d7accad7151336ea16ad1eaa2652f60ed88e2cbd14562c"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp39-cp39-win32.whl", hash = "sha256:51f4c9b6f3376913e3009bfe232cfc082357b24d6eeec098cf53f361527e1c1f"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:1e6895d5538539d0c6bd081374e7646684901038d4d2dede7841b63adfaf8086"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0845fca0841003ae446650ab6695c38d45623bc1e8e40a43e839e450a874fd6f"}, {file = "winrt_windows_devices_enumeration-2.2.0.tar.gz", hash = "sha256:cfe1780101e3ef9c5b4716cca608aa6b6ddf19f1d7a2a70434241d438db19d3d"}, ] [package.dependencies] winrt-runtime = "2.2.0" [package.extras] all = ["winrt-Windows.ApplicationModel.Background[all] (==2.2.0)", "winrt-Windows.Foundation.Collections[all] (==2.2.0)", "winrt-Windows.Foundation[all] (==2.2.0)", "winrt-Windows.Security.Credentials[all] (==2.2.0)", "winrt-Windows.Storage.Streams[all] (==2.2.0)", "winrt-Windows.UI.Popups[all] (==2.2.0)", "winrt-Windows.UI[all] (==2.2.0)"] [[package]] name = "winrt-windows-foundation" version = "2.2.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" files = [ {file = "winrt_Windows.Foundation-2.2.0-cp310-cp310-win32.whl", hash = "sha256:cb86bbf04f72d983e4ae13db0a48784638b36214bb2c44809f39686ef3314354"}, {file = "winrt_Windows.Foundation-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:2dbd0957216c07db4b91a144a0ffa7c8892cc668b19ca15b78067255445741b2"}, {file = "winrt_Windows.Foundation-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:5345f7d0504aa1a605be5b5fe0d1944b322591f7669c2c86b7c45384924c8c9b"}, {file = "winrt_Windows.Foundation-2.2.0-cp311-cp311-win32.whl", hash = "sha256:f6711adf8a34e48c94183e792f153de5f3796f8f3c045356544605384bbcb7e1"}, {file = "winrt_Windows.Foundation-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:0a5bfe2647659e7ec288d8552e61e577a931914531ccc9cb958469d85f049d6b"}, {file = "winrt_Windows.Foundation-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9eabbd1b179fd04f167884fa0feaa17ccd67d89f6eac4099b16c6c0dc22e9f32"}, {file = "winrt_Windows.Foundation-2.2.0-cp312-cp312-win32.whl", hash = "sha256:0f0319659f00d04d13fc5db45f574479a396147c955628dc2dda056397a0df28"}, {file = "winrt_Windows.Foundation-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:8bc605242d268cd8ccce68c78ec4a967b8e5431c3a969c9e7a01d454696dfb3f"}, {file = "winrt_Windows.Foundation-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f901b20c3a874a2cf9dcb1e97bbcff329d95fd3859a873be314a5a58073b4690"}, {file = "winrt_Windows.Foundation-2.2.0-cp313-cp313-win32.whl", hash = "sha256:c5cf43bb1dccf3a302d16572d53f26479d277e02606531782c364056c2323678"}, {file = "winrt_Windows.Foundation-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:10c84276ff182a06da6deb1ba9ad375f9b3fbc15c3684a160e775005d915197a"}, {file = "winrt_Windows.Foundation-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:473cc57664bfd5401ec171c8f55079cdc8a980210f2c82fb2945361ea640bfbf"}, {file = "winrt_Windows.Foundation-2.2.0-cp39-cp39-win32.whl", hash = "sha256:32578bd31eda714bc5cb5b10f0e778c720a2e45bc9b3c60690faa1615336047d"}, {file = "winrt_Windows.Foundation-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bfb62127959f56fdacad6a817176a8b22cf6917a0d5c3e5d25cdad33a90173a"}, {file = "winrt_Windows.Foundation-2.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:07ea5a2f05cb9fb433371e55f70fbe27f32a6eb07ae28042f01678b4d82d823a"}, {file = "winrt_windows_foundation-2.2.0.tar.gz", hash = "sha256:9a76291204900cd92008163fbe273ae43c9a925ca4a5a29cdd736e59cd397bf1"}, ] [package.dependencies] winrt-runtime = "2.2.0" [package.extras] all = ["winrt-Windows.Foundation.Collections[all] (==2.2.0)"] [[package]] name = "winrt-windows-foundation-collections" version = "2.2.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" files = [ {file = "winrt_Windows.Foundation.Collections-2.2.0-cp310-cp310-win32.whl", hash = "sha256:92a031fca53910c8bce683391888ba3427db178fc47653310de16fb7e9131e9d"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:a71925d738a443cf27522f34ced84730f1b325f69ccdd0145580e6078d4481c5"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:74c9419b26b510e6e95182e02dc55a78094b6f2af5002330467d030ae6d0b765"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp311-cp311-win32.whl", hash = "sha256:8a76d79be0af1840b9c5ac1879dcf5aa65b512accd8278ac6424dcbfdb2a6fe1"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:b18dcd7bc8cf70758b965397e26da725ac345dd9f16b922b0204e8f21ed4d7e6"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:1d6b0b04683e98989dd611940b5fe36c1338f6d91f43c1bdc88f2f2f1956a968"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp312-cp312-win32.whl", hash = "sha256:ade4ea4584ba96e39d2b34f1036d8cb40ff2e9609a090562cfd2b8837dc7f828"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:1e896291c5efe0566db84eab13888bee7300392a6811ae85c55ced51bac0b147"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:e44e13027597fcc638073459dcc159a21c57f9dbe0e9a2282326e32386c25bd0"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp313-cp313-win32.whl", hash = "sha256:ea7fa3a7ecb754eb09408e7127cd960d316cc1ba60a6440e191a81f14b42265c"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:f338860e27a8a67b386273c73ad10c680a9f40a42e0185cc6443d208a7425ece"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:dd705d4c62bd8c109f2bc667a0c76dc30ef9a1b2ced3e7bd95253a31e39781df"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp39-cp39-win32.whl", hash = "sha256:6798595621ad58473fe9e86f5f58d732628d88f06535b68c4d86cb5aed78f2b3"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:c8ac098a60dad586e950a8236bab09ae57b6a08147d36db6b0aed135a9a81831"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:c67105ebd88faf10d2941516c0ea9f73d9282fb8a7d2a73163a7a7e013bba839"}, {file = "winrt_windows_foundation_collections-2.2.0.tar.gz", hash = "sha256:10db64da49185af3e14465cd65ec4055eb122a96daedb73b774889f3b7fcfa63"}, ] [package.dependencies] winrt-runtime = "2.2.0" [package.extras] all = ["winrt-Windows.Foundation[all] (==2.2.0)"] [[package]] name = "winrt-windows-storage-streams" version = "2.2.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = "<3.14,>=3.9" files = [ {file = "winrt_Windows.Storage.Streams-2.2.0-cp310-cp310-win32.whl", hash = "sha256:e888ae08f1245f8b6d53783487581fc664683bb29778f2acca6bafb6a78bcc22"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:9213576d566398657142372aa34354b9f7b8ce0581cff308c7afbc0d908368a1"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:49d2bdd749994fb81c813f02f3c506fff580f358083b65a123308f322c2fe6cf"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp311-cp311-win32.whl", hash = "sha256:db4ebe7ed79a585a1bb78a3f8cea05f7d74a6a8bc913f61b31ddfe3ae10d134d"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:f9f77c5398eb90c58645c62b6f278f701d2636c0007817cc6fc28256adbebdcb"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:894c2616eeae887275a1a64a4233964f9466ee1281b8c11ec7c06d64aafec88a"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp312-cp312-win32.whl", hash = "sha256:85a2eefb2935db92d10b8e9be836c431d47298b566b55da633b11f822c63838d"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f88cdc6204219c7f1b58d793826ea2eff013a45306fbb340d61c10896c237547"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:78af200d0db5ebe151b1df194de97f1e71c2d5f5cba4da09798c15402f4ab91d"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp313-cp313-win32.whl", hash = "sha256:6408184ba5d17e0d408d7c0b85357a58f13c775521d17a8730f1a680553e0061"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:ad9cd8e97cf4115ba074ec153ab273c370e690abb010d8b3b970339d20f94321"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:c467cf04005b72efd769ea99c7c15973db44d5ac6084a7c7714af85e49981abd"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp39-cp39-win32.whl", hash = "sha256:f72559b5de7c3a0cab97cd50ab594a0e3278df4d38e03f79b5b2d2e13e926c4c"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:37bf5bb801aa1e4a4c6f3ddfe2b8c9b05d7726ebfdfc8b9bfe41bdcc3866749b"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:2dcab77a7affb1136503edec82a755b82716abd882fadd5f50ce260438b9c21b"}, {file = "winrt_windows_storage_streams-2.2.0.tar.gz", hash = "sha256:46a8718c4e00a129d305f03571789f4bed530c05e135c2476494af93f374b68a"}, ] [package.dependencies] winrt-runtime = "2.2.0" [package.extras] all = ["winrt-Windows.Foundation.Collections[all] (==2.2.0)", "winrt-Windows.Foundation[all] (==2.2.0)", "winrt-Windows.Storage[all] (==2.2.0)", "winrt-Windows.System[all] (==2.2.0)"] [[package]] name = "zipp" version = "3.19.1" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ {file = "zipp-3.19.1-py3-none-any.whl", hash = "sha256:2828e64edb5386ea6a52e7ba7cdb17bb30a73a858f5eb6eb93d8d36f5ea26091"}, {file = "zipp-3.19.1.tar.gz", hash = "sha256:35427f6d5594f4acf82d25541438348c26736fa9b3afa2754bcd63cdb99d8e8f"}, ] [package.extras] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<3.14" content-hash = "22885f16b59e72fb2bcf6fd88c81cabc7d48ebb05555f12375226200ca26c61e" bleak-0.22.3/pyproject.toml000066400000000000000000000054211470032643600155770ustar00rootroot00000000000000[tool.poetry] name = "bleak" version = "0.22.3" description = "Bluetooth Low Energy platform Agnostic Klient" authors = ["Henrik Blidh "] license = "MIT" readme = "README.rst" homepage = "https://github.com/hbldh/bleak" documentation = "https://bleak.readthedocs.io" classifiers = [ "Development Status :: 4 - Beta", "Framework :: AsyncIO", "Operating System :: Microsoft :: Windows :: Windows 10", "Operating System :: POSIX :: Linux", "Operating System :: MacOS :: MacOS X", "Operating System :: Android", ] [tool.poetry.urls] "Changelog" = "https://github.com/hbldh/bleak/blob/develop/CHANGELOG.rst" "Support" = "https://github.com/hbldh/bleak/discussions" "Issues" = "https://github.com/hbldh/bleak/issues" [tool.poetry.dependencies] python = ">=3.8,<3.14" async-timeout = { version = ">= 3.0.0, < 5", python = "<3.11" } typing-extensions = { version = ">=4.7.0", python = "<3.12" } pyobjc-core = { version = "^10.3", markers = "platform_system=='Darwin'" } pyobjc-framework-CoreBluetooth = { version = "^10.3", markers = "platform_system=='Darwin'" } pyobjc-framework-libdispatch = { version = "^10.3", markers = "platform_system=='Darwin'" } bleak-winrt = { version = "^1.2.0", markers = "platform_system=='Windows'", python = "<3.12" } "winrt-runtime" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12" } "winrt-Windows.Devices.Bluetooth" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12" } "winrt-Windows.Devices.Bluetooth.Advertisement" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12" } "winrt-Windows.Devices.Bluetooth.GenericAttributeProfile" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12" } "winrt-Windows.Devices.Enumeration" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12" } "winrt-Windows.Foundation" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12" } "winrt-Windows.Foundation.Collections" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12" } "winrt-Windows.Storage.Streams" = { version = "^2", markers = "platform_system=='Windows'", python = ">=3.12" } dbus-fast = { version = ">=1.83.0, < 3", markers = "platform_system == 'Linux'" } [tool.poetry.group.docs.dependencies] Sphinx = "^5.1.1" sphinx-rtd-theme = "^1.0.0" tomli = "^2.0.1" [tool.poetry.group.lint.dependencies] black = ">=24.3,<25.0" flake8 = "^5.0.0" isort = "^5.13.2" [tool.poetry.group.test.dependencies] pytest = "^8.2.1" pytest-asyncio = "^0.23.7" pytest-cov = "^3.0.0 " [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.isort] profile = "black" py_version=38 src_paths = ["bleak", "examples", "tests", "typings"] extend_skip = [".buildozer"] bleak-0.22.3/setup.cfg000066400000000000000000000001661470032643600145050ustar00rootroot00000000000000[flake8] exclude = docs,.venv,*.pyi,.buildozer,build,dist,.eggs ignore = E203,E501,E704,W503 [aliases] test = pytest bleak-0.22.3/tests/000077500000000000000000000000001470032643600140235ustar00rootroot00000000000000bleak-0.22.3/tests/__init__.py000066400000000000000000000000741470032643600161350ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Unit test package for bleak.""" bleak-0.22.3/tests/bleak/000077500000000000000000000000001470032643600151015ustar00rootroot00000000000000bleak-0.22.3/tests/bleak/backends/000077500000000000000000000000001470032643600166535ustar00rootroot00000000000000bleak-0.22.3/tests/bleak/backends/bluezdbus/000077500000000000000000000000001470032643600206525ustar00rootroot00000000000000bleak-0.22.3/tests/bleak/backends/bluezdbus/__init__.py000066400000000000000000000000001470032643600227510ustar00rootroot00000000000000bleak-0.22.3/tests/bleak/backends/bluezdbus/test_utils.py000066400000000000000000000012221470032643600234200ustar00rootroot00000000000000#!/usr/bin/env python """Tests for `bleak.backends.bluezdbus.utils` package.""" import sys import pytest @pytest.mark.skipif( not sys.platform.startswith("linux"), reason="requires dbus-fast on Linux" ) def test_device_path_from_characteristic_path(): """Test device_path_from_characteristic_path.""" from bleak.backends.bluezdbus.utils import ( # pylint: disable=import-outside-toplevel device_path_from_characteristic_path, ) assert ( device_path_from_characteristic_path( "/org/bluez/hci0/dev_11_22_33_44_55_66/service000c/char000d" ) == "/org/bluez/hci0/dev_11_22_33_44_55_66" ) bleak-0.22.3/tests/bleak/backends/bluezdbus/test_version.py000066400000000000000000000065441470032643600237610ustar00rootroot00000000000000#!/usr/bin/env python """Tests for `bleak.backends.bluezdbus.version` package.""" from unittest.mock import AsyncMock, Mock, patch import pytest from bleak.backends.bluezdbus.version import BlueZFeatures @pytest.mark.asyncio @pytest.mark.parametrize( "version,can_write_without_response,write_without_response_workaround_needed,hides_battery_characteristic,hides_device_name_characteristic", [ (b"bluetoothctl: 5.34", False, False, False, False), (b"bluetoothctl: 5.46", True, False, False, False), (b"bluetoothctl: 5.48", True, False, True, True), (b"bluetoothctl: 5.51", True, True, True, True), (b"bluetoothctl: 5.63", True, True, False, True), (b"", True, True, False, True), ], ) async def test_bluez_version( version, can_write_without_response, write_without_response_workaround_needed, hides_battery_characteristic, hides_device_name_characteristic, ): """Test we can determine supported feature from bluetoothctl.""" mock_proc = Mock( wait=AsyncMock(), stdout=Mock(read=AsyncMock(return_value=version)) ) with patch( "bleak.backends.bluezdbus.version.asyncio.create_subprocess_exec", AsyncMock(return_value=mock_proc), ): BlueZFeatures._check_bluez_event = None await BlueZFeatures.check_bluez_version() assert BlueZFeatures.checked_bluez_version is True assert BlueZFeatures.can_write_without_response == can_write_without_response assert ( not BlueZFeatures.write_without_response_workaround_needed == write_without_response_workaround_needed ) assert BlueZFeatures.hides_battery_characteristic == hides_battery_characteristic assert ( BlueZFeatures.hides_device_name_characteristic == hides_device_name_characteristic ) @pytest.mark.asyncio async def test_bluez_version_only_happens_once(): """Test we can determine supported feature from bluetoothctl.""" BlueZFeatures.checked_bluez_version = False BlueZFeatures._check_bluez_event = None mock_proc = Mock( wait=AsyncMock(), stdout=Mock(read=AsyncMock(return_value=b"bluetoothctl: 5.46")), ) with patch( "bleak.backends.bluezdbus.version.asyncio.create_subprocess_exec", AsyncMock(return_value=mock_proc), ): await BlueZFeatures.check_bluez_version() assert BlueZFeatures.checked_bluez_version is True with patch( "bleak.backends.bluezdbus.version.asyncio.create_subprocess_exec", side_effect=Exception, ): await BlueZFeatures.check_bluez_version() assert BlueZFeatures.checked_bluez_version is True @pytest.mark.asyncio async def test_exception_checking_bluez_features_does_not_block_forever(): """Test an exception while checking BlueZ features does not stall a second check.""" BlueZFeatures.checked_bluez_version = False BlueZFeatures._check_bluez_event = None with patch( "bleak.backends.bluezdbus.version.asyncio.create_subprocess_exec", side_effect=OSError, ): await BlueZFeatures.check_bluez_version() assert BlueZFeatures.checked_bluez_version is True with patch( "bleak.backends.bluezdbus.version.asyncio.create_subprocess_exec", side_effect=OSError, ): await BlueZFeatures.check_bluez_version() assert BlueZFeatures.checked_bluez_version is True bleak-0.22.3/tests/bleak/backends/winrt/000077500000000000000000000000001470032643600200165ustar00rootroot00000000000000bleak-0.22.3/tests/bleak/backends/winrt/test_utils.py000066400000000000000000000042761470032643600226000ustar00rootroot00000000000000#!/usr/bin/env python """Tests for `bleak.backends.winrt.util` package.""" import sys import pytest if not sys.platform.startswith("win"): pytest.skip("skipping windows-only tests", allow_module_level=True) from ctypes import windll, wintypes from bleak.backends.winrt.util import ( _check_hresult, allow_sta, assert_mta, uninitialize_sta, ) from bleak.exc import BleakError # https://learn.microsoft.com/en-us/windows/win32/api/objbase/ne-objbase-coinit COINIT_MULTITHREADED = 0x0 COINIT_APARTMENTTHREADED = 0x2 # https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-coinitializeex _CoInitializeEx = windll.ole32.CoInitializeEx _CoInitializeEx.restype = wintypes.LONG _CoInitializeEx.argtypes = [wintypes.LPVOID, wintypes.DWORD] _CoInitializeEx.errcheck = _check_hresult # https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-couninitialize _CoUninitialize = windll.ole32.CoUninitialize _CoUninitialize.restype = None _CoUninitialize.argtypes = [] @pytest.fixture(autouse=True) def run_around_tests(): # reset global state try: delattr(allow_sta, "_allowed") except AttributeError: pass yield @pytest.mark.asyncio async def test_assert_mta_no_init(): """Test device_path_from_characteristic_path.""" await assert_mta() @pytest.mark.asyncio async def test_assert_mta_init_mta(): """Test device_path_from_characteristic_path.""" _CoInitializeEx(None, COINIT_MULTITHREADED) try: await assert_mta() assert hasattr(allow_sta, "_allowed") finally: _CoUninitialize() @pytest.mark.asyncio async def test_assert_mta_init_sta(): """Test device_path_from_characteristic_path.""" _CoInitializeEx(None, COINIT_APARTMENTTHREADED) try: with pytest.raises( BleakError, match="Thread is configured for Windows GUI but callbacks are not working.", ): await assert_mta() finally: _CoUninitialize() @pytest.mark.asyncio async def test_uninitialize_sta(): """Test device_path_from_characteristic_path.""" _CoInitializeEx(None, COINIT_APARTMENTTHREADED) uninitialize_sta() await assert_mta() bleak-0.22.3/tests/test_platform_detection.py000066400000000000000000000017611470032643600213230ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """Tests for `bleak` package.""" import platform from bleak.backends.client import get_platform_client_backend_type from bleak.backends.scanner import get_platform_scanner_backend_type def test_platform_detection(): """Test by importing the client and assert correct client by OS.""" client_backend_type = get_platform_client_backend_type() scanner_backend_type = get_platform_scanner_backend_type() if platform.system() == "Linux": assert client_backend_type.__name__ == "BleakClientBlueZDBus" assert scanner_backend_type.__name__ == "BleakScannerBlueZDBus" elif platform.system() == "Windows": assert client_backend_type.__name__ == "BleakClientWinRT" assert scanner_backend_type.__name__ == "BleakScannerWinRT" elif platform.system() == "Darwin": assert client_backend_type.__name__ == "BleakClientCoreBluetooth" assert scanner_backend_type.__name__ == "BleakScannerCoreBluetooth" bleak-0.22.3/tests/test_uuid.py000066400000000000000000000015151470032643600164040ustar00rootroot00000000000000from bleak.uuids import normalize_uuid_16, normalize_uuid_32, normalize_uuid_str def test_uuid_length_normalization(): assert normalize_uuid_str("1801") == "00001801-0000-1000-8000-00805f9b34fb" assert normalize_uuid_str("DAF51C01") == "daf51c01-0000-1000-8000-00805f9b34fb" def test_uuid_case_normalization(): assert ( normalize_uuid_str("00001801-0000-1000-8000-00805F9B34FB") == "00001801-0000-1000-8000-00805f9b34fb" ) def test_uuid_16_normalization(): assert normalize_uuid_16(0x1801) == "00001801-0000-1000-8000-00805f9b34fb" assert normalize_uuid_16(0x1) == "00000001-0000-1000-8000-00805f9b34fb" def test_uuid_32_normalization(): assert normalize_uuid_32(0x12345678) == "12345678-0000-1000-8000-00805f9b34fb" assert normalize_uuid_32(0x1) == "00000001-0000-1000-8000-00805f9b34fb" bleak-0.22.3/typings/000077500000000000000000000000001470032643600143565ustar00rootroot00000000000000bleak-0.22.3/typings/CoreBluetooth/000077500000000000000000000000001470032643600171345ustar00rootroot00000000000000bleak-0.22.3/typings/CoreBluetooth/__init__.pyi000066400000000000000000000215651470032643600214270ustar00rootroot00000000000000from typing import Any, NewType, Optional, Type, TypeVar from ..Foundation import ( NSUUID, NSArray, NSData, NSDictionary, NSError, NSNumber, NSObject, NSString, ) from ..libdispatch import dispatch_queue_t class CBManager(NSObject): def state(self) -> CBManagerState: ... TCBCentralManager = TypeVar("TCBCentralManager", bound=CBCentralManager) class CBCentralManager(CBManager): @classmethod def init(cls: Type[TCBCentralManager]) -> Optional[TCBCentralManager]: ... @classmethod def initWithDelegate_queue_( cls: Type[TCBCentralManager], delegate: CBCentralManagerDelegate, queue: dispatch_queue_t, ) -> Optional[TCBCentralManager]: ... @classmethod def initWithDelegate_queue_options_( cls: Type[TCBCentralManager], delegate: CBCentralManagerDelegate, queue: dispatch_queue_t, options: NSDictionary, ) -> Optional[TCBCentralManager]: ... def connectPeripheral_options_( self, peripheral: CBPeripheral, options: Optional[NSDictionary] ) -> None: ... def cancelPeripheralConnection_(self, peripheral: CBPeripheral) -> None: ... def retrieveConnectedPeripheralsWithServices_( self, serviceUUIDs: NSArray ) -> NSArray: ... def retrievePeripheralsWithIdentifiers_(self, serviceUUIDs: NSArray) -> NSArray: ... def scanForPeripheralsWithServices_options_( self, serviceUUIDs: Optional[NSArray], options: Optional[NSDictionary] ) -> None: ... def stopScan(self) -> None: ... def isScanning(self) -> bool: ... @classmethod def supportsFeatures(cls, features: CBCentralManagerFeature) -> bool: ... def delegate(self) -> Optional[CBCentralManagerDelegate]: ... def registerForConnectionEventsWithOptions_( self, options: NSDictionary ) -> None: ... CBConnectPeripheralOptionNotifyOnConnectionKey: NSString CBConnectPeripheralOptionNotifyOnDisconnectionKey: NSString CBConnectPeripheralOptionNotifyOnNotificationKey: NSString CBConnectPeripheralOptionEnableTransportBridgingKey: NSString CBConnectPeripheralOptionRequiresANCS: NSString CBConnectPeripheralOptionStartDelayKey: NSString CBCentralManagerScanOptionAllowDuplicatesKey: NSString CBCentralManagerScanOptionSolicitedServiceUUIDsKey: NSString CBCentralManagerFeature = NewType("CBCentralManagerFeature", int) CBCentralManagerFeatureExtendedScanAndConnect: CBCentralManagerFeature CBManagerState = NewType("CBManagerState", int) CBManagerStatePoweredOff: CBManagerState CBManagerStatePoweredOn: CBManagerState CBManagerStateResetting: CBManagerState CBManagerStateUnauthorized: CBManagerState CBManagerStateUnknown: CBManagerState CBManagerStateUnsupported: CBManagerState CBConnectionEvent = NewType("CBConnectionEvent", int) CBConnectionEventPeerConnected: CBConnectionEvent CBConnectionEventPeerDisconnected: CBConnectionEvent class CBConnectionEventMatchingOption(NSString): ... CBConnectionEventMatchingOptionPeripheralUUIDs: CBConnectionEventMatchingOption CBConnectionEventMatchingOptionServiceUUIDs: CBConnectionEventMatchingOption class CBCentralManagerDelegate: ... class CBPeer(NSObject): def identifier(self) -> NSUUID: ... class CBPeripheral(CBPeer): def name(self) -> NSString: ... def delegate(self) -> CBPeripheralDelegate: ... def discoverServices_(self, serviceUUIDs: NSArray) -> None: ... def discoverIncludedServices_forService_( self, includedServiceUUIDs: NSArray, service: CBService ) -> None: ... def services(self) -> NSArray: ... def discoverCharacteristics_forService_( self, characteristicUUIDs: NSArray, service: CBService ) -> None: ... def discoverDescriptorsForCharacteristic_( self, characteristic: CBCharacteristic ) -> None: ... def readValueForCharacteristic_(self, characteristic: CBCharacteristic) -> None: ... def readValueForDescriptor_(self, descriptor: CBDescriptor) -> None: ... def writeValue_forCharacteristic_type_( self, data: NSData, characteristic: CBCharacteristic, type: CBCharacteristicWriteType, ) -> None: ... def writeValue_forDescriptor_( self, data: NSData, descriptor: CBDescriptor ) -> None: ... def maximumWriteValueLengthForType_( self, type: CBCharacteristicWriteType ) -> None: ... def setNotifyValue_forCharacteristic_( self, enabled: bool, characteristic: CBCharacteristic ) -> None: ... def state(self) -> CBPeripheralState: ... def canSendWriteWithoutResponse(self) -> bool: ... def readRSSI(self) -> None: ... def RSSI(self) -> NSNumber: ... CBCharacteristicWriteType = NewType("CBCharacteristicWriteType", int) CBCharacteristicWriteWithResponse: CBCharacteristicWriteType CBCharacteristicWriteWithoutResponse: CBCharacteristicWriteType CBPeripheralState = NewType("CBPeripheralState", int) CBPeripheralStateDisconnected: CBPeripheralState CBPeripheralStateConnecting: CBPeripheralState CBPeripheralStateConnected: CBPeripheralState CBPeripheralStateDisconnecting: CBPeripheralState class CBPeripheralDelegate: def peripheral_didDiscoverServices_( self, peripheral: CBPeripheral, error: Optional[NSError] ) -> None: ... def peripheral_didDiscoverIncludedServicesForService_error_( self, peripheral: CBPeripheral, service: CBService, error: Optional[NSError] ) -> None: ... def peripheral_didDiscoverCharacteristicsForService_error_( self, peripheral: CBPeripheral, service: CBService, error: Optional[NSError] ) -> None: ... def peripheral_didDiscoverDescriptorsForCharacteristic_error_( self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: Optional[NSError], ) -> None: ... def peripheral_didUpdateValueForCharacteristic_error_( self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: Optional[NSError], ) -> None: ... def peripheral_didUpdateValueForDescriptor_error_( self, peripheral: CBPeripheral, descriptor: CBDescriptor, error: Optional[NSError], ) -> None: ... def peripheral_didWriteValueForCharacteristic_error_( self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: Optional[NSError], ) -> None: ... def peripheral_didWriteValueForDescriptor_error_( self, peripheral: CBPeripheral, descriptor: CBDescriptor, error: Optional[NSError], ) -> None: ... def pperipheralIsReadyToSendWriteWithoutResponse_( self, peripheral: CBPeripheral ) -> None: ... def peripheral_didUpdateNotificationStateForCharacteristic_error_( self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: Optional[NSError], ) -> None: ... def peripheral_didReadRSSI_error_( self, peripheral: CBPeripheral, RSSI: NSNumber, error: Optional[NSError], ) -> None: ... def peripheralDidUpdateName_(self, peripheral: CBPeripheral) -> None: ... def peripheral_didModifyServices_( self, peripheral: CBPeripheral, invalidatedServices: NSArray ) -> None: ... class CBAttribute(NSObject): def UUID(self) -> CBUUID: ... class CBService(CBAttribute): def peripheral(self) -> CBPeripheral: ... def isPrimary(self) -> bool: ... def characteristics(self) -> Optional[NSArray]: ... def includedServices(self) -> Optional[NSArray]: ... class CBUUID(NSObject): @classmethod def UUIDWithString_(cls, theString: NSString) -> CBUUID: ... @classmethod def UUIDWithData_(cls, theData: NSData) -> CBUUID: ... @classmethod def UUIDWithNSUUID_(cls, theUUID: NSUUID) -> CBUUID: ... def data(self) -> NSData: ... def UUIDString(self) -> NSString: ... class CBCharacteristic(CBAttribute): def service(self) -> CBService: ... def value(self) -> Optional[NSData]: ... def descriptors(self) -> Optional[NSArray]: ... def properties(self) -> CBCharacteristicProperties: ... def isNotifying(self) -> bool: ... CBCharacteristicProperties = NewType("CBCharacteristicProperties", int) CBCharacteristicPropertyBroadcast: CBCharacteristicProperties CBCharacteristicPropertyRead: CBCharacteristicProperties CBCharacteristicPropertyWriteWithoutResponse: CBCharacteristicProperties CBCharacteristicPropertyWrite: CBCharacteristicProperties CBCharacteristicPropertyNotify: CBCharacteristicProperties CBCharacteristicPropertyIndicate: CBCharacteristicProperties CBCharacteristicPropertyAuthenticatedSignedWrites: CBCharacteristicProperties CBCharacteristicPropertyExtendedProperties: CBCharacteristicProperties CBCharacteristicPropertyNotifyEncryptionRequired: CBCharacteristicProperties CBCharacteristicPropertyIndicateEncryptionRequired: CBCharacteristicProperties class CBDescriptor(CBAttribute): def characteristic(self) -> CBCharacteristic: ... def value(self) -> Optional[Any]: ... bleak-0.22.3/typings/Foundation/000077500000000000000000000000001470032643600164645ustar00rootroot00000000000000bleak-0.22.3/typings/Foundation/__init__.pyi000066400000000000000000000027061470032643600207530ustar00rootroot00000000000000from typing import NewType, Optional, Sequence, Type, TypeVar TNSObject = TypeVar("TNSObject", bound=NSObject) class NSObject: @classmethod def alloc(cls: Type[TNSObject]) -> TNSObject: ... def init(self: TNSObject) -> Optional[TNSObject]: ... def addObserver_forKeyPath_options_context_( self, observer: NSObject, keyPath: NSString, options: NSKeyValueObservingOptions, context: int, ) -> None: ... def removeObserver_forKeyPath_( self, observer: NSObject, keyPath: NSString ) -> None: ... class NSDictionary(NSObject): ... class NSUUID(NSObject): ... class NSString(NSObject): ... class NSError(NSObject): ... class NSData(NSObject): ... class NSArray(NSObject): def initWithArray_(self, array: Sequence) -> NSArray: ... class NSValue(NSObject): ... class NSNumber(NSValue): ... NSKeyValueObservingOptions = NewType("NSKeyValueObservingOptions", int) NSKeyValueObservingOptionNew: NSKeyValueObservingOptions NSKeyValueObservingOptionOld: NSKeyValueObservingOptions NSKeyValueObservingOptionInitial: NSKeyValueObservingOptions NSKeyValueObservingOptionPrior: NSKeyValueObservingOptions NSKeyValueChangeKey = NewType("NSKeyValueChangeKey", NSString) NSKeyValueChangeIndexesKey: NSKeyValueChangeKey NSKeyValueChangeKindKey: NSKeyValueChangeKey NSKeyValueChangeNewKey: NSKeyValueChangeKey NSKeyValueChangeNotificationIsPriorKey: NSKeyValueChangeKey NSKeyValueChangeOldKey: NSKeyValueChangeKey bleak-0.22.3/typings/libdispatch/000077500000000000000000000000001470032643600166445ustar00rootroot00000000000000bleak-0.22.3/typings/libdispatch/__init__.pyi000066400000000000000000000003671470032643600211340ustar00rootroot00000000000000from typing import Optional class dispatch_queue_attr_t: ... DISPATCH_QUEUE_SERIAL: dispatch_queue_attr_t class dispatch_queue_t: ... def dispatch_queue_create( name: bytes, attr: Optional[dispatch_queue_attr_t] ) -> dispatch_queue_t: ... bleak-0.22.3/typings/objc/000077500000000000000000000000001470032643600152735ustar00rootroot00000000000000bleak-0.22.3/typings/objc/__init__.py000066400000000000000000000005201470032643600174010ustar00rootroot00000000000000from typing import Optional, Type, TypeVar from Foundation import NSObject T = TypeVar("T") def super(cls: Type[T], self: T) -> T: ... def macos_available(major: int, minor: int, patch: int = 0) -> bool: ... class WeakRef: def __init__(self, object: NSObject) -> None: ... def __call__(self) -> Optional[NSObject]: ...