Analog(-like) control over key inputs using PWM

Official forum for open source FreePIE discussion and development.
Post Reply
HarvesteR
One Eyed Hopeful
Posts: 19
Joined: Mon Jun 15, 2015 10:30 am

Analog(-like) control over key inputs using PWM

Post by HarvesteR »

Hi,

I wrote this script here for myself, to get axis control over GTA V's binary rudder controls in helis and planes, and I reckon it might be useful for others too.
It uses Pulse-width-modulation to produce a rapid stream of key up and down events (or button presses) in order to simulate analog control via the keyboard, even for actions that have no analog bindings (like GTA's rudders).

If you think about it, when you play on a keyboard, especially for driving games, you are already doing PWM 'manually', by modulating the duration and timing of your key taps. This script does the same, but at a much faster rate.

The script itself is quite generic. Any number of bindings can be set up, each with their own set of parameters. The joystick enumerations are specific to each user, but I expect that is simple enough to figure out. I've added a few watches at the bottom you can uncomment to find your device by moving its X axis, for convenience.


Code: Select all

# Pulse-Width-Modulated Input  
# Written by Felipe "HarvesteR" Falanghe	

# This script produces analog-like control over binary (keys or joy buttons) controls, using PWM.
# http://www.arduino.cc/en/Tutorial/PWM
# I wrote this originally for enabling analog rudder control in GTA V, but it should be usable
# in just about any situation. I'm leaving my comments and debug watches to find devices, as those are quite helpful.

# Binary inputs are either neutral or fully deflected. The script reads analog inputs to 
# rapidly pulse keystrokes (or vjoy button presses or whatever) on and off.

# For each duty cycle, the amount of time a key stays pressed is controlled by the analog input. 
# (e.g., for 60% deflection, the key will be pressed for 60% of the cycle duration, and released for the remaining 40%)

# This script supports any number of bindings: each 'axis' is defined in a tuple (see below for details) 
# The same input can be used for multiple bindings. Multiple bindings outputting the same key can be problematic, however.
# Each binding can have its own cycle length (in ms). This is useful as different controls often have different response times.  
# I suggest playing with this param to find the value that works best for your case. 

# Very low values may be problematic as the cycles become too short to provide enough resolution.
# High values (200+) are more accurate, but control may feel 'chunky' in games where control responds too quickly.

# feel free to modify and post improvements, but remember to give credit where credit is due.
# This software is provided freely. Any commercial use is prohibited without express permission from the author.

# Enjoy!



import time

def pwmInput(input):
	axisInput = filters.deadband(input[0](j), input[4])
	axisInput = filters.mapRange(axisInput, input[2], input[3], 0, 1)	
	
	diagnostics.watch(input[0](j))
	diagnostics.watch(axisInput)	
	
	t = ((time.clock() * 1000) % input[5]) / input[5]
	t = t ** input[6]
	
	input[1](t < axisInput)	
	diagnostics.watch(t)	

if starting:
	# joystick index to read
	global j
	j = 6
		
	# setup PWM axis bindings
	# arguments
	#	0 accessor to joy axis (x is the joy index defined above or override as needed)
	# 	1 setter to output (t is a boolean for the state of the control)
	# 	2 input axis min
	# 	3 input axis max
	#	4 input axis deadzone
	# 	5 duty cycle length in ms
	#	6 response curve (it's an exponent)
	global ax
	ax= ((lambda x: joystick[x].zRotation, lambda t: keyboard.setKey(Key.A, t), 0, -1000, 150, 50, 1.1),
	  	 (lambda x: joystick[x].zRotation, lambda t: keyboard.setKey(Key.D, t), 0, 1000, 150, 50, 1.1),	  	 
	  	 (lambda x: joystick[x].x, lambda t: keyboard.setKey(Key.S, t), -1000, 1000, 20, 250, 1.0),
	  	 (lambda x: joystick[x].y, lambda t: keyboard.setKey(Key.W, t), -1000, 1000, 20, 250, 1.0))
 	
 	# The script is set up to run at 1000hz. 
	# Increase the thread interval if you need more CPU power.
 	system.setThreadTiming(TimingTypes.HighresSystemTimer)
 	system.threadExecutionInterval = 1	 		
	global tLast
	tLast = 0

	# finally, there is a toggle switch currently set to a joy button. You can remap it to anything you prefer.
	# I don't recommend removing it, however, as once enabled, the script will produce a constant stream of input.
	global toggle
	toggle = False	
	
			
if joystick[3].getPressed(9):
	toggle = not toggle
	
	
diagnostics.watch(toggle)

Hz = 1 / (time.clock() - tLast)
tLast = time.clock()

diagnostics.watch(Hz)


if (toggle):	
	for i in ax:
		pwmInput(i)			
	
	# uncomment to find devices 
	#diagnostics.watch(joystick[0].x)
	#diagnostics.watch(joystick[1].x)
	#diagnostics.watch(joystick[2].x)
	#diagnostics.watch(joystick[3].x)
	#diagnostics.watch(joystick[4].x)
	#diagnostics.watch(joystick[5].x)
	#diagnostics.watch(joystick[6].zRotation)
	
I must admit I've not tested the high-res timing scheme very thoroughly, although the 'Hz' watch does seem to be correct. I set it to 1 so it will run at 1000Hz. That should be good for low to mid-high frequency cycles. Feel free to tune as needed.

Cheers
CyberVillain
Petrif-Eyed
Posts: 2166
Joined: Mon Jun 22, 2009 8:36 am
Location: Stockholm, Sweden

Re: Analog(-like) control over key inputs using PWM

Post by CyberVillain »

Nice work, I have also used something similar when testing virtual walking (Using a GPS to track movement when wearing a HMD outdoors on a big field). The GPS tracking code never got good enough for release though, I started trying to kalman fuse the GPS and accelerometer sensors for lower latency control.
Post Reply

Return to “FreePIE”