├── .gitignore ├── .gitlab-ci.yml ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── examples ├── interpolation_example.py ├── multi_device_example.py ├── print_current_time.py ├── simple_pinching_example.py ├── tracking_event_example.py └── visualiser.py ├── leapc-cffi ├── MANIFEST.in ├── README.md ├── setup.py └── src │ ├── leapc_cffi │ └── __init__.py │ └── scripts │ ├── cffi_build.py │ └── cffi_src.h ├── leapc-python-api ├── MANIFEST.in ├── README.md ├── setup.py └── src │ └── leap │ ├── __init__.py │ ├── connection.py │ ├── cstruct.py │ ├── datatypes.py │ ├── device.py │ ├── enums.py │ ├── event_listener.py │ ├── events.py │ ├── exceptions.py │ ├── functions.py │ └── recording.py ├── pyproject.toml └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | dist/ 3 | venv/ 4 | build/ 5 | 6 | leapc-python-api/build 7 | leapc-python-api/dist 8 | leapc-python-api/src/leap.egg-info 9 | 10 | leapc-cffi/build 11 | leapc-cffi/dist 12 | leapc-cffi/src/leapc_cffi.egg-info 13 | leapc-cffi/src/leapc_cffi/*.h 14 | leapc-cffi/src/leapc_cffi/*.dll 15 | leapc-cffi/src/leapc_cffi/*.pyd 16 | leapc-cffi/src/leapc_cffi/*.lib 17 | leapc-cffi/src/leapc_cffi/*.dylib 18 | leapc-cffi/src/leapc_cffi/*.so 19 | 20 | *.pyc 21 | *.pyd 22 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - analysis 3 | - build 4 | - test 5 | 6 | .common: 7 | variables: 8 | LIBTRACK_PROJECT_REF: leap-v5-platform/libtrack 9 | LIBTRACK_REF: develop # Use develop until we have a tagged release including the leapc_cffi module 10 | 11 | python-black: 12 | stage: analysis 13 | image: registry.ultrahaptics.com/leap-v5-platform/infrastructure/x64_linux_toolchain:v3.16.4 14 | allow_failure: false 15 | tags: 16 | - docker 17 | interruptible: true 18 | needs: [ ] 19 | script: 20 | - python3 -m venv python_env --system-site-packages 21 | - python_env/bin/python -m pip install black==22.3.0 22 | - python_env/bin/python -m black --diff --check examples leapc-cffi leapc-python-api 23 | 24 | Linux-Build: 25 | extends: .common 26 | stage: build 27 | image: ubuntu:20.04 28 | tags: 29 | - docker-builder 30 | before_script: 31 | - dpkg-deb -xv $CI_PROJECT_DIR/ultraleap-hand-tracking-service_*.deb $CI_PROJECT_DIR/unpack 32 | - apt-get update 33 | - apt-get install -y python3 python3-venv python3-dev build-essential 34 | script: 35 | - export LEAPC_HEADER_OVERRIDE="$CI_PROJECT_DIR/unpack/usr/include/LeapC.h" 36 | - export LEAPC_LIB_OVERRIDE="$CI_PROJECT_DIR/unpack/usr/lib/ultraleap-hand-tracking-service/libLeapC.so" 37 | - mkdir build 38 | - python3 -m venv build/venv --system-site-packages 39 | - build/venv/bin/python -m pip install -r requirements.txt 40 | - build/venv/bin/python -m build leapc-cffi -o build/dist 41 | needs: 42 | - project: $LIBTRACK_PROJECT_REF 43 | job: X64LinuxRelProd 44 | ref: $LIBTRACK_REF 45 | artifacts: true 46 | 47 | Linux-ARM-Build: 48 | extends: .common 49 | stage: build 50 | image: registry.ultrahaptics.com/leap-v5-platform/infrastructure/arm64_linux_python:latest 51 | tags: 52 | - docker 53 | before_script: 54 | - export TAR_NAME=$(ls ultraleap-hand-tracking-service*.tar.gz) 55 | - export EXTRACTED_DIR=${TAR_NAME%.tar.gz} 56 | - tar -xf ultraleap-hand-tracking-service*.tar.gz 57 | - apt-get update 58 | - apt-get install -y python3 python3-venv python3-dev build-essential 59 | script: 60 | - export LEAPSDK_INSTALL_LOCATION="${PWD}/${EXTRACTED_DIR}/LeapSDK" 61 | - mkdir build 62 | - python3 -m venv build/venv --system-site-packages 63 | - build/venv/bin/python -m pip install -r requirements.txt 64 | - build/venv/bin/python -m build leapc-cffi -o build/dist 65 | needs: 66 | - project: $LIBTRACK_PROJECT_REF 67 | job: ARM64LinuxRelProd-PythonBindings 68 | ref: $LIBTRACK_REF 69 | artifacts: true 70 | 71 | Windows-Build: 72 | extends: .common 73 | stage: build 74 | tags: 75 | - win 76 | - ctk 77 | before_script: 78 | - mkdir unpack 79 | - $env:zip_file = Get-ChildItem . -Filter "Ultraleap_*.zip" 80 | - Expand-Archive -Path $env:zip_file -OutputPath unpack 81 | - cd unpack/Ultraleap* 82 | - $env:EXTRACTED_INSTALLER_PATH=$pwd.Path 83 | - cd $env:CI_PROJECT_DIR 84 | script: 85 | - $env:LEAPSDK_INSTALL_LOCATION="$env:EXTRACTED_INSTALLER_PATH/LeapSDK" 86 | - mkdir build 87 | - python -m venv build/venv --system-site-packages 88 | - build/venv/Scripts/python.exe -m pip install -r requirements.txt 89 | - build/venv/Scripts/python.exe -m build leapc-cffi -o build/dist 90 | needs: 91 | - project: $LIBTRACK_PROJECT_REF 92 | job: WinRelDebProd 93 | ref: $LIBTRACK_REF 94 | artifacts: true 95 | 96 | MacOS-Build: 97 | extends: .common 98 | stage: build 99 | tags: 100 | - platform-macm1 101 | before_script: 102 | - unzip $CI_PROJECT_DIR/Ultraleap-Hand-Tracking*.zip -d $CI_PROJECT_DIR/unpack 103 | - cd $CI_PROJECT_DIR/unpack/Ultraleap* 104 | - export EXTRACTED_INSTALLER_PATH="${PWD}" 105 | - cd $CI_PROJECT_DIR 106 | script: 107 | - export LEAPSDK_INSTALL_LOCATION="$EXTRACTED_INSTALLER_PATH/Ultraleap Hand Tracking.app/Contents/LeapSDK" 108 | - mkdir build 109 | - python3 -m venv build/venv --system-site-packages 110 | - build/venv/bin/python -m pip install -r requirements.txt 111 | - build/venv/bin/python -m build leapc-cffi -o build/dist 112 | needs: 113 | - project: $LIBTRACK_PROJECT_REF 114 | job: MacOSArm64RelDebProd 115 | ref: $LIBTRACK_REF 116 | artifacts: true 117 | 118 | MacOS-Test: 119 | extends: .common 120 | stage: test 121 | tags: 122 | - platform-macm1 123 | before_script: 124 | - unzip $CI_PROJECT_DIR/Ultraleap-Hand-Tracking*.zip -d $CI_PROJECT_DIR/unpack 125 | - cd $CI_PROJECT_DIR/unpack/Ultraleap* 126 | - export EXTRACTED_INSTALLER_PATH="${PWD}" 127 | - cd $CI_PROJECT_DIR 128 | script: 129 | - export LEAPSDK_INSTALL_LOCATION="$EXTRACTED_INSTALLER_PATH/Ultraleap Hand Tracking.app/Contents/LeapSDK" 130 | - mkdir build 131 | - python3 -m venv build/venv --system-site-packages 132 | - build/venv/bin/python -m pip install cffi 133 | - build/venv/bin/python -m pip install -e leapc-python-api 134 | - build/venv/bin/python examples/print_current_time.py 135 | needs: 136 | - project: $LIBTRACK_PROJECT_REF 137 | job: MacOSArm64RelDebProd 138 | ref: $LIBTRACK_REF 139 | artifacts: true 140 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to LeapC Python Bindings 2 | 3 | We welcome contributions from the community. Feel free to create an issue or submit a pull request to address 4 | problems you've been having or add new functionality that others would be interested in. 5 | 6 | ## Review Threads 7 | Help the contributor when reviewing by using emoji's to show intent with each thread: 8 | * :eyes: - “Please do this, I want to look at it again once done / discuss it further” - reviewer is responsible for resolving thread 9 | * :see_no_evil: - “Please do this, I don’t need to look at it again once done” - MR submitter is responsible for resolving thread 10 | * :shrug: - “Suggestion, I don’t mind if you do it or not” - MR submitter is responsible for resolving thread 11 | * :+1: - “This is just a positive comment on your code, no changes needed” - MR submitter is responsible for resolving thread 12 | 13 | Got ideas for any other emojis that would be helpful? Add them here with an MR :eyes:. 14 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 Leap V5 Platform 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [apache]: http://www.apache.org/licenses/LICENSE-2.0 "Apache V2 License" 3 | 4 | [developer-site-tracking-software]: https://developer.leapmotion.com/tracking-software-download "Ultraleap Tracking Software" 5 | [developer-site-setup-camera]: https://developer.leapmotion.com/setup-camera "Ultraleap Setup Camera" 6 | [developer-forum]: https://forums.leapmotion.com/ "Developer Forum" 7 | [discord]: https://discord.com/invite/3VCndThqxS "Discord Server" 8 | [github-discussions]: https://github.com/ultraleap/leapc-python-bindings/discussions "Github Discussions" 9 | 10 | 11 | # Gemini LeapC Python Bindings 12 | 13 | [![mail](https://img.shields.io/badge/Contact-support%40ultraleap.com-00cf75)](mailto:support@ultraleap.com) 14 | [![discord](https://img.shields.io/badge/Discord-Server-blueviolet)][discord] 15 | ![GitHub](https://img.shields.io/github/license/ultraleap/leapc-python-bindings) 16 | 17 | Open-source Python bindings for the Gemini LeapC API. Allowing developers to use Ultraleaps Hand Tracking technology 18 | with Python. Including build instructions and some simple examples to get started with. 19 | 20 | ## Getting Started: 21 | 22 | To use this plugin you will need the following: 23 | 24 | 1. The latest Gemini Ultraleap Hand Tracking Software. You can get this [here][developer-site-tracking-software]. 25 | 2. An Ultraleap Hand Tracking Camera - follow setup process [here][developer-site-setup-camera]. 26 | 3. Follow one of the Installation workflows listed below. 27 | 28 | ## Installation: 29 | 30 | This module makes use of a compiled module called `leapc_cffi`. We include some pre-compiled python objects with our 31 | Gemini installation from 5.17 onwards. Supported versions can be found [here](#pre-compiled-module-support). If you 32 | have the matching python version and have installed Gemini into the default location you can follow the steps below: 33 | 34 | ``` 35 | # Create and activate a virtual environment 36 | pip install -r requirements.txt 37 | pip install -e leapc-python-api 38 | python examples/tracking_event_example.py 39 | ``` 40 | 41 | ### Custom Install 42 | 43 | This module assumes that you have the Leap SDK installed in the default location. If this is not the case 44 | for you, you can use an environment variable to define the installation location. Define the environment variable 45 | `LEAPSDK_INSTALL_LOCATION` to the path of the `LeapSDK` folder, if you have installed to a custom location or moved it 46 | somewhere else. 47 | 48 | Example: 49 | `export LEAPSDK_INSTALL_LOCATION="C:\Program Files\CustomDir\Ultraleap\LeapSDK"` 50 | 51 | By default, this path is the following for each operating system: 52 | - Windows: `C:/Program Files/Ultraleap/LeapSDK` 53 | - Linux x64: `/usr/lib/ultraleap-hand-tracking-service` 54 | - Linux ARM: `/opt/ultraleap/LeapSDK` 55 | - Darwin: `/Applications/Ultraleap Hand Tracking.app/Contents/LeapSDK` 56 | 57 | ## Pre-Compiled Module Support 58 | 59 | The included pre-compiled modules within our 5.17 release currently only support the following versions of python: 60 | 61 | - Windows: Python 3.8 62 | - Linux x64: Python 3.8 63 | - Darwin: Python 3.8 64 | - Linux ARM: Python 3.8, 3.9, 3.10, 3.11 65 | 66 | Expanded pre-compiled support will be added soon. However, this does not restrict you to these versions, if you wish to 67 | use a different python version please follow the instructions below to compile your own module. 68 | 69 | ### Missing Compiled Module? 70 | 71 | You might not have the correct matching compiled `leapc_cffi` module for your system, this can cause issues when importing 72 | leap, such as: `ModuleNotFoundError: No module named 'leapc_cffi._leapc_cffi'` 73 | If you'd like to build your own compiled module, you will still require a Gemini install and a C compiler of your 74 | choice. Follow the steps below: 75 | 76 | ``` 77 | # Create and activate a virtual environment 78 | pip install -r requirements.txt 79 | python -m build leapc-cffi 80 | pip install leapc-cffi/dist/leapc_cffi-0.0.1.tar.gz 81 | pip install -e leapc-python-api 82 | python examples/tracking_event_example.py 83 | ``` 84 | 85 | ## Contributing 86 | 87 | Our vision is to make it as easy as possible to design the best user experience for hand tracking. 88 | We learn and are inspired by the creations from our open source community - any contributions you make are 89 | greatly appreciated. 90 | 91 | 1. Fork the Project 92 | 2. Create your Feature Branch: 93 | git checkout -b feature/AmazingFeature 94 | 3. Commit your Changes: 95 | git commit -m "Add some AmazingFeature" 96 | 4. Push to the Branch: 97 | git push origin feature/AmazingFeature 98 | 5. Open a Pull Request 99 | 100 | ## License 101 | 102 | Use of the LeapC Python Bindings is subject to the [Apache V2 License Agreement][apache]. 103 | 104 | ## Community Support 105 | 106 | Our [Discord Server][discord], [Github Discussions][github-discussions] and [Developer Forum][developer-forum] are 107 | places where you are actively encouraged to share your questions, insights, ideas, feature requests and projects. 108 | -------------------------------------------------------------------------------- /examples/interpolation_example.py: -------------------------------------------------------------------------------- 1 | """Uses interpolation in Leap API to determine the location of hands based on 2 | previous data. We use the LatestEventListener to wait until we have tracking 3 | events. We delay by 0.02 seconds each frame to simulate some delay, we get a 4 | frame size of the frame closest to the time we want to interpolate from and 5 | then interpolate on that frame""" 6 | import leap 7 | import time 8 | from timeit import default_timer as timer 9 | from typing import Callable 10 | from leap.events import TrackingEvent 11 | from leap.event_listener import LatestEventListener 12 | from leap.datatypes import FrameData 13 | 14 | 15 | def wait_until(condition: Callable[[], bool], timeout: float = 5, poll_delay: float = 0.01): 16 | start_time = timer() 17 | while timer() - start_time < timeout: 18 | if condition(): 19 | return True 20 | time.sleep(poll_delay) 21 | if not condition(): 22 | return False 23 | 24 | 25 | def main(): 26 | tracking_listening = LatestEventListener(leap.EventType.Tracking) 27 | 28 | connection = leap.Connection() 29 | connection.add_listener(tracking_listening) 30 | 31 | with connection.open() as open_connection: 32 | wait_until(lambda: tracking_listening.event is not None) 33 | # ctr-c to exit 34 | while True: 35 | event = tracking_listening.event 36 | if event is None: 37 | continue 38 | event_timestamp = event.timestamp 39 | 40 | target_frame_size = leap.ffi.new("uint64_t*") 41 | frame_time = leap.ffi.new("int64_t*") 42 | frame_time[0] = event_timestamp 43 | 44 | # simulate 20 ms delay 45 | time.sleep(0.02) 46 | 47 | try: 48 | # we need to query the storage required for our interpolation 49 | # request, the size will depend on the number visible hands in 50 | # this frame 51 | leap.get_frame_size(open_connection, frame_time, target_frame_size) 52 | except Exception as e: 53 | print("get_frame_size() failed with: ", e) 54 | continue 55 | 56 | frame_data = FrameData(target_frame_size[0]) 57 | try: 58 | # actually interpolate and get frame data from the Leap API 59 | # this is the time of the frame plus the 20ms artificial 60 | # delay and an estimated 10ms processing time which should 61 | # get close to real time hand tracking with interpolation 62 | leap.interpolate_frame( 63 | open_connection, 64 | event_timestamp + 30000, 65 | frame_data.frame_ptr(), 66 | target_frame_size[0], 67 | ) 68 | except Exception as e: 69 | print("interpolate_frame() failed with: ", e) 70 | continue 71 | 72 | event = TrackingEvent(frame_data) 73 | print( 74 | "Frame ", 75 | event.tracking_frame_id, 76 | " with ", 77 | len(event.hands), 78 | "hands with a delay of ", 79 | leap.get_now() - event.timestamp, 80 | ) 81 | for hand in event.hands: 82 | hand_type = "left" if str(hand.type) == "HandType.Left" else "right" 83 | print( 84 | f"Hand id {hand.id} is a {hand_type} hand with position ({hand.palm.position.x}, {hand.palm.position.y}, {hand.palm.position.z})." 85 | ) 86 | 87 | 88 | if __name__ == "__main__": 89 | main() 90 | -------------------------------------------------------------------------------- /examples/multi_device_example.py: -------------------------------------------------------------------------------- 1 | """Prints tracking events from multiple devices. We create a listener to get 2 | device events to get an updated device list from the connection. The tracking 3 | listener is much the same as the `tracking_event_example.py` but the serial 4 | number of each tracking event is logged too. The tracking events are only 5 | logged every 100 frames. 6 | """ 7 | 8 | import leap 9 | import time 10 | from timeit import default_timer as timer 11 | from typing import Callable 12 | from leap.events import TrackingEvent 13 | from leap.event_listener import LatestEventListener 14 | from leap.datatypes import FrameData 15 | 16 | 17 | class MultiDeviceListener(leap.Listener): 18 | def __init__(self, event_type): 19 | super().__init__() 20 | self._event_type = event_type 21 | self.n_events = 0 22 | 23 | def on_event(self, event): 24 | if isinstance(event, self._event_type): 25 | self.n_events += 1 26 | 27 | 28 | def wait_until(condition: Callable[[], bool], timeout: float = 5, poll_delay: float = 0.01): 29 | start_time = timer() 30 | while timer() - start_time < timeout: 31 | if condition(): 32 | return True 33 | time.sleep(poll_delay) 34 | if not condition(): 35 | return False 36 | 37 | 38 | class TrackingEventListener(leap.Listener): 39 | def __init__(self): 40 | self.device_latest_tracking_event = {} 41 | 42 | def number_of_devices_tracking(self): 43 | return len(self.device_latest_tracking_event) 44 | 45 | def on_tracking_event(self, event): 46 | if event.tracking_frame_id % 100 == 0: 47 | print( 48 | f"Frame {event.tracking_frame_id} with {len(event.hands)} hands on device {event.metadata.device_id}" 49 | ) 50 | source_device = event.metadata.device_id 51 | self.device_latest_tracking_event[source_device] = event 52 | 53 | 54 | def get_updated_devices(connection): 55 | devices = connection.get_devices() 56 | 57 | for device in devices: 58 | with device.open(): 59 | connection.subscribe_events(device) 60 | 61 | 62 | def main(): 63 | tracking_listener = TrackingEventListener() 64 | device_listener = MultiDeviceListener(leap.events.DeviceEvent) 65 | 66 | connection = leap.Connection(multi_device_aware=True) 67 | connection.add_listener(tracking_listener) 68 | connection.add_listener(device_listener) 69 | 70 | with connection.open(): 71 | wait_until(lambda: device_listener.n_events > 1) 72 | 73 | current_device_events = device_listener.n_events 74 | get_updated_devices(connection) 75 | 76 | while True: 77 | if device_listener.n_events != current_device_events: 78 | print("device_listener got a new device event") 79 | current_device_events = device_listener.n_events 80 | get_updated_devices(connection) 81 | 82 | time.sleep(0.5) 83 | 84 | 85 | if __name__ == "__main__": 86 | main() 87 | -------------------------------------------------------------------------------- /examples/print_current_time.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a simple example that prints the time according to the LeapC library. 3 | 4 | It does not require a tracking camera or the Ultraleap Tracking service to be running. 5 | 6 | This can be used to check if the python module has built successfully. 7 | """ 8 | import leap 9 | 10 | 11 | def main(): 12 | # Print the current time according to LeapC 13 | now = leap.get_now() 14 | print(now) 15 | 16 | 17 | if __name__ == "__main__": 18 | main() 19 | -------------------------------------------------------------------------------- /examples/simple_pinching_example.py: -------------------------------------------------------------------------------- 1 | """Prints which hand is pinching every 50 frames, both hands can be tracked. 2 | The difference between the location of the distal of the index and the distal 3 | of the thumb is calculated and we check it against a threshold of 20 in each 4 | axis. If any one axis is off by more than 20, we say the finger and thumb are 5 | not pinching. 6 | """ 7 | 8 | import time 9 | import leap 10 | from leap import datatypes as ldt 11 | 12 | 13 | def location_end_of_finger(hand: ldt.Hand, digit_idx: int) -> ldt.Vector: 14 | digit = hand.digits[digit_idx] 15 | return digit.distal.next_joint 16 | 17 | 18 | def sub_vectors(v1: ldt.Vector, v2: ldt.Vector) -> list: 19 | return map(float.__sub__, v1, v2) 20 | 21 | 22 | def fingers_pinching(thumb: ldt.Vector, index: ldt.Vector): 23 | diff = list(map(abs, sub_vectors(thumb, index))) 24 | 25 | if diff[0] < 20 and diff[1] < 20 and diff[2] < 20: 26 | return True, diff 27 | else: 28 | return False, diff 29 | 30 | 31 | class PinchingListener(leap.Listener): 32 | def on_tracking_event(self, event): 33 | if event.tracking_frame_id % 50 == 0: 34 | for hand in event.hands: 35 | hand_type = "Left" if str(hand.type) == "HandType.Left" else "Right" 36 | 37 | thumb = location_end_of_finger(hand, 0) 38 | index = location_end_of_finger(hand, 1) 39 | 40 | pinching, array = fingers_pinching(thumb, index) 41 | pinching_str = "not pinching" if not pinching else "" + str("pinching") 42 | print( 43 | f"{hand_type} hand thumb and index {pinching_str} with position diff ({array[0]}, {array[1]}, {array[2]})." 44 | ) 45 | 46 | 47 | def main(): 48 | listener = PinchingListener() 49 | 50 | connection = leap.Connection() 51 | connection.add_listener(listener) 52 | 53 | with connection.open(): 54 | while True: 55 | time.sleep(1) 56 | 57 | 58 | if __name__ == "__main__": 59 | main() 60 | -------------------------------------------------------------------------------- /examples/tracking_event_example.py: -------------------------------------------------------------------------------- 1 | """Prints the palm position of each hand, every frame. When a device is 2 | connected we set the tracking mode to desktop and then generate logs for 3 | every tracking frame received. The events of creating a connection to the 4 | server and a device being plugged in also generate logs. 5 | """ 6 | 7 | import leap 8 | import time 9 | 10 | 11 | class MyListener(leap.Listener): 12 | def on_connection_event(self, event): 13 | print("Connected") 14 | 15 | def on_device_event(self, event): 16 | try: 17 | with event.device.open(): 18 | info = event.device.get_info() 19 | except leap.LeapCannotOpenDeviceError: 20 | info = event.device.get_info() 21 | 22 | print(f"Found device {info.serial}") 23 | 24 | def on_tracking_event(self, event): 25 | print(f"Frame {event.tracking_frame_id} with {len(event.hands)} hands.") 26 | for hand in event.hands: 27 | hand_type = "left" if str(hand.type) == "HandType.Left" else "right" 28 | print( 29 | f"Hand id {hand.id} is a {hand_type} hand with position ({hand.palm.position.x}, {hand.palm.position.y}, {hand.palm.position.z})." 30 | ) 31 | 32 | 33 | def main(): 34 | my_listener = MyListener() 35 | 36 | connection = leap.Connection() 37 | connection.add_listener(my_listener) 38 | 39 | running = True 40 | 41 | with connection.open(): 42 | connection.set_tracking_mode(leap.TrackingMode.Desktop) 43 | while running: 44 | time.sleep(1) 45 | 46 | 47 | if __name__ == "__main__": 48 | main() 49 | -------------------------------------------------------------------------------- /examples/visualiser.py: -------------------------------------------------------------------------------- 1 | import leap 2 | import numpy as np 3 | import cv2 4 | 5 | _TRACKING_MODES = { 6 | leap.TrackingMode.Desktop: "Desktop", 7 | leap.TrackingMode.HMD: "HMD", 8 | leap.TrackingMode.ScreenTop: "ScreenTop", 9 | } 10 | 11 | 12 | class Canvas: 13 | def __init__(self): 14 | self.name = "Python Gemini Visualiser" 15 | self.screen_size = [500, 700] 16 | self.hands_colour = (255, 255, 255) 17 | self.font_colour = (0, 255, 44) 18 | self.hands_format = "Skeleton" 19 | self.output_image = np.zeros((self.screen_size[0], self.screen_size[1], 3), np.uint8) 20 | self.tracking_mode = None 21 | 22 | def set_tracking_mode(self, tracking_mode): 23 | self.tracking_mode = tracking_mode 24 | 25 | def toggle_hands_format(self): 26 | self.hands_format = "Dots" if self.hands_format == "Skeleton" else "Skeleton" 27 | print(f"Set hands format to {self.hands_format}") 28 | 29 | def get_joint_position(self, bone): 30 | if bone: 31 | return int(bone.x + (self.screen_size[1] / 2)), int(bone.z + (self.screen_size[0] / 2)) 32 | else: 33 | return None 34 | 35 | def render_hands(self, event): 36 | # Clear the previous image 37 | self.output_image[:, :] = 0 38 | 39 | cv2.putText( 40 | self.output_image, 41 | f"Tracking Mode: {_TRACKING_MODES[self.tracking_mode]}", 42 | (10, self.screen_size[0] - 10), 43 | cv2.FONT_HERSHEY_SIMPLEX, 44 | 0.5, 45 | self.font_colour, 46 | 1, 47 | ) 48 | 49 | if len(event.hands) == 0: 50 | return 51 | 52 | for i in range(0, len(event.hands)): 53 | hand = event.hands[i] 54 | for index_digit in range(0, 5): 55 | digit = hand.digits[index_digit] 56 | for index_bone in range(0, 4): 57 | bone = digit.bones[index_bone] 58 | if self.hands_format == "Dots": 59 | prev_joint = self.get_joint_position(bone.prev_joint) 60 | next_joint = self.get_joint_position(bone.next_joint) 61 | if prev_joint: 62 | cv2.circle(self.output_image, prev_joint, 2, self.hands_colour, -1) 63 | 64 | if next_joint: 65 | cv2.circle(self.output_image, next_joint, 2, self.hands_colour, -1) 66 | 67 | if self.hands_format == "Skeleton": 68 | wrist = self.get_joint_position(hand.arm.next_joint) 69 | elbow = self.get_joint_position(hand.arm.prev_joint) 70 | if wrist: 71 | cv2.circle(self.output_image, wrist, 3, self.hands_colour, -1) 72 | 73 | if elbow: 74 | cv2.circle(self.output_image, elbow, 3, self.hands_colour, -1) 75 | 76 | if wrist and elbow: 77 | cv2.line(self.output_image, wrist, elbow, self.hands_colour, 2) 78 | 79 | bone_start = self.get_joint_position(bone.prev_joint) 80 | bone_end = self.get_joint_position(bone.next_joint) 81 | 82 | if bone_start: 83 | cv2.circle(self.output_image, bone_start, 3, self.hands_colour, -1) 84 | 85 | if bone_end: 86 | cv2.circle(self.output_image, bone_end, 3, self.hands_colour, -1) 87 | 88 | if bone_start and bone_end: 89 | cv2.line(self.output_image, bone_start, bone_end, self.hands_colour, 2) 90 | 91 | if ((index_digit == 0) and (index_bone == 0)) or ( 92 | (index_digit > 0) and (index_digit < 4) and (index_bone < 2) 93 | ): 94 | index_digit_next = index_digit + 1 95 | digit_next = hand.digits[index_digit_next] 96 | bone_next = digit_next.bones[index_bone] 97 | bone_next_start = self.get_joint_position(bone_next.prev_joint) 98 | if bone_start and bone_next_start: 99 | cv2.line( 100 | self.output_image, 101 | bone_start, 102 | bone_next_start, 103 | self.hands_colour, 104 | 2, 105 | ) 106 | 107 | if index_bone == 0 and bone_start and wrist: 108 | cv2.line(self.output_image, bone_start, wrist, self.hands_colour, 2) 109 | 110 | 111 | class TrackingListener(leap.Listener): 112 | def __init__(self, canvas): 113 | self.canvas = canvas 114 | 115 | def on_connection_event(self, event): 116 | pass 117 | 118 | def on_tracking_mode_event(self, event): 119 | self.canvas.set_tracking_mode(event.current_tracking_mode) 120 | print(f"Tracking mode changed to {_TRACKING_MODES[event.current_tracking_mode]}") 121 | 122 | def on_device_event(self, event): 123 | try: 124 | with event.device.open(): 125 | info = event.device.get_info() 126 | except leap.LeapCannotOpenDeviceError: 127 | info = event.device.get_info() 128 | 129 | print(f"Found device {info.serial}") 130 | 131 | def on_tracking_event(self, event): 132 | self.canvas.render_hands(event) 133 | 134 | 135 | def main(): 136 | canvas = Canvas() 137 | 138 | print(canvas.name) 139 | print("") 140 | print("Press in visualiser window to:") 141 | print(" x: Exit") 142 | print(" h: Select HMD tracking mode") 143 | print(" s: Select ScreenTop tracking mode") 144 | print(" d: Select Desktop tracking mode") 145 | print(" f: Toggle hands format between Skeleton/Dots") 146 | 147 | tracking_listener = TrackingListener(canvas) 148 | 149 | connection = leap.Connection() 150 | connection.add_listener(tracking_listener) 151 | 152 | running = True 153 | 154 | with connection.open(): 155 | connection.set_tracking_mode(leap.TrackingMode.Desktop) 156 | canvas.set_tracking_mode(leap.TrackingMode.Desktop) 157 | 158 | while running: 159 | cv2.imshow(canvas.name, canvas.output_image) 160 | 161 | key = cv2.waitKey(1) 162 | 163 | if key == ord("x"): 164 | break 165 | elif key == ord("h"): 166 | connection.set_tracking_mode(leap.TrackingMode.HMD) 167 | elif key == ord("s"): 168 | connection.set_tracking_mode(leap.TrackingMode.ScreenTop) 169 | elif key == ord("d"): 170 | connection.set_tracking_mode(leap.TrackingMode.Desktop) 171 | elif key == ord("f"): 172 | canvas.toggle_hands_format() 173 | 174 | 175 | if __name__ == "__main__": 176 | main() 177 | -------------------------------------------------------------------------------- /leapc-cffi/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include src/leapc_cffi/* 2 | include src/scripts/* 3 | -------------------------------------------------------------------------------- /leapc-cffi/README.md: -------------------------------------------------------------------------------- 1 | CFFI Python Binds For LeapC 2 | =========================== 3 | 4 | Low-level Python bindings for LeapC. These bindings are used by the leap module to interface with the LeapC API. 5 | 6 | A built shared object of this is included in the Gemini Hand Tracking install from v5.17 onwards. However, you can 7 | manually build this if it does not include a shared object for your python version or architecture. 8 | 9 | Below are the instructions on how to compile manually (requiring a C compiler): 10 | 11 | ``` 12 | # Create and activate a virtual environment 13 | pip install -r requirements.txt 14 | python -m build leapc-cffi 15 | pip install leapc-cffi/dist/leapc_cffi-0.0.1.tar.gz 16 | pip install -e leapc-python-api 17 | python examples/tracking_event_example.py 18 | ``` 19 | 20 | Building Errors 21 | --------------- 22 | 23 | This will try to use the LeapC shared object from your install of Gemini Hand Tracking. This module assumes that you 24 | have the Leap SDK installed in the default location. If this is not the case for you, you can use an environment 25 | variable to define the installation location. Define the environment variable `LEAPSDK_INSTALL_LOCATION` to the path of 26 | the `LeapSDK` folder, if you have installed to a custom location or moved it somewhere else. 27 | 28 | Example: 29 | `export LEAPSDK_INSTALL_LOCATION="C:\Program Files\CustomDir\Ultraleap\LeapSDK"` 30 | 31 | By default, this path is the following for each operating system: 32 | - Windows: `C:/Program Files/Ultraleap/LeapSDK` 33 | - Linux x64: `/usr/lib/ultraleap-hand-tracking-service` 34 | - Linux ARM: `/opt/ultraleap/LeapSDK` 35 | - Darwin: `/Applications/Ultraleap Hand Tracking.app/Contents/LeapSDK` 36 | -------------------------------------------------------------------------------- /leapc-cffi/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import setuptools 3 | import platform 4 | import shutil 5 | 6 | _HERE = os.path.abspath(os.path.dirname(__file__)) 7 | 8 | 9 | def get_system(): 10 | if platform.system() == "Linux" and platform.machine() == "aarch64": 11 | return "Linux-ARM" 12 | else: 13 | return platform.system() 14 | 15 | 16 | with open(os.path.join(_HERE, "README.md"), "r", encoding="utf-8") as fh: 17 | long_description = fh.read() 18 | 19 | # The resource directory needs to contain the LeapC headers and libraries 20 | _RESOURCE_DIRECTORY = os.path.join(_HERE, "src/leapc_cffi") 21 | 22 | _OS_DEFAULT_HEADER_INSTALL_LOCATION = { 23 | "Windows": "C:/Program Files/Ultraleap/LeapSDK", 24 | "Linux": "/usr/include", 25 | "Linux-ARM": "/opt/ultraleap/LeapSDK", 26 | "Darwin": "/Applications/Ultraleap Hand Tracking.app/Contents/LeapSDK", 27 | } 28 | 29 | _OS_DEFAULT_LIB_INSTALL_LOCATION = { 30 | "Windows": _OS_DEFAULT_HEADER_INSTALL_LOCATION[get_system()], 31 | "Linux": "/usr/lib/ultraleap-hand-tracking-service", 32 | "Linux-ARM": _OS_DEFAULT_HEADER_INSTALL_LOCATION[get_system()], 33 | "Darwin": _OS_DEFAULT_HEADER_INSTALL_LOCATION[get_system()], 34 | } 35 | 36 | _OS_SHARED_OBJECT = { 37 | "Windows": "LeapC.dll", 38 | "Linux": "libLeapC.so", 39 | "Linux-ARM": "libLeapC.so", 40 | "Darwin": "libLeapC.5.dylib", 41 | } 42 | 43 | 44 | def setup_symlink(file_path, destination_path): 45 | if os.path.islink(destination_path): 46 | os.unlink(destination_path) 47 | 48 | if os.path.exists(destination_path): 49 | os.remove(destination_path) 50 | 51 | if os.path.exists(file_path): 52 | try: 53 | if get_system() != "Windows": 54 | os.symlink(file_path, destination_path) 55 | else: 56 | # Just copy it for windows, so we don't need administrator privileges 57 | shutil.copy(file_path, destination_path) 58 | except OSError as error: 59 | print(error) 60 | error_msg = ( 61 | "Error " 62 | + ("creating symlink to " if get_system() != "Windows" else "copying file ") 63 | + file_path 64 | + "." 65 | ) 66 | raise Exception(error_msg) 67 | else: 68 | print("Looking for LeapC library at: " + file_path) 69 | raise Exception( 70 | "No " + str(_OS_SHARED_OBJECT[get_system()]) + " found, please ensure you " 71 | "have Ultraleap Gemini Hand Tracking installed, or define LEAPSDK_INSTALL_LOCATION environment " 72 | "variable to point to a LeapSDK directory." 73 | ) 74 | 75 | 76 | def gather_leap_sdk(): 77 | _USER_DEFINED_INSTALL_LOCATION = os.getenv("LEAPSDK_INSTALL_LOCATION") 78 | _OVERRIDE_HEADER_LOCATION = os.getenv("LEAPC_HEADER_OVERRIDE") 79 | _OVERRIDE_LIB_LOCATION = os.getenv("LEAPC_LIB_OVERRIDE") 80 | 81 | if _USER_DEFINED_INSTALL_LOCATION is not None: 82 | if get_system() == "Linux": 83 | print( 84 | "Warning: The LeapSDK directory with everything in doesn't currently exist on linux. Consider using " 85 | "LEAPC_HEADER_OVERRIDE and LEAPC_LIB_OVERRIDE environment variables to point to required files." 86 | ) 87 | 88 | print( 89 | "User defined install location given, using: " 90 | + str(_USER_DEFINED_INSTALL_LOCATION) 91 | + " to generate " 92 | "bindings." 93 | ) 94 | leapc_header_path = os.path.join( 95 | _USER_DEFINED_INSTALL_LOCATION, 96 | "include" if get_system() != "Linux" else "", 97 | "LeapC.h", 98 | ) 99 | libleapc_path = os.path.join( 100 | _USER_DEFINED_INSTALL_LOCATION, 101 | "lib" if get_system() != "Linux" else "", 102 | "x64" if get_system() == "Windows" else "", 103 | _OS_SHARED_OBJECT[get_system()], 104 | ) 105 | else: 106 | leapc_header_path = os.path.join( 107 | _OS_DEFAULT_HEADER_INSTALL_LOCATION[get_system()], 108 | "include" if get_system() != "Linux" else "", 109 | "LeapC.h", 110 | ) 111 | libleapc_path = os.path.join( 112 | _OS_DEFAULT_LIB_INSTALL_LOCATION[get_system()], 113 | "lib" if get_system() != "Linux" else "", 114 | "x64" if get_system() == "Windows" else "", 115 | _OS_SHARED_OBJECT[get_system()], 116 | ) 117 | 118 | # Override 119 | if _OVERRIDE_HEADER_LOCATION is not None: 120 | print("Header override location given, using: " + str(_OVERRIDE_HEADER_LOCATION)) 121 | leapc_header_path = _OVERRIDE_HEADER_LOCATION 122 | 123 | if _OVERRIDE_LIB_LOCATION is not None: 124 | print("Library override location given, using: " + str(_OVERRIDE_LIB_LOCATION)) 125 | libleapc_path = _OVERRIDE_LIB_LOCATION 126 | 127 | # Copy the found header 128 | if os.path.exists(leapc_header_path): 129 | shutil.copy(leapc_header_path, os.path.join(_RESOURCE_DIRECTORY, "LeapC.h")) 130 | else: 131 | raise Exception( 132 | "No LeapC.h found, please ensure you have Ultraleap Gemini Hand Tracking installed, or define " 133 | "LEAPSDK_INSTALL_LOCATION environment variable to point to a LeapSDK directory." 134 | ) 135 | 136 | # Create a symlink for the shared object 137 | symlink_path = os.path.join(_RESOURCE_DIRECTORY, _OS_SHARED_OBJECT[get_system()]) 138 | setup_symlink(libleapc_path, symlink_path) 139 | 140 | # On windows we also need to manage the LeapC.lib file 141 | if get_system() == "Windows": 142 | if _USER_DEFINED_INSTALL_LOCATION is not None: 143 | windows_lib_path = os.path.join( 144 | _USER_DEFINED_INSTALL_LOCATION, "lib", "x64", "LeapC.lib" 145 | ) 146 | else: 147 | windows_lib_path = os.path.join( 148 | _OS_DEFAULT_LIB_INSTALL_LOCATION[get_system()], "lib", "x64", "LeapC.lib" 149 | ) 150 | symlink_lib_path = os.path.join(_RESOURCE_DIRECTORY, "LeapC.lib") 151 | setup_symlink(windows_lib_path, symlink_lib_path) 152 | 153 | 154 | gather_leap_sdk() 155 | 156 | setuptools.setup( 157 | name="leapc_cffi", 158 | version="0.0.1", 159 | author="Ultraleap", 160 | description="Python CFFI bindings for LeapC", 161 | long_description=long_description, 162 | long_description_content_type="text/markdown", 163 | package_dir={"": "src"}, 164 | packages=setuptools.find_packages(where="src"), 165 | include_package_data=True, 166 | exclude_package_data={ 167 | "": ["*.h", "*.lib", "scripts/*"] 168 | }, # Excluded from the installed package 169 | python_requires=">=3.8", 170 | setup_requires=["cffi"], 171 | install_requires=["cffi"], 172 | ext_package="leapc_cffi", # The location that the CFFI module will be built 173 | cffi_modules=["src/scripts/cffi_build.py:ffibuilder"], 174 | ) 175 | -------------------------------------------------------------------------------- /leapc-cffi/src/leapc_cffi/__init__.py: -------------------------------------------------------------------------------- 1 | from ._leapc_cffi import ffi, lib as libleapc 2 | -------------------------------------------------------------------------------- /leapc-cffi/src/scripts/cffi_build.py: -------------------------------------------------------------------------------- 1 | """Build script for the LeapC CFFI module 2 | 3 | This script should never be imported directly, and only used by the package 4 | setup.py 5 | """ 6 | 7 | import os 8 | import platform 9 | 10 | from cffi import FFI 11 | 12 | _HERE = os.path.abspath(os.path.dirname(__file__)) 13 | 14 | # The resource directory needs to contain the LeapC headers and libraries 15 | _RESOURCE_DIRECTORY = os.path.join(_HERE, "..", "leapc_cffi") 16 | 17 | 18 | def sanitise_leapc_header(input_str): 19 | """Sanitise the LeapC Header so that it can be used as a cdef string in cffi""" 20 | lines = input_str.split("\n") 21 | 22 | # Some '#define' statements define numbers which need to be replaced in the header 23 | value_defines = {"LEAP_DISTORTION_MATRIX_N": None} 24 | for line in lines: 25 | if line.startswith("#define"): 26 | split_line = line.split(" ") 27 | key = split_line[1] 28 | if key in value_defines: 29 | value_defines[key] = split_line[2] 30 | 31 | header_replacements = {"LEAP_CALL": "", "LEAP_EXPORT": ""} 32 | header_replacements.update(value_defines) 33 | 34 | new_lines = [] 35 | for line in lines: 36 | for key, val in header_replacements.items(): 37 | line = line.replace(key, val) 38 | new_lines.append(line) 39 | lines = new_lines 40 | 41 | # Remove all lines that start with # 42 | # Remove anything that's in an #if statement (except the top level '#ifndef _LEAP_C_H') 43 | # This allows us to remove lines like the closing bracket of '#extern "C"{' 44 | # and lines such as 'typedef __int32 int32_t' 45 | new_lines = [] 46 | if_depth = 0 47 | for line in lines: 48 | if line.startswith("#"): 49 | no_spaced_line = line.replace(" ", "")[1:] 50 | if no_spaced_line.startswith("if"): 51 | if_depth += 1 52 | elif no_spaced_line.startswith("endif"): 53 | if_depth -= 1 54 | elif if_depth == 1: 55 | new_lines.append(line) 56 | 57 | lines = new_lines 58 | 59 | ignored_line_beginnings = [ 60 | "LEAP_STATIC_ASSERT", 61 | ] 62 | 63 | new_lines = [] 64 | for line in lines: 65 | include_line = True 66 | for beginning in ignored_line_beginnings: 67 | if line.startswith(beginning): 68 | include_line = False 69 | break 70 | if include_line: 71 | new_lines.append(line) 72 | 73 | return "\n".join(new_lines) 74 | 75 | 76 | leapc_header_fpath = os.path.join(_RESOURCE_DIRECTORY, "LeapC.h") 77 | with open(leapc_header_fpath) as fp: 78 | leapc_header = fp.read() 79 | 80 | cffi_cdef = sanitise_leapc_header(leapc_header) 81 | 82 | ffibuilder = FFI() 83 | ffibuilder.cdef(cffi_cdef, packed=True) 84 | 85 | cffi_src_fpath = os.path.join(os.path.dirname(__file__), "cffi_src.h") 86 | with open(cffi_src_fpath) as fp: 87 | cffi_src = fp.read() 88 | 89 | extra_link_args = { 90 | "Windows": [], 91 | "Linux": ["-Wl,-rpath=$ORIGIN"], 92 | "Darwin": ["-Wl,-rpath,@loader_path"], 93 | } 94 | 95 | os_libraries = {"Windows": ["LeapC"], "Linux": ["LeapC"], "Darwin": ["LeapC.5"]} 96 | 97 | ffibuilder.set_source( 98 | "_leapc_cffi", 99 | cffi_src, 100 | libraries=os_libraries[platform.system()], 101 | include_dirs=[_RESOURCE_DIRECTORY], 102 | library_dirs=[_RESOURCE_DIRECTORY], 103 | extra_link_args=extra_link_args[platform.system()], 104 | ) 105 | 106 | if __name__ == "__main__": 107 | ffibuilder.compile(verbose=True) 108 | -------------------------------------------------------------------------------- /leapc-cffi/src/scripts/cffi_src.h: -------------------------------------------------------------------------------- 1 | #include "LeapC.h" 2 | -------------------------------------------------------------------------------- /leapc-python-api/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include src/leap/leapc/* 2 | -------------------------------------------------------------------------------- /leapc-python-api/README.md: -------------------------------------------------------------------------------- 1 | LeapC Python Bindings 2 | ===================== 3 | 4 | This is the Python LeapC API Wrapper that allows you to interface with Gemini Hand Tracking through python. 5 | 6 | A built shared object is required to make use of this. This is included in the Gemini Hand Tracking install from v5.17 7 | onwards. However, you can manually build this if it does not include a shared object for your python version or 8 | architecture. Please view the readme of the `leapc-cffi` module on how to build this manually. 9 | -------------------------------------------------------------------------------- /leapc-python-api/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import setuptools 3 | 4 | _HERE = os.path.abspath(os.path.dirname(__file__)) 5 | 6 | with open(os.path.join(_HERE, "README.md"), "r", encoding="utf-8") as fh: 7 | long_description = fh.read() 8 | 9 | setuptools.setup( 10 | name="leap", 11 | version="0.0.1", 12 | author="Ultraleap", 13 | description="Python wrappers around LeapC bindings", 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | package_dir={"": "src"}, 17 | packages=setuptools.find_packages(where="src"), 18 | python_requires=">=3.8", 19 | ) 20 | -------------------------------------------------------------------------------- /leapc-python-api/src/leap/__init__.py: -------------------------------------------------------------------------------- 1 | """ Leap Package """ 2 | import fnmatch 3 | 4 | # Set up some functions we want to be available at the top level 5 | 6 | import sys 7 | import platform 8 | import os 9 | 10 | _OS_DEFAULT_CFFI_INSTALL_LOCATION = { 11 | "Windows": "C:/Program Files/Ultraleap/LeapSDK", 12 | "Linux": "/usr/lib/ultraleap-hand-tracking-service", 13 | "Linux-ARM": "/opt/ultraleap/LeapSDK", 14 | "Darwin": "/Applications/Ultraleap Hand Tracking.app/Contents/LeapSDK", 15 | } 16 | 17 | _OS_REQUIRED_CFFI_FILES = { 18 | "Windows": ["__init__.py", "LeapC.lib", "LeapC.dll"], 19 | "Linux": ["__init__.py", "libLeapC.so", "libLeapC.so.5"], 20 | "Linux-ARM": ["__init__.py", "libLeapC.so", "libLeapC.so.5"], 21 | "Darwin": ["__init__.py", "libLeapC.5.dylib", "libLeapC.dylib"], 22 | } 23 | 24 | _OS_CFFI_SHARED_OBJECT_PATTERN = { 25 | "Windows": "_leapc_cffi*.pyd", 26 | "Linux": "_leapc_cffi*.so", 27 | "Linux-ARM": "_leapc_cffi*.so", 28 | "Darwin": "_leapc_cffi*.so", 29 | } 30 | 31 | 32 | def get_system(): 33 | if platform.system() == "Linux" and platform.machine() == "aarch64": 34 | return "Linux-ARM" 35 | else: 36 | return platform.system() 37 | 38 | 39 | def check_required_files(cffi_dir): 40 | directory_files = [ 41 | f for f in os.listdir(cffi_dir) if os.path.isfile(os.path.join(cffi_dir, f)) 42 | ] 43 | 44 | shared_object_files = [ 45 | f 46 | for f in directory_files 47 | if fnmatch.fnmatch(f, _OS_CFFI_SHARED_OBJECT_PATTERN[get_system()]) 48 | ] 49 | if len(shared_object_files) < 1: 50 | return False 51 | 52 | for file in _OS_REQUIRED_CFFI_FILES[get_system()]: 53 | if file not in directory_files: 54 | return False 55 | 56 | return True 57 | 58 | 59 | _OVERRIDE_LEAPSDK_LOCATION = os.getenv("LEAPSDK_INSTALL_LOCATION") 60 | 61 | cffi_location = _OS_DEFAULT_CFFI_INSTALL_LOCATION[get_system()] 62 | if _OVERRIDE_LEAPSDK_LOCATION is not None: 63 | cffi_location = _OVERRIDE_LEAPSDK_LOCATION 64 | 65 | cffi_path = os.path.join(cffi_location, "leapc_cffi") 66 | if os.path.isdir(cffi_path): 67 | ret = check_required_files(cffi_path) 68 | 69 | # TODO: If we can't find leapc_cffi, we could try building it 70 | 71 | sys.path.append(cffi_location) 72 | 73 | try: 74 | from leapc_cffi import ffi, libleapc 75 | except ImportError as import_error: 76 | if not ret: 77 | error_msg = f"Missing required files within {cffi_location}." 78 | else: 79 | error_msg = f"Unknown error, please consult readme for help. Attempting to find leapc_cffi within {cffi_location}" 80 | raise ImportError( 81 | f"Cannot import leapc_cffi: {error_msg}. Caught ImportError: {import_error}" 82 | ) 83 | else: 84 | error_msg = f"Error: Unable to find leapc_cffi dir within directory {cffi_location}" 85 | raise Exception(error_msg) 86 | 87 | from .functions import ( 88 | get_now, 89 | get_server_status, 90 | get_frame_size, 91 | interpolate_frame, 92 | get_extrinsic_matrix, 93 | ) 94 | from .connection import Connection 95 | from .enums import EventType, TrackingMode, HandType 96 | from .event_listener import Listener 97 | from .exceptions import LeapError 98 | from .recording import Recording, Recorder 99 | -------------------------------------------------------------------------------- /leapc-python-api/src/leap/connection.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | import sys 3 | import threading 4 | from typing import Dict, Optional, List, Callable 5 | from timeit import default_timer as timer 6 | import time 7 | import json 8 | 9 | from leapc_cffi import ffi, libleapc 10 | 11 | from .device import Device 12 | from .enums import ( 13 | ConnectionStatus, 14 | EventType, 15 | RS as LeapRS, 16 | ConnectionConfig as ConnectionConfigEnum, 17 | TrackingMode, 18 | PolicyFlag, 19 | ) 20 | from .event_listener import LatestEventListener, Listener 21 | from .events import create_event, Event 22 | from .exceptions import ( 23 | create_exception, 24 | success_or_raise, 25 | LeapError, 26 | LeapConnectionAlreadyOpen, 27 | LeapConcurrentPollError, 28 | LeapNotConnectedError, 29 | LeapTimeoutError, 30 | ) 31 | 32 | 33 | class ConnectionConfig: 34 | """Configuration for a Connection 35 | 36 | Allows a user to enable multi device functionality prior to connection. 37 | """ 38 | 39 | def __init__( 40 | self, 41 | *, 42 | server_namespace: Optional[Dict[str, str]] = None, 43 | multi_device_aware: bool = False, 44 | ): 45 | self._data_ptr = ffi.new("LEAP_CONNECTION_CONFIG*") 46 | self._data_ptr.server_namespace = server_namespace 47 | self._data_ptr.flags = 0 48 | self._data_ptr.size = ffi.sizeof(self._data_ptr[0]) 49 | 50 | if multi_device_aware: 51 | self._data_ptr.flags |= ConnectionConfigEnum.MultiDeviceAware.value 52 | 53 | 54 | class Connection: 55 | """Connection to a Leap Server 56 | 57 | :param listeners: A List of event listeners. Defaults to None 58 | :param poll_timeout: A timeout of poll messages, in seconds. Defaults to 1 second. 59 | :param response_timeout: A timeout to wait for specific events in response to events. 60 | Defaults to 10 seconds. 61 | """ 62 | 63 | def __init__( 64 | self, 65 | *, 66 | server_namespace: Optional[Dict[str, str]] = None, 67 | multi_device_aware: bool = False, 68 | listeners: Optional[List[Listener]] = None, 69 | poll_timeout: float = 1, 70 | response_timeout: float = 10, 71 | ): 72 | if listeners is None: 73 | listeners = [] 74 | self._listeners = listeners 75 | 76 | self._connection_ptr = self._create_connection(server_namespace, multi_device_aware) 77 | 78 | self._poll_timeout = int(poll_timeout * 1000) # Seconds to milliseconds 79 | self._response_timeout = int(response_timeout) 80 | self._stop_poll_flag = False 81 | 82 | self._is_open = False 83 | self._poll_thread = None 84 | 85 | def __del__(self): 86 | # Since 'destroy_connection' only tells C to free the memory that it allocated 87 | # for our connection, it is appropriate to leave the deletion of this to the garbage 88 | # collector. 89 | if hasattr(self, "_connection_ptr"): 90 | # We have this 'if' statement to deal with the possibility that an Exception 91 | # could be raised in the __init__ method, before this has been assigned. 92 | self._destroy_connection(self._connection_ptr) 93 | 94 | def add_listener(self, listener: Listener): 95 | self._listeners.append(listener) 96 | 97 | def remove_listener(self, listener: Listener): 98 | self._listeners.remove(listener) 99 | 100 | def poll(self, timeout: Optional[float] = None) -> Event: 101 | """Manually poll the connection from this thread 102 | 103 | Do not notify listeners about the result of this poll. 104 | 105 | :param timeout: The timeout of the poll, in seconds. 106 | Defaults to the number the Connection was initialised with. 107 | """ 108 | if self._poll_thread is not None: 109 | raise LeapConcurrentPollError 110 | if timeout is None: 111 | timeout = self._poll_timeout 112 | else: 113 | timeout = int(timeout * 1000) # Seconds to milliseconds 114 | event_ptr = ffi.new("LEAP_CONNECTION_MESSAGE*") 115 | success_or_raise(libleapc.LeapPollConnection, self._connection_ptr[0], timeout, event_ptr) 116 | return create_event(event_ptr) 117 | 118 | def poll_until( 119 | self, 120 | event_type: EventType, 121 | *, 122 | timeout: Optional[float] = None, 123 | individual_poll_timeout: Optional[float] = None, 124 | ) -> Event: 125 | """Manually poll the connection until a specific event type is received 126 | 127 | Discard all other events. Do not notify listeners about the results of any polls. 128 | """ 129 | if timeout is None: 130 | timeout = self._response_timeout 131 | start_time = timer() 132 | while timer() - start_time < timeout: 133 | try: 134 | event = self.poll(individual_poll_timeout) 135 | if isinstance(event, event_type): 136 | return event 137 | except LeapTimeoutError: 138 | pass 139 | raise LeapTimeoutError 140 | 141 | def wait_for(self, event_type: EventType, *, timeout: Optional[float] = None) -> Event: 142 | """Wait until the specified event type is emitted 143 | 144 | Returns the next event of the requested type. 145 | """ 146 | if not self._is_open: 147 | raise LeapNotConnectedError 148 | 149 | return self._call_and_wait_for_event(event_type, func=None, timeout=timeout) 150 | 151 | @contextmanager 152 | def open(self, *, auto_poll: bool = True, timeout: float = 10): 153 | """Open the Connection 154 | 155 | Optionally starts a separate thread which continually polls the connection. 156 | 157 | :param auto_poll: Whether to launch a separate thread to poll the connection. 158 | Defaults to True. 159 | :param timeout: A timeout for initial connection in seconds. This may be greater than 160 | the usual poll timeout. Defaults to 10s. 161 | """ 162 | self.connect(auto_poll=auto_poll, timeout=timeout) 163 | try: 164 | yield self 165 | finally: 166 | self.disconnect() 167 | 168 | def connect(self, *, auto_poll: bool = True, timeout: float = 10): 169 | """Open the connection 170 | 171 | The caller is responsible for disconnecting afterwards. 172 | 173 | Optionally starts a separate thread which continually polls the connection. 174 | 175 | :param auto_poll: Whether to launch a separate thread to poll the connection. 176 | Defaults to True. 177 | :param timeout: A timeout for initial connection in seconds. This may be greater than 178 | the usual poll timeout. Defaults to 10s. 179 | """ 180 | if self._is_open: 181 | raise LeapConnectionAlreadyOpen 182 | 183 | self._open_connection() 184 | 185 | if auto_poll: 186 | self._start_poll_thread(timeout) 187 | 188 | def disconnect(self): 189 | self._stop_poll_thread() 190 | self._close_connection() 191 | 192 | def set_tracking_mode(self, mode: TrackingMode): 193 | """Set the Server tracking mode""" 194 | success_or_raise(libleapc.LeapSetTrackingMode, self._connection_ptr[0], mode.value) 195 | 196 | def get_tracking_mode(self) -> TrackingMode: 197 | """Get the Server tracking mode""" 198 | func = success_or_raise 199 | args = (libleapc.LeapGetTrackingMode, self._connection_ptr[0]) 200 | 201 | event = self._call_and_wait_for_event(EventType.TrackingMode, func, args) 202 | return event.current_tracking_mode 203 | 204 | def set_policy_flags( 205 | self, 206 | flags_to_set: Optional[List[PolicyFlag]] = None, 207 | flags_to_clear: Optional[List[PolicyFlag]] = None, 208 | ) -> List[PolicyFlag]: 209 | """Set the policy flags 210 | 211 | Returns a list of current policy flags. 212 | 213 | :param flags_to_set: A list of PolicyFlags to set. Defaults to None. 214 | :param flags_to_clear: A list of PolicyFlags to clear. Defaults to None. 215 | """ 216 | to_set = 0 217 | if flags_to_set is not None: 218 | for flag in flags_to_set: 219 | to_set |= flag.value 220 | 221 | to_clear = 0 222 | if flags_to_clear is not None: 223 | for flag in flags_to_clear: 224 | to_clear |= flag.value 225 | 226 | func = success_or_raise 227 | args = (libleapc.LeapSetPolicyFlags, self._connection_ptr[0], to_set, to_clear) 228 | event = self._call_and_wait_for_event(EventType.Policy, func, args) 229 | return event.current_policy_flags 230 | 231 | def get_policy_flags(self) -> List[PolicyFlag]: 232 | """Get the current policy flags""" 233 | return self.set_policy_flags() 234 | 235 | def get_status(self) -> ConnectionStatus: 236 | """Get information about the current connection""" 237 | connection_info_ptr = ffi.new("LEAP_CONNECTION_INFO*") 238 | size_of_info = ffi.sizeof(connection_info_ptr[0]) 239 | connection_info_ptr.size = size_of_info 240 | success_or_raise( 241 | libleapc.LeapGetConnectionInfo, self._connection_ptr[0], connection_info_ptr 242 | ) 243 | return ConnectionStatus(connection_info_ptr.status) 244 | 245 | def get_devices(self) -> List[Device]: 246 | """Get the devices which the Server knows about""" 247 | count_ptr = ffi.new("uint32_t*") 248 | success_or_raise(libleapc.LeapGetDeviceList, self._connection_ptr[0], ffi.NULL, count_ptr) 249 | devices_ptr = ffi.new("LEAP_DEVICE_REF[]", count_ptr[0]) 250 | success_or_raise( 251 | libleapc.LeapGetDeviceList, self._connection_ptr[0], devices_ptr, count_ptr 252 | ) 253 | return [Device(devices_ptr[i], owner=devices_ptr) for i in range(count_ptr[0])] 254 | 255 | def set_primary_device(self, device: Device, unsubscribe_others: bool = False): 256 | """Sets the primary device 257 | 258 | :param device: The device to make primary 259 | :param unsubscribe_others: Whether to unsubscribe other devices 260 | """ 261 | success_or_raise( 262 | libleapc.LeapSetPrimaryDevice, 263 | self._connection_ptr[0], 264 | device.c_data_device, 265 | unsubscribe_others, 266 | ) 267 | 268 | def get_connection_ptr(self) -> ffi.CData: 269 | return self._connection_ptr[0] 270 | 271 | def subscribe_events(self, device: Device): 272 | """Subscribe to events from the device 273 | 274 | :param device: The device to subscribe to 275 | """ 276 | success_or_raise( 277 | libleapc.LeapSubscribeEvents, self._connection_ptr[0], device.c_data_device 278 | ) 279 | 280 | def unsubscribe_events(self, device: Device): 281 | """Unsubscribe from events from the device 282 | 283 | :param device: The device to unsubscribe from 284 | """ 285 | success_or_raise( 286 | libleapc.LeapUnsubscribeEvents, 287 | self._connection_ptr[0], 288 | device.c_data_device, 289 | ) 290 | 291 | @staticmethod 292 | def _create_connection( 293 | server_namespace: Optional[Dict] = None, multi_device_aware: bool = False 294 | ) -> ffi.CData: 295 | connection_ptr = ffi.new("LEAP_CONNECTION*") 296 | ffi_server_namespace = ffi.new("char []", json.dumps(server_namespace).encode("ascii")) 297 | 298 | config = ConnectionConfig( 299 | server_namespace=ffi_server_namespace, multi_device_aware=multi_device_aware 300 | ) 301 | 302 | raw_result = libleapc.LeapCreateConnection(config._data_ptr, connection_ptr) 303 | result = LeapRS(raw_result) 304 | if result != LeapRS.Success: 305 | raise create_exception(result, "Unable to create connection") 306 | return connection_ptr 307 | 308 | @staticmethod 309 | def _destroy_connection(connection_ptr: ffi.CData): 310 | # Destroy the connection. Must be done on all created connections. 311 | libleapc.LeapDestroyConnection(connection_ptr[0]) 312 | 313 | def _open_connection(self): 314 | # Open the connection 315 | open_result = libleapc.LeapOpenConnection(self._connection_ptr[0]) 316 | if LeapRS(open_result) != LeapRS.Success: 317 | raise create_exception(LeapRS(open_result), "Unable to open connection") 318 | self._is_open = True 319 | 320 | def _close_connection(self): 321 | # Close the connection. Must be done on all opened connections. 322 | if self._connection_ptr is not None: 323 | libleapc.LeapCloseConnection(self._connection_ptr[0]) 324 | self._is_open = False 325 | 326 | def _start_poll_thread(self, startup_timeout: float): 327 | self._poll_thread = threading.Thread(target=self._poll_loop) 328 | try: 329 | self._call_and_wait_for_event( 330 | EventType.Connection, self._poll_thread.start, timeout=startup_timeout 331 | ) 332 | except LeapTimeoutError as exc: 333 | self._stop_poll_thread() 334 | raise exc 335 | 336 | def _stop_poll_thread(self): 337 | if self._poll_thread is not None: 338 | self._stop_poll_flag = True 339 | self._poll_thread.join() 340 | self._stop_poll_flag = False 341 | self._poll_thread = None 342 | 343 | def _poll_loop(self): 344 | event_ptr = ffi.new("LEAP_CONNECTION_MESSAGE*") 345 | while True: 346 | if self._stop_poll_flag: 347 | break 348 | try: 349 | success_or_raise( 350 | libleapc.LeapPollConnection, 351 | self._connection_ptr[0], 352 | self._poll_timeout, 353 | event_ptr, 354 | ) 355 | event = create_event(event_ptr) 356 | for listener in self._listeners: 357 | try: 358 | listener.on_event(event) 359 | except Exception as exc: 360 | msg = f"Caught exception in listener callback: {type(exc)}, {exc}, {exc.__traceback__}" 361 | print(msg, file=sys.stderr) 362 | except LeapError as exc: 363 | for listener in self._listeners: 364 | listener.on_error(exc) 365 | 366 | def _call_and_wait_for_event( 367 | self, 368 | event_type: EventType, 369 | func: Optional[Callable] = None, 370 | args: Optional[tuple] = None, 371 | *, 372 | timeout: Optional[float] = None, 373 | ) -> Event: 374 | """Wait for an event after an (optional) function call. 375 | 376 | If a function is supplied, it will be called with the specified args. This adds the 377 | event-listener to the connection before calling, so that the event is guaranteed to 378 | be found no matter how quickly after calling it is emitted. 379 | 380 | Return the requested event. 381 | """ 382 | listener = LatestEventListener(event_type) 383 | self.add_listener(listener) 384 | 385 | if func is not None: 386 | if args is None: 387 | args = [] 388 | 389 | try: 390 | func(*args) 391 | except Exception as exc: 392 | self.remove_listener(listener) 393 | raise exc 394 | 395 | if timeout is None: 396 | timeout = self._response_timeout 397 | 398 | start_time = timer() 399 | while listener.event is None and timer() - start_time < timeout: 400 | time.sleep(0.01) 401 | self.remove_listener(listener) 402 | 403 | if listener.event is None: 404 | raise LeapTimeoutError("Did not received expected event in time") 405 | return listener.event 406 | -------------------------------------------------------------------------------- /leapc-python-api/src/leap/cstruct.py: -------------------------------------------------------------------------------- 1 | from leapc_cffi import ffi 2 | 3 | 4 | class LeapCStruct: 5 | """Base class for objects which wrap around some raw C Data 6 | 7 | Classes which inherit from this should only be loose wrappers around 8 | some struct from the LeapC API. 9 | 10 | :param data: The raw CData 11 | """ 12 | 13 | def __init__(self, data: ffi.CData): 14 | self._data = data 15 | 16 | @property 17 | def c_data(self) -> ffi.CData: 18 | """Get the raw C data""" 19 | return self._data 20 | -------------------------------------------------------------------------------- /leapc-python-api/src/leap/datatypes.py: -------------------------------------------------------------------------------- 1 | """Wrappers for LeapC Data types""" 2 | 3 | from .cstruct import LeapCStruct 4 | from .enums import HandType 5 | from leapc_cffi import ffi 6 | 7 | 8 | class FrameData: 9 | """Wrapper which owns all the data required to read the Frame 10 | 11 | A LEAP_TRACKING_EVENT has a fixed size, but some fields are pointers to memory stored 12 | outside of the struct. This means the size required for all the information about a 13 | frame is larger than the size of the struct. 14 | 15 | This wrapper owns the buffer required for all of that data. Reading attributes or 16 | items from this wrapper returns the corresponding item or wrapper on the underlying 17 | LEAP_TRACKING_EVENT. 18 | 19 | It is intended to by used in the TrackingEvent constructor. 20 | """ 21 | 22 | def __init__(self, size): 23 | self._buffer = ffi.new("char[]", size) 24 | self._frame_ptr = ffi.cast("LEAP_TRACKING_EVENT*", self._buffer) 25 | 26 | def __getattr__(self, name): 27 | return getattr(self._frame_ptr, name) 28 | 29 | def __getitem__(self, key): 30 | return self._frame_ptr[key] 31 | 32 | def frame_ptr(self): 33 | return self._frame_ptr 34 | 35 | 36 | class FrameHeader(LeapCStruct): 37 | @property 38 | def frame_id(self): 39 | return self._data.frame_id 40 | 41 | @property 42 | def timestamp(self): 43 | return self._data.timestamp 44 | 45 | 46 | class Vector(LeapCStruct): 47 | def __getitem__(self, idx): 48 | return self._data.v[idx] 49 | 50 | def __iter__(self): 51 | return [self._data.v[i] for i in range(3)].__iter__() 52 | 53 | @property 54 | def x(self): 55 | return self._data.x 56 | 57 | @property 58 | def y(self): 59 | return self._data.y 60 | 61 | @property 62 | def z(self): 63 | return self._data.z 64 | 65 | 66 | class Quaternion(LeapCStruct): 67 | def __getitem__(self, idx): 68 | return self._data.v[idx] 69 | 70 | def __iter__(self): 71 | return [self._data.v[i] for i in range(4)].__iter__() 72 | 73 | @property 74 | def x(self): 75 | return self._data.x 76 | 77 | @property 78 | def y(self): 79 | return self._data.y 80 | 81 | @property 82 | def z(self): 83 | return self._data.z 84 | 85 | @property 86 | def w(self): 87 | return self._data.w 88 | 89 | 90 | class Palm(LeapCStruct): 91 | @property 92 | def position(self): 93 | return Vector(self._data.position) 94 | 95 | @property 96 | def stabilized_position(self): 97 | return Vector(self._data.stabilized_position) 98 | 99 | @property 100 | def velocity(self): 101 | return Vector(self._data.velocity) 102 | 103 | @property 104 | def normal(self): 105 | return Vector(self._data.normal) 106 | 107 | @property 108 | def width(self): 109 | return self._data.width 110 | 111 | @property 112 | def direction(self): 113 | return Vector(self._data.direction) 114 | 115 | @property 116 | def orientation(self): 117 | return Quaternion(self._data.orientation) 118 | 119 | 120 | class Bone(LeapCStruct): 121 | @property 122 | def prev_joint(self): 123 | return Vector(self._data.prev_joint) 124 | 125 | @property 126 | def next_joint(self): 127 | return Vector(self._data.next_joint) 128 | 129 | @property 130 | def width(self): 131 | return self._data.width 132 | 133 | @property 134 | def rotation(self): 135 | return Quaternion(self._data.rotation) 136 | 137 | 138 | class Digit(LeapCStruct): 139 | @property 140 | def finger_id(self): 141 | return self._data.finger_id 142 | 143 | @property 144 | def bones(self): 145 | return [self.metacarpal, self.proximal, self.intermediate, self.distal] 146 | 147 | @property 148 | def metacarpal(self): 149 | return Bone(self._data.metacarpal) 150 | 151 | @property 152 | def proximal(self): 153 | return Bone(self._data.proximal) 154 | 155 | @property 156 | def intermediate(self): 157 | return Bone(self._data.intermediate) 158 | 159 | @property 160 | def distal(self): 161 | return Bone(self._data.distal) 162 | 163 | @property 164 | def is_extended(self): 165 | return self._data.is_extended 166 | 167 | 168 | class Hand(LeapCStruct): 169 | @property 170 | def id(self): 171 | return self._data.id 172 | 173 | @property 174 | def flags(self): 175 | return self._data.flags 176 | 177 | @property 178 | def type(self): 179 | return HandType(self._data.type) 180 | 181 | @property 182 | def confidence(self): 183 | return self._data.confidence 184 | 185 | @property 186 | def visible_time(self): 187 | return self._data.visible_time 188 | 189 | @property 190 | def pinch_distance(self): 191 | return self._data.pinch_distance 192 | 193 | @property 194 | def grab_angle(self): 195 | return self._data.grab_angle 196 | 197 | @property 198 | def pinch_strength(self): 199 | return self._data.pinch_strength 200 | 201 | @property 202 | def grab_strength(self): 203 | return self._data.grab_strength 204 | 205 | @property 206 | def palm(self): 207 | return Palm(self._data.palm) 208 | 209 | @property 210 | def thumb(self): 211 | return Digit(self._data.thumb) 212 | 213 | @property 214 | def index(self): 215 | return Digit(self._data.index) 216 | 217 | @property 218 | def middle(self): 219 | return Digit(self._data.middle) 220 | 221 | @property 222 | def ring(self): 223 | return Digit(self._data.ring) 224 | 225 | @property 226 | def pinky(self): 227 | return Digit(self._data.pinky) 228 | 229 | @property 230 | def digits(self): 231 | return [self.thumb, self.index, self.middle, self.ring, self.pinky] 232 | 233 | @property 234 | def arm(self): 235 | return Bone(self._data.arm) 236 | 237 | 238 | class Image(LeapCStruct): 239 | @property 240 | def matrix_version(self): 241 | return self._data.matrix_version 242 | -------------------------------------------------------------------------------- /leapc-python-api/src/leap/device.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | 3 | from leapc_cffi import ffi, libleapc 4 | 5 | from .datatypes import LeapCStruct 6 | from .enums import get_enum_entries, DevicePID, DeviceStatus 7 | from .exceptions import success_or_raise, LeapError, LeapCannotOpenDeviceError 8 | 9 | 10 | class DeviceNotOpenException(LeapError): 11 | pass 12 | 13 | 14 | class DeviceStatusInfo: 15 | def __init__(self, status: ffi.CData): 16 | """Create the DeviceStatusInfo 17 | 18 | :param status: The CData defining the status 19 | """ 20 | self._status_flags = get_enum_entries(DeviceStatus, status) 21 | 22 | @staticmethod 23 | def _get_flags(status_int): 24 | return get_enum_entries(DeviceStatus, status_int) 25 | 26 | def check(self, flag: DeviceStatus): 27 | """Check if the flag is in the current flags 28 | 29 | :param flag: The flag to check 30 | """ 31 | return flag in self._status_flags 32 | 33 | @property 34 | def flags(self): 35 | return self._status_flags 36 | 37 | 38 | class DeviceInfo(LeapCStruct): 39 | @property 40 | def status(self): 41 | return DeviceStatusInfo(self._data.status) 42 | 43 | @property 44 | def caps(self): 45 | # TODO: Implement properly as flags 46 | return self._data.caps 47 | 48 | @property 49 | def pid(self): 50 | return DevicePID(self._data.pid) 51 | 52 | @property 53 | def baseline(self): 54 | return self._data.baseline 55 | 56 | @property 57 | def serial(self): 58 | return ffi.string(self._data.serial).decode("utf-8") 59 | 60 | @property 61 | def fov(self): 62 | """Get the horizontal & vertical field of view in radians""" 63 | return self._data.h_fov, self._data.v_fov 64 | 65 | @property 66 | def range(self): 67 | """Get the maximum range of this device, in micrometres""" 68 | return self._data.range 69 | 70 | 71 | class Device: 72 | def __init__(self, device_ref=None, *, device=None, owner=None): 73 | """A Device is usually constructed from a LEAP_DEVICE_REF object. 74 | 75 | Some functions require the device to be opened before they can be 76 | called. 77 | 78 | If a DeviceLost event occurs, this can be created from a LEAP_DEVICE 79 | object. In this case the Device is already open and does not need to 80 | be closed by the user. 81 | 82 | The 'owner' argument is a CFFI object that must be kept alive 83 | for the device ref to remain valid. It should never be used from 84 | within the class. 85 | """ 86 | self._device_ref = device_ref 87 | self._device = device 88 | self._owner = owner 89 | 90 | @property 91 | def c_data_device_ref(self): 92 | """Get the LEAP_DEVICE_REF object for this object""" 93 | return self._device_ref 94 | 95 | @property 96 | def c_data_device(self): 97 | """Get the LEAP_DEVICE object for this object 98 | 99 | If the device is not open, returns None 100 | """ 101 | return self._device 102 | 103 | @property 104 | def id(self): 105 | if self._device_ref is None: 106 | # The device must have been returned from a DeviceLostEvent 107 | # This means it does not have an id, so return None 108 | return 109 | return self._device_ref.id 110 | 111 | @contextmanager 112 | def open(self): 113 | if self._device is not None: 114 | raise LeapCannotOpenDeviceError("Device is already open") 115 | 116 | device_ptr = ffi.new("LEAP_DEVICE*") 117 | success_or_raise(libleapc.LeapOpenDevice, self._device_ref, device_ptr) 118 | self._device = device_ptr[0] 119 | try: 120 | yield self 121 | finally: 122 | self._device = None 123 | libleapc.LeapCloseDevice(device_ptr[0]) 124 | 125 | def get_info(self): 126 | """Get a DeviceInfo object containing information about this device 127 | 128 | Requires the Device to be open. 129 | Raises DeviceNotOpenException if the device is not open. 130 | """ 131 | if self._device is None: 132 | raise DeviceNotOpenException() 133 | info_ptr = ffi.new("LEAP_DEVICE_INFO*") 134 | info_ptr.size = ffi.sizeof(info_ptr[0]) 135 | info_ptr.serial = ffi.NULL 136 | success_or_raise(libleapc.LeapGetDeviceInfo, self._device, info_ptr) 137 | info_ptr.serial = ffi.new("char[]", info_ptr.serial_length) 138 | success_or_raise(libleapc.LeapGetDeviceInfo, self._device, info_ptr) 139 | return DeviceInfo(info_ptr[0]) 140 | 141 | def get_camera_count(self): 142 | if not self._device: 143 | raise DeviceNotOpenException() 144 | camera_count_ptr = ffi.new("uint8_t *") 145 | success_or_raise(libleapc.LeapGetDeviceCameraCount, self._device, camera_count_ptr) 146 | return camera_count_ptr[0] 147 | -------------------------------------------------------------------------------- /leapc-python-api/src/leap/enums.py: -------------------------------------------------------------------------------- 1 | """Wrappers around LeapC enums""" 2 | 3 | import enum 4 | from keyword import iskeyword 5 | 6 | from leapc_cffi import libleapc 7 | 8 | 9 | def _generate_enum_entries(container, name: str): 10 | """Generate enum entries based on the attributes of the container 11 | 12 | This searches for all attributes which start with "eLeap{name}_". 13 | 14 | It yields a tuple which is the remainder of the attribute name, and 15 | the corresponding attribute value. If the attribute name is a Python 16 | keyword then it is prefixed with the "name" input. 17 | 18 | Example: 19 | ``` 20 | class MyClass: 21 | eLeapFoo_One = 1 22 | eLeapFoo_Two = 2 23 | eLeapBar_Three = 3 24 | eLeapFoo_None = 4 25 | 26 | list(generate_enum_entries(MyClass, 'Foo')) 27 | > [('One', 1), ('Two', 2), ('FooNone', 4)] 28 | ``` 29 | """ 30 | prefix = f"eLeap{name}_" 31 | for attr in dir(container): 32 | if attr.startswith(prefix): 33 | enum_key = attr[len(prefix) :] 34 | enum_value = getattr(container, attr) 35 | if iskeyword(enum_key): 36 | enum_key = f"{name}{enum_key}" 37 | yield enum_key, enum_value 38 | 39 | 40 | class LeapEnum(type): 41 | """Metaclass used to generate Python Enum classes from LeapC enums 42 | 43 | Usage: Defining an empty class with this as its metaclass will create an 44 | enum.Enum class with the same name. The LeapC API will be searched for an 45 | enum of a matching name, and all entries will be created in this class. 46 | 47 | Example: 48 | Suppose the LeapC API has an enum `Foo` defined by: 49 | `[libleapc.eLeapFoo_One, libleapc.eLeapFoo_Two]` 50 | 51 | If we define a class via 52 | `class Foo(metaclass=LeapEnum): pass` 53 | Then a class will be generated which is equivalent to 54 | ``` 55 | class Foo(enum.Enum): 56 | One = libleapc.eLeapFoo_One 57 | Two = libleapc.eLeapFoo_Two 58 | ``` 59 | 60 | If an enum name is a Python keyword, it will be prefixed with the class 61 | name. Eg, instead of generating `Foo.None` it will generate `Foo.FooNone`. 62 | """ 63 | 64 | _LIBLEAPC = libleapc 65 | 66 | def __new__(cls, name, bases, dct): 67 | entries = _generate_enum_entries(cls._LIBLEAPC, name) 68 | return enum.Enum(name, entries) 69 | 70 | 71 | def get_enum_entries(enum_type, flags): 72 | """Interpret the flags as a bitwise combination of enum values 73 | 74 | Returns a list of enum entries which are present in the 'flags'. 75 | """ 76 | return list(filter(lambda entry: entry.value & flags != 0, enum_type)) 77 | 78 | 79 | class RS(metaclass=LeapEnum): 80 | pass 81 | 82 | 83 | class TrackingMode(metaclass=LeapEnum): 84 | pass 85 | 86 | 87 | class ConnectionConfig(metaclass=LeapEnum): 88 | pass 89 | 90 | 91 | class AllocatorType(metaclass=LeapEnum): 92 | pass 93 | 94 | 95 | class ServiceDisposition(metaclass=LeapEnum): 96 | pass 97 | 98 | 99 | class ConnectionStatus(metaclass=LeapEnum): 100 | pass 101 | 102 | 103 | class PolicyFlag(metaclass=LeapEnum): 104 | pass 105 | 106 | 107 | class ValueType(metaclass=LeapEnum): 108 | pass 109 | 110 | 111 | class DevicePID(metaclass=LeapEnum): 112 | pass 113 | 114 | 115 | class DeviceStatus(metaclass=LeapEnum): 116 | pass 117 | 118 | 119 | class ImageType(metaclass=LeapEnum): 120 | pass 121 | 122 | 123 | class ImageFormat(metaclass=LeapEnum): 124 | pass 125 | 126 | 127 | class PerspectiveType(metaclass=LeapEnum): 128 | pass 129 | 130 | 131 | class CameraCalibrationType(metaclass=LeapEnum): 132 | pass 133 | 134 | 135 | class HandType(metaclass=LeapEnum): 136 | pass 137 | 138 | 139 | class LogSeverity(metaclass=LeapEnum): 140 | pass 141 | 142 | 143 | class DroppedFrameType(metaclass=LeapEnum): 144 | pass 145 | 146 | 147 | class IMUFlag(metaclass=LeapEnum): 148 | pass 149 | 150 | 151 | class EventType(metaclass=LeapEnum): 152 | pass 153 | 154 | 155 | class RecordingFlags(metaclass=LeapEnum): 156 | pass 157 | 158 | 159 | class VersionPart(metaclass=LeapEnum): 160 | pass 161 | -------------------------------------------------------------------------------- /leapc-python-api/src/leap/event_listener.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from .events import Event 4 | from .enums import EventType 5 | from .exceptions import LeapError 6 | 7 | 8 | class Listener: 9 | """Base class for custom Listeners to Connections 10 | 11 | This should be subclassed and methods overridden to handle events and errors. 12 | """ 13 | 14 | def on_event(self, event: Event): 15 | """Called every event 16 | 17 | Note that if this method is overridden, the more specific event functions will not be called 18 | unless the overridden method calls this method. 19 | """ 20 | getattr(self, self._EVENT_CALLS[event.type])(event) 21 | 22 | def on_error(self, error: LeapError): 23 | """If an error occurs in polling, the Exception is passed to this function""" 24 | pass 25 | 26 | def on_none_event(self, event: Event): 27 | pass 28 | 29 | def on_connection_event(self, event: Event): 30 | pass 31 | 32 | def on_connection_lost_event(self, event: Event): 33 | pass 34 | 35 | def on_device_event(self, event: Event): 36 | pass 37 | 38 | def on_device_failure_event(self, event: Event): 39 | pass 40 | 41 | def on_policy_event(self, event: Event): 42 | pass 43 | 44 | def on_tracking_event(self, event: Event): 45 | pass 46 | 47 | def on_image_request_error_event(self, event: Event): 48 | pass 49 | 50 | def on_image_complete_event(self, event: Event): 51 | pass 52 | 53 | def on_log_event(self, event: Event): 54 | pass 55 | 56 | def on_device_lost_event(self, event: Event): 57 | pass 58 | 59 | def on_config_response_event(self, event: Event): 60 | pass 61 | 62 | def on_config_change_event(self, event: Event): 63 | pass 64 | 65 | def on_device_status_change_event(self, event: Event): 66 | pass 67 | 68 | def on_dropped_frame_event(self, event: Event): 69 | pass 70 | 71 | def on_image_event(self, event: Event): 72 | pass 73 | 74 | def on_point_mapping_change_event(self, event: Event): 75 | pass 76 | 77 | def on_tracking_mode_event(self, event: Event): 78 | pass 79 | 80 | def on_log_events(self, event: Event): 81 | pass 82 | 83 | def on_head_pose_event(self, event: Event): 84 | pass 85 | 86 | def on_eyes_event(self, event: Event): 87 | pass 88 | 89 | def on_imu_event(self, event: Event): 90 | pass 91 | 92 | _EVENT_CALLS = { 93 | EventType.EventTypeNone: "on_none_event", 94 | EventType.Connection: "on_connection_event", 95 | EventType.ConnectionLost: "on_connection_lost_event", 96 | EventType.Device: "on_device_event", 97 | EventType.DeviceFailure: "on_device_failure_event", 98 | EventType.Policy: "on_policy_event", 99 | EventType.Tracking: "on_tracking_event", 100 | EventType.ImageRequestError: "on_image_request_error_event", 101 | EventType.ImageComplete: "on_image_complete_event", 102 | EventType.LogEvent: "on_log_event", 103 | EventType.DeviceLost: "on_device_lost_event", 104 | EventType.ConfigResponse: "on_config_response_event", 105 | EventType.ConfigChange: "on_config_change_event", 106 | EventType.DeviceStatusChange: "on_device_status_change_event", 107 | EventType.DroppedFrame: "on_dropped_frame_event", 108 | EventType.Image: "on_image_event", 109 | EventType.PointMappingChange: "on_point_mapping_change_event", 110 | EventType.TrackingMode: "on_tracking_mode_event", 111 | EventType.LogEvents: "on_log_events", 112 | EventType.HeadPose: "on_head_pose_event", 113 | EventType.Eyes: "on_eyes_event", 114 | EventType.IMU: "on_imu_event", 115 | } 116 | 117 | 118 | class LatestEventListener(Listener): 119 | def __init__(self, target: EventType): 120 | self._target = target 121 | self.event: Optional[Event] = None 122 | 123 | def on_event(self, event: Event): 124 | if event.type == self._target: 125 | self.event = event 126 | -------------------------------------------------------------------------------- /leapc-python-api/src/leap/events.py: -------------------------------------------------------------------------------- 1 | """Classes for each of the LeapC Events 2 | 3 | These are created so that the members can be accessed as our custom Python objects 4 | instead of C Objects. 5 | """ 6 | 7 | from .cstruct import LeapCStruct 8 | from .datatypes import FrameHeader, Hand, Vector, Image 9 | from .device import Device, DeviceStatusInfo 10 | from .enums import EventType, get_enum_entries, TrackingMode, PolicyFlag, IMUFlag 11 | from leapc_cffi import ffi 12 | 13 | 14 | class EventMetadata(LeapCStruct): 15 | def __init__(self, data): 16 | super().__init__(data) 17 | self._event_type = EventType(data.type) 18 | self._device_id = data.device_id 19 | 20 | @property 21 | def event_type(self): 22 | return self._event_type 23 | 24 | @property 25 | def device_id(self): 26 | return self._device_id 27 | 28 | 29 | class Event(LeapCStruct): 30 | """Base class for Events 31 | 32 | Events have extra 'type' and 'metadata' properties. 33 | 34 | If the Event is constructed using the default constructor, the metadata is not populated. 35 | 36 | If the event is constructed using a `LEAP_CONNECTION_MESSAGE*` via the 37 | `from_connection_message` method, extra metadata will be available on 38 | the event. 39 | """ 40 | 41 | # The type of event this class corresponds to 42 | _EVENT_TYPE = EventType.EventTypeNone 43 | # The member on the `LEAP_CONNECTION_MESSAGE` that corresponds to the 44 | # event data. 45 | _EVENT_MESSAGE_ATTRIBUTE = "pointer" 46 | 47 | def __init__(self, data): 48 | super().__init__(data) 49 | self._metadata = None 50 | 51 | @classmethod 52 | def from_connection_message(cls, c_message): 53 | """Construct an Event from a LEAP_CONNECTION_MESSAGE* object 54 | 55 | Constructing an event in this way populates the event metadata. 56 | """ 57 | if EventType(c_message.type) != cls._EVENT_TYPE: 58 | raise ValueError("Incorect event type") 59 | 60 | event = cls(getattr(c_message, cls._EVENT_ATTRIBUTE)) 61 | event._metadata = EventMetadata(c_message) 62 | return event 63 | 64 | @classmethod 65 | def _get_event_cdata(cls, c_message): 66 | return getattr(c_message, cls._EVENT_ATTRIBUTE) 67 | 68 | @property 69 | def metadata(self): 70 | return self._metadata 71 | 72 | @property 73 | def type(self): 74 | return self._EVENT_TYPE 75 | 76 | 77 | class NoneEvent(Event): 78 | _EVENT_TYPE = EventType.EventTypeNone 79 | _EVENT_ATTRIBUTE = "pointer" 80 | 81 | 82 | class ConnectionEvent(Event): 83 | _EVENT_TYPE = EventType.Connection 84 | _EVENT_ATTRIBUTE = "connection_event" 85 | 86 | 87 | class ConnectionLostEvent(Event): 88 | _EVENT_TYPE = EventType.ConnectionLost 89 | _EVENT_ATTRIBUTE = "connection_lost_event" 90 | 91 | 92 | class DeviceEvent(Event): 93 | _EVENT_TYPE = EventType.Device 94 | _EVENT_ATTRIBUTE = "device_event" 95 | 96 | def __init__(self, data): 97 | super().__init__(data) 98 | self._device = Device(data.device) 99 | self._status = DeviceStatusInfo(data.status) 100 | 101 | @property 102 | def device(self): 103 | return self._device 104 | 105 | @property 106 | def status(self): 107 | return self._status 108 | 109 | 110 | class DeviceFailureEvent(Event): 111 | _EVENT_TYPE = EventType.DeviceFailure 112 | _EVENT_ATTRIBUTE = "device_failure_event" 113 | 114 | def __init__(self, data): 115 | super().__init__(data) 116 | self._device = Device(device=data.hDevice) 117 | self._status = DeviceStatusInfo(data.status) 118 | 119 | @property 120 | def device(self): 121 | return self._device 122 | 123 | @property 124 | def status(self): 125 | return self._status 126 | 127 | 128 | class PolicyEvent(Event): 129 | _EVENT_TYPE = EventType.Policy 130 | _EVENT_ATTRIBUTE = "policy_event" 131 | 132 | def __init__(self, data): 133 | super().__init__(data) 134 | self._flags = data.current_policy 135 | 136 | @property 137 | def current_policy_flags(self): 138 | return get_enum_entries(PolicyFlag, self._flags) 139 | 140 | 141 | class TrackingEvent(Event): 142 | _EVENT_TYPE = EventType.Tracking 143 | _EVENT_ATTRIBUTE = "tracking_event" 144 | 145 | def __init__(self, data): 146 | super().__init__(data) 147 | self._info = FrameHeader(data.info) 148 | self._tracking_frame_id = data.tracking_frame_id 149 | self._num_hands = data.nHands 150 | self._framerate = data.framerate 151 | 152 | # Copy hands to safe region of memory to protect against use-after-free (UAF) 153 | self._hands = ffi.new("LEAP_HAND[2]") 154 | ffi.memmove(self._hands, data.pHands, ffi.sizeof("LEAP_HAND") * data.nHands) 155 | 156 | @property 157 | def info(self): 158 | return self._info 159 | 160 | @property 161 | def timestamp(self): 162 | return self._info.timestamp 163 | 164 | @property 165 | def tracking_frame_id(self): 166 | return self._tracking_frame_id 167 | 168 | @property 169 | def hands(self): 170 | return [Hand(self._hands[i]) for i in range(self._num_hands)] 171 | 172 | @property 173 | def framerate(self): 174 | return self._framerate 175 | 176 | 177 | class ImageRequestErrorEvent(Event): 178 | _EVENT_TYPE = EventType.ImageRequestError 179 | _EVENT_ATTRIBUTE = "pointer" 180 | 181 | 182 | class ImageCompleteEvent(Event): 183 | _EVENT_TYPE = EventType.ImageComplete 184 | _EVENT_ATTRIBUTE = "pointer" 185 | 186 | 187 | class LogEvent(Event): 188 | _EVENT_TYPE = EventType.LogEvent 189 | _EVENT_ATTRIBUTE = "log_event" 190 | 191 | 192 | class DeviceLostEvent(Event): 193 | _EVENT_TYPE = EventType.DeviceLost 194 | _EVENT_ATTRIBUTE = "device_event" 195 | 196 | def __init__(self, data): 197 | super().__init__(data) 198 | self._device = Device(data.device) 199 | self._status = DeviceStatusInfo(data.status) 200 | 201 | @property 202 | def device(self): 203 | return self._device 204 | 205 | @property 206 | def status(self): 207 | return self._status 208 | 209 | 210 | class ConfigResponseEvent(Event): 211 | _EVENT_TYPE = EventType.ConfigResponse 212 | _EVENT_ATTRIBUTE = "config_response_event" 213 | 214 | 215 | class ConfigChangeEvent(Event): 216 | _EVENT_TYPE = EventType.ConfigChange 217 | _EVENT_ATTRIBUTE = "config_change_event" 218 | 219 | 220 | class DeviceStatusChangeEvent(Event): 221 | _EVENT_TYPE = EventType.DeviceStatusChange 222 | _EVENT_ATTRIBUTE = "device_status_change_event" 223 | 224 | def __init__(self, data): 225 | super().__init__(data) 226 | self._device = Device(data.device) 227 | self._last_status = DeviceStatusInfo(data.last_status) 228 | self._status = DeviceStatusInfo(data.status) 229 | 230 | @property 231 | def device(self): 232 | return self._device 233 | 234 | @property 235 | def last_status(self): 236 | return self._last_status 237 | 238 | @property 239 | def status(self): 240 | return self._status 241 | 242 | 243 | class DroppedFrameEvent(Event): 244 | _EVENT_TYPE = EventType.DroppedFrame 245 | _EVENT_ATTRIBUTE = "dropped_frame_event" 246 | 247 | 248 | class ImageEvent(Event): 249 | _EVENT_TYPE = EventType.Image 250 | _EVENT_ATTRIBUTE = "image_event" 251 | 252 | def __init__(self, data): 253 | super().__init__(data) 254 | self._images = data.image 255 | 256 | @property 257 | def image(self): 258 | return [Image(self._images[0]), Image(self._images[1])] 259 | 260 | 261 | class PointMappingChangeEvent(Event): 262 | _EVENT_TYPE = EventType.PointMappingChange 263 | _EVENT_ATTRIBUTE = "point_mapping_change_event" 264 | 265 | 266 | class TrackingModeEvent(Event): 267 | _EVENT_TYPE = EventType.TrackingMode 268 | _EVENT_ATTRIBUTE = "tracking_mode_event" 269 | 270 | def __init__(self, data): 271 | super().__init__(data) 272 | self._tracking_mode = TrackingMode(data.current_tracking_mode) 273 | 274 | @property 275 | def current_tracking_mode(self): 276 | return self._tracking_mode 277 | 278 | 279 | class LogEvents(Event): 280 | _EVENT_TYPE = EventType.LogEvents 281 | _EVENT_ATTRIBUTE = "log_events" 282 | 283 | 284 | class HeadPoseEvent(Event): 285 | _EVENT_TYPE = EventType.HeadPose 286 | _EVENT_ATTRIBUTE = "head_pose_event" 287 | 288 | 289 | class EyesEvent(Event): 290 | _EVENT_TYPE = EventType.Eyes 291 | _EVENT_ATTRIBUTE = "eye_event" 292 | 293 | 294 | class IMUEvent(Event): 295 | _EVENT_TYPE = EventType.IMU 296 | _EVENT_ATTRIBUTE = "imu_event" 297 | 298 | def __init__(self, data): 299 | super().__init__(data) 300 | self._timestamp = data.timestamp 301 | self._timestamp_hardware = data.timestamp_hw 302 | self._flags = data.flags 303 | self._accelerometer = data.accelerometer 304 | self._gyroscope = data.gyroscope 305 | self._temperature = data.temperature 306 | 307 | @property 308 | def timestamp(self): 309 | return self._timestamp 310 | 311 | @property 312 | def timestamp_hardware(self): 313 | return self._timestamp_hardware 314 | 315 | @property 316 | def flags(self): 317 | return get_enum_entries(IMUFlag, self._flags) 318 | 319 | @property 320 | def acceleration(self): 321 | return Vector(self._accelerometer) 322 | 323 | @property 324 | def angular_velocity(self): 325 | return Vector(self._gyroscope) 326 | 327 | @property 328 | def temperature(self): 329 | return self._temperature 330 | 331 | 332 | def create_event(data): 333 | """Create an Event from `LEAP_CONNECTION_MESSAGE*` cdata""" 334 | events = { 335 | EventType.EventTypeNone: NoneEvent, 336 | EventType.Connection: ConnectionEvent, 337 | EventType.ConnectionLost: ConnectionLostEvent, 338 | EventType.Device: DeviceEvent, 339 | EventType.DeviceFailure: DeviceFailureEvent, 340 | EventType.Policy: PolicyEvent, 341 | EventType.Tracking: TrackingEvent, 342 | EventType.ImageRequestError: ImageRequestErrorEvent, 343 | EventType.ImageComplete: ImageCompleteEvent, 344 | EventType.LogEvent: LogEvent, 345 | EventType.DeviceLost: DeviceLostEvent, 346 | EventType.ConfigResponse: ConfigResponseEvent, 347 | EventType.ConfigChange: ConfigChangeEvent, 348 | EventType.DeviceStatusChange: DeviceStatusChangeEvent, 349 | EventType.DroppedFrame: DroppedFrameEvent, 350 | EventType.Image: ImageEvent, 351 | EventType.PointMappingChange: PointMappingChangeEvent, 352 | EventType.TrackingMode: TrackingModeEvent, 353 | EventType.LogEvents: LogEvents, 354 | EventType.HeadPose: HeadPoseEvent, 355 | EventType.Eyes: EyesEvent, 356 | EventType.IMU: IMUEvent, 357 | } 358 | return events[EventType(data.type)].from_connection_message(data) 359 | -------------------------------------------------------------------------------- /leapc-python-api/src/leap/exceptions.py: -------------------------------------------------------------------------------- 1 | """Exceptions which are available in the LeapC API""" 2 | 3 | from .enums import RS as LeapRS 4 | 5 | 6 | class LeapError(Exception): 7 | pass 8 | 9 | 10 | class LeapConnectionAlreadyOpen(LeapError): 11 | pass 12 | 13 | 14 | # All following Exceptions are translated from the LeapRS enum 15 | 16 | 17 | class LeapUnknownError(LeapError): 18 | pass 19 | 20 | 21 | class LeapInvalidArgumentError(LeapError): 22 | pass 23 | 24 | 25 | class LeapInsufficientResourcesError(LeapError): 26 | pass 27 | 28 | 29 | class LeapInsufficientBufferError(LeapError): 30 | pass 31 | 32 | 33 | class LeapTimeoutError(LeapError): 34 | pass 35 | 36 | 37 | class LeapNotConnectedError(LeapError): 38 | pass 39 | 40 | 41 | class LeapHandshakeIncompleteError(LeapError): 42 | pass 43 | 44 | 45 | class LeapBufferSizeOverflowError(LeapError): 46 | pass 47 | 48 | 49 | class LeapProtocolError(LeapError): 50 | pass 51 | 52 | 53 | class LeapInvalidClientIDError(LeapError): 54 | pass 55 | 56 | 57 | class LeapUnexpectedClosedError(LeapError): 58 | pass 59 | 60 | 61 | class LeapUnknownImageFrameRequestError(LeapError): 62 | pass 63 | 64 | 65 | class LeapRoutineIsNotSeerError(LeapError): 66 | pass 67 | 68 | 69 | class LeapTimestampTooEarlyError(LeapError): 70 | pass 71 | 72 | 73 | class LeapConcurrentPollError(LeapError): 74 | pass 75 | 76 | 77 | class LeapNotAvailableError(LeapError): 78 | pass 79 | 80 | 81 | class LeapNotStreamingError(LeapError): 82 | pass 83 | 84 | 85 | class LeapCannotOpenDeviceError(LeapError): 86 | pass 87 | 88 | 89 | def create_exception(result: LeapRS, *args, **kwargs): 90 | """Create an exception from a LeapRS object 91 | 92 | Extra args and kwargs are forwarded to the Exception constructor. 93 | 94 | :param result: The result to create an Exception from 95 | """ 96 | if result == LeapRS.Success: 97 | raise ValueError("Success is not an Error") 98 | 99 | _ERRORS = { 100 | LeapRS.UnknownError: LeapUnknownError, 101 | LeapRS.InvalidArgument: LeapInvalidArgumentError, 102 | LeapRS.InsufficientResources: LeapInsufficientResourcesError, 103 | LeapRS.InsufficientBuffer: LeapInsufficientBufferError, 104 | LeapRS.Timeout: LeapTimeoutError, 105 | LeapRS.NotConnected: LeapNotConnectedError, 106 | LeapRS.HandshakeIncomplete: LeapHandshakeIncompleteError, 107 | LeapRS.BufferSizeOverflow: LeapBufferSizeOverflowError, 108 | LeapRS.ProtocolError: LeapProtocolError, 109 | LeapRS.InvalidClientID: LeapInvalidClientIDError, 110 | LeapRS.UnexpectedClosed: LeapUnexpectedClosedError, 111 | LeapRS.UnknownImageFrameRequest: LeapUnknownImageFrameRequestError, 112 | LeapRS.RoutineIsNotSeer: LeapRoutineIsNotSeerError, 113 | LeapRS.TimestampTooEarly: LeapTimestampTooEarlyError, 114 | LeapRS.ConcurrentPoll: LeapConcurrentPollError, 115 | LeapRS.NotAvailable: LeapNotAvailableError, 116 | LeapRS.NotStreaming: LeapNotStreamingError, 117 | LeapRS.CannotOpenDevice: LeapCannotOpenDeviceError, 118 | } 119 | 120 | return _ERRORS[result](args, kwargs) 121 | 122 | 123 | def success_or_raise(func, *args): 124 | """Call the function with the args, and raise an exception if the result is not success 125 | 126 | The function must be a LeapC cffi function which returns a LeapRS object. 127 | """ 128 | result = LeapRS(func(*args)) 129 | if result != LeapRS.Success: 130 | raise create_exception(result) 131 | -------------------------------------------------------------------------------- /leapc-python-api/src/leap/functions.py: -------------------------------------------------------------------------------- 1 | """Wrap around LeapC functions""" 2 | import leap.enums 3 | 4 | from .enums import PerspectiveType 5 | from .connection import Connection 6 | from .exceptions import success_or_raise 7 | from leapc_cffi import ffi, libleapc 8 | 9 | from typing import Optional, List, Dict 10 | 11 | 12 | def get_now() -> int: 13 | """Get the current time""" 14 | return libleapc.LeapGetNow() 15 | 16 | 17 | def get_server_status(timeout: float) -> Dict[str, Optional[List[Dict[str, str]]]]: 18 | server_status_pp = ffi.new("LEAP_SERVER_STATUS**") 19 | success_or_raise(libleapc.LeapGetServerStatus, timeout, server_status_pp) 20 | 21 | try: 22 | result = { 23 | "version": ffi.string(server_status_pp[0].version).decode("utf-8"), 24 | "devices": [], 25 | } 26 | 27 | for i in range(server_status_pp[0].device_count): 28 | result["devices"].append( 29 | { 30 | "serial": ffi.string(server_status_pp[0].devices[i].serial).decode("utf-8"), 31 | "type": ffi.string(server_status_pp[0].devices[i].type).decode("utf-8"), 32 | } 33 | ) 34 | 35 | finally: 36 | libleapc.LeapReleaseServerStatus(server_status_pp[0]) 37 | 38 | return result 39 | 40 | 41 | def get_frame_size( 42 | connection: Connection, target_frame_time: ffi.CData, target_frame_size: ffi.CData 43 | ) -> None: 44 | success_or_raise( 45 | libleapc.LeapGetFrameSize, 46 | connection.get_connection_ptr(), 47 | target_frame_time[0], 48 | target_frame_size, 49 | ) 50 | 51 | 52 | def interpolate_frame( 53 | connection: Connection, 54 | target_frame_time: ffi.CData, 55 | frame_ptr: ffi.CData, 56 | frame_size: ffi.CData, 57 | ) -> None: 58 | success_or_raise( 59 | libleapc.LeapInterpolateFrame, 60 | connection.get_connection_ptr(), 61 | target_frame_time, 62 | frame_ptr, 63 | frame_size, 64 | ) 65 | 66 | 67 | def get_extrinsic_matrix(connection: Connection, camera: PerspectiveType) -> ffi.CData: 68 | matrix = ffi.new("float[]", 16) 69 | libleapc.LeapExtrinsicCameraMatrix(connection.get_connection_ptr(), camera.value, matrix) 70 | return matrix 71 | -------------------------------------------------------------------------------- /leapc-python-api/src/leap/recording.py: -------------------------------------------------------------------------------- 1 | from leapc_cffi import libleapc, ffi 2 | 3 | from .enums import RecordingFlags 4 | from .event_listener import Listener 5 | from .events import TrackingEvent 6 | from .exceptions import success_or_raise, LeapUnknownError 7 | 8 | 9 | class Recording: 10 | def __init__(self, fpath, mode="r"): 11 | self._fpath = ffi.new("char[]", fpath.encode("utf-8")) 12 | self._recording_ptr = ffi.new("LEAP_RECORDING*") 13 | self._recording_params_ptr = ffi.new("LEAP_RECORDING_PARAMETERS*") 14 | self._recording_params_ptr.mode = self._parse_mode(mode) 15 | self._read_buffer = ffi.new("uint8_t*", 0) 16 | 17 | def __enter__(self): 18 | success_or_raise( 19 | libleapc.LeapRecordingOpen, 20 | self._recording_ptr, 21 | self._fpath, 22 | self._recording_params_ptr[0], 23 | ) 24 | return self 25 | 26 | def __exit__(self, exc_type, exc_val, exc_tb): 27 | success_or_raise(libleapc.LeapRecordingClose, self._recording_ptr) 28 | 29 | def write(self, frame): 30 | """Write a frame of tracking data to the recording""" 31 | bytes_written = ffi.new("uint64_t*") 32 | success_or_raise( 33 | libleapc.LeapRecordingWrite, 34 | self._recording_ptr[0], 35 | frame._data, 36 | bytes_written, 37 | ) 38 | 39 | def __iter__(self): 40 | return self 41 | 42 | def __next__(self): 43 | return self.read_frame() 44 | 45 | def read(self): 46 | """Read the recording 47 | 48 | Returns a list of TrackingEvents in the recording. 49 | """ 50 | return list(self) 51 | 52 | def read_frame(self): 53 | frame_size = ffi.new("uint64_t*") 54 | try: 55 | success_or_raise(libleapc.LeapRecordingReadSize, self._recording_ptr[0], frame_size) 56 | except LeapUnknownError: 57 | # When the recording has finished reading, an "UnknownError" is 58 | # returned from the LeapC API. 59 | raise StopIteration 60 | 61 | frame_data = self._FrameData(frame_size[0]) 62 | 63 | success_or_raise( 64 | libleapc.LeapRecordingRead, 65 | self._recording_ptr[0], 66 | frame_data.buffer_ptr(), 67 | frame_size[0], 68 | ) 69 | return TrackingEvent(frame_data) 70 | 71 | def status(self): 72 | """Get the current recording status 73 | 74 | Return a string, which may contain or omit the following characters: 75 | 'rwfc' 76 | 'r': Reading 77 | 'w': Writing 78 | 'f': Flushing 79 | 'c': Compressed 80 | 81 | Raises a RuntimeError if the recording is an invalid state. 82 | """ 83 | recording_status = ffi.new("LEAP_RECORDING_STATUS*") 84 | success_or_raise(libleapc.LeapRecordingGetStatus, self._recording_ptr[0], recording_status) 85 | 86 | flags = recording_status.mode 87 | mode = "" 88 | 89 | if flags & RecordingFlags.Reading.value: 90 | mode += "r" 91 | if flags & RecordingFlags.Writing.value: 92 | mode += "w" 93 | if flags & RecordingFlags.Flushing.value: 94 | mode += "f" 95 | if flags & RecordingFlags.Compressed.value: 96 | mode += "c" 97 | 98 | if len(mode) == 0: 99 | raise RuntimeError("Recording is in an invalid state") 100 | return mode 101 | 102 | @staticmethod 103 | def _parse_mode(mode): 104 | flags = RecordingFlags.Error.value 105 | if "r" in mode: 106 | flags |= RecordingFlags.Reading.value 107 | if "w" in mode: 108 | flags |= RecordingFlags.Writing.value 109 | if "c" in mode: 110 | flags |= RecordingFlags.Compressed.value 111 | return flags 112 | 113 | class _FrameData: 114 | """Wrapper which owns all the data required to read the Frame 115 | 116 | A LEAP_TRACKING_EVENT has a fixed size, but some fields are pointers to memory stored 117 | outside of the struct. This means the size required for all the information about a 118 | frame is larger than the size of the struct. 119 | 120 | This wrapper owns the buffer required for all of that data. Reading attributes or 121 | items from this wrapper returns the corresponding item or wrapper on the underlying 122 | LEAP_TRACKING_EVENT. 123 | 124 | It is intended to by used in the TrackingEvent constructor. 125 | """ 126 | 127 | def __init__(self, size): 128 | self._buffer = ffi.new("char[]", size) 129 | self._frame_ptr = ffi.cast("LEAP_TRACKING_EVENT*", self._buffer) 130 | 131 | def __getattr__(self, name): 132 | return getattr(self._frame_ptr, name) 133 | 134 | def __getitem__(self, key): 135 | return self._frame_ptr[key] 136 | 137 | def buffer_ptr(self): 138 | return self._frame_ptr 139 | 140 | 141 | class Recorder(Listener): 142 | def __init__(self, recording, *, auto_start=True): 143 | self._recording = recording 144 | self._running = auto_start 145 | 146 | def on_tracking_event(self, event): 147 | if self._running: 148 | self._recording.write(event) 149 | 150 | def start(self): 151 | self._running = True 152 | 153 | def stop(self): 154 | self._running = False 155 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 99 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | build 2 | cffi 3 | opencv-python 4 | numpy 5 | --------------------------------------------------------------------------------