diff --git a/CenterX.tact b/CenterX.tact deleted file mode 100644 index 0ba670f..0000000 --- a/CenterX.tact +++ /dev/null @@ -1 +0,0 @@ -{"project":{"createdAt":1604636683360,"description":"","layout":{"layouts":{"VestBack":[{"index":0,"x":0,"y":0},{"index":1,"x":0.333,"y":0},{"index":2,"x":0.667,"y":0},{"index":3,"x":1,"y":0},{"index":4,"x":0,"y":0.25},{"index":5,"x":0.333,"y":0.25},{"index":6,"x":0.667,"y":0.25},{"index":7,"x":1,"y":0.25},{"index":8,"x":0,"y":0.5},{"index":9,"x":0.333,"y":0.5},{"index":10,"x":0.667,"y":0.5},{"index":11,"x":1,"y":0.5},{"index":12,"x":0,"y":0.75},{"index":13,"x":0.333,"y":0.75},{"index":14,"x":0.667,"y":0.75},{"index":15,"x":1,"y":0.75},{"index":16,"x":0,"y":1},{"index":17,"x":0.333,"y":1},{"index":18,"x":0.667,"y":1},{"index":19,"x":1,"y":1}],"VestFront":[{"index":0,"x":0,"y":0},{"index":1,"x":0.333,"y":0},{"index":2,"x":0.667,"y":0},{"index":3,"x":1,"y":0},{"index":4,"x":0,"y":0.25},{"index":5,"x":0.333,"y":0.25},{"index":6,"x":0.667,"y":0.25},{"index":7,"x":1,"y":0.25},{"index":8,"x":0,"y":0.5},{"index":9,"x":0.333,"y":0.5},{"index":10,"x":0.667,"y":0.5},{"index":11,"x":1,"y":0.5},{"index":12,"x":0,"y":0.75},{"index":13,"x":0.333,"y":0.75},{"index":14,"x":0.667,"y":0.75},{"index":15,"x":1,"y":0.75},{"index":16,"x":0,"y":1},{"index":17,"x":0.333,"y":1},{"index":18,"x":0.667,"y":1},{"index":19,"x":1,"y":1}]},"name":"Tactot","type":"Tactot"},"mediaFileDuration":3,"name":"CenterX","tracks":[{"effects":[{"modes":{"VestBack":{"dotMode":{"dotConnected":false,"feedback":[{"endTime":800,"playbackType":"NONE","startTime":0,"pointList":[]}]},"mode":"PATH_MODE","pathMode":{"feedback":[]}},"VestFront":{"dotMode":{"dotConnected":false,"feedback":[{"endTime":800,"playbackType":"NONE","startTime":0,"pointList":[]}]},"mode":"PATH_MODE","pathMode":{"feedback":[{"movingPattern":"CONST_TDM","playbackType":"NONE","pointList":[{"intensity":0.2,"time":0,"x":0.5,"y":0.51},{"intensity":0.2,"time":266,"x":0.18,"y":0.19},{"intensity":0.2,"time":533,"x":0.05,"y":0.05},{"intensity":0.2,"time":800,"x":0,"y":0}],"visible":true},{"movingPattern":"CONST_TDM","playbackType":"NONE","pointList":[{"intensity":0.2,"time":0,"x":0.5,"y":0.52},{"intensity":0.2,"time":266,"x":0.81,"y":0.19},{"intensity":0.2,"time":533,"x":0.96,"y":0.05},{"intensity":0.2,"time":800,"x":1,"y":0}],"visible":true},{"movingPattern":"CONST_TDM","playbackType":"NONE","pointList":[{"intensity":0.2,"time":0,"x":0.51,"y":0.51},{"intensity":0.2,"time":266,"x":0.18,"y":0.84},{"intensity":0.2,"time":533,"x":0.05,"y":0.95},{"intensity":0.2,"time":800,"x":0,"y":1}],"visible":true},{"movingPattern":"CONST_TDM","playbackType":"NONE","pointList":[{"intensity":0.2,"time":0,"x":0.5,"y":0.52},{"intensity":0.2,"time":266,"x":0.83,"y":0.83},{"intensity":0.2,"time":533,"x":0.95,"y":0.96},{"intensity":0.2,"time":800,"x":1,"y":1}],"visible":true}]}}},"name":"Effect 1","offsetTime":800,"startTime":0},{"modes":{"VestBack":{"dotMode":{"dotConnected":false,"feedback":[{"endTime":800,"playbackType":"NONE","startTime":0,"pointList":[]}]},"mode":"PATH_MODE","pathMode":{"feedback":[{"movingPattern":"CONST_SPEED","playbackType":"FADE_OUT","pointList":[{"intensity":0.2,"time":0,"x":1,"y":1},{"intensity":0.2,"time":800,"x":0.51,"y":0.49}],"visible":true},{"movingPattern":"CONST_SPEED","playbackType":"FADE_OUT","pointList":[{"intensity":0.2,"time":0,"x":0,"y":1},{"intensity":0.2,"time":800,"x":0.5,"y":0.49}],"visible":true},{"movingPattern":"CONST_SPEED","playbackType":"FADE_OUT","pointList":[{"intensity":0.2,"time":0,"x":1,"y":0},{"intensity":0.2,"time":800,"x":0.5,"y":0.48}],"visible":true},{"movingPattern":"CONST_SPEED","playbackType":"FADE_OUT","pointList":[{"intensity":0.2,"time":0,"x":0,"y":0},{"intensity":0.2,"time":800,"x":0.51,"y":0.49}],"visible":true}]}},"VestFront":{"dotMode":{"dotConnected":false,"feedback":[{"endTime":800,"playbackType":"NONE","startTime":0,"pointList":[]}]},"mode":"PATH_MODE","pathMode":{"feedback":[]}}},"name":"Effect 1 copy 1","offsetTime":800,"startTime":1007},{"modes":{"VestBack":{"dotMode":{"dotConnected":false,"feedback":[{"endTime":200,"playbackType":"NONE","pointList":[{"index":1,"intensity":0.4},{"index":2,"intensity":0.4}],"startTime":0}]},"mode":"DOT_MODE","pathMode":{"feedback":[]}},"VestFront":{"dotMode":{"dotConnected":false,"feedback":[]},"mode":"DOT_MODE","pathMode":{"feedback":[{"movingPattern":"CONST_SPEED","playbackType":"NONE","visible":true,"pointList":[]}]}}},"name":"Effect 3 copy 1","offsetTime":200,"startTime":2217},{"modes":{"VestBack":{"dotMode":{"dotConnected":false,"feedback":[{"endTime":200,"playbackType":"NONE","pointList":[{"index":1,"intensity":0.4},{"index":2,"intensity":0.4}],"startTime":0}]},"mode":"DOT_MODE","pathMode":{"feedback":[{"movingPattern":"CONST_SPEED","playbackType":"NONE","visible":true,"pointList":[]}]}},"VestFront":{"dotMode":{"dotConnected":false,"feedback":[]},"mode":"DOT_MODE","pathMode":{"feedback":[{"movingPattern":"CONST_SPEED","playbackType":"NONE","visible":true,"pointList":[]}]}}},"name":"Effect 3","offsetTime":200,"startTime":1950}],"enable":true},{"enable":true,"effects":[]}],"updatedAt":1604636683360,"id":"-MLQn9fi64waSdC4DTxe"},"durationMillis":0,"intervalMillis":20,"size":20} \ No newline at end of file diff --git a/Circle.tact b/Circle.tact deleted file mode 100644 index acb3e74..0000000 --- a/Circle.tact +++ /dev/null @@ -1 +0,0 @@ -{"project":{"column":0,"createdAt":1583225916944,"description":"","hapticEffectSize":0,"id":"-M1Ub_0ak_rLU6PSrUqG","layout":{"layouts":{"VestBack":[{"index":0,"x":0,"y":0},{"index":1,"x":0.333,"y":0},{"index":2,"x":0.667,"y":0},{"index":3,"x":1,"y":0},{"index":4,"x":0,"y":0.25},{"index":5,"x":0.333,"y":0.25},{"index":6,"x":0.667,"y":0.25},{"index":7,"x":1,"y":0.25},{"index":8,"x":0,"y":0.5},{"index":9,"x":0.333,"y":0.5},{"index":10,"x":0.667,"y":0.5},{"index":11,"x":1,"y":0.5},{"index":12,"x":0,"y":0.75},{"index":13,"x":0.333,"y":0.75},{"index":14,"x":0.667,"y":0.75},{"index":15,"x":1,"y":0.75},{"index":16,"x":0,"y":1},{"index":17,"x":0.333,"y":1},{"index":18,"x":0.667,"y":1},{"index":19,"x":1,"y":1}],"VestFront":[{"index":0,"x":0,"y":0},{"index":1,"x":0.333,"y":0},{"index":2,"x":0.667,"y":0},{"index":3,"x":1,"y":0},{"index":4,"x":0,"y":0.25},{"index":5,"x":0.333,"y":0.25},{"index":6,"x":0.667,"y":0.25},{"index":7,"x":1,"y":0.25},{"index":8,"x":0,"y":0.5},{"index":9,"x":0.333,"y":0.5},{"index":10,"x":0.667,"y":0.5},{"index":11,"x":1,"y":0.5},{"index":12,"x":0,"y":0.75},{"index":13,"x":0.333,"y":0.75},{"index":14,"x":0.667,"y":0.75},{"index":15,"x":1,"y":0.75},{"index":16,"x":0,"y":1},{"index":17,"x":0.333,"y":1},{"index":18,"x":0.667,"y":1},{"index":19,"x":1,"y":1}]},"name":"Tactot","type":"Tactot"},"mediaFileDuration":2,"name":"Circle","parentId":82,"row":0,"tracks":[{"effects":[{"id":1772,"modes":{"VestBack":{"dotMode":{"dotConnected":false,"feedback":[{"endTime":1000,"playbackType":"NONE","startTime":0,"pointList":[]}]},"mode":"PATH_MODE","pathMode":{"feedback":[{"movingPattern":"CONST_SPEED","playbackType":"NONE","visible":true,"pointList":[]}]},"texture":0},"VestFront":{"dotMode":{"dotConnected":false,"feedback":[{"endTime":1000,"playbackType":"NONE","startTime":0,"pointList":[]}]},"mode":"PATH_MODE","pathMode":{"feedback":[{"movingPattern":"CONST_TDM","playbackType":"NONE","pointList":[{"intensity":"0.700","time":0,"x":"0.836","y":"0.821"},{"intensity":"0.700","time":111,"x":"0.489","y":"1.000"},{"intensity":"0.700","time":222,"x":"0.165","y":"0.895"},{"intensity":"0.700","time":333,"x":"0.000","y":"0.503"},{"intensity":"0.700","time":444,"x":"0.181","y":"0.128"},{"intensity":"0.700","time":555,"x":"0.508","y":"0.000"},{"intensity":"0.700","time":666,"x":"0.836","y":"0.134"},{"intensity":"0.700","time":777,"x":"1.000","y":"0.502"},{"intensity":"0.700","time":888,"x":"0.833","y":"0.824"},{"intensity":"0.700","time":1000,"x":"0.489","y":"1.000"}],"visible":true}]},"texture":0}},"name":"path","offsetTime":1000,"priority":0,"startTime":0,"trackId":923}],"enable":true,"id":923,"projectId":462},{"enable":true,"id":924,"projectId":462,"effects":[]}],"updateTime":"2018-01-31T04:49:10.000Z","updatedAt":1583226441434},"durationMillis":0,"intervalMillis":20,"size":20} \ No newline at end of file diff --git a/README.md b/README.md index a7057a9..a1850ed 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,60 @@ -### Sample code for python +# Python SDK for bHaptics Hub -### Prerequisite -* bHaptics Player has to be installed (Windows) - * The app can be found in - bHaptics webpage: [http://www.bhaptics.com](http://bhaptics.com/) +## Prerequisites +- One or more bHaptics TactSuit devices (e.g., X40, X16). +- A mobile device with bHaptics Hub App ([Android](https://bit.ly/bhaptics-hub-android)) installed. +- A haptic project deployed in [bHaptics Developer Portal](https://developer.bhaptics.com/). + - [How to Create Projects](#how-to-create-projects) + +### Conditions +- Tested under Python 3.9 ### Dependencies -* websocket-client +- requests + +## Getting started +1. Connect a mobile device and a desktop to the network under the same Wi-Fi. +2. Clone this repository on your desktop and install the required dependencies inside the directory. + ```bash + pip3 install -r requirements.txt + ``` +3. Install bHaptics Hub App on your mobile device - [Android](https://bit.ly/bhaptics-hub-android). +4. Connect your TactSuit with bHaptics Hub App - [bHaptics Hub Guide](https://bit.ly/bHaptics-Hub-Guide). +5. Press the start button on the app. +6. Run the sample code inside your desktop. + ```bash + python3 sample.py + ``` + +## Example +This example demonstrates how to use the bHaptics SDK with Python: + +```python +import time +from bhaptics.haptic_player import BhapticsSDK2 + +sdk_instance = None + +if __name__ == '__main__': + # Replace `yourAppId` and `yourApiKey` with values of your own project + appId = "yourAppId" + apiKey = "yourApiKey" + sdk_instance = BhapticsSDK2(appId, apiKey) + try: + while True: + time.sleep(5) + # Replace `shoot_test` with event name of your own project + sdk_instance.play_event("shoot_test") + except KeyboardInterrupt: + print("Stopping the client...") + sdk_instance.stop() + +``` + +## How to Create Projects +- Visit [bHaptics Developer Portal](https://developer.bhaptics.com/). +- Create your own project with one or more haptic patterns: [refer to this guide for details](https://bhaptics.notion.site/Create-haptic-events-using-bHaptics-Developer-Portal-b056c5a56e514afeb0ed436873dd87c6). +- Save the haptic patterns and deploy the project. +- Go to Settings page and check the `appId` and `apiKey` of your project. +- Update the `appId` and `apiKey` in the code with your project. diff --git a/bhaptics/better_haptic_player.py b/bhaptics/better_haptic_player.py deleted file mode 100644 index 6482224..0000000 --- a/bhaptics/better_haptic_player.py +++ /dev/null @@ -1,157 +0,0 @@ -import json -import socket -from websocket import create_connection, WebSocket -import threading - -ws = None - -active_keys = set([]) -connected_positions = set([]) - - -class WebSocketReceiver(WebSocket): - def recv_frame(self): - global active_keys - global connected_positions - frame = super().recv_frame() - try: - frame_obj = json.loads(frame.data) - active = frame_obj['ActiveKeys'] - - # if len(active) > 0: - # print (active) - active_keys = set(active) - connected_positions = set(frame_obj['ConnectedPositions']) - except: - # active_keys = set([]) - # connected_positions = set([]) - print('') - - return frame - - -def thread_function(name): - while True: - if ws is not None: - ws.recv_frame() - - -def initialize(): - global ws - try: - ws = create_connection("ws://localhost:15881/v2/feedbacks", - sockopt=((socket.IPPROTO_TCP, socket.TCP_NODELAY, 1),), - class_=WebSocketReceiver) - - x = threading.Thread(target=thread_function, args=(1,)) - x.start() - except: - print("Couldn't connect") - return - - -def destroy(): - if ws is not None: - ws.close() - - -def is_playing(): - return len(active_keys) > 0 - - -def is_playing_key(key): - return key in active_keys - - -# position Vest Head ForeamrL ForearmR HandL HandR FootL FootR -def is_device_connected(position): - return position in connected_positions - - -def register(key, file_directory): - json_data = open(file_directory).read() - - print(json_data) - - data = json.loads(json_data) - project = data["project"] - - layout = project["layout"] - tracks = project["tracks"] - - request = { - "Register": [{ - "Key": key, - "Project": { - "Tracks": tracks, - "Layout": layout - } - }] - } - - json_str = json.dumps(request) - __submit(json_str) - - -def submit_registered(key): - request = { - "Submit": [{ - "Type": "key", - "Key": key, - }] - } - - json_str = json.dumps(request) - - __submit(json_str) - - -def submit_registered_with_option( - key, alt_key, - scale_option, - rotation_option): - # scaleOption: {"intensity": 1, "duration": 1} - # rotationOption: {"offsetAngleX": 90, "offsetY": 0} - request = { - "Submit": [{ - "Type": "key", - "Key": key, - "Parameters": { - "altKey": alt_key, - "rotationOption": rotation_option, - "scaleOption": scale_option, - } - }] - } - - json_str = json.dumps(request); - - __submit(json_str) - - -def submit(key, frame): - request = { - "Submit": [{ - "Type": "frame", - "Key": key, - "Frame": frame - }] - } - - json_str = json.dumps(request); - - __submit(json_str) - - -def submit_dot(key, position, dot_points, duration_millis): - front_frame = { - "position": position, - "dotPoints": dot_points, - "durationMillis": duration_millis - } - submit(key, front_frame) - - -def __submit(json_str): - if ws is not None: - ws.send(json_str) diff --git a/bhaptics/haptic_player.py b/bhaptics/haptic_player.py index d1e4545..6afebb9 100644 --- a/bhaptics/haptic_player.py +++ b/bhaptics/haptic_player.py @@ -1,122 +1,492 @@ +import re +import random +import requests +from enum import Enum + import json -from websocket import create_connection - -# # send individual point for 1 seconds -# dotFrame = { -# "Position": "Left", -# "DotPoints": [{ -# "Index": 0, -# "Intensity": 100 -# }, { -# "Index": 3, -# "Intensity": 50 -# }], -# "DurationMillis": 1000 -# } -# player.submit("dotPoint", dotFrame) -# sleep(2) -# -# pathFrame = { -# "Position": "VestFront", -# "PathPoints": [{ -# "X": "0.5", -# "Y": "0.5", -# "Intensity": 100 -# }, { -# "X": "0.3", -# "Y": "0.3", -# "Intensity": 50 -# }], -# "DurationMillis": 1000 -# } -# player.submit("pathPoint", pathFrame) -# sleep(2) - - -class HapticPlayer: - def __init__(self): - try: - self.ws = create_connection("ws://localhost:15881/v2/feedbacks") - except: - print("Couldn't connect") - return - - def register(self, key, file_directory): - json_data = open(file_directory).read() - - data = json.loads(json_data) - project = data["project"] - - layout = project["layout"] - tracks = project["tracks"] - - request = { - "Register": [{ - "Key": key, - "Project": { - "Tracks": tracks, - "Layout": layout - } - }] - } +import time +import threading - json_str = json.dumps(request) - self.ws.send(json_str) +if __name__ == '__main__': + import udp_server as udp_server + import tcp_client as tcp_client +else: + import bhaptics.udp_server as udp_server + import bhaptics.tcp_client as tcp_client - def submit_registered(self, key): - submit = { - "Submit": [{ - "Type": "key", - "Key": key, - }] - } +__client: tcp_client.TCPClient = None +__is_client_connected = False +__is_client_api_verified = False + +__is_verbose = False + +__ping_thread = None +__ping_thread_active = False + +__version__ = "py 1.0.0" +__version_number__ = 1 +__version_info__ = tuple([ int(num) for num in re.sub(r"[^\d.]", "", __version__).split('.')]) + +__conf = { + "applicationId": "", + "sdkApiKey": "", + "sdkVersionName": __version__, + "sdkVersion": __version_number__ +} + +def __print(*args, **kwargs): + if __is_verbose: + print(*args, **kwargs) + +def __message_received(message): + msg = json.loads(message) - json_str = json.dumps(submit); - - self.ws.send(json_str) - - def submit_registered_with_option( - self, key, alt_key, - scale_option, - rotation_option): - # scaleOption: {"intensity": 1, "duration": 1} - # rotationOption: {"offsetAngleX": 90, "offsetY": 0} - submit = { - "Submit": [{ - "Type": "key", - "Key": key, - "Parameters": { - "altKey": alt_key, - "rotationOption": rotation_option, - "scaleOption": scale_option, - } - }] + message_type = msg["type"] + __print(f"\nreceived: ({message_type}) {message}") + + if message_type == "ServerTokenMessage": + parsed_msg = json.loads(msg["message"]) + __check_api(parsed_msg["token"], parsed_msg["tokenKey"]) + +def __check_api(token, token_key): + global __is_client_api_verified + + # Make the GET request + host = "https://sdk-apis.bhaptics.com" + url = f"{host}/api/v1/tacthub-api/verify?token={token}&token-key={token_key}" + response = requests.get(url) + + # Check if the request was successful + if response.status_code == 200: + __is_client_api_verified = True + __print("Client api is verified!") + else: + __is_client_api_verified = False + __print(f"Client api failed verification: {response.status_code}.") + +def __udp_message_received(message, addr): + sender_ip = addr[0] + sender_port = addr[1] + + msg = json.loads(message) + # __print(message, sender_ip, sender_port, msg["userId"]) + + global __client, __is_client_connected, __is_verbose + + if __client is None: + __client = tcp_client.TCPClient( + sender_ip, + msg["port"], + message_received_callback=__message_received, + verbose=__is_verbose + ) + __client.start() + + while __client.is_connected() is False: + __print("Waiting for client ...") + __is_client_connected = False + time.sleep(0.5) + + if __client.is_connected(): + __print("Client is connected!") + __is_client_connected = True + __client.send_message(__generate_message("SdkRequestAuthInit", __conf)) + time.sleep(0.5) + + udp_server.stop() + +def __generate_message(message_type, message = None): + if isinstance(message, str): + msg = { + "message": message, + "type": message_type + } + else: + msg = { + "message": json.dumps(message), + "type": message_type } + return json.dumps(msg) + '\n' - json_str = json.dumps(submit); +def is_client_connected(): + global __is_client_connected + return __is_client_connected - self.ws.send(json_str) +def is_client_api_verified(): + global __is_client_api_verified + return __is_client_api_verified - def submit(self, key, frame): - submit = { - "Submit": [{ - "Type": "frame", - "Key": key, - "Frame": frame - }] +def initialize(appId: str, apiKey: str, verbose: bool = False): + global __conf, __is_verbose, __version__, __version_info__ + + __is_verbose = verbose + + __conf = { + "applicationId": appId, + "sdkApiKey": apiKey, + "sdkVersionName": __version__, + "sdkVersion": __version_number__ + } + + udp_server.listen(callback=__udp_message_received, verbose=__is_verbose) + +def destroy(): + global __client, __is_client_connected, __is_client_api_verified, __ping_thread, __ping_thread_active + + # For iOS Hub App, let server prepare for the disconnection + if __client is not None: + __ping_to_server() + + # Terminate the ping thread + if __ping_thread_active: + __ping_thread_active = False + + if __ping_thread is not None: + __ping_thread.join() + __ping_thread = None + + # Terminate the remaining threads + udp_server.stop() + __client.stop() + __is_client_connected = False + __is_client_api_verified = False + +def play_event( + name, + requestId:int|None = None, + intensity:float = 1, + duration:float = 1, + offsetAngleX:float = 0, + offsetY:float = 0, + ): + global __client + + if __client is None: + return + + if requestId is None: + requestId = random.randint(0, 999999) + + play_message = { + "eventName": name, + "requestId": requestId, + "intensity": intensity, + "duration": duration, + "offsetAngleX": offsetAngleX, + "offsetY": offsetY, + } + + __client.send_message(__generate_message("SdkPlay", play_message)) + __ping_while_waiting() + return requestId + +class Position(Enum): + VEST = 0 + FOREARM_L = 1 + FOREARM_R = 2 + HEAD = 3 + HAND_L = 4 + HAND_R = 5 + FOOT_L = 6 + FOOT_R = 7 + GLOVE_L = 8 + GLOVE_R = 9 + +def play_dot( + position:Position, + motorValues:list[int], # 40 motor values ranging from 0 to 100 + requestId:int|None = None, + duration:int = 1000, + ): + global __client + + if __client is None: + return + + if requestId is None: + requestId = random.randint(0, 999999) + + play_message = { + "requestId": requestId, + "pos": position.value, + "motors": motorValues, + "durationMillis": duration, + } + + __client.send_message(__generate_message("SdkPlayDotMode", play_message)) + __ping_while_waiting() + return requestId + +class GloveMode: + def __init__(self, intensity: int = 0, playTime: int = 0, shape: int = 0): + self.intensity = intensity + self.playTime = playTime + self.shape = shape + + def to_dict(self): + # Convert to dictionary for JSON serialization + return { + "intensity": self.intensity, + "playTime": self.playTime, + "shape": self.shape, } - json_str = json.dumps(submit); +def play_glove( + position:Position, + gloveModes:list[GloveMode], + requestId:int|None = None, + ): + global __client + + if __client is None: + return + + if requestId is None: + requestId = random.randint(0, 999999) + + motorValues = [] + playTimeValues = [] + shapeValues = [] + for mode in gloveModes: + motorValues.append(mode.intensity) + playTimeValues.append(mode.playTime) + shapeValues.append(mode.shape) + + play_message = { + "requestId": requestId, + "pos": position.value, + "motorValues": motorValues, + "playTimeValues": playTimeValues, + "shapeValues": shapeValues, + } - self.ws.send(json_str) + __client.send_message(__generate_message("SdkPlayWaveformMode", play_message)) + __ping_while_waiting() + return requestId - def submit_dot(self, key, position, dot_points, duration_millis): - front_frame = { - "position": position, - "dotPoints": dot_points, - "durationMillis": duration_millis +class PathPoint: + def __init__(self, x: float, y: float, intensity: int, motorCount: int = 3): + self.x = max(0, min(x, 1)) # Ensure x is within [0, 1] + self.y = max(0, min(y, 1)) # Ensure y is within [0, 1] + self.intensity = max(0, min(intensity, 100)) # Ensure intensity is within [0, 100] + self.motorCount = max(0, min(motorCount, 3)) # Ensure motorCount is within [0, 3] + + def to_dict(self): + # Convert to dictionary for JSON serialization + return { + "x": self.x, + "y": self.y, + "intensity": self.intensity, + "motorCount": self.motorCount, } - self.submit(key, front_frame) - def __del__(self): - self.ws.close() +def play_path( + position:Position, + pathPoints:list[PathPoint], + requestId:int|None = None, + duration:int = 1000, + ): + global __client + + if __client is None: + return + + if requestId is None: + requestId = random.randint(0, 999999) + + x = [] + y = [] + intensity = [] + for point in pathPoints: + x.append(point.x) + y.append(point.y) + intensity.append(point.intensity) + + play_message = { + "requestId": requestId, + "pos": position.value, + "x": x, + "y": y, + "intensity": intensity, + "durationMillis": duration, + } + + __client.send_message(__generate_message("SdkPlayPathMode", play_message)) + __ping_while_waiting() + return requestId + +def play_loop( + name, + requestId:int|None = None, + intensity:float = 1, + duration:float = 1, + interval:int = 0, + maxCount:int = 0, + offsetAngleX:float = 0, + offsetY:float = 0, + ): + global __client + + if __client is None: + return + + if requestId is None: + requestId = random.randint(0, 999999) + + play_message = { + "eventName": name, + "requestId": requestId, + "intensity": intensity, + "duration": duration, + "interval": interval, + "maxCount": maxCount, + "offsetAngleX": offsetAngleX, + "offsetY": offsetY, + } + + __client.send_message(__generate_message("SdkPlayLoop", play_message)) + __ping_while_waiting() + return requestId + +def __ping_while_waiting(): + global __ping_thread, __ping_thread_active + + def __ping_loop(): + global __ping_thread, __ping_thread_active + + __ping_thread_active = True + try: + while __ping_thread_active: + time.sleep(1) + __ping_to_server() + except Exception as e: + pass + finally: + __ping_thread_active = False + + # Assure the ping loop is joined if it is active + if __ping_thread_active: + __ping_thread_active = False + + if __ping_thread is not None: + __ping_thread.join() + __ping_thread = None + + # Ping for every second to keep the Hub app alive (especially for iOS) + __ping_thread = threading.Thread(target=__ping_loop) + __ping_thread.start() + +def __ping_to_server(): + __client.send_message(__generate_message("SdkPingToServer")) + +def ping_all(): + __client.send_message(__generate_message("SdkPingAll")) + __ping_while_waiting() + + +def stop_by_event(name: str): + __client.send_message(__generate_message("SdkStopByEventId", name)) + __ping_while_waiting() + + +def stop_by_request(id: int): + __client.send_message(__generate_message("SdkStopByRequestId", id)) + __ping_while_waiting() + + +def stop_all(): + __client.send_message(__generate_message("SdkStopAll")) + __ping_while_waiting() + + +if __name__ == '__main__': + # TODO: Replace `appId` and `apiKey` with values of your app + appId = "mWK8BbDgpx9LdZVR22ij" + apiKey = "m9ef4q9oQRXbPeJY9z4J" + + # Load `HelloFps` game + initialize( + appId = appId, + apiKey = apiKey, + verbose = False + ) + + print(f"Testing bHaptics Hub Python SDK v{__version_info__}.\n") + print("1. Open TactHub app and connect with TactSuit x40.") + print("2. Press the play button to start the server.") + print() + + # Wait until client is connected + while not is_client_connected(): + time.sleep(0.3) + + print("Python SDK connected to bHaptics Hub! Now verifying client...") + + # Wait for max 5 seconds until api gets verified + wait_time = 0 + while not is_client_api_verified() and wait_time < 5: + time.sleep(0.3) + wait_time += 0.3 + + print(f"Client verification: {is_client_api_verified()}") + print() + + print("Testing: play_dot") + play_dot( + Position.VEST, + [ + # Front side + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + + # Back side + 0, 0, 0, 0, + 0, 0, 0, 0, + 100, 100, 100, 100, + 0, 0, 0, 0, + 0, 0, 0, 0, + ], + duration = 3000 + ) + time.sleep(2) + + print("Testing: play_path") + play_path( + Position.VEST, + [PathPoint(0.2, 0.2, 100, 3), PathPoint(0.8, 0.8, 100, 3)], + duration = 500 + ) + time.sleep(2) + + print("Testing: play_loop") + play_loop( + "shoot_test", + interval = 1, + maxCount = 5, + ) + time.sleep(2) + + print("Testing: play_glove(L)") + play_glove( + position=Position.GLOVE_L, + gloveModes=[GloveMode(100, 100, 2), GloveMode(100, 100, 0), GloveMode(100, 100, 1)] + ) + time.sleep(2) + + print("Testing: play_glove(R)") + play_glove( + position=Position.GLOVE_R, + gloveModes=[GloveMode(100, 100, 2), GloveMode(100, 100, 0), GloveMode(100, 100, 1)] + ) + time.sleep(2) + + print("Testing: play_event") + play_event("shoot_test") + time.sleep(2) + + print() + print("Test Finished") + destroy() diff --git a/bhaptics/tcp_client.py b/bhaptics/tcp_client.py new file mode 100644 index 0000000..0ebadcd --- /dev/null +++ b/bhaptics/tcp_client.py @@ -0,0 +1,121 @@ +import socket +import threading + +class TCPClient: + def __init__(self, server_host, server_port, message_received_callback=None, verbose=False): + self.server_host = server_host + self.server_port = server_port + self.message_received_callback = message_received_callback + self.verbose = verbose + self.client_socket = None + self.connected = False + self.receive_thread = None + + def __print(self, *args, **kwargs): + if self.verbose: + print(*args, **kwargs) + + def __receive_messages(self): + try: + while self.connected: + response = self.client_socket.recv(4096) # Buffer size of 4096 bytes + if response: + # Call the callback function if any message is received + if self.message_received_callback: + self.__print("App → Client: ", response) + self.message_received_callback(response.decode()) + else: + # No response indicates the server has closed the connection + break + except Exception as e: + if self.connected: + self.__print(f"An error occurred in TCP: {e}") + finally: + self.connected = False + self.client_socket.close() + self.__print(f"Closed TCP connection with app.") + + def send_message(self, message): + self.__print("Client → App: ", message) + + if self.connected: + try: + self.client_socket.sendall(message.encode('utf-8')) + except Exception as e: + self.__print(f"\t sending failed: {e}.") + else: + self.__print("\t sending failed: client is not connected.") + + def start(self, initial_message=None): + if not self.connected: + self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + self.client_socket.connect((self.server_host, self.server_port)) + self.connected = True + self.__print(f"Connected to {self.server_host} on port {self.server_port}") + + # Start listening for messages from the server + self.receive_thread = threading.Thread(target=self.__receive_messages) + self.receive_thread.start() + + # Send the initial message after connecting + if initial_message: + self.send_message(initial_message) + + except Exception as e: + self.connected = False + self.__print(f"An error occurred while connecting: {e}") + else: + self.__print("Client is already connected.") + + def stop(self): + if self.connected: + try: + self.connected = False + self.client_socket.close() + except OSError as e: + self.__print(f"Error closing TCP socket: {e}") + return + + if threading.current_thread() != self.receive_thread: + try: + if self.receive_thread: + self.receive_thread.join() + self.receive_thread = None + except Exception as e: + self.__print(f"Error joining TCP thread: {e}") + return + + def is_connected(self): + return self.connected + + +# Example callback function +def message_received(message): + print(f"Received: {message}") + + +# Example of module usage +if __name__ == "__main__": + client = TCPClient("127.0.0.1", 15884, message_received_callback=message_received) + + # Start client and send a message + client.start("Hello, TCP Server!") + + # Send another message + client.send_message("Another message") + + # Check connection status + if client.is_connected(): + print("Client is connected.") + else: + print("Client is not connected.") + + # The client will now listen for messages and invoke the callback when messages are received + try: + # Keep the main thread alive while the client is running. + while client.is_connected(): + pass + except KeyboardInterrupt: + print("Stopping the client...") + client.stop() diff --git a/bhaptics/udp_server.py b/bhaptics/udp_server.py new file mode 100644 index 0000000..eaba886 --- /dev/null +++ b/bhaptics/udp_server.py @@ -0,0 +1,72 @@ +import socket +import threading + +class UDPServer: + def __init__(self, host="0.0.0.0", port=15884, callback=None, verbose=False): + self.host = host + self.port = port + self.callback = callback + self.verbose = verbose + self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.running = False + self.thread = None + + def __print(self, *args, **kwargs): + if self.verbose: + print(*args, **kwargs) + + def __listen(self): + try: + while self.running: + data, addr = self.sock.recvfrom(1024) # Buffer size of 1024 bytes + message = data.decode() + self.__print(f"Received message: {message} from {addr}") + if self.callback: + self.callback(message, addr) + except Exception as e: + if self.running: + self.__print(f"An error occurred in UDP: {e}") + finally: + self.running = False + self.sock.close() + self.__print("Closed UDP server.") + + def start_server(self): + self.sock.bind((self.host, self.port)) + self.running = True + self.thread = threading.Thread(target=self.__listen) + self.thread.start() + self.__print(f"UDP server listening on {self.host}:{self.port}") + + def stop_server(self): + if self.running: + try: + self.running = False + self.sock.close() + except OSError as e: + self.__print(f"Error closing UDP socket: {e}") + return + + if threading.current_thread() != self.thread: + try: + if self.thread: + self.thread.join() + self.thread = None + except Exception as e: + self.__print(f"Error joining UDP thread: {e}") + return + +# Global server instance +udp_server = None + +# Exportable functions +def listen(host="0.0.0.0", port=15884, callback=None, verbose=False): + global udp_server + udp_server = UDPServer(host, port, callback, verbose=verbose) + udp_server.start_server() + +def stop(): + global udp_server + if udp_server: + udp_server.stop_server() diff --git a/osc_client.py b/osc_client.py deleted file mode 100644 index 9a85fe9..0000000 --- a/osc_client.py +++ /dev/null @@ -1,14 +0,0 @@ -from time import sleep - -from pythonosc import udp_client - -client = udp_client.SimpleUDPClient("127.0.0.1", 5005) -# -for x in range(10): - print('send front {0}'.format(x)) - client.send_message("/vest_front", '{0},{1}'.format(x, 100)) - sleep(1) - print('Send back {0}'.format(x)) - client.send_message("/vest_back", '{0},{1}'.format(x, 100)) - sleep(1) - diff --git a/osc_server.py b/osc_server.py deleted file mode 100644 index 3b5bc1f..0000000 --- a/osc_server.py +++ /dev/null @@ -1,40 +0,0 @@ -import argparse -import math -from bhaptics import haptic_player; - -from pythonosc import dispatcher -from pythonosc import osc_server - -player = haptic_player.HapticPlayer() - - -def handle_front(unused_addr, args): - print("front {0}".format(args)) - res = args.split(',') - player.submit_dot('back', 'VestFront', [{"index": res[0], "intensity": res[1]}], 100) - - -def handle_back(unused_addr, args): - print("back: {0}".format(args)) - res = args.split(',') - player.submit_dot('back', 'VestBack', [{"index": res[0], "intensity": res[1]}], 100) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--ip", - default="127.0.0.1", help="The ip to listen on") - parser.add_argument("--port", - type=int, default=5005, help="The port to listen on") - args = parser.parse_args() - - dispatcher = dispatcher.Dispatcher() - dispatcher.map("/vest_front", handle_front) - dispatcher.map("/vest_back", handle_back) - - server = osc_server.ThreadingOSCUDPServer( - (args.ip, args.port), dispatcher) - print("Serving on {}".format(server.server_address)) - - - server.serve_forever() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 43b7a7a..6d3327c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1 @@ -websocket-client~=0.57.0 -python-osc~=1.7.4 -keyboard~=0.13.5 \ No newline at end of file +requests>=2.8.1 diff --git a/sample-with-better.py b/sample-with-better.py deleted file mode 100644 index 3d2453c..0000000 --- a/sample-with-better.py +++ /dev/null @@ -1,71 +0,0 @@ -from time import sleep -from bhaptics import better_haptic_player as player -import keyboard - -player.initialize() - -# tact file can be exported from bhaptics designer -print("register CenterX") -player.register("CenterX", "CenterX.tact") -print("register Circle") -player.register("Circle", "Circle.tact") - -interval = 0.5 -durationMillis = 100 - - - -for i in range(20): - print(i, "back") - player.submit_dot("backFrame", "VestBack", [{"index": i, "intensity": 100}], durationMillis) - sleep(interval) - - print(i, "front") - player.submit_dot("frontFrame", "VestFront", [{"index": i, "intensity": 100}], durationMillis) - sleep(interval) - - -def play(index): - if index == 1: - print("submit CenterX") - player.submit_registered("CenterX") - elif index == 2: - print("submit Circle") - player.submit_registered_with_option("Circle", "alt", - scale_option={"intensity": 1, "duration": 1}, - rotation_option={"offsetAngleX": 180, "offsetY": 0}) - elif index == 3: - print("submit Circle With Diff AltKey") - player.submit_registered_with_option("Circle", "alt2", - scale_option={"intensity": 1, "duration": 1}, - rotation_option={"offsetAngleX": 0, "offsetY": 0}) - - -def run(): - # sleep(0.5) - # play(1) - # sleep(0.5) - - print("Press Q to quit") - while True: - key = keyboard.read_key() - if key == "q" or key == "Q": - break - elif key == "1": - play(1) - elif key == "2": - play(3) - elif key == "3": - play(3) - - - print('=================================================') - print('is_playing', player.is_playing()) - print('is_playing_key(CenterX)', player.is_playing_key('CenterX')) - print('is_device_connected(Vest)', player.is_device_connected('Vest')) - print('is_device_connected(ForearmL)', player.is_device_connected('ForearmL')) - print('=================================================') - - -if __name__ == "__main__": - run() diff --git a/sample.py b/sample.py index 441d7f3..dc39876 100644 --- a/sample.py +++ b/sample.py @@ -1,39 +1,159 @@ -from time import sleep -from bhaptics import haptic_player - - -player = haptic_player.HapticPlayer() -sleep(0.4) - -# tact file can be exported from bhaptics designer -print("register CenterX") -player.register("CenterX", "CenterX.tact") -print("register Circle") -player.register("Circle", "Circle.tact") - -sleep(0.3) -print("submit CenterX") -player.submit_registered("CenterX") -sleep(4) -print("submit Circle") -player.submit_registered_with_option("Circle", "alt", - scale_option={"intensity": 1, "duration": 1}, - rotation_option={"offsetAngleX": 180, "offsetY": 0}) -print("submit Circle With Diff AltKey") -player.submit_registered_with_option("Circle", "alt2", - scale_option={"intensity": 1, "duration": 1}, - rotation_option={"offsetAngleX": 0, "offsetY": 0}) -sleep(3) - -interval = 0.5 -durationMillis = 100 - -for i in range(20): - print(i, "back") - player.submit_dot("backFrame", "VestBack", [{"index": i, "intensity": 100}], durationMillis) - sleep(interval) - - print(i, "front") - player.submit_dot("frontFrame", "VestFront", [{"index": i, "intensity": 100}], durationMillis) - sleep(interval) +import sys +import time +import bhaptics.haptic_player as player + +__todo_text = "" + +def __push_todo(text): + global __todo_text + + __pop_todo() + + __todo_text = text + print(f" ◻ {__todo_text}", end='') + sys.stdout.flush() + +def __pop_todo(): + global __todo_text + + if len(__todo_text) > 0: + print(f"\r ☑ {__todo_text}") + sys.stdout.flush() + +if __name__ == '__main__': + # TODO: Replace `appId` and `apiKey` with values of your app + appId = "mWK8BbDgpx9LdZVR22ij" + apiKey = "m9ef4q9oQRXbPeJY9z4J" + + # Load your app + player.initialize( + appId = appId, + apiKey = apiKey, + verbose = False + ) + + print(f"Testing bHaptics Hub Python SDK v{player.__version__}.\n") + print("1. Open TactHub app and connect with TactSuit x40.") + print("2. Press the play button to start the server.") + print() + + # Wait until client is connected + while not player.is_client_connected(): + time.sleep(0.3) + + print("Python SDK connected to bHaptics Hub! Now verifying client...") + + # Wait for max 5 seconds until api gets verified + wait_time = 0 + while not player.is_client_api_verified() and wait_time < 5: + time.sleep(0.3) + wait_time += 0.3 + + print(f"Client verification: {player.is_client_api_verified()}") + print() + + print("Now playing hapting events...") + + __push_todo("play_dot(\"sample_front\")") + player.play_dot( + player.Position.VEST, + [ + # Front side + 20, 20, 20, 20, + 20, 20, 20, 20, + 20, 20, 20, 20, + 20, 20, 20, 20, + 20, 20, 20, 20, + + # Back side + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + ], + duration = 3000 + ) + time.sleep(5) + + __push_todo("play_dot(\"sample_back\")") + player.play_dot( + player.Position.VEST, + [ + # Front side + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + + # Back side + 0, 0, 0, 0, + 0, 0, 0, 0, + 100, 100, 100, 100, + 0, 0, 0, 0, + 0, 0, 0, 0, + ], + duration = 3000 + ) + time.sleep(5) + + __push_todo("play_path(\"sample_scan\")") + for i in [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]: + player.play_path( + player.Position.VEST, + [player.PathPoint(x=i, y=0.5, intensity=50)], + duration = 500 + ) + time.sleep(0.5) + time.sleep(5) + + # Play "shoot_test" event. + __push_todo("play_event(\"shoot_test\")") + player.play_event("shoot_test") + time.sleep(5) + + # Play "shoot_test" event with its duration doubled. + __push_todo("play_event(\"shoot_test\") with doubled duration") + player.play_event("shoot_test", duration=2) + time.sleep(5) + + # Begin "shoot_test" event and stop immediately. + __push_todo("begin play_event(\"shoot_test\") with doubled duration and stop immediately") + player.play_event("shoot_test", duration=2) + time.sleep(0.2) + player.stop_all() + time.sleep(5) + + # Repeat "shoot_test" event 3 times. + __push_todo("repeat play_event(\"shoot_test\") three times") + player.play_loop("shoot_test", interval=1, maxCount=3) + time.sleep(5) + + __push_todo("play_glove(L)") + player.play_glove( + position=player.Position.GLOVE_L, + gloveModes=[ + player.GloveMode(100, 100, 2), + player.GloveMode(100, 100, 0), + player.GloveMode(100, 100, 1) + ] + ) + time.sleep(2) + + __push_todo("play_glove(R)") + player.play_glove( + position=player.Position.GLOVE_R, + gloveModes=[ + player.GloveMode(100, 100, 2), + player.GloveMode(100, 100, 0), + player.GloveMode(100, 100, 1) + ] + ) + time.sleep(2) + __pop_todo() + + player.destroy() + print() + print("All test finished!")