Jump to content

Arduino Esplora Thumb Joystick Throttle And Steering Control


10 replies to this topic

#1 Andilar

    Member

  • Pip
  • Philanthropist
  • 17 posts

Posted 29 January 2014 - 07:03 AM

My MWO controls are a trackball mouse which I use right-handed for targeting and firing and the thumb joystick on an Arduino Esplora which I use left-handed for throttle control and turning. The Arduino Esplora gives me point-and-go acceleration and steering on the fastest lights and does not need any IGP joystick support because it issues keystroke commands directly.

Here is the custom code I wrote for the thumb joystick, which you all are welcome to copy or modify:

#include <Esplora.h>
 
/* MWO keyboard emulator, version 10 from January 26, 2014 */
 
/* written by andilar */
 
/* The basic idea is to read the joystick on an Arduino Esplora and
use the measurements to issue throttle-setting keystrokes and turning
keystrokes for MWO. */
 
/* keystroke control mapping */
 
/* The keys can be adjusted to suit personal preferences.  In the present version,
only regular keyboard keys are valid, as I haven't found Esplora support for
the numeric keypad keys. */
 
const char upThrottle = '='; // keystroke for a positive throttle increment
const char downThrottle = '-'; // keystroke for a negative throttle increment
const char fullStop = 'x'; // keystroke for a full stop
const char leftTurn = 'a'; // keystroke for a left turn
const char rightTurn = 'd'; // keystroke for a right turn
const char maxThrottleKey = '0'; // keystroke for 100% throttle
 
/* other constants */
 
const int maxThrottle = 10; // value of maximum throttle
 
/* The throttle threshold is a fix I put in because I didn't like having intermediate-high
speeds.  Instead I wanted a range of slow speeds and then max speed, mainly for piloting
lights.*/
 
const int throttleThreshold = 6; // value at which the loop switches from intermediate
						   // speeds to maximum throttle.  I liked 6, but it can
						   // be from 1 to 10.  1 will give only stop and maximum
						   // throttle, while 10 will give all the intermediate
						   // throttle settings.
const int minDelay = 47; // minimum delay in msec between turning commands
const int turnSpeeds = 10; // number of turn speed settings
const int keyPressTime = 47; // amount of time in msec to press a key
							 // This appears to be mandatory for turning commands, but
							 // not for throttle increment commands.
 
/* variables */
 
boolean joystickActive = true; // to allow or block keystroke output
int lastSwitchState = LOW; // blocking switch starts out low
 
/* throttle variables */
 
// Left-hand throttle control from the X axis of the joystick
 
int xValueDC; // initial x value of joystick at rest
int lastThrottleX = 0; // previous value of throttle setting
int thisThrottleX = 0; // current value of throttle setting
int deltaThrottleX; // throttle change
int throttleKeyPressed = 0; // 0 for none, 1 for upThrottle, 2 for downThrottle, 3 for fullStop, 4 for maxThrottle
unsigned long lastTimeX; // time of last throttle keystroke
unsigned long thisTimeX; // present time
unsigned long deltaTimeX; // time since last throttle keystroke
unsigned long throttleKeyStartTime;
 
/* turn variables */
 
// Left-hand turning control from the Y axis of the joystick
 
int yValueDC; // initial y value of joystick at rest
int turnKeyPressed = 0; // 0 for none, 1 for left, 2 for right
unsigned long turnKeyStartTime; // time when a turning key was last pressed
 
/* the time variables below are for the time between turning commands */
 
unsigned long lastTimeY; // time of last turn keystroke
unsigned long thisTimeY; // present time
unsigned long deltaTimeY; // time since last turn keystroke
 
void setup(){
 
  Serial.begin(9600);	   // initialize serial communication with your computer
 
  Keyboard.begin();			// take control of the keyboard
 
  xValueDC = Esplora.readJoystickX();		// read the joystick's X position
  yValueDC = Esplora.readJoystickY();		// read the joystick's Y position
 
  Keyboard.press(fullStop);  // zero throttle; this also sets the direction to forward
  delay(keyPressTime); // wait for the fullStop to be accepted
  Keyboard.release(fullStop);
 
  lastThrottleX = 0; // initialize last throttle setting
  thisThrottleX = 0; // initialize next throttle setting
 
  lastTimeY = millis(); // initialize turning rate clock
  thisTimeY = millis();
 
  turnKeyStartTime = millis();
  throttleKeyStartTime = millis(); // initialize throttle clock
 
}
 
void loop()
{
int switchState = Esplora.readButton(SWITCH_DOWN);  // read the esplora button designated
													// to lock out keystrokes when needed
 
if (switchState != lastSwitchState){ // On a change in switch state...
  if (switchState == HIGH){
	joystickActive = !joystickActive; // ...flip the joystick state.
	if (joystickActive){
	  Esplora.writeRGB(0,10,0); // turn Esplora LED green - the Esplora now can issue MWO
								// keyboard commands
	}
	else {
	  Esplora.writeRGB(10,0,0); // turn Esplora LED red - the Esplora is prevented from
								// issuing further MWO keyboard commands
	
/*	 Keyboard.press(fullStop); // set throttle to 0
	  delay(keyPressTime);
	  Keyboard.release(fullStop);
	  lastThrottleX = 0;
	  Keyboard.release(rightTurn); // set right turning to 0
	  Keyboard.release(leftTurn);  // set left turning to 0
*/
	}
	delay(250); // a time delay to allow the switch to go off
  }
}
lastSwitchState = switchState;
 
// (1) Read the joystick signals
 
int xValue = Esplora.readJoystickX() - xValueDC; // read the joystick's X position and correct for initial reading
int yValue = Esplora.readJoystickY() - yValueDC; // read the joystick's Y position and correct for initial reading
  
int thisThrottleX = map( xValue,-512, 512, -10, 10);  // map the X value to a range of movement for the mouse X
int thisTurnY = map(yValue,-512, 512, -turnSpeeds, turnSpeeds);  // map the Y value to a range of movement for the mouse Y
 
 
 
// (2) Determine throttle changes and issue commands for them
 
deltaThrottleX = thisThrottleX - lastThrottleX;
thisTimeY = millis();
 
if (throttleKeyPressed > 0){ // allow throttle key releases regardless of joystick state
  if (thisTimeY - throttleKeyStartTime  > keyPressTime){
	if (throttleKeyPressed == 1){
	  Keyboard.release(upThrottle);
	}
	else if (throttleKeyPressed == 2){
	  Keyboard.release(downThrottle);
	}
	else if (throttleKeyPressed == 3){
	  Keyboard.release(fullStop);
	}
	else if (throttleKeyPressed == 4){
	  Keyboard.release(maxThrottleKey);
	}
	throttleKeyPressed = 0; // record end of throttle key press
  }
}
else if (joystickActive){  // only allow new key presses when joystick is active
  if (thisThrottleX >= throttleThreshold){ // When the throttle is maximum...
	  Keyboard.press(maxThrottleKey); //...start a full-throttle keystroke
	  throttleKeyStartTime = millis(); // set the throttle keystroke timer
	  throttleKeyPressed = 4; // record which keystroke
	  lastThrottleX = maxThrottle; // record the total throttle
  }
  else if (thisThrottleX == 0){ // When the throttle is zero...
	  Keyboard.press(fullStop); // ...start a stop keystroke
	  throttleKeyStartTime = millis(); // set the throttle keystroke timer
	  throttleKeyPressed = 3; // record which keystroke
	  lastThrottleX = 0; // record the total throttle
  }
  else if (deltaThrottleX > 0){ // on a positive throttle difference...
	Keyboard.press(upThrottle); // ...start an increase-throttle keystroke
	throttleKeyStartTime = millis(); // set the throttle keystroke timer
	throttleKeyPressed = 1; // record which keystroke
	lastThrottleX += lastThrottleX; // increase the cumulative throttle
  }
  else if (deltaThrottleX  < 0){ // on a negative throttle difference...
	Keyboard.press(downThrottle); // ...start a decrease-throttle keystroke
	throttleKeyStartTime = millis(); // set the throttle keystroke timer
	throttleKeyPressed = 2; // record which keystroke
	lastThrottleX -= lastThrottleX; // decrease the cumulative throttle
  }
}
 
// (3) Determine steering changes and issue commands for them
 
thisTimeY = millis();  // get the current time
deltaTimeY = thisTimeY - lastTimeY; // get the time since the last steering key press
 
// The next statement determines if enough time has elapsed and
// issues an appropriate turn command.
 
if (turnKeyPressed > 0){ // Allow turning key releases regardless of joystick state
  if (thisTimeY - turnKeyStartTime  > keyPressTime){
	if (turnKeyPressed == 1){
	  Keyboard.release(leftTurn);
	}
	else if (turnKeyPressed == 2);{
	  Keyboard.release(rightTurn);
	}
	turnKeyPressed = 0; // record end of turning key press
}
}
else if (joystickActive){ // only allow new key presses when joystick is active
  if (thisTurnY != 0){
	if (deltaTimeY >= (turnSpeeds*minDelay/abs(thisTurnY) - keyPressTime)){
	  if(thisTurnY < -1){ // fixed a bug where < 0 resulted in unwanted constant turning
		Keyboard.press(rightTurn); // press right turn key
		turnKeyStartTime = millis(); // record when turning key was pressed
		turnKeyPressed = 2; // record which turning key was pressed
	  }
	  else if (thisTurnY > 1){ // fixed a bug where > 0 resulted in unwanted constant turning
		Keyboard.press(leftTurn); // press left turn key
		turnKeyStartTime = millis(); // record when turning key was pressed
		turnKeyPressed = 1; // record which turning key was pressed
	  }
	  lastTimeY = thisTimeY; // restart inter-press time count
	}
  }
}
}

Edited by Andilar, 29 January 2014 - 07:05 AM.


#2 Gevurah

    Member

  • PipPipPipPipPipPipPip
  • The Flame
  • The Flame
  • 500 posts

Posted 29 January 2014 - 08:23 AM

Very nice. Might use some of that for my own project.

Thanks!

#3 evilC

    Member

  • PipPipPipPipPipPipPipPip
  • Legendary Founder
  • Legendary Founder
  • 1,298 posts
  • LocationLondon, UK

Posted 04 February 2014 - 09:48 AM

Out of curiosity, what shape is the aperture on the Esplora joystick?
Is it like consoles and circular, or like windows joysticks and square?

By this I mean the axes - is it possible to hit full left and full up together (square aperture) or can you not move it left at all if you are 100% up? (Circular aperture)

#4 mikelovskij

    Member

  • PipPipPip
  • Philanthropist
  • 60 posts

Posted 04 February 2014 - 11:36 AM

View PostevilC, on 04 February 2014 - 09:48 AM, said:

Out of curiosity, what shape is the aperture on the Esplora joystick?
Is it like consoles and circular, or like windows joysticks and square?

By this I mean the axes - is it possible to hit full left and full up together (square aperture) or can you not move it left at all if you are 100% up? (Circular aperture)

I have a similar one (in an arduino joystick shield) and it feels circular, but after trying it,I discovered it is in reality square. I mean that there is a virtual square inscribed in the circle and going out of the square boundaries does not change the output, because the maximum (or the minimum) is already reached, so it allows to reach maximum of both l/r and up/down at the same time.

Edited by mikelovskij, 04 February 2014 - 11:37 AM.


#5 evilC

    Member

  • PipPipPipPipPipPipPipPip
  • Legendary Founder
  • Legendary Founder
  • 1,298 posts
  • LocationLondon, UK

Posted 04 February 2014 - 02:45 PM

Thanks for confirming.
I would imagine that you are correct in that the stick is capable of reporting more, but the board is configured to give a square aperture so it is more compatible with a wider variety of applications.

#6 Andilar

    Member

  • Pip
  • Philanthropist
  • 17 posts

Posted 06 February 2014 - 02:53 PM

The physical aperture is a circle, defined by a hole in a metal sheet that limits joystick shaft movement. The digital measurement aperture is square within the circle, and you are able to get to the corners.

Hmm.... Maybe a next project might be adapting an Xbox controller as an MWO controller...

#7 evilC

    Member

  • PipPipPipPipPipPipPipPip
  • Legendary Founder
  • Legendary Founder
  • 1,298 posts
  • LocationLondon, UK

Posted 06 February 2014 - 03:37 PM

View PostAndilar, on 06 February 2014 - 02:53 PM, said:

The physical aperture is a circle, defined by a hole in a metal sheet that limits joystick shaft movement. The digital measurement aperture is square within the circle, and you are able to get to the corners.

Hmm.... Maybe a next project might be adapting an Xbox controller as an MWO controller...

I would be interested to find out if you could take a circular aperture stick and file out the corners - would it still report beyond the normal range?

Failing that, you can do it in software. I have a thread here with the basics rigged up, though I am not that happy with the maths of squaring the circle. Plugging in new maths would be very easy though.
The fundamental fact though is that using a joystick for turning and speed sucks (You get bleedthrough - you want to only speed up but you turn too) and this is worse the smaller the "throw" (range of movement) of the stick.
Probably suitable if you used the trigger(s) for speed, and the stick only for turning though, but then the circular aperture wouldn't affect you.

#8 Andilar

    Member

  • Pip
  • Philanthropist
  • 17 posts

Posted 07 February 2014 - 06:15 AM

Filing out the physical aperture wouldn't add much for the following reasons... For the Arduino microcontroller - which you can program - the joystick module is an on-board serial device whose inner workings you almost certainly can't program. The mapping between analog potentiometer values - what moving the joystick is physically changing - to digital values is set by A/D converter hardware and firmware in the joystick module.

As far as accelerating without turning, I have a dead zone programmed in where turning isn't initiated unless "thisTurnY" is more than 1 or less than -1, where "thisTurnY" is one of 21 bins on the 1024-level output range. So in roughly 15% of the turning range, turning is blocked and acceleration is not. There is no bleedthrough.

PS I will look at your GitHub project later today. To the extent that you can get the original A/D converter outputs with the third-party programs that are interacting with the controller you should be able to easily create a throttle dead zone and do custom throttle mappings as I did with the Esplora.

Edited by Andilar, 07 February 2014 - 06:45 AM.


#9 Andilar

    Member

  • Pip
  • Philanthropist
  • 17 posts

Posted 07 February 2014 - 07:20 AM

Okay, I had a quick look at your other project.

The good news is that you should be able to eliminate bleedthrough and get to acceleration and turning corners quite easily using a vJoy "feeder application".

To eliminate bleedthrough you could create a deadzone by having the feeder application read in the actual joystick axis signal for turning, but write out a zero or no-turn virtual joystick axis signal if the actual signal isn't large enough in magnitude.

To reach acceleration and turning corners, you can find out what actual joystick signals you get at your desired physical corner positions, and then map those corners to desired virtual corners. In other words, if a game wants (+512,+512) for (maximum forward, maximum right) and your joystick only gives (+300,+300), the feeder program could scale the (+300,+300) by 512/300, truncate, and pass the resulting values to the game.

#10 evilC

    Member

  • PipPipPipPipPipPipPipPip
  • Legendary Founder
  • Legendary Founder
  • 1,298 posts
  • LocationLondon, UK

Posted 07 February 2014 - 10:57 AM

I already worked all that out :P I was just saying that a more elegant way to deform a circular input into a square output would be desirable.

All I can really work out is that the deformation should be proportional to how equal the x and y coordinates are.

ie deform the most at the diagonals.

That's as far as I got before, but recently I realized that the amount to deform could be worked out by finding out the ratio of the circumference of a circle of diameter n, against the hypotenuse of a right-angled triangle, where the other two sides are n length.

Pythagoras's theorem is a2 = b2 + c2

So plug in the values 1 for the other two sides:
a2 = 2
So a = root(2) = 1.4142

So all I need to do is multiply x and y by a proportion of 1.42, depending on how diagonal they are (where 100% is the limits of a *circle*, not a square.)
If you fancy finishing off the algorithms for me, by my guest...

The deadzone approach is fine on paper, but when dealing with a stick with such a small throw, it leaves you with way too little "granularity" (ie it is pretty pointless it being analog as you can only get a few different turn rates out of it).

Edited by evilC, 07 February 2014 - 10:58 AM.


#11 Andilar

    Member

  • Pip
  • Philanthropist
  • 17 posts

Posted 07 February 2014 - 02:37 PM

I'm not really clear on your discussion of mapping a circular input to a square output. On these little joysticks, moving along the x axis turns a tiny potentiometer, and you presumably get outputs that are equally spaced over some range of rotation. The same is true for the y axis, which also has a tiny potentiometer. The digital outputs are already from a square grid of Cartesian coordinates, but of course you can map them to polar coordinates (radius and angle from the positive X axis).

As far as granularity effects of a dead zone, suppose there are 1024 joystick outputs, and you map the middle 15% to zero. There are still about 860 non-zero outputs, and the step size between adjacent values is the same - i.e. there is no granularity change outside the dead zone.

In MWO there don't seem to be many viable turning speeds anyway. Pointing and then keying the legs to follow the torso, I can move in 2 degree increments as measured by the on-screen orientation values, or 180 step to go around in a complete revolution. When I do a keystroke single-turn, I seem to be getting between 3 and 9 degrees per step. With the keyboard, the maximum number of turn-steps that can be executed in one second is about 20, since each keystroke needs about 50 msec to be recognized. Half of that turning speed would be 10 steps per second, while one-quarter of maximum would be 5 steps per second. At maximum speed turning seem smooth, but at slower turning speeds it is really jerky, basically a series of steps and halts. I'm not sure if things are different with joystick commands, but that's my experience with joystick emulating keystrokes.





1 user(s) are reading this topic

0 members, 1 guests, 0 anonymous users