vJoy implementation using data from Wireless IMU
Controlling Project Cars with an Android phone!
In other words: how to drive cars in PC games using the smartphone. The application’s purpose is more general than that, actually.
For some time now I tried to find a solution to implement a virtual game controller, be it a joystick, gamepad or wheel. it’s not that easy, and the documentation offered by Microsoft is very foggy. The most obvious way was to learn how to write drivers for a Human Interface Device, using this guide. Considering that I’m not Mr. Robot, I looked for easier solutions, for the simple fact that I’m not in the league of driver-writing programmers, and time is also precious.
Those who seek will find, and indeed I found a nice little driver accompanied bya C++ library, written by a guy named Shaul Eizikovich. The driver is called vJoy and it allows you to transform any input device(even a simulated virtual input) into a joystick.
Let’s briefly look at how it works: in Windows there are some software layers that the information from the joystick’s sensors must go through. First it goes through HAL(hardware abstraction layer), and no, this is not the same HAL from the 2001 movie. After that it goes through the joystick driver which runs in the kernel space( and this is hard to code), and after that in runs through the Win32 subsystem where it can be picked up by an application. I will use the images from the original page:
vJoy tricks Windows into thinking that the joystick data is authentic. Because the system is layered, an application from the User space has no idea where the Kernel space application gets its data. Thus the sensor information is injected with the help of the vJoy .dll into the virtual driver, from which the Win32 subsystem reads them and passes them to an application:
Now what we can make us of in this scheme is the Feeder Application. The vJoy SDK lets us create feeder applications in C# or C++. I made a C++ feeder for this demonstration.
I’ve got a broader vision for this application, but in this article I wanted to present briefly the functionality using accelerometer data from a smartphone. Given that I didn’t have to create an application from scratch, having already used Wireless IMU in other projects, I went to work. The plan for the test application was to send UDP packets containing CSV strings towards the feeder.
For this there’s another thing that we need: sockets! Another topic where I made use of Stack Overflow’s wisdom. I found there somebody who simplified a lot my work by making a wrapper for Windows’s winsock library: Winsock UDP wrapper by Peter R. Peter also wrote a client, which doesn’t interest me for now because for me the “client”(which send the UDP data) is the Wireless IMU Android app. I forgot to mention: for everything presented here to work, you need a wireless router.
Let’s see the code. First, Network.h:
#include "WinSock2.h" #include "WS2tcpip.h" #include "system_error" #include "string" #include "iostream" #include "math.h" #pragma comment(lib, "Ws2_32.lib") #pragma once class WSASession { public: WSASession() { int ret = WSAStartup(MAKEWORD(2, 2), &data); if (ret != 0) throw std::system_error(WSAGetLastError(), std::system_category(), "WSAStartup Failed"); } ~WSASession() { WSACleanup(); } private: WSAData data; }; class UDPSocket { public: UDPSocket() { sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (sock == INVALID_SOCKET) throw std::system_error(WSAGetLastError(), std::system_category(), "Error opening socket"); } ~UDPSocket() { closesocket(sock); } void SendTo(const std::string& address, unsigned short port, const char* buffer, int len, int flags = 0) { sockaddr_in add; add.sin_family = AF_INET; add.sin_addr.s_addr = inet_addr(address.c_str()); // tried to use the suggested function but I have no idea how yet //add.sin_addr.s_addr = inet_pton(AF_INET, address.c_str(), buffer); add.sin_port = htons(port); int ret = sendto(sock, buffer, len, flags, reinterpret_cast(&add), sizeof(add)); if (ret < 0) throw std::system_error(WSAGetLastError(), std::system_category(), "sendto failed"); } void SendTo(sockaddr_in& address, const char* buffer, int len, int flags = 0) { int ret = sendto(sock, buffer, len, flags, reinterpret_cast(&address), sizeof(address)); if (ret < 0) throw std::system_error(WSAGetLastError(), std::system_category(), "sendto failed"); } sockaddr_in RecvFrom(char* buffer, int len, int flags = 0) { sockaddr_in from; int size = sizeof(from); int ret = recvfrom(sock, buffer, len, flags, reinterpret_cast(&from), &size); if (ret < 0) throw std::system_error(WSAGetLastError(), std::system_category(), "recvfrom failed"); // make the buffer zero terminated buffer[ret] = 0; return from; } void Bind(unsigned short port) { sockaddr_in add; add.sin_family = AF_INET; add.sin_addr.s_addr = htonl(INADDR_ANY); add.sin_port = htons(port); int ret = bind(sock, reinterpret_cast(&add), sizeof(add)); if (ret < 0) throw std::system_error(WSAGetLastError(), std::system_category(), "Bind failed"); } private: SOCKET sock; }; // this function maps a value to another range of values // automatically offsets the output with "inMin" // this means only positive ranges are outputed float mapRange(float input, float inMin, float inMax, float outMax, float outMin);
vJoyClient.cpp:
// vJoyClient.cpp : Simple feeder application // // Supports both types of POV Hats #include "Network.h" #include "stdafx.h" #include "public.h" #include "vjoyinterface.h" #include "malloc.h" #include "stdlib.h" #pragma comment( lib, "VJOYINTERFACE" ) #define _CRT_SECURE_NO_WARNINGS #define PORTUDP 8888 // Use either ROBUST or EFFICIENT functions #define ROBUST //#define EFFICIENT int __cdecl _tmain(__in int argc, __in PZPWSTR argv) { USHORT X, Y, Z, ZR, XR; // Position of several axes BYTE id=1; // ID of the target vjoy device (Default is 1) UINT iInterface=1; // Default target vJoy device BOOL ContinuousPOV=FALSE; // Continuous POV hat (or 4-direction POV Hat) int count=0; double maxOutput = 100.0; /* Here I modify the original code by inserting a UDP socket and using it later to receive data to be used with vjoy */ WSASession Session; UDPSocket Socket; char buffer[300]; Socket.Bind(PORTUDP); int leCounter; // split the data into its parts char * strtokIndx; // this is used by strtok() as an index int receivedVals[13]; // Get the ID of the target vJoy device if (argc>1 && wcslen(argv[1])) sscanf_s((char *)(argv[1]), "%d", &iInterface); // Get the driver attributes (Vendor ID, Product ID, Version Number) if (!vJoyEnabled()) { _tprintf("vJoy driver not enabled: Failed Getting vJoy attributes.\n"); return -2; } else { _tprintf("Vendor: %S\nProduct :%S\nVersion Number:%S\n", TEXT(GetvJoyManufacturerString()), TEXT(GetvJoyProductString()), TEXT(GetvJoySerialNumberString())); }; WORD VerDll, VerDrv; if (!DriverMatch(&VerDll, &VerDrv)) _tprintf("Failed\r\nvJoy Driver (version %04x) does not match vJoyInterface DLL (version %04x)\n", VerDrv ,VerDll); else _tprintf( "OK - vJoy Driver and vJoyInterface DLL match vJoyInterface DLL (version %04x)\n", VerDrv); // Get the state of the requested device VjdStat status = GetVJDStatus(iInterface); switch (status) { case VJD_STAT_OWN: _tprintf("vJoy Device %d is already owned by this feeder\n", iInterface); break; case VJD_STAT_FREE: _tprintf("vJoy Device %d is free\n", iInterface); break; case VJD_STAT_BUSY: _tprintf("vJoy Device %d is already owned by another feeder\nCannot continue\n", iInterface); return -3; case VJD_STAT_MISS: _tprintf("vJoy Device %d is not installed or disabled\nCannot continue\n", iInterface); return -4; default: _tprintf("vJoy Device %d general error\nCannot continue\n", iInterface); return -1; }; // Check which axes are supported BOOL AxisX = GetVJDAxisExist(iInterface, HID_USAGE_X); BOOL AxisY = GetVJDAxisExist(iInterface, HID_USAGE_Y); BOOL AxisZ = GetVJDAxisExist(iInterface, HID_USAGE_Z); BOOL AxisRX = GetVJDAxisExist(iInterface, HID_USAGE_RX); BOOL AxisRZ = GetVJDAxisExist(iInterface, HID_USAGE_RZ); // Get the number of buttons and POV Hat switchessupported by this vJoy device int nButtons = GetVJDButtonNumber(iInterface); int ContPovNumber = GetVJDContPovNumber(iInterface); int DiscPovNumber = GetVJDDiscPovNumber(iInterface); // Print results _tprintf("\nvJoy Device %d capabilities:\n", iInterface); _tprintf("Numner of buttons\t\t%d\n", nButtons); _tprintf("Numner of Continuous POVs\t%d\n", ContPovNumber); _tprintf("Numner of Descrete POVs\t\t%d\n", DiscPovNumber); _tprintf("Axis X\t\t%s\n", AxisX?"Yes":"No"); _tprintf("Axis Y\t\t%s\n", AxisX?"Yes":"No"); _tprintf("Axis Z\t\t%s\n", AxisX?"Yes":"No"); _tprintf("Axis Rx\t\t%s\n", AxisRX?"Yes":"No"); _tprintf("Axis Rz\t\t%s\n", AxisRZ?"Yes":"No"); // Acquire the target if ((status == VJD_STAT_OWN) || ((status == VJD_STAT_FREE) && (!AcquireVJD(iInterface)))) { _tprintf("Failed to acquire vJoy device number %d.\n", iInterface); return -1; } else { _tprintf("Acquired: vJoy device number %d.\n", iInterface); } _tprintf("\npress enter to start feeding"); getchar(); X = 20; Y = 30; Z = 40; XR = 60; ZR = 80; long value = 0; BOOL res = FALSE; #ifdef ROBUST // Reset this device to default values ResetVJD(iInterface); // Feed the device in endless loop while(1) { res = SetAxis(X, iInterface, HID_USAGE_X); res = SetAxis(Z, iInterface, HID_USAGE_Z); res = SetContPov((DWORD) XR, iInterface, 1); sockaddr_in add = Socket.RecvFrom(buffer, sizeof(buffer)); std::string input(buffer); std::reverse(std::begin(input), std::end(input)); std::cout << "Received: " << buffer << std::endl; //start parsing data: strtokIndx = strtok(buffer, ","); // get the first part - the number of paramters (nrParams+1) //leCounter = atoi(strtokIndx); //not using this here, I'm coupling this with Wireless IMU app on Android which transmits 13 CSV data values in total leCounter = 13; receivedVals[0] = atoi(strtokIndx); //convert to an integer std::cout << "(parseData output) Received vals, parsed: [0]="<< receivedVals[0]; for (int i = 1; i<leCounter; i++){ strtokIndx = strtok(NULL, ","); // this will continue, the subsequent strtok calls need the NULL parameter if (!strtokIndx){ break; } receivedVals[i] = atoi(strtokIndx); std::cout << " [" <<i<< "]=" << receivedVals[i]; } std::cout << std::endl; // - end of parsing data /* used to test if the received values worked, on a button if (receivedVals[2]){ res = SetBtn(TRUE, iInterface, 1); } else{ res = SetBtn(FALSE, iInterface, 1); }*/ X = (LONG) (mapRange((float)receivedVals[3], -10.0, 10.0, 30000.0, 0.0) + 0.5); // casts to int( range 0-30,000 for the joystick) with rounding ( hence the +0.5) std::cout << "Mapped X axis: " << X << std::endl; Z = (LONG)(mapRange((float)receivedVals[4], -10.0, 10.0, 35000.0, 0.0) + 0.5); // casts to int( range 0-30,000 for the joystick) with rounding ( hence the +0.5) std::cout << "Mapped Z axis: " << Z << std::endl; // Sleep(5); } // While #endif #ifdef EFFICIENT // Start feeding in an endless loop while (1) { /*** Create the data packet that holds the entire position info ***/ id = (BYTE)iInterface; iReport.bDevice = id; iReport.wAxisX=X; iReport.wAxisY=Y; iReport.wAxisZ=Z; iReport.wAxisZRot=ZR; iReport.wAxisXRot=XR; // Set buttons one by one iReport.lButtons = 1<<count/20; if (ContPovNumber) { // Make Continuous POV Hat spin iReport.bHats = (DWORD)(count*70); iReport.bHatsEx1 = (DWORD)(count*70)+3000; iReport.bHatsEx2 = (DWORD)(count*70)+5000; iReport.bHatsEx3 = 15000 - (DWORD)(count*70); if ((count*70) > 36000) { iReport.bHats = -1; // Neutral state iReport.bHatsEx1 = -1; // Neutral state iReport.bHatsEx2 = -1; // Neutral state iReport.bHatsEx3 = -1; // Neutral state }; } else { // Make 5-position POV Hat spin unsigned char pov[4]; pov[0] = ((count/20) + 0)%4; pov[1] = ((count/20) + 1)%4; pov[2] = ((count/20) + 2)%4; pov[3] = ((count/20) + 3)%4; iReport.bHats = (pov[3]<<12) | (pov[2]<<8) | (pov[1]<<4) | pov[0]; if ((count) > 550) iReport.bHats = -1; // Neutral state }; /*** Feed the driver with the position packet - is fails then wait for input then try to re-acquire device ***/ if (!UpdateVJD(iInterface, (PVOID)&iReport)) { _tprintf("Feeding vJoy device number %d failed - try to enable device then press enter\n", iInterface); getchar(); AcquireVJD(iInterface); ContinuousPOV = (BOOL)GetVJDContPovNumber(iInterface); } Sleep(20); count++; if (count > 640) count=0; X+=150; Y+=250; Z+=350; ZR-=200; }; #endif _tprintf("OK\n"); return 0; } float mapRange(float input, float inMin, float inMax, float outMax, float outMin){ return (input + fabs(inMin)) * (outMax - outMin) / (inMax - inMin); }
What I removed and what I’ve added:
At the end of Network.h you can observe the line:
float mapRange(float input, float inMin, float inMax, float outMax, float outMin);
This is the only method I’ve had to write from scratch, it is defined in the .cpp file at the end, and I needed it to be able to convert from one interval to another. To be specific, the values sent by the accelerometer ar within the (-10.0, 10.0) interval and from what I’ve experimented the virtual joystick best works with values within the (0.0, 30000.0) interval, where 15000.0 would be the middle point.
By the way, in Network.h there’sa statement at the beginning: #pragma comment(lib, “Ws2_32.lib”). That is a library needed to run the application, and #pragma_once prevents multiple inclusion of the header file. An additional setting that must be made before running the project is No(/sdl-) in Configuration Properties> C/C++>General> SDL checks( in the Project’s properties menu, assuming you use Visual Studio). Otherwise it might not compile because of tome deprecated methods.
In _tmain, somewhere at the beginning I’m doing the initial settings for the UDP socket and I’m assigning the port(8888 in this case). It’s a lot of boilerplate code, but the action happens after #ifdef ROBUST, in the while(1) loop. In the original file there was more testing code, including some buttons testing, but I was interested just in the two axis that could be used to control the direction and speed of the car.
In this loop I notify the driver that I have 2 axis(ignore the third thing, I don’t actually use it), and after that I receive in the buffer the data, if it exists. Then I will parse the data from the string format using a string tokenizer into an int array. Finally, I assign the data which interests me to the joystick’s axis( after using the mapRange function). How did I know what data was the correct data? Trial and error :).
It’s obvious that the whole thing is not a very polished experiment, and that the car’s motion won’t be algorithmically compensated. In short, it’s kinda jittery. The first enhancements that pass through my had would be an ease-in/ease-out algorithm for the car direction and a non-linear acceleration.
An because a videoclip makes a thousand images, and an image makes a thousand words, I’ll let you see a story of 10^6 words. Hehe. (you can watch the clip at the top of the page)
network.h needs reinterpret_cast
Hey I am starting out with vJoy and having problem to even test the device attributes. Its the bolier plate code specifically the vJoy header file “vjoyinterface” I am not being able to access its code cause when I am using this minimalistic code I am getting an error:
C:\Users\HP\AppData\Local\Temp\ccCQLLBb.o vJoyClient1.cpp:(.text+0x40): undefined reference to `__imp_vJoyEnabled’
C:\Users\HP\AppData\Local\Temp\ccCQLLBb.o vJoyClient1.cpp:(.text+0x65): undefined reference to `__imp_GetvJoySerialNumberString’
C:\Users\HP\AppData\Local\Temp\ccCQLLBb.o vJoyClient1.cpp:(.text+0x71): undefined reference to `__imp_GetvJoyProductString’
C:\Users\HP\AppData\Local\Temp\ccCQLLBb.o vJoyClient1.cpp:(.text+0x7d): undefined reference to `__imp_GetvJoyManufacturerString’
E:\projects\XTNT\SDK\src\collect2.exe [Error] ld returned 1 exit status
for the following code:
#include “stdafx.h”
#include “../inc/public.h”
#include “../inc/vjoyinterface.h”
#include “malloc.h”
#include “stdlib.h”
#pragma comment( lib, “VJOYINTERFACE” )
#define _CRT_SECURE_NO_WARNINGS
#define PORTUDP 8888
// Use either ROBUST or EFFICIENT functions
#define ROBUST
//#define EFFICIENT
int
__cdecl
_tmain(__in int argc, __in PZPWSTR argv)
{
if (!vJoyEnabled())
{
_tprintf(“vJoy driver not enabled: Failed Getting vJoy attributes.\n”);
return -2;
}
else
{
_tprintf(“Vendor: %S\nProduct :%S\nVersion Number:%S\n”, TEXT(GetvJoyManufacturerString()), TEXT(GetvJoyProductString()), TEXT(GetvJoySerialNumberString()));
};
}
and on compiling your code it gives the follwing errors:
24 13 E:\projects\XTNT\SDK\src\vJoyClient1.cpp [Warning] ‘__cdecl__’ attribute only applies to function types [-Wattributes]
24 8 E:\projects\XTNT\SDK\src\vJoyClient1.cpp [Error] ‘__in’ was not declared in this scope
24 23 E:\projects\XTNT\SDK\src\vJoyClient1.cpp [Error] ‘__in’ was not declared in this scope
24 40 E:\projects\XTNT\SDK\src\vJoyClient1.cpp [Error] expression list treated as compound expression in initializer [-fpermissive]
25 1 E:\projects\XTNT\SDK\src\vJoyClient1.cpp [Error] expected ‘,’ or ‘;’ before ‘{‘ token