Compare commits

..

1 Commits

Author SHA1 Message Date
Mel
92254a43d1 finished version 0.1.0 2025-12-13 04:37:03 +01:00
9 changed files with 561 additions and 159 deletions

View File

@@ -1,7 +0,0 @@
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>deadzone</w>
</words>
</dictionary>
</component>

View File

@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated
View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="uv (vts-gamepad-input)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="uv (vts-gamepad-input)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/vts-gamepad-input.iml" filepath="$PROJECT_DIR$/.idea/vts-gamepad-input.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="uv (vts-gamepad-input)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

68
.idea/workspace.xml generated

File diff suppressed because one or more lines are too long

View File

@@ -1,10 +1,34 @@
#! /usr/bin/env python
import asyncio
import time
import inquirer
from inputs import GamePad, devices
from vts_connection import VTSConnection
# initiate gamepad buttons
dpad_up, dpad_down, dpad_left, dpad_right = (0.0, 0.0, 0.0, 0.0)
action_up, action_down, action_left, action_right = (0.0, 0.0, 0.0, 0.0)
button_start, button_select = (0.0, 0.0)
button_l1, button_l2, button_l3 = (0.0, 0.0, 0.0)
button_r1, button_r2, button_r3 = (0.0, 0.0, 0.0)
left_stick_x_axis, left_stick_y_axis, right_stick_x_axis, right_stick_y_axis = (
0.0,
0.0,
0.0,
0.0,
)
left_button_pressed, right_button_pressed, left_button_hold, right_button_hold = (
0.0,
0.0,
0.0,
0.0,
)
left_thumb_stick, right_thumb_stick = (0.0, 0.0)
left_thumb_X, right_thumb_X, left_thumb_Y, right_thumb_Y = (0.0, 0.0, 0.0, 0.0)
left_index, right_index = (0.0, 0.0)
def _get_gamepads() -> list[GamePad]:
"""Get list of connected gamepad"""
@@ -39,14 +63,8 @@ def select_gamepad() -> GamePad | None:
return gamepads[0]
def _in_deadzone(analog_value: int, percentage: float = 10) -> bool:
center: float = 255 / 2
dead_range = center * (percentage / 100)
if center - dead_range < analog_value < center + dead_range:
return True
else:
return False
def _deadzone(analog_value: float, deadzone: float = 0.1) -> float:
return 0 if abs(analog_value) < deadzone else analog_value
def _normalize_analog_sticks(analog_value: float) -> float:
@@ -64,31 +82,194 @@ def _normalize_analog_trigger(analog_value: float) -> float:
return formula
def main() -> None:
def set_inputs(code: str, state: float) -> None:
global dpad_up, dpad_down, dpad_left, dpad_right
global action_up, action_down, action_left, action_right
global left_stick_x_axis, left_stick_y_axis, right_stick_x_axis, right_stick_y_axis
global button_start, button_l1, button_l2, button_l3
global button_select, button_r1, button_r2, button_r3
global left_thumb_stick, right_thumb_stick
global left_thumb_X, right_thumb_X, left_thumb_Y, right_thumb_Y
global left_index, right_index
global \
left_button_pressed, \
right_button_pressed, \
left_button_hold, \
right_button_hold
if code == "ABS_HAT0Y":
dpad_up = state * -1 if state < 0 else 0
dpad_down = state if state > 0 else 0
elif code == "ABS_HAT0X":
dpad_left = state * -1 if state < 0 else 0
dpad_right = state if state > 0 else 0
elif code == "ABS_X":
left_stick_x_axis = state
elif code == "ABS_Y":
left_stick_y_axis = state * -1
elif code == "ABS_RX":
right_stick_x_axis = state
elif code == "ABS_RY":
right_stick_y_axis = state * -1
elif code == "ABS_Z":
button_l2 = state
elif code == "ABS_RZ":
button_r2 = state
elif code == "BTN_TL":
button_l1 = state
elif code == "BTN_TR":
button_r1 = state
elif code == "BTN_SOUTH":
action_down = state
elif code == "BTN_WEST":
action_left = state
elif code == "BTN_EAST":
action_right = state
elif code == "BTN_NORTH":
action_up = state
elif code == "BTN_THUMBL":
button_l3 = state
elif code == "BTN_THUMBR":
button_r3 = state
elif code == "BTN_START":
button_start = state
elif code == "BTN_SELECT":
button_select = state
left_button_pressed = dpad_left + dpad_up + dpad_down + dpad_right
right_button_pressed = action_left + action_right + action_up + action_down
left_thumb_X = dpad_right - dpad_left if left_button_pressed >= 1 else 0
left_thumb_Y = dpad_up - dpad_down if left_button_pressed >= 1 else 0
right_thumb_X = action_right - action_left if right_button_pressed >= 1 else 0
right_thumb_Y = action_up - action_down if right_button_pressed >= 1 else 0
left_thumb_stick = (
1
if dpad_down == dpad_up == dpad_left == dpad_right == 0 or button_l3 == 1
else 0
)
right_thumb_stick = (
1
if action_up == action_down == action_left == action_right == 0
or button_r3 == 1
else 0
)
left_index = 1 if button_l1 == 0 else 0
right_index = 1 if button_r1 == 0 else 0
def create_vts_input_request() -> list[dict]:
params = [
{"id": "NP_ON", "value": 1.0, "weight": 1.0},
{"id": "NP_DPadDown", "value": dpad_down, "weight": 1.0},
{"id": "NP_DPadUp", "value": dpad_up, "weight": 1.0},
{"id": "NP_DPadLeft", "value": dpad_left, "weight": 1.0},
{"id": "NP_DPadRight", "value": dpad_right, "weight": 1.0},
{"id": "NP_ButtonA", "value": action_down, "weight": 1.0},
{"id": "NP_ButtonB", "value": action_right, "weight": 1.0},
{"id": "NP_ButtonX", "value": action_left, "weight": 1.0},
{"id": "NP_ButtonY", "value": action_up, "weight": 1.0},
{"id": "NP_StartDown", "value": button_start, "weight": 1.0},
{"id": "NP_SelectDown", "value": button_select, "weight": 1.0},
{"id": "NP_ButtonLS", "value": button_l3, "weight": 1.0},
{"id": "NP_ButtonRS", "value": button_r3, "weight": 1.0},
{"id": "NP_L1", "value": button_l1, "weight": 1.0},
{"id": "NP_R1", "value": button_r1, "weight": 1.0},
{"id": "NP_L2", "value": button_l2, "weight": 1.0},
{"id": "NP_R2", "value": button_r2, "weight": 1.0},
{"id": "NP_LStickX", "value": left_stick_x_axis, "weight": 1.0},
{"id": "NP_LStickY", "value": left_stick_y_axis, "weight": 1.0},
{"id": "NP_RStickX", "value": right_stick_x_axis, "weight": 1.0},
{"id": "NP_RStickY", "value": right_stick_y_axis, "weight": 1.0},
{"id": "NP_LButtonDown", "value": left_button_pressed, "weight": 1.0},
{"id": "NP_RButtonDown", "value": right_button_pressed, "weight": 1.0},
{"id": "NP_LButtonPress", "value": left_button_pressed, "weight": 1.0},
{"id": "NP_RButtonPress", "value": right_button_pressed, "weight": 1.0},
{"id": "NP_LThumbX", "value": left_thumb_X, "weight": 1.0},
{"id": "NP_LThumbY", "value": left_thumb_Y, "weight": 1.0},
{"id": "NP_RThumbX", "value": right_thumb_X, "weight": 1.0},
{"id": "NP_RThumbY", "value": right_thumb_Y, "weight": 1.0},
{"id": "NP_LOnStick", "value": left_thumb_stick, "weight": 1.0},
{"id": "NP_ROnStick", "value": right_thumb_stick, "weight": 1.0},
{"id": "NP_LIndexPos", "value": left_index, "weight": 1.0},
{"id": "NP_RIndexPos", "value": right_index, "weight": 1.0},
]
return params
async def main() -> None:
gamepad = select_gamepad()
connection = VTSConnection()
vts = VTSConnection()
try:
await vts.connect()
except asyncio.CancelledError:
await vts.close()
axis_states = {
"ABS_X": 0.0,
"ABS_Y": 0.0,
"ABS_RX": 0.0,
"ABS_RY": 0.0,
}
trigger_states = {
"ABS_Z": 0.0,
"ABS_RZ": 0.0,
}
last_time = time.monotonic()
frequency = 1 / 30
while True:
for event in gamepad.read():
if event.code != "SYN_REPORT":
# Add some deadzone of about 5% to analog stick
# Add some deadzone of about 10% to analog stick
if event.code in ["ABS_X", "ABS_Y", "ABS_RX", "ABS_RY"]:
if not _in_deadzone(analog_value=event.state):
print(
f"{event.ev_type} {event.code} {_normalize_analog_sticks(analog_value=event.state)}"
)
elif event.code in ["ABS_Z", "ABS_RZ"]:
print(
f"{event.ev_type} {event.code} {_normalize_analog_trigger(event.state)}"
axis_states[event.code] = _deadzone(
_normalize_analog_sticks(event.state)
)
elif event.code in ["ABS_Z", "ABS_RZ"]:
trigger_states[event.code] = _normalize_analog_trigger(event.state)
else:
print(event.ev_type, event.code, event.state)
set_inputs(event.code, event.state)
asyncio.run(connection.update_parameters(event.code, event.state))
now = time.monotonic()
if now - last_time >= frequency:
last_time = now
for key in trigger_states:
set_inputs(key, trigger_states[key])
for key in axis_states:
set_inputs(key, axis_states[key])
input_data = create_vts_input_request()
await vts.update_parameters(input_data)
await asyncio.sleep(0.001)
if __name__ == "__main__":
try:
main()
asyncio.run(main())
except KeyboardInterrupt:
exit(0)

View File

@@ -4,21 +4,57 @@ import asyncio
import os
import pyvts
AUTHOR = "Melody LaFae"
PLUGIN_NAME = "VTS Gamepad Input"
token_path = f"{os.environ['HOME']}/.vts-gamepad-input-token"
ip_path = f"{os.environ['HOME']}/.vts-gamepad-input-ip"
plugin_info = {
"plugin_name": "vts-gampad-input",
"developer": "Melody LaFae",
"authentication_token_path": f"{os.environ['HOME']}/.vts-gampad-authentication-token",
"plugin_name": PLUGIN_NAME,
"developer": AUTHOR,
"authentication_token_path": token_path,
}
class VTSConnection:
def __init__(self):
self.vts = pyvts.vts(plugin_info=plugin_info)
self.vts = pyvts.vts(
plugin_info=plugin_info, vts_api_info=self._get_vts_api_info()
)
self.request = pyvts.VTSRequest(plugin_info=plugin_info)
if asyncio.run(self._connect()):
asyncio.run(self._create_vts_params())
async def _connect(self) -> bool:
async def connect(self):
conn_success = await self._connect_to_api()
if conn_success:
await self._create_vts_params()
await self.close()
async def close(self):
await self.vts.close()
@staticmethod
def _get_vts_api_info() -> dict[str, str]:
host_uri = "ws://localhost:8001"
vts_api_info: dict[str, str] = {}
if os.path.exists(ip_path) and os.path.isfile(ip_path):
with open(ip_path, "r") as file:
host_uri = file.read()
else:
with open(ip_path, "w") as file:
file.write(host_uri)
host_url = host_uri.split(":")[1].replace("//", "")
host_port = host_uri.split(":")[2]
vts_api_info["host"] = host_url
vts_api_info["port"] = host_port
vts_api_info["name"] = "VTubeStudioPublicAPI"
vts_api_info["version"] = "1.0"
return vts_api_info
async def _connect_to_api(self) -> bool:
try:
await self.vts.connect()
except OSError:
@@ -27,7 +63,14 @@ class VTSConnection:
await self.vts.request_authenticate_token()
await self.vts.request_authenticate()
connection_success = await self.vts.request(
if not self.vts.get_connection_status():
raise ConnectionError("Connection failed")
else:
return True
async def _create_vts_params(self) -> None:
# Param to enable or disable inputs
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_ON",
info="VTS Gamepad Input Enabled",
@@ -36,37 +79,327 @@ class VTSConnection:
default_value=0.0,
)
)
if not connection_success:
raise ConnectionError("Connection failed")
else:
return True
async def _create_vts_params(self):
params: list[dict] = [
# DPad params
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_LButtonDown",
info="Left side face buttons down",
parameter="NP_DPadDown",
info="DPad Down pressed",
min=0.0,
max=1.0,
default_value=0.0,
)
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_DPadLeft",
info="DPad Left pressed",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_LButtonDown",
info="Left side face buttons down",
parameter="NP_DPadUp",
info="DPad Up pressed",
min=0.0,
max=1.0,
default_value=0.0,
),
]
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_DPadRight",
info="DPad Right pressed",
min=0.0,
max=1.0,
default_value=0.0,
),
)
# Action Buttons params
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_ButtonA",
info="Down Action button pressed (XBox A, PS Cross)",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_ButtonB",
info="Right Action button pressed (XBox B, PS Circle)",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_ButtonY",
info="Up Action button pressed (XBox Y, PS Triangle)",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_ButtonX",
info="Left Action button pressed (XBox X, PS Square)",
min=0.0,
max=1.0,
default_value=0.0,
),
)
# Shoulder buttons params
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_L1",
info="Left front shoulder button pressed (XBox LB, PS L1, Switch L)",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_L2",
info="Left back shoulder button pressed (XBox LT, PS L2, Switch ZL)",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_R1",
info="Right front shoulder button pressed (XBox RB, PS R1, Switch R)",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_R2",
info="Right back shoulder button pressed (XBox RT, PS R2, Switch ZR)",
min=0.0,
max=1.0,
default_value=0.0,
),
)
# Start and select params
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_StartDown",
info="Start button pressed",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_SelectDown",
info="Select button pressed",
min=0.0,
max=1.0,
default_value=0.0,
),
)
# Left Analog stick param
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_LStickX",
info="Left stick moved on X axis",
min=-1.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_LStickY",
info="Left stick moved on Y axis",
min=-1.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_ButtonLS",
info="Left analog stick pressed",
min=0.0,
max=1.0,
default_value=0.0,
),
)
# Right Analog stick params
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_RStickX",
info="Right stick moved on X axis",
min=-1.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_RStickY",
info="Right stick moved on Y axis",
min=-1.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_ButtonRS",
info="Right analog stick pressed",
min=0.0,
max=1.0,
default_value=0.0,
),
)
## Rigging Logic params
# Thumb on sticks if no buttons are pressed
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_LOnStick",
info="Left thumb on left analog stick",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_ROnStick",
info="Right thumb on right analog stick",
min=0.0,
max=1.0,
default_value=0.0,
),
)
# Thumb locations
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_LThumbX",
info="Left thumb movement on X axis",
min=-1.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_LThumbY",
info="Left thumb movement on Y axis",
min=-1.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_RThumbX",
info="Right thumb movement on X axis",
min=-1.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_RThumbY",
info="Right thumb movement on Y axis",
min=-1.0,
max=1.0,
default_value=0.0,
),
)
# Index finger location
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_LIndexPos",
info="Left index finger position (on L2/LT/ZL if L1 not pressed)",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_RIndexPos",
info="Right index finger position (on R2/RT/ZR if R1 not pressed)",
min=0.0,
max=1.0,
default_value=0.0,
),
)
# Button pressed or hold params
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_LButtonPress",
info="Left button pressed",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_RButtonPress",
info="Right button pressed",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_LButtonDown",
info="Left button hold",
min=0.0,
max=1.0,
default_value=0.0,
),
)
await self.vts.request(
self.request.requestCustomParameter(
parameter="NP_RButtonDown",
info="Right button hold",
min=0.0,
max=1.0,
default_value=0.0,
)
)
async def update_parameters(self, button: str, value: float) -> None:
pass
async def update_parameters(self, parameters: list[dict]) -> None:
await self._connect_to_api()
request = self.request.BaseRequest(
data={
"faceFound": False,
"mode": "set",
"parameterValues": parameters,
},
message_type="InjectParameterDataRequest",
)
await self.vts.request(request)
await self.close()
def main():
VTSConnection()
async def main():
vts_conn = VTSConnection()
await vts_conn.vts.connect()
await vts_conn.vts.request_authenticate_token()
# await vts_conn.vts.request_authenticate()
# await vts_conn.connect()
if __name__ == "__main__":
main()
asyncio.run(main())