From 782b13bbf5379f359f86bd95b8bfac9b5c859df1 Mon Sep 17 00:00:00 2001 From: Sangwon Choi Date: Thu, 25 Jan 2024 20:24:28 +0900 Subject: [PATCH 01/18] feat: add tacthub sdk --- CenterX.tact | 1 - Circle.tact | 1 - README.md | 58 ++++++- bhaptics/better_haptic_player.py | 157 ------------------- bhaptics/haptic_player.py | 256 +++++++++++++++++-------------- bhaptics/tcp_client.py | 109 +++++++++++++ bhaptics/udp_server.py | 58 +++++++ osc_client.py | 14 -- osc_server.py | 40 ----- requirements.txt | 3 +- sample-with-better.py | 71 --------- sample.py | 76 ++++----- 12 files changed, 402 insertions(+), 442 deletions(-) delete mode 100644 CenterX.tact delete mode 100644 Circle.tact delete mode 100644 bhaptics/better_haptic_player.py create mode 100644 bhaptics/tcp_client.py create mode 100644 bhaptics/udp_server.py delete mode 100644 osc_client.py delete mode 100644 osc_server.py delete mode 100644 sample-with-better.py 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..baaf4a0 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,60 @@ ### Sample code for python ### Prerequisite -* bHaptics Player has to be installed (Windows) - * The app can be found in - bHaptics webpage: [http://www.bhaptics.com](http://bhaptics.com/) +* bHaptics Beta App is installed. +* To get access, please contact developer@bhaptics.com. +* Setup your project at the bHaptics Developer Portal: [Create haptic events using bHaptics Developer Portal](https://bhaptics.notion.site/Create-haptic-events-using-bHaptics-Developer-Portal-b056c5a56e514afeb0ed436873dd87c6). + +### Conditions +* Tested under Python 3.9 ### Dependencies -* websocket-client +* requests +* keyboard + +### Example Code +Here's a sample code snippet demonstrating how to use the bHaptics SDK with Python: + +```python +import keyboard +import time +from bhaptics.haptic_player import BhapticsSDK2 + +def on_space_pressed(): + sdk_instance.play_event("interaction-open-box") + +def on_2_pressed(): + print("stop_by_event") + sdk_instance.stop_by_event("interaction-open-box") + +def on_1_pressed(): + print("play_event") + sdk_instance.play_event("hit-rush", offset_angle_x=180) + +if __name__ == '__main__': + appId = "yourAppIdfromDeveloperPortal" + apiKey = "yourApiKeyfromDeveloperPortal" + sdk_instance = BhapticsSDK2(appId, apiKey) + try: + keyboard.add_hotkey('space', on_space_pressed) + keyboard.add_hotkey('1', on_1_pressed) + keyboard.add_hotkey('2', on_2_pressed) + print("Press space to play") + while True: + time.sleep(1) + except KeyboardInterrupt: + print("Stopping the client...") + sdk_instance.stop() + finally: + keyboard.remove_hotkey('space') + keyboard.remove_hotkey('1') + keyboard.remove_hotkey('2') + print("Client stopped.") + +``` +### Usage Instructions +Install the required dependencies. +Update the appId and apiKey in the code with your credentials. +Run the script. Press space, '1', or '2' to interact with the bHaptics device. +To stop, press Ctrl+C. \ No newline at end of file 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..ea6d54e 100644 --- a/bhaptics/haptic_player.py +++ b/bhaptics/haptic_player.py @@ -1,122 +1,150 @@ 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 - } - }] - } - - json_str = json.dumps(request) - self.ws.send(json_str) - - def submit_registered(self, key): - submit = { - "Submit": [{ - "Type": "key", - "Key": key, - }] +import time +import requests +import bhaptics.udp_server +import bhaptics.tcp_client +import keyboard +import random + +# Constants and Configuration +API_URL = "https://sdk-apis.bhaptics.com/api/v1/tacthub-api/verify" + + +# Helper Functions +def send_request(url, params): + """Send a GET request and return the response.""" + try: + response = requests.get(url, params=params) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + print(f"Request error: {e}") + return None + + +def generate_message(message_type, message): + """Generate a JSON message.""" + msg = {"message": json.dumps(message), "type": message_type} + return json.dumps(msg) + '\n' + + +def generate_string_message(message_type, message): + """Generate a JSON message.""" + msg = {"message": message, "type": message_type} + return json.dumps(msg) + '\n' + + +def random_request_id(): + return random.randint(1, 99999) + + +# Main Functionalities +class BhapticsSDK2: + def __init__(self, appId, apiKey): + self.appId = appId + self.apiKey = apiKey + self.client = None + self.start() + + def start(self): + bhaptics.udp_server.listen(callback=self.udp_message_received) + + def stop(self): + if self.client: + self.client.stop() + + def udp_message_received(self, message, addr): + """Handle UDP messages.""" + msg = json.loads(message) + print(f"Received UDP message from {addr[0]}:{addr[1]} - {msg}") + + if self.client is None: + self.client = bhaptics.tcp_client.TCPClient(addr[0], msg["port"], + message_received_callback=self.message_received) + self.client.start() + + self.wait_for_client_connection() + self.send_auth_request() + bhaptics.udp_server.stop() + + def wait_for_client_connection(self): + """Wait until the client is connected.""" + while not self.client.status(): + print("Waiting for client to connect...") + time.sleep(1) + + def send_auth_request(self): + """Send authentication request to the client.""" + config = { + "applicationId": self.appId, + "sdkApiKey": self.apiKey, } - 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, - } - }] + auth_message = generate_message("SdkRequestAuthInit", config) + self.client.send_message(auth_message) + + def message_received(self, message): + """Handle TCP messages.""" + msg = json.loads(message) + print(f"Received TCP message: {msg}") + message_type = msg["type"] + print(f"Received TCP type: {message_type}") + + if message_type == "ServerTokenMessage": + message_message = msg["message"] + parsed_message = json.loads(message_message) + token = parsed_message["token"] + token_key = parsed_message["tokenKey"] + self.verify_token(token, token_key) + + def verify_token(self, token, token_key): + """Verify the token with the API.""" + params = {"token": token, "token-key": token_key} + response = send_request(API_URL, params) + if response is not None: + print(f"API Response: {response}") + else: + print("Failed to verify token.") + + def play_event(self, event, intensity=1, duration=1, offset_angle_x=0, offset_y=0): + """Play an event.""" + if self.client is None: + return -1 + + request_id = random_request_id() + play_message = { + "eventName": event, + "requestId": request_id, + "intensity ": intensity, + "duration ": duration, + "offsetAngleX ": offset_angle_x, + "offsetY ": offset_y, } - json_str = json.dumps(submit); - - self.ws.send(json_str) - - def submit(self, key, frame): - submit = { - "Submit": [{ - "Type": "frame", - "Key": key, - "Frame": frame - }] - } + message = generate_message("SdkPlay", play_message) + self.client.send_message(message) - json_str = json.dumps(submit); + return request_id - self.ws.send(json_str) - - def submit_dot(self, key, position, dot_points, duration_millis): - front_frame = { - "position": position, - "dotPoints": dot_points, - "durationMillis": duration_millis - } - self.submit(key, front_frame) + def stop_by_event(self, event): + """Play an event.""" + if self.client is None: + return - def __del__(self): - self.ws.close() + message = generate_message("SdkStopByEventId", event) + self.client.send_message(message) + + +if __name__ == '__main__': + bhaptics_client = BhapticsSDK2("dd", "dd") + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + # If a KeyboardInterrupt (Ctrl+C) is detected, the program will exit this loop + print("Stopping the client...") + bhaptics_client.stop() + finally: + # Remove the hotkey hook + keyboard.remove_hotkey('space') + print("Client stopped.") diff --git a/bhaptics/tcp_client.py b/bhaptics/tcp_client.py new file mode 100644 index 0000000..f5e3a5f --- /dev/null +++ b/bhaptics/tcp_client.py @@ -0,0 +1,109 @@ +import socket +import threading + + +class TCPClient: + def __init__(self, server_host, server_port, message_received_callback=None): + self.server_host = server_host + self.server_port = server_port + self.message_received_callback = message_received_callback + self.client_socket = None + self.connected = False + self.lock = threading.Lock() + self.receive_thread = None + + def start(self, initial_message=None): + with self.lock: + 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 + 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 + print(f"An error occurred while connecting: {e}") + else: + print("Client is already connected.") + + def receive_messages(self): + while self.connected: + try: + response = self.client_socket.recv(4096) # buffer size is 4096 bytes + if response: + # Call the callback function if any message is received + if self.message_received_callback: + print("received", response) + self.message_received_callback(response.decode()) + else: + # No response indicates the server has closed the connection + print("The server has closed the connection.") + self.stop() + break + except Exception as e: + print(f"An error occurred: {e}") + self.stop() + break + + def send_message(self, message): + if self.connected: + try: + print("sendMessage", message) + self.client_socket.sendall(message.encode('utf-8')) + except Exception as e: + print(f"An error occurred while sending message: {e}") + else: + print("Cannot send message. The client is not connected.") + + def stop(self): + with self.lock: + if self.connected: + self.connected = False + self.client_socket.close() + self.receive_thread.join() + print("Connection closed.") + else: + print("Client is not connected.") + + def status(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.status(): + 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.status(): + 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..f4761d6 --- /dev/null +++ b/bhaptics/udp_server.py @@ -0,0 +1,58 @@ +import socket +import threading + + +class UDPServer: + def __init__(self, host="0.0.0.0", port=15884, callback=None): + self.host = host + self.port = port + self.callback = callback + self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.running = False + self.thread = None + + def start_server(self): + self.sock.bind((self.host, self.port)) + self.running = True + self.thread = threading.Thread(target=self.listen) + self.thread.start() + print(f"UDP server listening on {self.host}:{self.port}") + + def listen(self): + try: + while self.running: + data, addr = self.sock.recvfrom(1024) # buffer size is 1024 bytes + message = data.decode() + print(f"Received message: {message} from {addr}") + if self.callback: + self.callback(message, addr) + except Exception as e: + print(f"An error occurred: {e}") + finally: + self.sock.close() + + def stop_server(self): + self.running = False + # We send a dummy message to the server to unblock the recvfrom call + self.sock.sendto(b'', (self.host, self.port)) + self.sock.close() + if self.thread is not None: + self.thread.join() + print("UDP server stopped.") + + +# Global server instance +udp_server = None + + +# Exportable functions +def listen(host="0.0.0.0", port=15884, callback=None): + global udp_server + udp_server = UDPServer(host, port, callback) + 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..0220d7f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ -websocket-client~=0.57.0 -python-osc~=1.7.4 +requests>=2.8.1 keyboard~=0.13.5 \ No newline at end of file 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..c8a5418 100644 --- a/sample.py +++ b/sample.py @@ -1,39 +1,39 @@ -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 keyboard +import time +from bhaptics.haptic_player import BhapticsSDK2 +sdk_instance = None + + +def on_space_pressed(): + sdk_instance.play_event("interaction-open-box") + + +def on_2_pressed(): + print("stop_by_event") + sdk_instance.stop_by_event("interaction-open-box") + + +def on_1_pressed(): + print("play_event") + sdk_instance.play_event("hit-rush", offset_angle_x=180) + + +if __name__ == '__main__': + # sdk_instance = BhapticsSDK2("yourAppId", "yourApiKey") + appId = "appId" + apiKey = "apiKey" + sdk_instance = BhapticsSDK2(appId, apiKey) + try: + keyboard.add_hotkey('space', on_space_pressed) + keyboard.add_hotkey('1', on_1_pressed) + keyboard.add_hotkey('2', on_2_pressed) + print("Press space to play") + while True: + time.sleep(1) + except KeyboardInterrupt: + print("Stopping the client...") + sdk_instance.stop() + finally: + keyboard.remove_hotkey('space') + print("Client stopped.") From a99134dd296e5778786de4c3a4590616de64ac92 Mon Sep 17 00:00:00 2001 From: Sangwon Date: Fri, 26 Jan 2024 18:45:39 +0900 Subject: [PATCH 02/18] fix: minor update --- README.md | 17 ++++++++++++----- bhaptics/haptic_player.py | 29 ++++++++++++++++------------- sample.py | 31 +++++++------------------------ 3 files changed, 35 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index baaf4a0..4e2b543 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ### Dependencies * requests -* keyboard + ### Example Code Here's a sample code snippet demonstrating how to use the bHaptics SDK with Python: @@ -54,7 +54,14 @@ if __name__ == '__main__': ``` ### Usage Instructions -Install the required dependencies. -Update the appId and apiKey in the code with your credentials. -Run the script. Press space, '1', or '2' to interact with the bHaptics device. -To stop, press Ctrl+C. \ No newline at end of file +* Install the required dependencies. + +``` +pip install -r requirements.txt +``` + +* Update the appId and apiKey in the code with your credentials. + +Visit developer.bhaptics.com to create your own project. + +* Execute the script, and it will periodically play haptic patterns every 5 seconds. \ No newline at end of file diff --git a/bhaptics/haptic_player.py b/bhaptics/haptic_player.py index ea6d54e..68a705c 100644 --- a/bhaptics/haptic_player.py +++ b/bhaptics/haptic_player.py @@ -3,7 +3,6 @@ import requests import bhaptics.udp_server import bhaptics.tcp_client -import keyboard import random # Constants and Configuration @@ -81,7 +80,7 @@ def send_auth_request(self): } auth_message = generate_message("SdkRequestAuthInit", config) - self.client.send_message(auth_message) + # self.client.send_message(auth_message) def message_received(self, message): """Handle TCP messages.""" @@ -106,11 +105,16 @@ def verify_token(self, token, token_key): else: print("Failed to verify token.") - def play_event(self, event, intensity=1, duration=1, offset_angle_x=0, offset_y=0): + def play_event(self, event): # , intensity=1, duration=1, offset_angle_x=0, offset_y=0): """Play an event.""" if self.client is None: return -1 + intensity = 1 + duration = 1 + offset_angle_x = 0 + offset_y = 0 + request_id = random_request_id() play_message = { "eventName": event, @@ -125,14 +129,14 @@ def play_event(self, event, intensity=1, duration=1, offset_angle_x=0, offset_y= self.client.send_message(message) return request_id - - def stop_by_event(self, event): - """Play an event.""" - if self.client is None: - return - - message = generate_message("SdkStopByEventId", event) - self.client.send_message(message) + # + # def stop_by_event(self, event): + # """Play an event.""" + # if self.client is None: + # return + # + # message = generate_message("SdkStopByEventId", event) + # self.client.send_message(message) if __name__ == '__main__': @@ -145,6 +149,5 @@ def stop_by_event(self, event): print("Stopping the client...") bhaptics_client.stop() finally: - # Remove the hotkey hook - keyboard.remove_hotkey('space') + print("") print("Client stopped.") diff --git a/sample.py b/sample.py index c8a5418..6559b13 100644 --- a/sample.py +++ b/sample.py @@ -1,39 +1,22 @@ -import keyboard import time from bhaptics.haptic_player import BhapticsSDK2 sdk_instance = None -def on_space_pressed(): - sdk_instance.play_event("interaction-open-box") - - -def on_2_pressed(): - print("stop_by_event") - sdk_instance.stop_by_event("interaction-open-box") - - -def on_1_pressed(): - print("play_event") - sdk_instance.play_event("hit-rush", offset_angle_x=180) +def on_play(): + sdk_instance.play_event("shoot_test") if __name__ == '__main__': - # sdk_instance = BhapticsSDK2("yourAppId", "yourApiKey") - appId = "appId" - apiKey = "apiKey" + # Replace 'yourAppId' and 'yourApiKey' with your actual appId and apiKey + appId = "mWK8BbDgpx9LdZVR22ij" + apiKey = "m9ef4q9oQRXbPeJY9z4J" sdk_instance = BhapticsSDK2(appId, apiKey) try: - keyboard.add_hotkey('space', on_space_pressed) - keyboard.add_hotkey('1', on_1_pressed) - keyboard.add_hotkey('2', on_2_pressed) - print("Press space to play") while True: - time.sleep(1) + time.sleep(5) + sdk_instance.play_event("shoot_test") except KeyboardInterrupt: print("Stopping the client...") sdk_instance.stop() - finally: - keyboard.remove_hotkey('space') - print("Client stopped.") From f8dde3ec1debadde32fc685a5bbecc290e62d805 Mon Sep 17 00:00:00 2001 From: Sangwon Choi Date: Fri, 26 Jan 2024 18:51:02 +0900 Subject: [PATCH 03/18] Update README.md --- README.md | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 4e2b543..949525d 100644 --- a/README.md +++ b/README.md @@ -16,40 +16,28 @@ Here's a sample code snippet demonstrating how to use the bHaptics SDK with Python: ```python -import keyboard import time from bhaptics.haptic_player import BhapticsSDK2 -def on_space_pressed(): - sdk_instance.play_event("interaction-open-box") +sdk_instance = None -def on_2_pressed(): - print("stop_by_event") - sdk_instance.stop_by_event("interaction-open-box") -def on_1_pressed(): - print("play_event") - sdk_instance.play_event("hit-rush", offset_angle_x=180) +def on_play(): + sdk_instance.play_event("shoot_test") + if __name__ == '__main__': - appId = "yourAppIdfromDeveloperPortal" - apiKey = "yourApiKeyfromDeveloperPortal" + # Replace 'yourAppId' and 'yourApiKey' with your actual appId and apiKey + appId = "yourAppId" + apiKey = "yourApiKey" sdk_instance = BhapticsSDK2(appId, apiKey) try: - keyboard.add_hotkey('space', on_space_pressed) - keyboard.add_hotkey('1', on_1_pressed) - keyboard.add_hotkey('2', on_2_pressed) - print("Press space to play") while True: - time.sleep(1) + time.sleep(5) + sdk_instance.play_event("shoot_test") except KeyboardInterrupt: print("Stopping the client...") sdk_instance.stop() - finally: - keyboard.remove_hotkey('space') - keyboard.remove_hotkey('1') - keyboard.remove_hotkey('2') - print("Client stopped.") ``` @@ -64,4 +52,4 @@ pip install -r requirements.txt Visit developer.bhaptics.com to create your own project. -* Execute the script, and it will periodically play haptic patterns every 5 seconds. \ No newline at end of file +* Execute the script, and it will periodically play haptic patterns every 5 seconds. From 7a869713aa23aed4b622af79a7cbfe98bf8ccd61 Mon Sep 17 00:00:00 2001 From: Sangwon Choi Date: Fri, 26 Jan 2024 19:09:02 +0900 Subject: [PATCH 04/18] fix: remove whitespace --- bhaptics/haptic_player.py | 10 +++++----- sample.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bhaptics/haptic_player.py b/bhaptics/haptic_player.py index 68a705c..4cf4985 100644 --- a/bhaptics/haptic_player.py +++ b/bhaptics/haptic_player.py @@ -105,7 +105,7 @@ def verify_token(self, token, token_key): else: print("Failed to verify token.") - def play_event(self, event): # , intensity=1, duration=1, offset_angle_x=0, offset_y=0): + def play_event(self, event, intensity=1, duration=1, offset_angle_x=0, offset_y=0): """Play an event.""" if self.client is None: return -1 @@ -119,10 +119,10 @@ def play_event(self, event): # , intensity=1, duration=1, offset_angle_x=0, off play_message = { "eventName": event, "requestId": request_id, - "intensity ": intensity, - "duration ": duration, - "offsetAngleX ": offset_angle_x, - "offsetY ": offset_y, + "intensity": intensity, + "duration": duration, + "offsetAngleX": offset_angle_x, + "offsetY": offset_y, } message = generate_message("SdkPlay", play_message) diff --git a/sample.py b/sample.py index 6559b13..6de8744 100644 --- a/sample.py +++ b/sample.py @@ -15,7 +15,7 @@ def on_play(): sdk_instance = BhapticsSDK2(appId, apiKey) try: while True: - time.sleep(5) + time.sleep(3) sdk_instance.play_event("shoot_test") except KeyboardInterrupt: print("Stopping the client...") From df1be0557ee27d3681a827d355cdd10914f2db03 Mon Sep 17 00:00:00 2001 From: Laeyoung Chang Date: Fri, 26 Jan 2024 20:37:04 +0900 Subject: [PATCH 05/18] feat: remove stop_by_event(), and stop_all() # Conflicts: # bhaptics/haptic_player.py # sample.py --- bhaptics/haptic_player.py | 17 ++++++++--------- sample.py | 21 ++++++++++++++++++++- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/bhaptics/haptic_player.py b/bhaptics/haptic_player.py index 4cf4985..9830135 100644 --- a/bhaptics/haptic_player.py +++ b/bhaptics/haptic_player.py @@ -80,7 +80,7 @@ def send_auth_request(self): } auth_message = generate_message("SdkRequestAuthInit", config) - # self.client.send_message(auth_message) + self.client.send_message(auth_message) def message_received(self, message): """Handle TCP messages.""" @@ -129,15 +129,14 @@ def play_event(self, event, intensity=1, duration=1, offset_angle_x=0, offset_y= self.client.send_message(message) return request_id - # - # def stop_by_event(self, event): - # """Play an event.""" - # if self.client is None: - # return - # - # message = generate_message("SdkStopByEventId", event) - # self.client.send_message(message) + + def stop_all(self): + """Stop all events.""" + if self.client is None: + return + message = generate_message("SdkStopAll", "") + self.client.send_message(message) if __name__ == '__main__': bhaptics_client = BhapticsSDK2("dd", "dd") diff --git a/sample.py b/sample.py index 6de8744..a0a7284 100644 --- a/sample.py +++ b/sample.py @@ -1,4 +1,6 @@ +# import keyboard import time + from bhaptics.haptic_player import BhapticsSDK2 sdk_instance = None @@ -14,9 +16,26 @@ def on_play(): apiKey = "m9ef4q9oQRXbPeJY9z4J" sdk_instance = BhapticsSDK2(appId, apiKey) try: + print("Play 'shoot_test' event") + time.sleep(3) + while True: - time.sleep(3) sdk_instance.play_event("shoot_test") + time.sleep(5) + + sdk_instance.play_event("shoot_test", duration=2) + time.sleep(5) + + sdk_instance.play_event("shoot_test", duration=2) + time.sleep(0.2) + sdk_instance.stop_all() + time.sleep(5) + except KeyboardInterrupt: print("Stopping the client...") sdk_instance.stop() + + finally: + print("Finally...") + + print("Client stopped.") From 623c48fc7d79bd0e03bab3b029e264fd6fbbfb88 Mon Sep 17 00:00:00 2001 From: Laeyoung Chang Date: Mon, 29 Jan 2024 19:53:57 +0900 Subject: [PATCH 06/18] fix: remove reassigned default values --- bhaptics/haptic_player.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/bhaptics/haptic_player.py b/bhaptics/haptic_player.py index 9830135..8ace7f6 100644 --- a/bhaptics/haptic_player.py +++ b/bhaptics/haptic_player.py @@ -110,11 +110,6 @@ def play_event(self, event, intensity=1, duration=1, offset_angle_x=0, offset_y= if self.client is None: return -1 - intensity = 1 - duration = 1 - offset_angle_x = 0 - offset_y = 0 - request_id = random_request_id() play_message = { "eventName": event, From d7ee6db31a6349943a96491c3a79eed63d2af370 Mon Sep 17 00:00:00 2001 From: Sangwon Choi Date: Fri, 5 Apr 2024 18:49:07 +0900 Subject: [PATCH 07/18] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 949525d..6c384a0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ ### Sample code for python ### Prerequisite -* bHaptics Beta App is installed. -* To get access, please contact developer@bhaptics.com. +* [bHaptics Hub App](https://play.google.com/store/apps/details?id=com.bhaptics.hub) is installed. * Setup your project at the bHaptics Developer Portal: [Create haptic events using bHaptics Developer Portal](https://bhaptics.notion.site/Create-haptic-events-using-bHaptics-Developer-Portal-b056c5a56e514afeb0ed436873dd87c6). ### Conditions From 8f0f86d9604ac9febf41f475212ab098ffa93151 Mon Sep 17 00:00:00 2001 From: Laeyoung Chang Date: Mon, 8 Apr 2024 12:58:18 +0900 Subject: [PATCH 08/18] feat: add play_dot_mode method --- bhaptics/haptic_player.py | 21 +++++++++++++++++ sample.py | 47 ++++++++++++++++++++++++++++++++++----- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/bhaptics/haptic_player.py b/bhaptics/haptic_player.py index 8ace7f6..8c902d8 100644 --- a/bhaptics/haptic_player.py +++ b/bhaptics/haptic_player.py @@ -125,6 +125,27 @@ def play_event(self, event, intensity=1, duration=1, offset_angle_x=0, offset_y= return request_id + + + # pos 0 is for TactSuit series device like X40 or X16. + def play_dot_mode(self, motors, pos=0, duration=1): + """Play an dot mode event.""" + if self.client is None: + return -1 + + request_id = random_request_id() + play_message = { + "requestId": request_id, + "pos": pos, + "durationMillis": duration * 1000, + "motors": motors + } + + message = generate_message("SdkPlayDotMode", play_message) + self.client.send_message(message) + + return request_id + def stop_all(self): """Stop all events.""" if self.client is None: diff --git a/sample.py b/sample.py index a0a7284..9836f74 100644 --- a/sample.py +++ b/sample.py @@ -20,16 +20,51 @@ def on_play(): time.sleep(3) while True: - sdk_instance.play_event("shoot_test") - time.sleep(5) + sdk_instance.play_dot_mode( + [ + 30, 30, 30, 30, + 30, 30, 30, 30, + 30, 30, 30, 30, + 30, 30, 30, 30, + 30, 30, 30, 30, - sdk_instance.play_event("shoot_test", duration=2) + 30, 30, 30, 30, + 30, 30, 30, 30, + 30, 30, 30, 30, + 30, 30, 30, 30, + 30, 30, 30, 30, + ] + ) time.sleep(5) - sdk_instance.play_event("shoot_test", duration=2) - time.sleep(0.2) - sdk_instance.stop_all() + sdk_instance.play_dot_mode( + [ + 0, 0, 0, 0, + 0, 0, 0, 0, + 50, 50, 50, 50, + 0, 0, 0, 0, + 0, 0, 0, 0, + + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + ], + duration=3 + ) time.sleep(5) + + # sdk_instance.play_event("shoot_test") + # time.sleep(5) + + # sdk_instance.play_event("shoot_test", duration=2) + # time.sleep(5) + + # sdk_instance.play_event("shoot_test", duration=2) + # time.sleep(0.2) + # sdk_instance.stop_all() + # time.sleep(5) except KeyboardInterrupt: print("Stopping the client...") From e885442e16dc15cce015a4e90536b44efe8370b7 Mon Sep 17 00:00:00 2001 From: Laeyoung Chang Date: Mon, 8 Apr 2024 13:59:29 +0900 Subject: [PATCH 09/18] feat: add play_path_mode method (not working well) --- bhaptics/haptic_player.py | 36 ++++++++++++++++++-- sample.py | 71 ++++++++++++++++++++++----------------- 2 files changed, 73 insertions(+), 34 deletions(-) diff --git a/bhaptics/haptic_player.py b/bhaptics/haptic_player.py index 8c902d8..7e72787 100644 --- a/bhaptics/haptic_player.py +++ b/bhaptics/haptic_player.py @@ -125,9 +125,7 @@ def play_event(self, event, intensity=1, duration=1, offset_angle_x=0, offset_y= return request_id - - - # pos 0 is for TactSuit series device like X40 or X16. + # pos 0 is for TactSuit series like X40 or X16. def play_dot_mode(self, motors, pos=0, duration=1): """Play an dot mode event.""" if self.client is None: @@ -146,6 +144,38 @@ def play_dot_mode(self, motors, pos=0, duration=1): return request_id + # pos 0 is for TactSuit series like X40 or X16. + # ------- + # 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_path_mode(self, x_list, y_list, intensity_list, pos=0, duration=1): + """Play an dot mode event.""" + if self.client is None: + return -1 + + request_id = random_request_id() + play_message = { + "requestId": request_id, + "pos": pos, + "durationMillis": duration * 1000, + "x": x_list, + "y": y_list, + "intensity": intensity_list + } + + message = generate_message("SdkPlayPathMode", play_message) + self.client.send_message(message) + + return request_id + def stop_all(self): """Stop all events.""" if self.client is None: diff --git a/sample.py b/sample.py index 9836f74..f821ba7 100644 --- a/sample.py +++ b/sample.py @@ -20,41 +20,50 @@ def on_play(): time.sleep(3) while True: - sdk_instance.play_dot_mode( - [ - 30, 30, 30, 30, - 30, 30, 30, 30, - 30, 30, 30, 30, - 30, 30, 30, 30, - 30, 30, 30, 30, - - 30, 30, 30, 30, - 30, 30, 30, 30, - 30, 30, 30, 30, - 30, 30, 30, 30, - 30, 30, 30, 30, - ] - ) - time.sleep(5) + # sdk_instance.play_dot_mode( + # [ + # 30, 30, 30, 30, + # 30, 30, 30, 30, + # 30, 30, 30, 30, + # 30, 30, 30, 30, + # 30, 30, 30, 30, + + # 30, 30, 30, 30, + # 30, 30, 30, 30, + # 30, 30, 30, 30, + # 30, 30, 30, 30, + # 30, 30, 30, 30, + # ] + # ) + # time.sleep(5) - sdk_instance.play_dot_mode( - [ - 0, 0, 0, 0, - 0, 0, 0, 0, - 50, 50, 50, 50, - 0, 0, 0, 0, - 0, 0, 0, 0, - - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, - ], - duration=3 + # sdk_instance.play_dot_mode( + # [ + # 0, 0, 0, 0, + # 0, 0, 0, 0, + # 50, 50, 50, 50, + # 0, 0, 0, 0, + # 0, 0, 0, 0, + + # 0, 0, 0, 0, + # 0, 0, 0, 0, + # 0, 0, 0, 0, + # 0, 0, 0, 0, + # 0, 0, 0, 0, + # ], + # duration=3 + # ) + # time.sleep(5) + + sdk_instance.play_path_mode( + x_list=[0.0, 1.0, 0.5], + y_list=[0.0, 1.0, 0.5], + intensity_list=[20, 100, 20], + duration=2 ) time.sleep(5) + # sdk_instance.play_event("shoot_test") # time.sleep(5) From ca3888859051a9215e8ec2cc3b9d8892adb03228 Mon Sep 17 00:00:00 2001 From: Laeyoung Chang Date: Tue, 9 Apr 2024 11:14:42 +0900 Subject: [PATCH 10/18] feat: update path mode sample and update comments --- bhaptics/haptic_player.py | 37 ++++++++--- sample.py | 130 ++++++++++++++++++++++++-------------- 2 files changed, 110 insertions(+), 57 deletions(-) diff --git a/bhaptics/haptic_player.py b/bhaptics/haptic_player.py index 7e72787..7ab4a0c 100644 --- a/bhaptics/haptic_player.py +++ b/bhaptics/haptic_player.py @@ -125,21 +125,24 @@ def play_event(self, event, intensity=1, duration=1, offset_angle_x=0, offset_y= return request_id - # pos 0 is for TactSuit series like X40 or X16. - def play_dot_mode(self, motors, pos=0, duration=1): - """Play an dot mode event.""" + def play_loop(self, event, intensity=1, duration=1, interval=0, maxCount=0, offset_angle_x=0, offset_y=0): + """Play loop event.""" if self.client is None: return -1 request_id = random_request_id() play_message = { + "eventName": event, "requestId": request_id, - "pos": pos, - "durationMillis": duration * 1000, - "motors": motors + "intensity": intensity, + "duration": duration, + "interval": interval, + "maxCount": maxCount, + "offsetAngleX": offset_angle_x, + "offsetY": offset_y, } - message = generate_message("SdkPlayDotMode", play_message) + message = generate_message("SdkPlayLoop", play_message) self.client.send_message(message) return request_id @@ -156,11 +159,29 @@ def play_dot_mode(self, motors, pos=0, duration=1): # FOOT_R = 7 # GLOVE_L = 8 # GLOVE_R = 9 - def play_path_mode(self, x_list, y_list, intensity_list, pos=0, duration=1): + def play_dot_mode(self, motors, pos=0, duration=1): """Play an dot mode event.""" if self.client is None: return -1 + request_id = random_request_id() + play_message = { + "requestId": request_id, + "pos": pos, + "durationMillis": duration * 1000, + "motors": motors + } + + message = generate_message("SdkPlayDotMode", play_message) + self.client.send_message(message) + + return request_id + + def play_path_mode(self, x_list, y_list, intensity_list, pos=0, duration=1): + """Play an path mode event.""" + if self.client is None: + return -1 + request_id = random_request_id() play_message = { "requestId": request_id, diff --git a/sample.py b/sample.py index f821ba7..9391851 100644 --- a/sample.py +++ b/sample.py @@ -16,64 +16,96 @@ def on_play(): apiKey = "m9ef4q9oQRXbPeJY9z4J" sdk_instance = BhapticsSDK2(appId, apiKey) try: - print("Play 'shoot_test' event") time.sleep(3) while True: - # sdk_instance.play_dot_mode( - # [ - # 30, 30, 30, 30, - # 30, 30, 30, 30, - # 30, 30, 30, 30, - # 30, 30, 30, 30, - # 30, 30, 30, 30, - - # 30, 30, 30, 30, - # 30, 30, 30, 30, - # 30, 30, 30, 30, - # 30, 30, 30, 30, - # 30, 30, 30, 30, - # ] - # ) - # time.sleep(5) - - # sdk_instance.play_dot_mode( - # [ - # 0, 0, 0, 0, - # 0, 0, 0, 0, - # 50, 50, 50, 50, - # 0, 0, 0, 0, - # 0, 0, 0, 0, - - # 0, 0, 0, 0, - # 0, 0, 0, 0, - # 0, 0, 0, 0, - # 0, 0, 0, 0, - # 0, 0, 0, 0, - # ], - # duration=3 - # ) - # time.sleep(5) - - sdk_instance.play_path_mode( - x_list=[0.0, 1.0, 0.5], - y_list=[0.0, 1.0, 0.5], - intensity_list=[20, 100, 20], - duration=2 + print("\nPlay haptic samples!!!") + + # Play Dot Mode - sample 1 + print("Play Dot Mode - sample 1") + sdk_instance.play_dot_mode( + [ + # 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, + ] ) time.sleep(5) + # Play Dot Mode - sample 2 + print("Play Dot Mode - sample 2") + sdk_instance.play_dot_mode( + [ + # 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=3 + ) + time.sleep(5) + + + # Play Path Mode - sample + print("Play Path Mode - sample") + for i in [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]: + sdk_instance.play_path_mode( + x_list=[i], + y_list=[0.5], + intensity_list=[5], + duration=1 + ) + + time.sleep(0.5) + + time.sleep(5) + - # sdk_instance.play_event("shoot_test") - # time.sleep(5) + # Play an "shoot_test" event. + print("Play an shoot_test event.") + sdk_instance.play_event("shoot_test") + time.sleep(5) + + # Play an "shoot_test" event with duration to double. + print("Play an shoot_test event with duration to double.") + sdk_instance.play_event("shoot_test", duration=2) + time.sleep(5) + + # Play an "shoot_test" event, and stop all event. + print("Play an shoot_test event, and stop all event.") + sdk_instance.play_event("shoot_test", duration=2) + time.sleep(0.2) + sdk_instance.stop_all() + time.sleep(5) + + + # Repeat the event 3 times + print("Repeat the event 3 times") + sdk_instance.play_loop("shoot_test", interval=1, maxCount=3) + time.sleep(5) - # sdk_instance.play_event("shoot_test", duration=2) - # time.sleep(5) - # sdk_instance.play_event("shoot_test", duration=2) - # time.sleep(0.2) - # sdk_instance.stop_all() - # time.sleep(5) + print("Cooldown....") + time.sleep(10) except KeyboardInterrupt: print("Stopping the client...") From da657b972d0449e28b839d5c3be94a88b75476cd Mon Sep 17 00:00:00 2001 From: Laeyoung Date: Tue, 9 Apr 2024 12:23:23 +0900 Subject: [PATCH 11/18] Update README.md --- README.md | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 6c384a0..5ee97a3 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,29 @@ -### Sample code for python +# Sample code for python -### Prerequisite -* [bHaptics Hub App](https://play.google.com/store/apps/details?id=com.bhaptics.hub) is installed. -* Setup your project at the bHaptics Developer Portal: [Create haptic events using bHaptics Developer Portal](https://bhaptics.notion.site/Create-haptic-events-using-bHaptics-Developer-Portal-b056c5a56e514afeb0ed436873dd87c6). +## Prerequisite +- One or more TactSuits (X40, X16, and so on) +- bHaptics Hub App ([android](https://bit.ly/bhaptics-hub-android)) is installed. +- Setup your project at the bHaptics Developer Portal: [Create haptic events using bHaptics Developer Portal](https://bhaptics.notion.site/Create-haptic-events-using-bHaptics-Developer-Portal-b056c5a56e514afeb0ed436873dd87c6). ### Conditions -* Tested under Python 3.9 +- Tested under Python 3.9 ### Dependencies -* requests - - -### Example Code +- requests + +## Getting started with sample code +1. Install bHaptics Hub App - [android](https://bit.ly/bhaptics-hub-android) +2. Setup bHaptics Hub App with your TactSuit - [bHaptics Hub Guide](https://bit.ly/bHaptics-Hub-Guide) +3. Go source directory and install the required dependencies. + ```bash + pip install -r requirements.txt + ``` +4. Run sample code + ```bash + python sample.py + ``` + +## Example Code Here's a sample code snippet demonstrating how to use the bHaptics SDK with Python: ```python @@ -40,15 +52,7 @@ if __name__ == '__main__': ``` -### Usage Instructions -* Install the required dependencies. - -``` -pip install -r requirements.txt -``` - -* Update the appId and apiKey in the code with your credentials. - -Visit developer.bhaptics.com to create your own project. +## Next +- Visit [developer.bhaptics.com](https://developer.bhaptics.com/) to create your own project. +- Update the appId and apiKey in the code with your credentials. -* Execute the script, and it will periodically play haptic patterns every 5 seconds. From 6f36eea4e8f46651cca40102e1b2e1dc37be4d19 Mon Sep 17 00:00:00 2001 From: Auejin Ham Date: Tue, 9 Apr 2024 15:07:14 +0900 Subject: [PATCH 12/18] feat: make readme more readable --- README.md | 48 +++++++++++++++++++++++++----------------------- requirements.txt | 1 - sample.py | 22 ++++++++-------------- 3 files changed, 33 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 5ee97a3..a1850ed 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ -# Sample code for python +# Python SDK for bHaptics Hub -## Prerequisite -- One or more TactSuits (X40, X16, and so on) -- bHaptics Hub App ([android](https://bit.ly/bhaptics-hub-android)) is installed. -- Setup your project at the bHaptics Developer Portal: [Create haptic events using bHaptics Developer Portal](https://bhaptics.notion.site/Create-haptic-events-using-bHaptics-Developer-Portal-b056c5a56e514afeb0ed436873dd87c6). +## 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 @@ -11,20 +12,22 @@ ### Dependencies - requests -## Getting started with sample code -1. Install bHaptics Hub App - [android](https://bit.ly/bhaptics-hub-android) -2. Setup bHaptics Hub App with your TactSuit - [bHaptics Hub Guide](https://bit.ly/bHaptics-Hub-Guide) -3. Go source directory and install the required dependencies. +## 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 - pip install -r requirements.txt + pip3 install -r requirements.txt ``` -4. Run sample code +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 - python sample.py + python3 sample.py ``` -## Example Code -Here's a sample code snippet demonstrating how to use the bHaptics SDK with Python: +## Example +This example demonstrates how to use the bHaptics SDK with Python: ```python import time @@ -32,19 +35,15 @@ from bhaptics.haptic_player import BhapticsSDK2 sdk_instance = None - -def on_play(): - sdk_instance.play_event("shoot_test") - - if __name__ == '__main__': - # Replace 'yourAppId' and 'yourApiKey' with your actual appId and apiKey + # 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...") @@ -52,7 +51,10 @@ if __name__ == '__main__': ``` -## Next -- Visit [developer.bhaptics.com](https://developer.bhaptics.com/) to create your own project. -- Update the appId and apiKey in the code with your credentials. +## 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/requirements.txt b/requirements.txt index 0220d7f..6d3327c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ requests>=2.8.1 -keyboard~=0.13.5 \ No newline at end of file diff --git a/sample.py b/sample.py index 9391851..6d650a5 100644 --- a/sample.py +++ b/sample.py @@ -1,15 +1,9 @@ -# import keyboard import time from bhaptics.haptic_player import BhapticsSDK2 sdk_instance = None - -def on_play(): - sdk_instance.play_event("shoot_test") - - if __name__ == '__main__': # Replace 'yourAppId' and 'yourApiKey' with your actual appId and apiKey appId = "mWK8BbDgpx9LdZVR22ij" @@ -80,26 +74,26 @@ def on_play(): time.sleep(5) - # Play an "shoot_test" event. - print("Play an shoot_test event.") + # Play "shoot_test" event. + print("Play `shoot_test` event.") sdk_instance.play_event("shoot_test") time.sleep(5) - # Play an "shoot_test" event with duration to double. - print("Play an shoot_test event with duration to double.") + # Play "shoot_test" event with its duration doubled. + print("Play `shoot_test` event with its duration doubled.") sdk_instance.play_event("shoot_test", duration=2) time.sleep(5) - # Play an "shoot_test" event, and stop all event. - print("Play an shoot_test event, and stop all event.") + # Start playing "shoot_test" event and stop all events immediately. + print("Start playing `shoot_test` event and stop all events immediately.") sdk_instance.play_event("shoot_test", duration=2) time.sleep(0.2) sdk_instance.stop_all() time.sleep(5) - # Repeat the event 3 times - print("Repeat the event 3 times") + # Repeat "shoot_test" event 3 times. + print("Repeat `shoot_test` event 3 times.") sdk_instance.play_loop("shoot_test", interval=1, maxCount=3) time.sleep(5) From b39153bcdf55af2c1705a5999bff5cebdb0f422d Mon Sep 17 00:00:00 2001 From: Auejin Ham Date: Tue, 16 Apr 2024 16:59:55 +0900 Subject: [PATCH 13/18] refactor: tcp_client, udp_server --- bhaptics/haptic_player.py | 12 ++--- bhaptics/tcp_client.py | 107 +++++++++++++++++++++----------------- bhaptics/udp_server.py | 61 +++++++++++++--------- 3 files changed, 100 insertions(+), 80 deletions(-) diff --git a/bhaptics/haptic_player.py b/bhaptics/haptic_player.py index 7ab4a0c..6fd297c 100644 --- a/bhaptics/haptic_player.py +++ b/bhaptics/haptic_player.py @@ -1,8 +1,8 @@ import json import time import requests -import bhaptics.udp_server -import bhaptics.tcp_client +import bhaptics.udp_server as udp_server +import bhaptics.tcp_client as tcp_client import random # Constants and Configuration @@ -46,7 +46,7 @@ def __init__(self, appId, apiKey): self.start() def start(self): - bhaptics.udp_server.listen(callback=self.udp_message_received) + udp_server.listen(callback=self.udp_message_received) def stop(self): if self.client: @@ -58,17 +58,17 @@ def udp_message_received(self, message, addr): print(f"Received UDP message from {addr[0]}:{addr[1]} - {msg}") if self.client is None: - self.client = bhaptics.tcp_client.TCPClient(addr[0], msg["port"], + self.client = tcp_client.TCPClient(addr[0], msg["port"], message_received_callback=self.message_received) self.client.start() self.wait_for_client_connection() self.send_auth_request() - bhaptics.udp_server.stop() + udp_server.stop() def wait_for_client_connection(self): """Wait until the client is connected.""" - while not self.client.status(): + while not self.client.is_connected(): print("Waiting for client to connect...") time.sleep(1) diff --git a/bhaptics/tcp_client.py b/bhaptics/tcp_client.py index f5e3a5f..70405f4 100644 --- a/bhaptics/tcp_client.py +++ b/bhaptics/tcp_client.py @@ -1,80 +1,89 @@ import socket import threading - class TCPClient: - def __init__(self, server_host, server_port, message_received_callback=None): + 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.lock = threading.Lock() self.receive_thread = None - def start(self, initial_message=None): - with self.lock: - 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 - 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 - print(f"An error occurred while connecting: {e}") - else: - print("Client is already connected.") - - def receive_messages(self): - while self.connected: - try: - response = self.client_socket.recv(4096) # buffer size is 4096 bytes + 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: - print("received", response) + self.__print("App → Client: ", response) self.message_received_callback(response.decode()) else: # No response indicates the server has closed the connection - print("The server has closed the connection.") - self.stop() break - except Exception as e: - print(f"An error occurred: {e}") - self.stop() - break + except Exception as e: + if self.connected: + self.__print(f"An error occurred in TCP: {e}") + finally: + self.stop() def send_message(self, message): + self.__print("Client → App: ", message) + if self.connected: try: - print("sendMessage", message) self.client_socket.sendall(message.encode('utf-8')) except Exception as e: - print(f"An error occurred while sending message: {e}") + self.__print(f"\t sending failed: {e}.") else: - print("Cannot send message. The client is not connected.") + 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): - with self.lock: - if self.connected: + if self.connected: + try: self.connected = False self.client_socket.close() - self.receive_thread.join() - print("Connection closed.") - else: - print("Client is not connected.") + except OSError as e: + self.__print(f"Error closing TCP socket: {e}") + + 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}") + + self.__print(f"Closed TCP connection with app.") - def status(self): + def is_connected(self): return self.connected @@ -94,7 +103,7 @@ def message_received(message): client.send_message("Another message") # Check connection status - if client.status(): + if client.is_connected(): print("Client is connected.") else: print("Client is not connected.") @@ -102,7 +111,7 @@ def message_received(message): # 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.status(): + while client.is_connected(): pass except KeyboardInterrupt: print("Stopping the client...") diff --git a/bhaptics/udp_server.py b/bhaptics/udp_server.py index f4761d6..3a9107f 100644 --- a/bhaptics/udp_server.py +++ b/bhaptics/udp_server.py @@ -1,57 +1,68 @@ import socket import threading - class UDPServer: - def __init__(self, host="0.0.0.0", port=15884, callback=None): + 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 start_server(self): - self.sock.bind((self.host, self.port)) - self.running = True - self.thread = threading.Thread(target=self.listen) - self.thread.start() - print(f"UDP server listening on {self.host}:{self.port}") + def __print(self, *args, **kwargs): + if self.verbose: + print(*args, **kwargs) - def listen(self): + def __listen(self): try: while self.running: - data, addr = self.sock.recvfrom(1024) # buffer size is 1024 bytes + data, addr = self.sock.recvfrom(1024) # Buffer size of 1024 bytes message = data.decode() - print(f"Received message: {message} from {addr}") + self.__print(f"Received message: {message} from {addr}") if self.callback: self.callback(message, addr) except Exception as e: - print(f"An error occurred: {e}") + if self.running: + self.__print(f"An error occurred in UDP: {e}") finally: - self.sock.close() + self.stop_server() - def stop_server(self): - self.running = False - # We send a dummy message to the server to unblock the recvfrom call - self.sock.sendto(b'', (self.host, self.port)) - self.sock.close() - if self.thread is not None: - self.thread.join() - print("UDP server stopped.") + 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}") + + try: + if self.thread: + self.thread.join() + self.thread = None + except Exception as e: + self.__print(f"Error joining UDP thread: {e}") + + self.__print("Closed UDP server.") # Global server instance udp_server = None - # Exportable functions -def listen(host="0.0.0.0", port=15884, callback=None): +def listen(host="0.0.0.0", port=15884, callback=None, verbose=False): global udp_server - udp_server = UDPServer(host, port, callback) + udp_server = UDPServer(host, port, callback, verbose=verbose) udp_server.start_server() - def stop(): global udp_server if udp_server: From c5c08c36bd621ba718c5d78f4f6de6f04cf9534d Mon Sep 17 00:00:00 2001 From: Auejin Ham Date: Tue, 16 Apr 2024 18:59:05 +0900 Subject: [PATCH 14/18] refactor: haptic_player --- bhaptics/haptic_player.py | 595 ++++++++++++++++++++++++++------------ bhaptics/tcp_client.py | 23 +- bhaptics/udp_server.py | 21 +- sample.py | 235 ++++++++------- 4 files changed, 562 insertions(+), 312 deletions(-) diff --git a/bhaptics/haptic_player.py b/bhaptics/haptic_player.py index 6fd297c..cae9f13 100644 --- a/bhaptics/haptic_player.py +++ b/bhaptics/haptic_player.py @@ -1,219 +1,434 @@ -import json -import time -import requests -import bhaptics.udp_server as udp_server -import bhaptics.tcp_client as tcp_client import random +import requests +from enum import Enum -# Constants and Configuration -API_URL = "https://sdk-apis.bhaptics.com/api/v1/tacthub-api/verify" - - -# Helper Functions -def send_request(url, params): - """Send a GET request and return the response.""" - try: - response = requests.get(url, params=params) - response.raise_for_status() - return response.json() - except requests.RequestException as e: - print(f"Request error: {e}") - return None +import json +import time +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 + +__client: tcp_client.TCPClient = None +__is_client_connected = False +__is_client_api_verified = False + +__is_verbose = False + +__conf = { + "applicationId": "", + "sdkApiKey": "", +} + +def __print(*args, **kwargs): + if __is_verbose: + print(*args, **kwargs) + +def __message_received(message): + msg = json.loads(message) + + 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): - """Generate a JSON message.""" - msg = {"message": json.dumps(message), "type": message_type} +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' +def is_client_connected(): + global __is_client_connected + return __is_client_connected -def generate_string_message(message_type, message): - """Generate a JSON message.""" - msg = {"message": message, "type": message_type} - return json.dumps(msg) + '\n' +def is_client_api_verified(): + global __is_client_api_verified + return __is_client_api_verified + +def initialize(appId: str, apiKey: str, verbose: bool = False): + global __conf, __is_verbose + __is_verbose = verbose + + __conf = { + "applicationId": appId, + "sdkApiKey": apiKey, + } + + udp_server.listen(callback=__udp_message_received, verbose=__is_verbose) -def random_request_id(): - return random.randint(1, 99999) +def destroy(): + global __client, __is_client_connected, __is_client_api_verified + # For iOS Hub App, let server prepare for the disconnection + if __client is not None: + __ping_to_server() + + 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)) + 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)) + 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, + } -# Main Functionalities -class BhapticsSDK2: - def __init__(self, appId, apiKey): - self.appId = appId - self.apiKey = apiKey - self.client = None - self.start() +def play_glove( + position:Position, + gloveModes:list[GloveMode], + requestId:int|None = None, + ): + global __client - def start(self): - udp_server.listen(callback=self.udp_message_received) + 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, + } + + __client.send_message(__generate_message("SdkPlayWaveformMode", play_message)) + return requestId + +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, + } - def stop(self): - if self.client: - self.client.stop() +def play_path( + position:Position, + pathPoints:list[PathPoint], + requestId:int|None = None, + duration:int = 1000, + ): + global __client - def udp_message_received(self, message, addr): - """Handle UDP messages.""" - msg = json.loads(message) - print(f"Received UDP message from {addr[0]}:{addr[1]} - {msg}") + 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)) + 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) - if self.client is None: - self.client = tcp_client.TCPClient(addr[0], msg["port"], - message_received_callback=self.message_received) - self.client.start() + play_message = { + "eventName": name, + "requestId": requestId, + "intensity": intensity, + "duration": duration, + "interval": interval, + "maxCount": maxCount, + "offsetAngleX": offsetAngleX, + "offsetY": offsetY, + } - self.wait_for_client_connection() - self.send_auth_request() - udp_server.stop() + __client.send_message(__generate_message("SdkPlayLoop", play_message)) + return requestId - def wait_for_client_connection(self): - """Wait until the client is connected.""" - while not self.client.is_connected(): - print("Waiting for client to connect...") - time.sleep(1) - def send_auth_request(self): - """Send authentication request to the client.""" - config = { - "applicationId": self.appId, - "sdkApiKey": self.apiKey, - } +def __ping_to_server(): + __client.send_message(__generate_message("SdkPingToServer")) - auth_message = generate_message("SdkRequestAuthInit", config) - self.client.send_message(auth_message) - - def message_received(self, message): - """Handle TCP messages.""" - msg = json.loads(message) - print(f"Received TCP message: {msg}") - message_type = msg["type"] - print(f"Received TCP type: {message_type}") - - if message_type == "ServerTokenMessage": - message_message = msg["message"] - parsed_message = json.loads(message_message) - token = parsed_message["token"] - token_key = parsed_message["tokenKey"] - self.verify_token(token, token_key) - - def verify_token(self, token, token_key): - """Verify the token with the API.""" - params = {"token": token, "token-key": token_key} - response = send_request(API_URL, params) - if response is not None: - print(f"API Response: {response}") - else: - print("Failed to verify token.") - - def play_event(self, event, intensity=1, duration=1, offset_angle_x=0, offset_y=0): - """Play an event.""" - if self.client is None: - return -1 - - request_id = random_request_id() - play_message = { - "eventName": event, - "requestId": request_id, - "intensity": intensity, - "duration": duration, - "offsetAngleX": offset_angle_x, - "offsetY": offset_y, - } - message = generate_message("SdkPlay", play_message) - self.client.send_message(message) - - return request_id - - def play_loop(self, event, intensity=1, duration=1, interval=0, maxCount=0, offset_angle_x=0, offset_y=0): - """Play loop event.""" - if self.client is None: - return -1 - - request_id = random_request_id() - play_message = { - "eventName": event, - "requestId": request_id, - "intensity": intensity, - "duration": duration, - "interval": interval, - "maxCount": maxCount, - "offsetAngleX": offset_angle_x, - "offsetY": offset_y, - } +def ping_all(): + __client.send_message(__generate_message("SdkPingAll")) - message = generate_message("SdkPlayLoop", play_message) - self.client.send_message(message) - - return request_id - - # pos 0 is for TactSuit series like X40 or X16. - # ------- - # 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_mode(self, motors, pos=0, duration=1): - """Play an dot mode event.""" - if self.client is None: - return -1 - - request_id = random_request_id() - play_message = { - "requestId": request_id, - "pos": pos, - "durationMillis": duration * 1000, - "motors": motors - } - message = generate_message("SdkPlayDotMode", play_message) - self.client.send_message(message) +def stop_by_event(name: str): + __client.send_message(__generate_message("SdkStopByEventId", name)) - return request_id - - def play_path_mode(self, x_list, y_list, intensity_list, pos=0, duration=1): - """Play an path mode event.""" - if self.client is None: - return -1 - request_id = random_request_id() - play_message = { - "requestId": request_id, - "pos": pos, - "durationMillis": duration * 1000, - "x": x_list, - "y": y_list, - "intensity": intensity_list - } +def stop_by_request(id: int): + __client.send_message(__generate_message("SdkStopByRequestId", id)) - message = generate_message("SdkPlayPathMode", play_message) - self.client.send_message(message) - return request_id - - def stop_all(self): - """Stop all events.""" - if self.client is None: - return +def stop_all(): + __client.send_message(__generate_message("SdkStopAll")) - message = generate_message("SdkStopAll", "") - self.client.send_message(message) if __name__ == '__main__': - bhaptics_client = BhapticsSDK2("dd", "dd") - try: - while True: - time.sleep(1) - except KeyboardInterrupt: - # If a KeyboardInterrupt (Ctrl+C) is detected, the program will exit this loop - print("Stopping the client...") - bhaptics_client.stop() - finally: - print("") - print("Client stopped.") + # 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("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 index 70405f4..0ebadcd 100644 --- a/bhaptics/tcp_client.py +++ b/bhaptics/tcp_client.py @@ -31,7 +31,9 @@ def __receive_messages(self): if self.connected: self.__print(f"An error occurred in TCP: {e}") finally: - self.stop() + 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) @@ -73,15 +75,16 @@ def stop(self): self.client_socket.close() except OSError as e: self.__print(f"Error closing TCP socket: {e}") - - 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}") - - self.__print(f"Closed TCP connection with app.") + 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 diff --git a/bhaptics/udp_server.py b/bhaptics/udp_server.py index 3a9107f..eaba886 100644 --- a/bhaptics/udp_server.py +++ b/bhaptics/udp_server.py @@ -28,7 +28,9 @@ def __listen(self): if self.running: self.__print(f"An error occurred in UDP: {e}") finally: - self.stop_server() + self.running = False + self.sock.close() + self.__print("Closed UDP server.") def start_server(self): self.sock.bind((self.host, self.port)) @@ -44,15 +46,16 @@ def stop_server(self): self.sock.close() except OSError as e: self.__print(f"Error closing UDP socket: {e}") + return - try: - if self.thread: - self.thread.join() - self.thread = None - except Exception as e: - self.__print(f"Error joining UDP thread: {e}") - - self.__print("Closed UDP server.") + 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 diff --git a/sample.py b/sample.py index 6d650a5..676657c 100644 --- a/sample.py +++ b/sample.py @@ -1,111 +1,140 @@ import time -from bhaptics.haptic_player import BhapticsSDK2 - -sdk_instance = None +import bhaptics.haptic_player as player if __name__ == '__main__': - # Replace 'yourAppId' and 'yourApiKey' with your actual appId and apiKey + # TODO: Replace `appId` and `apiKey` with values of your app appId = "mWK8BbDgpx9LdZVR22ij" apiKey = "m9ef4q9oQRXbPeJY9z4J" - sdk_instance = BhapticsSDK2(appId, apiKey) - try: - time.sleep(3) - - while True: - print("\nPlay haptic samples!!!") - - # Play Dot Mode - sample 1 - print("Play Dot Mode - sample 1") - sdk_instance.play_dot_mode( - [ - # 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, - ] - ) - time.sleep(5) - - # Play Dot Mode - sample 2 - print("Play Dot Mode - sample 2") - sdk_instance.play_dot_mode( - [ - # 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=3 - ) - time.sleep(5) - - - # Play Path Mode - sample - print("Play Path Mode - sample") - for i in [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]: - sdk_instance.play_path_mode( - x_list=[i], - y_list=[0.5], - intensity_list=[5], - duration=1 - ) - - time.sleep(0.5) - - time.sleep(5) - - - # Play "shoot_test" event. - print("Play `shoot_test` event.") - sdk_instance.play_event("shoot_test") - time.sleep(5) - - # Play "shoot_test" event with its duration doubled. - print("Play `shoot_test` event with its duration doubled.") - sdk_instance.play_event("shoot_test", duration=2) - time.sleep(5) - - # Start playing "shoot_test" event and stop all events immediately. - print("Start playing `shoot_test` event and stop all events immediately.") - sdk_instance.play_event("shoot_test", duration=2) - time.sleep(0.2) - sdk_instance.stop_all() - time.sleep(5) - - - # Repeat "shoot_test" event 3 times. - print("Repeat `shoot_test` event 3 times.") - sdk_instance.play_loop("shoot_test", interval=1, maxCount=3) - time.sleep(5) - - - print("Cooldown....") - time.sleep(10) - - except KeyboardInterrupt: - print("Stopping the client...") - sdk_instance.stop() - - finally: - print("Finally...") + # Load your app + player.initialize( + appId = appId, + apiKey = apiKey, + verbose = False + ) + + 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("\nPlay haptic samples!!!") + + # Play Dot Mode - sample 1 + print("Play Dot Mode - sample 1") + 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) + + # Play Dot Mode - sample 2 + print("Play Dot Mode - sample 2") + 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) + + # Play Path Mode - sample + print("Play Path Mode - sample") + 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=5)], + duration = 500 + ) + time.sleep(0.5) + time.sleep(5) + + # Play "shoot_test" event. + print("Play `shoot_test` event.") + player.play_event("shoot_test") + time.sleep(5) + + # Play "shoot_test" event with its duration doubled. + print("Play `shoot_test` event with its duration doubled.") + player.play_event("shoot_test", duration=2) + time.sleep(5) + + # Start playing "shoot_test" event and stop all events immediately. + print("Start playing `shoot_test` event and stop all events immediately.") + player.play_event("shoot_test", duration=2) + time.sleep(0.2) + player.stop_all() + time.sleep(5) + + # Repeat "shoot_test" event 3 times. + print("Repeat `shoot_test` event 3 times.") + player.play_loop("shoot_test", interval=1, maxCount=3) + time.sleep(5) + + print("Testing: 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) + + print("Testing: 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) + + player.destroy() print("Client stopped.") From 0e72c405f8f05c7cedc739be7742903b73a50ffa Mon Sep 17 00:00:00 2001 From: Auejin Ham Date: Wed, 17 Apr 2024 10:23:44 +0900 Subject: [PATCH 15/18] feat: implement ping loop --- bhaptics/haptic_player.py | 40 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/bhaptics/haptic_player.py b/bhaptics/haptic_player.py index cae9f13..387b9a4 100644 --- a/bhaptics/haptic_player.py +++ b/bhaptics/haptic_player.py @@ -4,6 +4,7 @@ import json import time +import threading if __name__ == '__main__': import udp_server as udp_server @@ -18,6 +19,9 @@ __is_verbose = False +__ping_thread = None +__ping_thread_active = False + __conf = { "applicationId": "", "sdkApiKey": "", @@ -155,6 +159,7 @@ def play_event( } __client.send_message(__generate_message("SdkPlay", play_message)) + __ping_while_waiting() return requestId class Position(Enum): @@ -191,6 +196,7 @@ def play_dot( } __client.send_message(__generate_message("SdkPlayDotMode", play_message)) + __ping_while_waiting() return requestId class GloveMode: @@ -237,6 +243,7 @@ def play_glove( } __client.send_message(__generate_message("SdkPlayWaveformMode", play_message)) + __ping_while_waiting() return requestId class PathPoint: @@ -287,6 +294,7 @@ def play_path( } __client.send_message(__generate_message("SdkPlayPathMode", play_message)) + __ping_while_waiting() return requestId def play_loop( @@ -319,27 +327,57 @@ def play_loop( } __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 + + try: + while __ping_thread_active: + time.sleep(1) + __ping_to_server() + except Exception as e: + pass + finally: + __ping_thread_active = False + + # Halt the ping loop 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__': From ee3b97965e9aec95c6492d973aa7e20fef647a63 Mon Sep 17 00:00:00 2001 From: Auejin Ham Date: Wed, 17 Apr 2024 10:51:53 +0900 Subject: [PATCH 16/18] chore: change marker when event is finished --- sample.py | 48 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/sample.py b/sample.py index 676657c..9fb2ad0 100644 --- a/sample.py +++ b/sample.py @@ -1,7 +1,26 @@ +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" @@ -33,10 +52,9 @@ print(f"Client verification: {player.is_client_api_verified()}") print() - print("\nPlay haptic samples!!!") + print("Now playing hapting events...") - # Play Dot Mode - sample 1 - print("Play Dot Mode - sample 1") + __push_todo("play_dot(\"sample_front\")") player.play_dot( player.Position.VEST, [ @@ -58,8 +76,7 @@ ) time.sleep(5) - # Play Dot Mode - sample 2 - print("Play Dot Mode - sample 2") + __push_todo("play_dot(\"sample_back\")") player.play_dot( player.Position.VEST, [ @@ -81,8 +98,7 @@ ) time.sleep(5) - # Play Path Mode - sample - print("Play Path Mode - sample") + __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, @@ -93,28 +109,28 @@ time.sleep(5) # Play "shoot_test" event. - print("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. - print("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) - # Start playing "shoot_test" event and stop all events immediately. - print("Start playing `shoot_test` event and stop all events immediately.") + # 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. - print("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) - print("Testing: play_glove(L)") + __push_todo("play_glove(L)") player.play_glove( position=player.Position.GLOVE_L, gloveModes=[ @@ -125,7 +141,7 @@ ) time.sleep(2) - print("Testing: play_glove(R)") + __push_todo("play_glove(R)") player.play_glove( position=player.Position.GLOVE_R, gloveModes=[ @@ -135,6 +151,8 @@ ] ) time.sleep(2) + __pop_todo() player.destroy() - print("Client stopped.") + print() + print("All test finished!") From 75f372d7d33956aa5fbfac2335dd5886c410edfc Mon Sep 17 00:00:00 2001 From: Auejin Ham Date: Wed, 17 Apr 2024 14:54:27 +0900 Subject: [PATCH 17/18] fix: bug of not starting the ping thread --- bhaptics/haptic_player.py | 18 ++++++++++++++---- sample.py | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/bhaptics/haptic_player.py b/bhaptics/haptic_player.py index 387b9a4..4936244 100644 --- a/bhaptics/haptic_player.py +++ b/bhaptics/haptic_player.py @@ -122,12 +122,21 @@ def initialize(appId: str, apiKey: str, verbose: bool = False): udp_server.listen(callback=__udp_message_received, verbose=__is_verbose) def destroy(): - global __client, __is_client_connected, __is_client_api_verified + 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 @@ -336,6 +345,7 @@ def __ping_while_waiting(): def __ping_loop(): global __ping_thread, __ping_thread_active + __ping_thread_active = True try: while __ping_thread_active: time.sleep(1) @@ -344,8 +354,8 @@ def __ping_loop(): pass finally: __ping_thread_active = False - - # Halt the ping loop if it is active + + # Assure the ping loop is joined if it is active if __ping_thread_active: __ping_thread_active = False diff --git a/sample.py b/sample.py index 9fb2ad0..85ee4fb 100644 --- a/sample.py +++ b/sample.py @@ -102,7 +102,7 @@ def __pop_todo(): 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=5)], + [player.PathPoint(x=i, y=0.5, intensity=50)], duration = 500 ) time.sleep(0.5) From d4af197e122e43d6a61392edb7ccd7b7c9dcf7ab Mon Sep 17 00:00:00 2001 From: Auejin Ham Date: Mon, 22 Apr 2024 14:09:22 +0900 Subject: [PATCH 18/18] feat: send client sdk version info to hub app --- bhaptics/haptic_player.py | 12 +++++++++++- sample.py | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/bhaptics/haptic_player.py b/bhaptics/haptic_player.py index 4936244..6afebb9 100644 --- a/bhaptics/haptic_player.py +++ b/bhaptics/haptic_player.py @@ -1,3 +1,4 @@ +import re import random import requests from enum import Enum @@ -22,9 +23,15 @@ __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): @@ -110,13 +117,15 @@ def is_client_api_verified(): return __is_client_api_verified def initialize(appId: str, apiKey: str, verbose: bool = False): - global __conf, __is_verbose + 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) @@ -402,6 +411,7 @@ def stop_all(): 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() diff --git a/sample.py b/sample.py index 85ee4fb..dc39876 100644 --- a/sample.py +++ b/sample.py @@ -33,6 +33,7 @@ def __pop_todo(): 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()