PS4 Dual Shock Motion Control

GPC2 script programming for Titan Two. Code examples, questions, requests.

Re: PS4 Dual Shock Motion Control

Postby SirBrass » Fri Mar 26, 2021 2:56 pm

DeviateSquirrel wrote:Most of the code is in the Arduino board on the EDTracker. I’ll pull the relative files and post a link later today.

I think there’s more to this. Accel values can go beyond 25 either way, though practically it’s unlikely as you need to put more effort into moving the controller than you’re likely to do in game. Likewise, gyroscope values aren’t limited to -10 to +10.

The code for JoyShockMapper (https://github.com/JibbSmart/JoyShockMapper) shows one way to use a device with a gyroscope and accelerometer to determine orientation and maps the vector components to XY. EDTracker (https://github.com/brumster/EDTracker2) essentially does the same thing. On a PC these XY values are enough for Elite Dangerous. I think they’d also be enough for Elite Dangerous on a console if you only wanted/ needed headlook occasionally (through a momentary or long lasting toggle). I want headlook to work continually like it does on a PC, however. For that, using the above approach, I’d either need a pair of analog axes or four digital inputs that wouldn’t get in the way of other inputs. There’s already so much to control in Elite Dangerous that, with the limited inputs available on console, I’ve not gone down this path yet (though, at this point, I think I’ll have to). Finding those inputs, and programming the T2 so they don’t get stepped on, is my next step. If we can do that we can ignore the accel/gyro inputs completely.


I obtained those values by having my DS4 (and only my DS4) plugged into the T2,and watched the accel and gyro values as I rotated the controller through 90° in each axial direction. 25 was the highest value. Likewise the gyro value was an absolute value. It never went negative. It only moved between 1 and 10.
User avatar
SirBrass
First Sergeant
First Sergeant
 
Posts: 55
Joined: Sat Dec 05, 2020 3:47 am

Re: PS4 Dual Shock Motion Control

Postby DeviateSquirrel » Fri Mar 26, 2021 5:15 pm

Right, I get it.

You have to do more than rotate the controller to get the larger absolute values for acceleration. This graph shows that you can get larger values than 25. Notice the noise in this graph. Most of the controller movement was from pitching up and down. The rest is just the typical noise coming from an accelerometer which has to be managed.

DS4_ ACCEL.png
DS4_ ACCEL.png (99.08 KiB) Viewed 1223 times

The gyroscope values for similar movement look like this and show values larger than absolute 10. Note that there's a lot less noise from the other axis. A gyroscope's biggest issue is drift over time away from its starting position.

DS4_ GYRO.png
DS4_ GYRO.png (63.88 KiB) Viewed 1223 times


Typically combining the acceleration and gyroscope values is necessary using some form of sensor fusion to remove the noise and drift. Combining this with a magnetometer (found in the EDTracker) is even better. I'm almost positive sensor fusion is being done outside of the controller either within the PS4 operating system or within the game.

Speaking of the EDTracker, these graphs show its acceleration and gyroscope values;

EDTracker_ ACCEL.png
note: the z-axis here is inverted -- easy fix at the device level
EDTracker_ ACCEL.png (90.5 KiB) Viewed 1223 times

EDTracker_ GYRO.png
EDTracker_ GYRO.png (62.58 KiB) Viewed 1223 times


The movement with both the DS4 and EDTracker are similar but the graphs are quite different which could be part of the larger issue I'm having. From what I've been able to find, I don't believe the DS4 is processing the raw motion values in any way and there is minimal processing on the EDTracker (just unit conversion). I suspect the differences between the graphs lie in the different IMU sensors in use.

This is the DS4 sensor -- https://www.bosch-sensortec.com/product ... us/bmi055/

This is the EDTracker sensor -- https://invensense.tdk.com/products/mot ... /mpu-9250/

I also wonder what the T2 is doing with the data coming from the DS4 and EDTracker. The raw data from these devices is not on a -100 to 100 scale -- it looks like something this;

Code: Select all
ACCEL:    -530,576,14780
GRYO:    -3,-2,-2
 
ACCEL:    -533,525,14630
GRYO:    -1,1,-1
 
ACCEL:    -553,570,14666
GRYO:    -2,-2,-2


(Unfortunately, I lost the work I had done with conversion/etc. on the EDTracker recently so these aren't the same type of values I've been using but the range, particularly with the ACCEL values, is typical.)

I'm pretty sure it's taking these values and converting them to a 16-bit number (dividing by 32,767) to get in a -1, 1 scale and then multiplying by 100 to get in the -100, 100 range. But I don't know what it's then sending to console. If it's these -100, 100 values then I'm sure that's a big part of the problem. Hopefully, the -100, 100 range is just for display in the T2's Device Monitor and it's passing along the original values --- though I suspect it's taking the -100, 100 and reconverting them to 16 bit which means we're probably losing some detail in the conversion. Is it enough to cause my issues? Maybe.

As I said, I lost all of the work I had done with the EDTracker recently (reinstalled Windows without these folders backed up) so I'll need to redo that work before I can post. The code in the T2 is just to remap a set of values and to suppress another but here it is for giggles;

Code: Select all
#pragma METAINFO("EDTracker", 1, 0, "DeviateSquirrel")
#include <ps4.gph>
 
main {
 
    set_val(PS4_GYROX, get_actual(ACCEL_2_Y)); // pitch
    set_val(PS4_GYROY, get_actual(ACCEL_2_X)); // roll
    set_val(PS4_GYROZ, get_actual(ACCEL_2_Z)); // yaw
 
    if (get_actual(PS4_LY) && get_port(PS4_LY) == PORT_USB_A) set_val(PS4_LY, 0.0);
    if (get_actual(PS4_LX) && get_port(PS4_LX) == PORT_USB_A) set_val(PS4_LX, 0.0);
    if (get_actual(PS4_RX) && get_port(PS4_RX) == PORT_USB_A) set_val(PS4_RX, 0.0);
 
}
 
User avatar
DeviateSquirrel
First Sergeant
First Sergeant
 
Posts: 46
Joined: Sun Jan 17, 2021 11:07 pm

Re: PS4 Dual Shock Motion Control

Postby SirBrass » Tue Mar 30, 2021 4:04 pm

So, essentially if there's an IMU in the mix which the T2 obviously has no ability to control, then we can't use the motion sensors. However, if the IMU is processing the accel and gyro inputs coming from the DS4, then the IMU is the recipient of those values and we can emulate head movement as the IMU will be performing sensor fusion on its own.

However, as we've been getting the "gimble lock" behavior, I'm betting that there's an IMU inside the DS4 and that is what is being read by the PS4 and/or Elite: Dangerous instead of the raw values. So the IMU is getting full passthrough to the PS4 if it's part of the DS4 hardware, and it's reporting nothing or just noise compared to the raw values, so the result is some movement that only results in gimble lock.

What I've done instead is institute "headlook mode" in the Titan 2 code. In bindings in the game, the d-pad controls the headlook camera when headlook mode is activated. In the T2 code, in the same argument which will execute the headlook input combo, I set toggle a boolean. That boolean will alter the POV hat on the right stick (which is a 4-way hat), and the left POV mini-stick still just outputs normal d-pad values. In key bindings in game, I have an alternate control set up for UI navigation and distributor pips (which are both normally done using d-pad). So, when headlook is activated (or at least that button is pressed), the alternate UI and distributor controls will be what the POV hat on right stick will use, as those functions are disabled by use of the d-pad while the d-pad is controlling the headlook camera.

I also have a QoL combo set for "rapid pips" which lets the left stick pov mini-stick execute rapid press of the d-pad, instead of just holding the button active. This lets me cycle distro pips very fast using the mini-stick. So, I had to disable that when headlook mode is activated as it causes very slow and awkward headlook movement.

What I'd suggest in the interim is your headtracker control the headlook camera with the d-pad, and when it returns to center, have it execute headlook reset. And when in headlook mode, have alternate controls for UI interaction and distro pips (I have my alternate set to PS4_CROSS + D-Pad, while next tab and previous tab are Square + D-Pad right and left respectively).

It's not as elegant a solution as emulating the motion controls, but until we figure out what we're missing, then it's a workable solution.
User avatar
SirBrass
First Sergeant
First Sergeant
 
Posts: 55
Joined: Sat Dec 05, 2020 3:47 am

Re: PS4 Dual Shock Motion Control

Postby DeviateSquirrel » Tue Mar 30, 2021 4:47 pm

If sensor fusion is being done in the DS4 (and from what I've been able to find I don't think it is but I've been wrong before) then the results are either being sent to the PS4 as a quaternion set or a set of euler angles/ XYZ coordinates. Either way we should be able to interrupt them and replace them with our own (I mean if the devs made it so). So, yeah, for now I think using the gyro to control headlook directly is DOA but we can, as you suggest use it to control headlook via analog/digital inputs.

I've had a hard time finding combinations that don't step on each other. I haven't been using my HOTAS 4 through the T2 because it's not registering several inputs correctly and I keep stepping on inputs with the combinations I've tried. I'm sure I'm missing something obvious.

My VKB NXTs are on their way so I'll probably wait until I get them to dig into this further (using your GPC/GIT code). When I get that setup I'll be sure to give your suggestion a try. Do you think it'll work without needing to toggle headlood continually? Being able to just turn it on at the beginning of the session and then leave it on would be ideal.
User avatar
DeviateSquirrel
First Sergeant
First Sergeant
 
Posts: 46
Joined: Sun Jan 17, 2021 11:07 pm

Re: PS4 Dual Shock Motion Control

Postby DeviateSquirrel » Tue Mar 30, 2021 4:48 pm

SirBrass wrote:I also have a QoL combo set for "rapid pips" which lets the left stick pov mini-stick execute rapid press of the d-pad, instead of just holding the button active. This lets me cycle distro pips very fast using the mini-stick.


Is this in your GPC code already?

edit: I see you have new code up! I'll take a look at your changes. Thanks!
User avatar
DeviateSquirrel
First Sergeant
First Sergeant
 
Posts: 46
Joined: Sun Jan 17, 2021 11:07 pm

Re: PS4 Dual Shock Motion Control

Postby SirBrass » Wed Mar 31, 2021 1:09 am

DeviateSquirrel wrote:
SirBrass wrote:I also have a QoL combo set for "rapid pips" which lets the left stick pov mini-stick execute rapid press of the d-pad, instead of just holding the button active. This lets me cycle distro pips very fast using the mini-stick.


Is this in your GPC code already?

edit: I see you have new code up! I'll take a look at your changes. Thanks!


Yup. I changed it to square + d-pad because UI select and back are cross and circle respectively. I'm also finding that there's more touchpad + d-pad binds that no longer work, and it's always refusing to bind with d-pad up and down. And it's NEW too, as in within the past week. Once bound the combo works, but binding doesn't.

Also, don't bind square + anything in the alternate (secondary) space for a command. It just automatically applies changes and drops out, leaving that slot bound to plain old square. However, if you try it in the primary bind slot, it works.

ooFDev spaghetti code at its finest.

About a quarter of the work in coding this is making sure the combos chosen don't get stepped on by the buttons and the other 75% is working around the limitations on the controls imposed unnecessarily onto the console version of Elite by FDev.

I'm thinking of reversing how I go about headlook, actually.

I'm thinking of something like this (in pseudocode):

If left ministick activates and headmoveactive is FALSE, combo square then d-pad and set a flag headmoveactive TRUE;
If left ministick has activated and headmoveactive is TRUE, set square and d-pad' to 100
if left ministick releases, zero out square and d-pad and set headmoveactive toFALSE

This can allow smooth, constant motion using a combo + d-pad I hope. and then release those buttons when the ministick is released. This will allow easier UI interfacing and pip movement. I'll just comment out current implementation and experiment with this alternate idea maybe tomorrow.

Here's why I'm scratching my head: I want to easily be able to move between pages and tabs with the controller, for things like Galaxy Map, Galactic Powers, station menus, or navigating the panels when not in active flight, as well as move between panels and interact with them during combat (critical in Anti-Xeno combat, as we regularly are in the internal panel to synth heatsinks, AFMU ammo, ax flak, and repair internals, and sometimes synth gauss ammo, and often go to the external panel to quickly switch between targeting the interceptor and the thargon swarm when facing away from them, or when fighting hydra to find and subtarget the exerted heart)
User avatar
SirBrass
First Sergeant
First Sergeant
 
Posts: 55
Joined: Sat Dec 05, 2020 3:47 am

Re: PS4 Dual Shock Motion Control

Postby Yearoh » Wed Apr 28, 2021 9:05 am

DeviateSquirrel wrote:Did you forget the link?



Facepalm


Ok I'll try and be brief, but only because I don't know what I'm doing, but this video shows how good motion can be and this gizmo had the same sensors etc as a DS4

[YouTube] https://youtu.be/BXsGWoOMtmU [/YouTube]


The code below is supposed to be really great and has optimisation for minimal CPU and power use. There's references to Arduino libraries when you google it. Anyway LSS there's one problem (which might not be a problem for you) in that it's written in C or C+ or whatever, but the titan is obviously limited and my usual approach of 'keep commenting lines out until it compiles' didn't help me because there are commands that I couldnt resolve and the forum didn't engage too much with it.

In a nutshell sensor fusion is the minimum level of noise removal/precision gain. The Big Dick version is using the partially smoothed current and previous position/orientation of the device to predict where the next position is likely to be, which it uses to smooth/filter the signal further. This all sounds like a bit much at first, but you can see in the code below which sections are the 'good parts' and use it if its applicable.

I hope its of some use, because I thought I read you were using an arduino, and thats in there somewhere too. They also use an algorithm to work around a lack of magnetometer cos I don't think DS4 has one, but could be wrong.
Alot of the is obviously not needed but they've commented it thoroughly, so better to have it and not need it....
Good Luck
PS its in GPC2 code tags but it's not GPC2

Code: Select all
 
//=====================================================================================================
// MahonyAHRS.h
//=====================================================================================================
//
// Madgwick's implementation of Mayhony's AHRS algorithm.
// See: http://www.x-io.co.uk/node/8#open_sourc ... algorithms
//
// Date            Author            Notes
// 29/09/2011    SOH Madgwick    Initial release
// 02/10/2011    SOH Madgwick    Optimised for reduced CPU load
//
//=====================================================================================================
#ifndef MahonyAHRS_h
#define MahonyAHRS_h
 
//----------------------------------------------------------------------------------------------------
// Variable declaration
 
extern volatile float twoKp;            // 2 * proportional gain (Kp)
extern volatile float twoKi;            // 2 * integral gain (Ki)
extern volatile float q0, q1, q2, q3;    // quaternion of sensor frame relative to auxiliary frame
 
//---------------------------------------------------------------------------------------------------
// Function declarations
 
void MahonyAHRSupdate(float gx, float gy, float gz, float ax, float ay, float az, float mx, float my, float mz);
void MahonyAHRSupdateIMU(float gx, float gy, float gz, float ax, float ay, float az);
 
#endif
//=====================================================================================================
// End of file
//=====================================================================================================


Code: Select all
 
//=====================================================================================================
// MahonyAHRS.c
//=====================================================================================================
//
// Madgwick's implementation of Mayhony's AHRS algorithm.
// See: http://www.x-io.co.uk/node/8#open_sourc ... algorithms
//
// Date            Author            Notes
// 29/09/2011    SOH Madgwick    Initial release
// 02/10/2011    SOH Madgwick    Optimised for reduced CPU load
//
//=====================================================================================================
 
//---------------------------------------------------------------------------------------------------
// Header files
 
#include "MahonyAHRS.h"
#include <math.h>
 
//---------------------------------------------------------------------------------------------------
// Definitions
 
#define sampleFreq    512.0f            // sample frequency in Hz
#define twoKpDef    (2.0f * 0.5f)    // 2 * proportional gain
#define twoKiDef    (2.0f * 0.0f)    // 2 * integral gain
 
//---------------------------------------------------------------------------------------------------
// Variable definitions
 
volatile float twoKp = twoKpDef;                                            // 2 * proportional gain (Kp)
volatile float twoKi = twoKiDef;                                            // 2 * integral gain (Ki)
volatile float q0 = 1.0f, q1 = 0.0f, q2 = 0.0f, q3 = 0.0f;                    // quaternion of sensor frame relative to auxiliary frame
volatile float integralFBx = 0.0f,  integralFBy = 0.0f, integralFBz = 0.0f;    // integral error terms scaled by Ki
 
//---------------------------------------------------------------------------------------------------
// Function declarations
 
float invSqrt(float x);
 
//====================================================================================================
// Functions
 
//---------------------------------------------------------------------------------------------------
// AHRS algorithm update
 
void MahonyAHRSupdate(float gx, float gy, float gz, float ax, float ay, float az, float mx, float my, float mz) {
    float recipNorm;
    float q0q0, q0q1, q0q2, q0q3, q1q1, q1q2, q1q3, q2q2, q2q3, q3q3;
    float hx, hy, bx, bz;
    float halfvx, halfvy, halfvz, halfwx, halfwy, halfwz;
    float halfex, halfey, halfez;
    float qa, qb, qc;
 
    // Use IMU algorithm if magnetometer measurement invalid (avoids NaN in magnetometer normalisation)
    if((mx == 0.0f) && (my == 0.0f) && (mz == 0.0f)) {
        MahonyAHRSupdateIMU(gx, gy, gz, ax, ay, az);
        return;
    }
 
    // Compute feedback only if accelerometer measurement valid (avoids NaN in accelerometer normalisation)
    if(!((ax == 0.0f) && (ay == 0.0f) && (az == 0.0f))) {
 
        // Normalise accelerometer measurement
        recipNorm = invSqrt(ax * ax + ay * ay + az * az);
        ax *= recipNorm;
        ay *= recipNorm;
        az *= recipNorm;     
 
        // Normalise magnetometer measurement
        recipNorm = invSqrt(mx * mx + my * my + mz * mz);
        mx *= recipNorm;
        my *= recipNorm;
        mz *= recipNorm;   
 
        // Auxiliary variables to avoid repeated arithmetic
        q0q0 = q0 * q0;
        q0q1 = q0 * q1;
        q0q2 = q0 * q2;
        q0q3 = q0 * q3;
        q1q1 = q1 * q1;
        q1q2 = q1 * q2;
        q1q3 = q1 * q3;
        q2q2 = q2 * q2;
        q2q3 = q2 * q3;
        q3q3 = q3 * q3;   
 
        // Reference direction of Earth's magnetic field
        hx = 2.0f * (mx * (0.5f - q2q2 - q3q3) + my * (q1q2 - q0q3) + mz * (q1q3 + q0q2));
        hy = 2.0f * (mx * (q1q2 + q0q3) + my * (0.5f - q1q1 - q3q3) + mz * (q2q3 - q0q1));
        bx = sqrt(hx * hx + hy * hy);
        bz = 2.0f * (mx * (q1q3 - q0q2) + my * (q2q3 + q0q1) + mz * (0.5f - q1q1 - q2q2));
 
        // Estimated direction of gravity and magnetic field
        halfvx = q1q3 - q0q2;
        halfvy = q0q1 + q2q3;
        halfvz = q0q0 - 0.5f + q3q3;
        halfwx = bx * (0.5f - q2q2 - q3q3) + bz * (q1q3 - q0q2);
        halfwy = bx * (q1q2 - q0q3) + bz * (q0q1 + q2q3);
        halfwz = bx * (q0q2 + q1q3) + bz * (0.5f - q1q1 - q2q2);
 
        // Error is sum of cross product between estimated direction and measured direction of field vectors
        halfex = (ay * halfvz - az * halfvy) + (my * halfwz - mz * halfwy);
        halfey = (az * halfvx - ax * halfvz) + (mz * halfwx - mx * halfwz);
        halfez = (ax * halfvy - ay * halfvx) + (mx * halfwy - my * halfwx);
 
        // Compute and apply integral feedback if enabled
        if(twoKi > 0.0f) {
            integralFBx += twoKi * halfex * (1.0f / sampleFreq);    // integral error scaled by Ki
            integralFBy += twoKi * halfey * (1.0f / sampleFreq);
            integralFBz += twoKi * halfez * (1.0f / sampleFreq);
            gx += integralFBx;    // apply integral feedback
            gy += integralFBy;
            gz += integralFBz;
        }
        else {
            integralFBx = 0.0f;    // prevent integral windup
            integralFBy = 0.0f;
            integralFBz = 0.0f;
        }
 
        // Apply proportional feedback
        gx += twoKp * halfex;
        gy += twoKp * halfey;
        gz += twoKp * halfez;
    }
 
    // Integrate rate of change of quaternion
    gx *= (0.5f * (1.0f / sampleFreq));        // pre-multiply common factors
    gy *= (0.5f * (1.0f / sampleFreq));
    gz *= (0.5f * (1.0f / sampleFreq));
    qa = q0;
    qb = q1;
    qc = q2;
    q0 += (-qb * gx - qc * gy - q3 * gz);
    q1 += (qa * gx + qc * gz - q3 * gy);
    q2 += (qa * gy - qb * gz + q3 * gx);
    q3 += (qa * gz + qb * gy - qc * gx);
 
    // Normalise quaternion
    recipNorm = invSqrt(q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3);
    q0 *= recipNorm;
    q1 *= recipNorm;
    q2 *= recipNorm;
    q3 *= recipNorm;
}
 
//---------------------------------------------------------------------------------------------------
// IMU algorithm update
 
void MahonyAHRSupdateIMU(float gx, float gy, float gz, float ax, float ay, float az) {
    float recipNorm;
    float halfvx, halfvy, halfvz;
    float halfex, halfey, halfez;
    float qa, qb, qc;
 
    // Compute feedback only if accelerometer measurement valid (avoids NaN in accelerometer normalisation)
    if(!((ax == 0.0f) && (ay == 0.0f) && (az == 0.0f))) {
 
        // Normalise accelerometer measurement
        recipNorm = invSqrt(ax * ax + ay * ay + az * az);
        ax *= recipNorm;
        ay *= recipNorm;
        az *= recipNorm;       
 
        // Estimated direction of gravity and vector perpendicular to magnetic flux
        halfvx = q1 * q3 - q0 * q2;
        halfvy = q0 * q1 + q2 * q3;
        halfvz = q0 * q0 - 0.5f + q3 * q3;
 
        // Error is sum of cross product between estimated and measured direction of gravity
        halfex = (ay * halfvz - az * halfvy);
        halfey = (az * halfvx - ax * halfvz);
        halfez = (ax * halfvy - ay * halfvx);
 
        // Compute and apply integral feedback if enabled
        if(twoKi > 0.0f) {
            integralFBx += twoKi * halfex * (1.0f / sampleFreq);    // integral error scaled by Ki
            integralFBy += twoKi * halfey * (1.0f / sampleFreq);
            integralFBz += twoKi * halfez * (1.0f / sampleFreq);
            gx += integralFBx;    // apply integral feedback
            gy += integralFBy;
            gz += integralFBz;
        }
        else {
            integralFBx = 0.0f;    // prevent integral windup
            integralFBy = 0.0f;
            integralFBz = 0.0f;
        }
 
        // Apply proportional feedback
        gx += twoKp * halfex;
        gy += twoKp * halfey;
        gz += twoKp * halfez;
    }
 
    // Integrate rate of change of quaternion
    gx *= (0.5f * (1.0f / sampleFreq));        // pre-multiply common factors
    gy *= (0.5f * (1.0f / sampleFreq));
    gz *= (0.5f * (1.0f / sampleFreq));
    qa = q0;
    qb = q1;
    qc = q2;
    q0 += (-qb * gx - qc * gy - q3 * gz);
    q1 += (qa * gx + qc * gz - q3 * gy);
    q2 += (qa * gy - qb * gz + q3 * gx);
    q3 += (qa * gz + qb * gy - qc * gx);
 
    // Normalise quaternion
    recipNorm = invSqrt(q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3);
    q0 *= recipNorm;
    q1 *= recipNorm;
    q2 *= recipNorm;
    q3 *= recipNorm;
}
 
//---------------------------------------------------------------------------------------------------
// Fast inverse square-root
// See: http://en.wikipedia.org/wiki/Fast_inverse_square_root
 
float invSqrt(float x) {
    float halfx = 0.5f * x;
    float y = x;
    long i = *(long*)&y;
    i = 0x5f3759df - (i>>1);
    y = *(float*)&i;
    y = y * (1.5f - (halfx * y * y));
    return y;
}
 
//====================================================================================================
// END OF CODE
//====================================================================================================
User avatar
Yearoh
Master Sergeant
Master Sergeant
 
Posts: 38
Joined: Fri Jul 10, 2020 9:32 am

Previous

Return to GPC2 Script Programming

Who is online

Users browsing this forum: No registered users and 159 guests