├── .github └── workflows │ └── build-ubuntu-sdist.yml ├── LICENSE ├── README.md ├── examples ├── haptic_game.py ├── requirements.txt └── simple_callback.py ├── pyOpenHaptics ├── __init__.py ├── exceptions.py ├── hd.py ├── hd_callback.py ├── hd_define.py ├── hd_device.py └── hdu_matrix.py └── pyproject.toml /.github/workflows/build-ubuntu-sdist.yml: -------------------------------------------------------------------------------- 1 | # this workflow tests sdist builds and also doubles as a way to test that 2 | # pyHD compiles on all ubuntu LTS versions 3 | # the main difference between this and the manylinux builds is that this runs 4 | # directly under ubuntu and uses apt installed dependencies, while the 5 | # manylinux workflow runs with centos docker and self-compiled dependencies 6 | # IMPORTANT: binaries are not to be uploaded from this workflow! 7 | 8 | name: Ubuntu sdist 9 | 10 | # Run CI only when a release is created, on changes to main branch, or any PR 11 | # to main. Do not run CI on any other branch. Also, skip any non-source changes 12 | # from running on CI 13 | on: 14 | release: 15 | types: [created] 16 | push: 17 | branches: main 18 | paths-ignore: 19 | - 'examples/**' 20 | - '.gitignore' 21 | - '*.md' 22 | - '.github/workflows/*.yml' 23 | # re-include current file to not be excluded 24 | - '!.github/workflows/build-ubuntu-sdist.yml' 25 | 26 | pull_request: 27 | branches: 28 | - main 29 | - 'v**' 30 | paths-ignore: 31 | - 'examples/**' 32 | - '.gitignore' 33 | - '*.md' 34 | - '.github/workflows/*.yml' 35 | # re-include current file to not be excluded 36 | - '!.github/workflows/build-ubuntu-sdist.yml' 37 | 38 | jobs: 39 | build: 40 | runs-on: ${{ matrix.os }} 41 | strategy: 42 | fail-fast: false 43 | matrix: 44 | os: [ubuntu-20.04] 45 | python-version: [ "3.10" ] 46 | 47 | steps: 48 | - uses: actions/checkout@v3.0.2 49 | 50 | - name: Setup Python 51 | uses: actions/setup-python@v3 52 | with: 53 | python-version: ${{ matrix.python-version }} 54 | architecture: x64 55 | 56 | - name: Install deps 57 | run: | 58 | sudo apt-get update --fix-missing 59 | sudo apt-get upgrade 60 | sudo apt-get install libncurses5-dev qt5-default freeglut3 build-essential 61 | 62 | - name: Make sdist and install it 63 | env: 64 | PIP_CONFIG_FILE: "buildconfig/pip_config.ini" 65 | run: | 66 | python3 -m pip install --upgrade pip 67 | python3 -m pip install --upgrade build 68 | python3 -m build 69 | pip install dist/*.tar.gz -vv 70 | 71 | - name: Upload sdist 72 | if : matrix.os == 'ubuntu-20.04' && matrix.python-version == '3.10' 73 | uses: actions/upload-artifact@v3 74 | with: 75 | name: pyOpenHaptics-sdist 76 | path: dist/*.tar.gz 77 | 78 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Mikel De Iturrate Reyzabal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyOpenHaptics 2 | 3 | Python wrapper for the OpenHaptics HD library to use the haptic device directly from Python. Used to control the [3dSystems](https://www.3dsystems.com/) devices (Touch/Touch X) directly from Python. 4 | 5 | ## Prerequisites 6 | 7 | This library requires from the pre-installation of the OpenHaptics libraries and Touch X drivers. For this please follow their official installation tutorial for [Linux](https://support.3dsystems.com/s/article/OpenHaptics-for-Linux-Developer-Edition-v34?language=en_US) or [Windows](https://support.3dsystems.com/s/article/OpenHaptics-for-Windows-Developer-Edition-v35?language=en_US). 8 | 9 | OpenHaptics in Linux requires from additional libraries. You can use the following command to install them. 10 | 11 | ```shell 12 | sudo apt-get update && sudo apt-get upgrade 13 | sudo apt-get install libncurses5-dev freeglut3 build-essential 14 | ``` 15 | 16 | ## Installation 17 | 18 | Use the latest version from the [PyPi project](https://pypi.org/project/pyOpenHaptics/). 19 | 20 | ```shell 21 | python3 -m pip install pyOpenHaptics 22 | ``` 23 | 24 | ## How to use it 25 | 26 | The library contains multiple functionalities to get and set different variables. Most of the functionalities are gathered into 3 different files *hd.py*, *hd_callback.py* and *hd_device.py*. 27 | The first file contains a Python mimic of most of the OpenHaptics HD library main functions. The second file contains the schedulers and a python wrapper to wrap your python callback function into a C callback function to interact with the already compiled shared library. Lastly, this defines the Python class to initialize your Haptic hardware and interface. 28 | 29 | ## Haptic device template 30 | 31 | Here is a small template on how to setup your callback loop and your haptic device to gather the desired information from the device. 32 | 33 | ```python 34 | import pyOpenHaptics.hd as hd 35 | from pyOpenHaptics.hd_callback import hd_callback 36 | from pyOpenHaptics.hd_device import HapticDevice 37 | from dataclasses import dataclass 38 | 39 | # Data class to keep track of the device state and use it in other parts of the code 40 | @dataclass 41 | class DeviceState: 42 | # Define the variables you want to safe here 43 | 44 | # Callback to gather the device state 45 | @hd_callback 46 | def device_callback(): 47 | # Make the device_state global to be accesed in other parts of the code 48 | global device_state 49 | # your callback function, gather the different variables on the device 50 | # YOUR CODE HERE 51 | 52 | if __name__ == "__main__": 53 | # Initialize the data class 54 | device_state = DeviceState() 55 | 56 | # Initialize the haptic device and the callback loop 57 | device = HapticDevice(callback = device_callback, scheduler_type="async") 58 | 59 | # YOUR CODE HERE 60 | 61 | # Close the device to avoid segmentation faults 62 | device.close() 63 | ``` 64 | 65 | You can find more complex examples [here](examples) -------------------------------------------------------------------------------- /examples/haptic_game.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ pyOpenHaptics.example.haptic_game.py 3 | 4 | This example contains a small demo of a haptic game of a rectangle trapped in a confined space. 5 | The closer the red rectangle gets to the ends the more force will you feel in the device. 6 | This example is to show the capabilities of developing such applications in pure Python language 7 | using the pyOpenHaptics library. 8 | 9 | """ 10 | 11 | import pygame 12 | from pyOpenHaptics.hd_device import HapticDevice 13 | import pyOpenHaptics.hd as hd 14 | import time 15 | from dataclasses import dataclass, field 16 | from pyOpenHaptics.hd_callback import hd_callback 17 | 18 | @dataclass 19 | class DeviceState: 20 | button: bool = False 21 | position: list = field(default_factory=list) 22 | joints: list = field(default_factory=list) 23 | gimbals: list = field(default_factory=list) 24 | force: list = field(default_factory=list) 25 | 26 | @hd_callback 27 | def state_callback(): 28 | global device_state 29 | transform = hd.get_transform() 30 | joints = hd.get_joints() 31 | gimbals = hd.get_gimbals() 32 | device_state.position = [transform[3][0], -transform[3][1], transform[3][2]] 33 | device_state.joints = [joints[0], joints[1], joints[2]] 34 | device_state.gimbals = [gimbals[0], gimbals[1], gimbals[2]] 35 | hd.set_force(device_state.force) 36 | button = hd.get_buttons() 37 | device_state.button = True if button==1 else False 38 | 39 | def wall_feedback(big: pygame.Rect, small: pygame.Rect): 40 | device_state.force = [0, 0, 0] 41 | 42 | if small.y <= 80: 43 | device_state.force[1] = -0.05 * (80 - small.y) 44 | elif small.y + 60 >= 1000: 45 | device_state.force[1] = 0.05 * (small.y + 60 - 1000) 46 | 47 | if small.x <= 80: 48 | device_state.force[0] = 0.05 * (80 - small.x) 49 | elif small.x + 60 >= 1800: 50 | device_state.force[0] = -0.0275 * (small.x + 60 - 1800) 51 | 52 | 53 | def main(): 54 | pygame.init() 55 | 56 | surface = pygame.display.set_mode((1920, 1080)) 57 | 58 | # Initializing color 59 | red = (255, 0, 0) 60 | green = (0, 255, 0) 61 | 62 | pre_dev_x, pre_dev_y = device_state.position[0], device_state.position[1] 63 | 64 | # Drawing rectangle 65 | small_rectangle = pygame.Rect(surface.get_width() // 2 - 30, surface.get_height() - 50, 60, 60) 66 | big_rectangle = pygame.Rect(30, 30, surface.get_width() - 60, surface.get_height() - 60) 67 | pygame.draw.rect(surface, red, small_rectangle) 68 | pygame.draw.rect(surface, green, big_rectangle, 2) 69 | clock = pygame.time.Clock() 70 | run = True 71 | pygame.display.flip() 72 | 73 | while run: 74 | clock.tick(100) 75 | dev_x, dev_y = device_state.position[0], device_state.position[1] 76 | small_rectangle.move_ip(10 * (dev_x - pre_dev_x), 9 * (dev_y - pre_dev_y)) 77 | small_rectangle.move_ip(0, 0) 78 | small_rectangle.clamp_ip(big_rectangle) 79 | surface.fill((1, 1, 1)) 80 | pygame.draw.rect(surface, red, small_rectangle) 81 | pygame.draw.rect(surface, green, big_rectangle, 2) 82 | wall_feedback(big_rectangle, small_rectangle) 83 | for event in pygame.event.get(): 84 | if event.type == pygame.KEYDOWN: 85 | if event.key == pygame.K_ESCAPE: 86 | run = False 87 | pygame.display.flip() 88 | pre_dev_x, pre_dev_y = dev_x, dev_y 89 | 90 | if __name__ == "__main__": 91 | device_state = DeviceState() 92 | device = HapticDevice(device_name="Default Device", callback=state_callback) 93 | time.sleep(0.2) 94 | main() 95 | device.close() -------------------------------------------------------------------------------- /examples/requirements.txt: -------------------------------------------------------------------------------- 1 | pygame -------------------------------------------------------------------------------- /examples/simple_callback.py: -------------------------------------------------------------------------------- 1 | """pyOpenHaptics.examples.haptic_device.py 2 | Simple example on how to write your custom haptic device class and callback function for any type. 3 | In this case we are just getting the value of the button on the Touch X to change an ASCII drawing. 4 | It only updates the drawing if the state of the button changes. 5 | """ 6 | 7 | from pyOpenHaptics.hd_device import HapticDevice 8 | import pyOpenHaptics.hd as hd 9 | import time 10 | from dataclasses import dataclass 11 | from pyOpenHaptics.hd_callback import hd_callback 12 | 13 | @dataclass 14 | class DeviceState: 15 | """Class to store the state of the device""" 16 | # Device ID 17 | device_id: int = 0 18 | # Button state 19 | button: bool = False 20 | 21 | @hd_callback 22 | def button_callback(): 23 | global device_state 24 | # Set the device to the current device 25 | hd.make_current_device(device_state.device_id) 26 | button = hd.get_buttons() 27 | device_state.button = True if button==1 else False 28 | 29 | 30 | if __name__ == "__main__": 31 | device_state = DeviceState() 32 | device = HapticDevice(callback = button_callback, device_name = "Default Device", scheduler_type = "async") 33 | pre_button = device_state.button 34 | for i in range(10000): 35 | if pre_button != device_state.button: 36 | if device_state.button: 37 | print("⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠛⠛⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿\n⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠋⠉⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿\n⣿⣿⣿⣿⣿⣿⠟⠋⣠⣿⣦⣄⣀⣀⣀⡀⠀⠀⠀⠀⠙⣷⠙⠻⣿⣿⣿⣿⣿⣿\n⣿⣿⣿⣿⡿⠁⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣤⣤⣴⣾⣿⣿⣦⠈⢿⣿⣿⣿⣿\n⣿⣿⣿⡟⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⢻⣿⣿⣿\n⣿⣿⣿⢁⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡈⣿⣿⣿\n⣿⣿⡇⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⢸⣿⣿\n⣿⣿⡇⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⢸⣿⣿\n⣿⣿⡇⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⢸⣿⣿\n⣿⣿⣿⡈⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠿⠿⠥⣿⣿⣿\n⣿⣿⣿⣿⠋⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠋⠁⠤⠤⠤⠄⢀⠀⣿⣿\n⣿⣿⣿⠁⠀⠀⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⢰⣶⣶⣶⡆⠀⣿⡄⢸⣿\n⣿⣿⡇⠀⠀⠀⠀⣿⠙⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿⡇⢸⣿⣿⣿⡿⠀⣿⠀⣼⣿\n⣿⣿⣿⣤⣀⣠⣾⣿⣿⣶⣦⣬⣉⣉⣉⣉⣉⣉⣽⣇⣈⣉⣉⣉⣁⣀⣤⣶⣿⣿\n⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿") 38 | else: 39 | print("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣤⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣶⣿⣿⣿⣿⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⠀⠀⣠⣴⠟⠀⠙⠻⠿⠿⠿⢿⣿⣿⣿⣿⣦⠈⣦⣄⠀⠀⠀⠀⠀⠀\n⠀⠀⠀⠀⢀⣾⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠛⠋⠁⠀⠀⠙⣷⡀⠀⠀⠀⠀\n⠀⠀⠀⢠⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢿⡄⠀⠀⠀\n⠀⠀⠀⡾⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢷⠀⠀⠀\n⠀⠀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀\n⠀⠀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀\n⠀⠀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀\n⠀⠀⠀⢷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣀⣚⠀⠀⠀\n⠀⠀⠀⠀⣴⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣾⣛⣛⣛⣻⡿⣿⠀⠀\n⠀⠀⠀⣾⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡏⠉⠉⠉⢹⣿⠀⢻⡇⠀\n⠀⠀⢸⣿⣿⣿⣿⠀⣦⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⠀⢀⣿⠀⣿⠃⠀\n⠀⠀⠀⠛⠿⠟⠁⠀⠀⠉⠙⠓⠶⠶⠶⠶⠶⠶⠂⠸⠷⠶⠶⠶⠾⠿⠛⠉⠀⠀\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀") 40 | pre_button = device_state.button 41 | time.sleep(0.001) 42 | device.close() -------------------------------------------------------------------------------- /pyOpenHaptics/__init__.py: -------------------------------------------------------------------------------- 1 | from .hd import * 2 | -------------------------------------------------------------------------------- /pyOpenHaptics/exceptions.py: -------------------------------------------------------------------------------- 1 | class DeviceInitException(Exception): 2 | "Raised when the device is not initialized properly" 3 | pass 4 | 5 | class InvalidEnumException(Exception): 6 | "Raised when there is a problem with the enum" 7 | pass 8 | 9 | class InvalidValueException(Exception): 10 | "Raised when there is an invalid value on a function" 11 | pass 12 | 13 | class InvalidOperationException(Exception): 14 | "Raised when you perform an invalid operation" 15 | pass 16 | 17 | class InvalidInputTypeException(Exception): 18 | "Raised when the input of a function is invalid" 19 | pass 20 | 21 | class ForceTypeExceptions(Exception): 22 | "Raised when there is a problem with the force" 23 | pass -------------------------------------------------------------------------------- /pyOpenHaptics/hd.py: -------------------------------------------------------------------------------- 1 | from ctypes import * 2 | from .hd_define import * 3 | from typing import List 4 | from .hdu_matrix import hduMatrix, hduVector3Dd 5 | from sys import platform 6 | from .exceptions import * 7 | 8 | exception_dict = { 9 | HD_BAD_HANDLE: DeviceInitException, 10 | HD_INVALID_ENUM: InvalidEnumException, 11 | HD_INVALID_OPERATION: InvalidOperationException, 12 | HD_INVALID_HANDLE: DeviceInitException, 13 | HD_FORCE_ERROR: ForceTypeExceptions, 14 | } 15 | 16 | if platform == "linux" or platform == "linux2": 17 | _lib_hd = CDLL("libHD.so") 18 | elif platform == "win32": 19 | _lib_hd = CDLL("HD.dll") 20 | 21 | def _get_doublev(code: int, dtype): 22 | data = dtype() 23 | _lib_hd.hdGetDoublev.argtypes = [HDenum, POINTER(dtype)] 24 | _lib_hd.hdGetDoublev.restype = None 25 | _lib_hd.hdGetDoublev(code, data) 26 | return data 27 | 28 | def _get_integerv(code: int, dtype): 29 | data = dtype() 30 | _lib_hd.hdGetIntegerv.argtypes = [HDenum, POINTER(dtype)] 31 | _lib_hd.hdGetIntegerv.restype = None 32 | _lib_hd.hdGetIntegerv(code, data) 33 | return data 34 | 35 | def _set_doublev(code: int, value, dtype): 36 | data = dtype(*value) 37 | _lib_hd.hdSetDoublev.argtypes = [HDenum, POINTER(dtype)] 38 | _lib_hd.hdSetDoublev.restype = None 39 | _lib_hd.hdSetDoublev(code, data) 40 | 41 | def _get_error() -> HDErrorInfo: 42 | _lib_hd.hdGetError.restype = HDErrorInfo 43 | return _lib_hd.hdGetError() 44 | 45 | 46 | def init_device(name: str = "Default Device") -> int: 47 | _lib_hd.hdInitDevice.argtypes = [c_char_p] 48 | _lib_hd.hdInitDevice.restype = HHD 49 | try: 50 | id = _lib_hd.hdInitDevice(name.encode()) 51 | if id == HD_BAD_HANDLE: 52 | raise DeviceInitException 53 | else: 54 | return id 55 | except DeviceInitException: 56 | print("Unable to initialize the device. Check the connection!") 57 | 58 | def get_buttons() -> int: 59 | return _get_integerv(HD_CURRENT_BUTTONS, HDint).value 60 | 61 | def get_transform() -> hduMatrix: 62 | return _get_doublev(HD_CURRENT_TRANSFORM, hduMatrix) 63 | 64 | def get_joints() -> hduVector3Dd: 65 | return _get_doublev(HD_CURRENT_JOINT_ANGLES, hduVector3Dd) 66 | 67 | def get_gimbals() -> hduVector3Dd: 68 | return _get_doublev(HD_CURRENT_GIMBAL_ANGLES, hduVector3Dd) 69 | 70 | def get_error() -> bool: 71 | error = _get_error().errorCode 72 | try: 73 | if error == HD_SUCCESS: 74 | return False 75 | else: 76 | raise exception_dict[error] 77 | except exception_dict[error] as e: 78 | print("{}: Error during the main scheduler.".format(type(e).__name__)) 79 | return True 80 | 81 | def set_force(feedback: List[float]) -> None: 82 | _set_doublev(HD_CURRENT_FORCE, feedback, hduVector3Dd) 83 | 84 | 85 | def close_device(id: int): 86 | _lib_hd.hdDisableDevice.argtypes = [HHD] 87 | _lib_hd.hdDisableDevice(id) 88 | 89 | def get_current_device() -> HHD: 90 | _lib_hd.hdGetCurrentDevice.restype = HHD 91 | return _lib_hd.hdGetCurrentDevice() 92 | 93 | def make_current_device(id: int) -> None: 94 | _lib_hd.hdMakeCurrentDevice.argtypes = [HHD] 95 | _lib_hd.hdMakeCurrentDevice(id) 96 | 97 | def start_scheduler() -> None: 98 | _lib_hd.hdStartScheduler() 99 | 100 | def stop_scheduler() -> None: 101 | _lib_hd.hdStopScheduler() 102 | 103 | def enable_force() -> None: 104 | _lib_hd.hdEnable.argtypes = [HDenum] 105 | _lib_hd.hdEnable.restype = None 106 | _lib_hd.hdEnable(HD_FORCE_OUTPUT) 107 | print("Force feedback enabled!") 108 | 109 | def begin_frame(id: int) -> None: 110 | _lib_hd.hdBeginFrame.argtypes = [HHD] 111 | _lib_hd.hdBeginFrame.restype = None 112 | _lib_hd.hdBeginFrame(id) 113 | 114 | def end_frame(id: int) -> None: 115 | _lib_hd.hdEndFrame.argtypes = [HHD] 116 | _lib_hd.hdEndFrame.restype = None 117 | _lib_hd.hdEndFrame(id) 118 | 119 | def get_model() -> str: 120 | _lib_hd.hdGetString.argtypes = [HDenum] 121 | _lib_hd.hdGetString.restype = HDstring 122 | return _lib_hd.hdGetString(HD_DEVICE_MODEL_TYPE).decode() 123 | 124 | def get_vendor() -> str: 125 | _lib_hd.hdGetString.argtypes = [HDenum] 126 | _lib_hd.hdGetString.restype = HDstring 127 | return _lib_hd.hdGetString(HD_DEVICE_VENDOR).decode() 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /pyOpenHaptics/hd_callback.py: -------------------------------------------------------------------------------- 1 | from ctypes import * 2 | from .hd_define import * 3 | import functools 4 | from .hd import * 5 | from sys import platform 6 | 7 | if platform == "linux" or platform == "linux2": 8 | _lib_hd = CDLL("libHD.so") 9 | elif platform == "win32": 10 | _lib_hd = CDLL("HD.dll") 11 | 12 | def hd_callback(input_function): 13 | @functools.wraps(input_function) 14 | @CFUNCTYPE(HDCallbackCode, POINTER(c_void_p)) 15 | def _callback(pUserData): 16 | """Callback function for the haptic device. 17 | This function is called by the haptic device when it is ready to process input. 18 | It calls the input_function passed as an argument and checks for errors. 19 | """ 20 | begin_frame(get_current_device()) 21 | input_function() 22 | end_frame(get_current_device()) 23 | if(get_error()): 24 | return HD_CALLBACK_DONE 25 | return HD_CALLBACK_CONTINUE 26 | 27 | return _callback 28 | 29 | def hdAsyncSheduler(callback): 30 | pUserData = c_void_p() 31 | _lib_hd.hdScheduleAsynchronous(callback, byref(pUserData), HD_MAX_SCHEDULER_PRIORITY) 32 | 33 | def hdSyncSheduler(callback): 34 | pUserData = c_void_p() 35 | _lib_hd.hdScheduleSynchronous(callback, byref(pUserData), HD_MAX_SCHEDULER_PRIORITY) -------------------------------------------------------------------------------- /pyOpenHaptics/hd_define.py: -------------------------------------------------------------------------------- 1 | from ctypes import * 2 | 3 | # Defining base HD data types 4 | HDint = c_int 5 | HDuint = c_uint 6 | HDboolean = c_bool 7 | HDulong = c_ulong 8 | HDfloat = c_float 9 | HDdouble = c_double 10 | HDlong = c_long 11 | HDchar = c_char 12 | HDerror = c_uint 13 | HDenum = c_uint 14 | HDstring = c_char_p 15 | HHD = c_uint 16 | HDCallbackCode = c_uint 17 | class HDErrorInfo(Structure): 18 | _fields_ = [ 19 | ("errorCode", HDerror), 20 | ("internalErrorCode", c_int), 21 | ("hHD", HHD) 22 | ] 23 | 24 | # Callback 25 | HD_CALLBACK_DONE = 0 26 | HD_CALLBACK_CONTINUE = 1 27 | 28 | # Boolean 29 | HD_TRUE = 1 30 | HD_FALSE = 0 31 | 32 | # Version information 33 | HD_VERSION_MAJOR_NUMBER = 3 34 | HD_VERSION_MINOR_NUMBER = 30 35 | HD_VERSION_BUILD_NUMBER = 0 36 | 37 | ############################################ 38 | ############## ERROR CODES ################# 39 | ############################################ 40 | HD_SUCCESS = 0x000 41 | #Function errors 42 | HD_INVALID_ENUM = 0x0100 43 | HD_INVALID_VALUE = 0x0101 44 | HD_INVALID_OPERATION = 0x0103 45 | HD_BAD_HANDLE = 0x0104 46 | # Force errors 47 | HD_WARM_MOTORS = 0x0200 48 | HD_EXCEED_MAX_FORCE = 0x0201 49 | HD_EXCEEDED_MAX_FORCE_IMPULSE = 0x0202 50 | HD_EXCEEDED_MAX_VELOCITY = 0x0203 51 | HD_FORCE_ERROR = 0x0204 52 | # Device errors 53 | HD_DEVICE_FAULT = 0x0300 54 | HD_DEVICE_ALREADY_INITIATED = 0x0301 55 | HD_COMM_ERROR = 0x0302 56 | HD_COMM_CONFIG_ERROR = 0x0303 57 | HD_TIMER_ERROR = 0x0304 58 | # Haptic rendering context 59 | HD_ILLEGAL_BEGIN = 0x0400 60 | HD_ILLEGAL_END = 0x0401 61 | HD_FRAME_ERROR = 0x0402 62 | # Scheduler errors 63 | HD_INVALID_PRIORITY = 0x0500 64 | HD_SCHEDULER_FULL = 0x0501 65 | # Licensing errors 66 | HD_INVALID_LICENSE = 0x0600 67 | 68 | ############################################ 69 | ############# GET PARAMETERS ############### 70 | ############################################ 71 | # Raw values 72 | HD_CURRENT_BUTTONS = 0x2000 73 | HD_CURRENT_SAFETY_SWITCH = 0x2001 74 | HD_CURRENT_INKWELL_SWITCH = 0x2002 75 | HD_CURRENT_ENCODER_VALUES = 0x2010 76 | HD_CURRENT_PINCH_VALUE = 0x2011 77 | HD_LAST_PINCH_VALUE = 0x2012 78 | # Cartesian space values 79 | HD_CURRENT_POSITION = 0x2050 80 | HD_CURRENT_VELOCITY = 0x2051 81 | HD_CURRENT_TRANSFORM = 0x2052 82 | HD_CURRENT_ANGULAR_VELOCITY = 0x2053 83 | HD_CURRENT_JACOBIAN = 0x2054 84 | # Joint space values 85 | HD_CURRENT_JOINT_ANGLES = 0x2100 86 | HD_CURRENT_GIMBAL_ANGLES = 0x2150 87 | HD_LAST_BUTTONS = 0x2200 88 | HD_LAST_SAFETY_SWITCH = 0x2201 89 | HD_LAST_INKWELL_SWITCH = 0x2202 90 | HD_LAST_ENCODER_VALUES = 0x2210 91 | HD_LAST_POSITION = 0x2250 92 | HD_LAST_VELOCITY = 0x2251 93 | HD_LAST_TRANSFORM = 0x2252 94 | HD_LAST_ANGULAR_VELOCITY = 0x2253 95 | HD_LAST_JACOBIAN = 0x2254 96 | HD_LAST_JOINT_ANGLES = 0x2300 97 | HD_LAST_GIMBAL_ANGLES = 0x2350 98 | # Identification 99 | HD_VERSION = 0x2500 100 | HD_DEVICE_MODEL_TYPE = 0x2501 101 | HD_DEVICE_DRIVER_VERSION = 0x2502 102 | HD_DEVICE_VENDOR = 0x2503 103 | HD_DEVICE_SERIAL_NUMBER = 0x2504 104 | HD_DEVICE_FIRMWARE_VERSION = 0x2505 105 | # Device hardware properties 106 | HD_MAX_WORKSPACE_DIMENSIONS = 0x2550 107 | HD_USABLE_WORKSPACE_DIMENSIONS = 0x2551 108 | HD_TABLETOP_OFFSET = 0x2552 109 | HD_INPUT_DOF = 0x2553 110 | HD_OUTPUT_DOF = 0x2554 111 | HD_CALIBRATION_STYLE = 0x2555 112 | # Device forces and measurements 113 | HD_UPDATE_RATE = 0x2600 114 | HD_INSTANTANEOUS_UPDATE_RATE = 0x2601 115 | HD_NOMINAL_MAX_STIFFNESS = 0x2602 116 | HD_NOMINAL_MAX_DAMPING = 0x2609 117 | HD_NOMINAL_MAX_FORCE = 0x2603 118 | HD_NOMINAL_MAX_CONTINUOUS_FORCE = 0x2604 119 | HD_MOTOR_TEMPERATURE = 0x2605 120 | HD_SOFTWARE_VELOCITY_LIMIT = 0x2606 121 | HD_SOFTWARE_FORCE_IMPULSE_LIMIT = 0x2607 122 | HD_FORCE_RAMPING_RATE = 0x2608 123 | HD_NOMINAL_MAX_TORQUE_STIFFNESS = 0x2620 124 | HD_NOMINAL_MAX_TORQUE_DAMPING = 0x2621 125 | HD_NOMINAL_MAX_TORQUE_FORCE = 0x2622 126 | HD_NOMINAL_MAX_TORQUE_CONTINUOUS_FORCE = 0x2623 127 | # Cartesian space values 128 | HD_CURRENT_FORCE = 0x2700 129 | HD_CURRENT_TORQUE = 0x2701 130 | HD_JOINT_ANGLE_REFERENCES = 0x2702 131 | # Joint space values 132 | HD_CURRENT_JOINT_TORQUE = 0x2703 133 | HD_CURRENT_GIMBAL_TORQUE = 0x2704 134 | # Motor space values 135 | HD_LAST_FORCE = 0x2800 136 | HD_LAST_TORQUE = 0x2801 137 | HD_LAST_JOINT_TORQUE = 0x2802 138 | HD_LAST_GIMBAL_TORQUE = 0x2803 139 | # LED status light 140 | HD_USER_STATUS_LIGHT = 0x2900 141 | 142 | ############################################ 143 | ############# SET PARAMETERS ############### 144 | ############################################ 145 | # Enable/Disable capabilities 146 | HD_FORCE_OUTPUT = 0x4000 147 | HD_MAX_FORCE_CLAMPING = 0x4001 148 | HD_FORCE_RAMPING = 0x4002 149 | HD_SOFTWARE_FORCE_LIMIT = 0x4003 150 | HD_ONE_FRAME_LIMIT = 0x4004 151 | 152 | ############################################ 153 | ############## MISCELLANEOUS ############### 154 | ############################################ 155 | # Scheduler priority ranges 156 | HD_MAX_SCHEDULER_PRIORITY = (1 << 16) - 1 157 | HD_MIN_SCHEDULER_PRIORITY = 0 158 | HD_DEFAULT_SCHEDULER_PRIORITY = ((HD_MAX_SCHEDULER_PRIORITY + HD_MIN_SCHEDULER_PRIORITY)/2) 159 | # Calibration return values 160 | HD_CALIBRATION_OK = 0x5000 161 | HD_CALIBRATION_NEEDS_UPDATE = 0x5001 162 | HD_CALIBRATION_NEEDS_MANUAL_INPUT = 0x5002 163 | # Calibration styles 164 | HD_CALIBRATION_ENCODER_RESET = (1 << 0) 165 | HD_CALIBRATION_AUTO = (1 << 1) 166 | HD_CALIBRATION_INKWELL = (1 << 2) 167 | # Button Masks 168 | HD_DEVICE_BUTTON_1 = (1 << 0) 169 | HD_DEVICE_BUTTON_2 = (1 << 1) 170 | HD_DEVICE_BUTTON_3 = (1 << 2) 171 | HD_DEVICE_BUTTON_4 = (1 << 3) 172 | # Null device handle 173 | HD_INVALID_HANDLE = 0xFFFFFFFF 174 | # Used by device initialization 175 | HD_DEFAULT_DEVICE = None 176 | # LED status light states 177 | LED_MASK = 0x07 178 | LED_STATUS_FAST_GRNYEL = 0x00 179 | LED_STATUS_SLOW_YEL = 0x01 180 | LED_STATUS_SLOW_GRN = 0x02 181 | LED_STATUS_FAST_GRN = 0x03 182 | LED_STATUS_SOLID_GRNYEL = 0x04 183 | LED_STATUS_SOLID_YEL = 0x05 184 | LED_STATUS_SOLID_GRN = 0x06 185 | LED_STATUS_FAST_YEL = 0x07 -------------------------------------------------------------------------------- /pyOpenHaptics/hd_device.py: -------------------------------------------------------------------------------- 1 | from .hd import * 2 | from.hd_callback import * 3 | from .hd_define import * 4 | 5 | class HapticDevice(object): 6 | def __init__(self, callback: hd_callback, device_name: str = "Default Device", scheduler_type: str = "async"): 7 | 8 | print("Initializing haptic device with name {}".format(device_name)) 9 | current_id = get_current_device() 10 | 11 | self.id = init_device(device_name) 12 | if self.id == HD_BAD_HANDLE: 13 | print("Unable to initialize the device. Check the connection!") 14 | return 15 | 16 | if current_id != self.id: 17 | make_current_device(self.id) 18 | print("Device {} is already initialized.".format(device_name)) 19 | return 20 | 21 | print("Intialized device! {}/{}".format(self.__vendor__(), self.__model__())) 22 | enable_force() 23 | start_scheduler() 24 | if get_error(): 25 | SystemError() 26 | self.scheduler(callback, scheduler_type) 27 | 28 | def close(self): 29 | stop_scheduler() 30 | close_device(self.id) 31 | 32 | def scheduler(self, callback, scheduler_type): 33 | if scheduler_type == "async": 34 | hdAsyncSheduler(callback) 35 | else: 36 | hdSyncSheduler(callback) 37 | 38 | 39 | @staticmethod 40 | def __vendor__() -> str: 41 | return get_vendor() 42 | 43 | @staticmethod 44 | def __model__() -> str: 45 | return get_model() -------------------------------------------------------------------------------- /pyOpenHaptics/hdu_matrix.py: -------------------------------------------------------------------------------- 1 | from ctypes import * 2 | 3 | hduMatrix = (c_double * 4) * 4 4 | hduVector3Dd = (c_double * 3) 5 | hduQuaternion = (c_double * 4) -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "pyOpenHaptics" 7 | version = "1.0.1" 8 | description = "A minimalist Python wrapper for OpenHaptics" 9 | authors = [ 10 | {name = "Mikel De Iturrate Reyzabal", email = "mikel.de_iturrate_reyzabal@kcl.ac.uk"}, 11 | ] 12 | readme = {file = "README.md", content-type = "text/markdown"} 13 | license = "MIT" 14 | requires-python = ">= 3.9" 15 | classifiers = [ 16 | "Programming Language :: Python :: 3", 17 | "Programming Language :: Python :: 3.11", 18 | "Programming Language :: Python :: 3.12", 19 | "License :: OSI Approved :: MIT License", 20 | "Operating System :: OS Independent", 21 | ] 22 | 23 | [projects.url] 24 | Homepage = "https://github.com/mikelitu/pyOpenHaptics" --------------------------------------------------------------------------------