Welcome to Hackster!
Hackster is a community dedicated to learning hardware, from beginner to pro. Join us, it's free!
abrakhim
Published © GPL3+

Rumblebutt HOTAS Chair

An old, cheap massage chair hooked up to the throttle of a HOTAS to simulate engine hum while playing in VR...

IntermediateShowcase (no instructions)354
Rumblebutt HOTAS Chair

Things used in this project

Hardware components

Wemos D1 Mini
Espressif Wemos D1 Mini
×1

Story

Read more

Code

Rumblebutt Raw Input to UDP

C/C++
Grabs throttle axis of my HOTAS and does some conversion, then sends it via UDP
///////////////////////////////////////////////////////////////////////////////
//
// Rumblebutt Raw Input to UDP
//
///////////////////////////////////////////////////////////////////////////////

// Attention, including winsock2 and Windows does clash, to prevent that, define _WINSOCKAPI_ like this first:
#define _WINSOCKAPI_

#include <Windows.h>
#include <tchar.h>
#include <math.h>
#include <hidsdi.h>
#include <stdio.h>
#include <winsock2.h>


#pragma comment(lib,"ws2_32.lib") //Winsock Library

#define _USE_MATH_DEFINES
#define SERVER "xxx.xxx.xxx.xxx"	//ip address of udp server
#define BUFLEN 512	//Max length of buffer
#define PORT 4210	//The port on which to listen for incoming data

#define ARRAY_SIZE(x)	(sizeof(x) / sizeof((x)[0]))
#define WC_MAINFRAME	TEXT("MainFrame")
#define CHECK(exp)		{ if(!(exp)) goto Error; }
#define SAFE_FREE(p)	{ if(p) { HeapFree(hHeap, 0, p); (p) = NULL; } }

//
// Global variables
//

LONG lAxisX;
LONG lAxisY;
LONG lAxisZ;
LONG lAxisRz;
LONG lHat;
INT  g_NumberOfButtons;


//Prototyp winsock
int startWinsock(void);

void ParseRawInput(PRAWINPUT pRawInput)
{
	PHIDP_PREPARSED_DATA pPreparsedData;
	HIDP_CAPS            Caps;
	PHIDP_BUTTON_CAPS    pButtonCaps;
	PHIDP_VALUE_CAPS     pValueCaps;
	USHORT               capsLength;
	UINT                 bufferSize=0;
	HANDLE               hHeap;
	ULONG                value;

	pPreparsedData = NULL;
	pButtonCaps    = NULL;
	pValueCaps     = NULL;
	hHeap          = GetProcessHeap();

	//
	// Get the preparsed data block
	//

	CHECK( GetRawInputDeviceInfo(pRawInput->header.hDevice, RIDI_PREPARSEDDATA, NULL, &bufferSize) == 0 );
	CHECK( pPreparsedData = (PHIDP_PREPARSED_DATA)HeapAlloc(hHeap, 0, bufferSize) );
	CHECK( (int)GetRawInputDeviceInfo(pRawInput->header.hDevice, RIDI_PREPARSEDDATA, pPreparsedData, &bufferSize) >= 0 );

	//
	// Get the joystick's capabilities
	//

	CHECK( HidP_GetCaps(pPreparsedData, &Caps) == HIDP_STATUS_SUCCESS )

	g_NumberOfButtons = 0;

	// Value caps
	CHECK( pValueCaps = (PHIDP_VALUE_CAPS)HeapAlloc(hHeap, 0, sizeof(HIDP_VALUE_CAPS) * Caps.NumberInputValueCaps) );
	capsLength = Caps.NumberInputValueCaps;
	CHECK(HidP_GetValueCaps(HidP_Input, pValueCaps, &capsLength, pPreparsedData) == HIDP_STATUS_SUCCESS)


		//
		// Get the state of discrete-valued-controls
		//


	for(int i = 0; i < Caps.NumberInputValueCaps; i++)
	{
		CHECK(
			HidP_GetUsageValue(
				HidP_Input, pValueCaps[i].UsagePage, 0, pValueCaps[i].Range.UsageMin, &value, pPreparsedData,
				(PCHAR)pRawInput->data.hid.bRawData, pRawInput->data.hid.dwSizeHid
			) == HIDP_STATUS_SUCCESS );

		switch(pValueCaps[i].Range.UsageMin)
		{
		case 0x30:	// X-axis
			lAxisX = (LONG)value - 128;
			break;

		case 0x31:	// Y-axis
			lAxisY = (LONG)value - 128;
			break;

		case 0x32: // Z-axis
			lAxisZ = (LONG)value - 128;
			break;

		case 0x35: // Rotate-Z
			lAxisRz = (LONG)value - 128;
			break;

		case 0x39:	// Hat Switch
			lHat = value;
			break;
		}
	}

	//
	// Clean up
	//

Error:
	SAFE_FREE(pPreparsedData);
	SAFE_FREE(pButtonCaps);
	SAFE_FREE(pValueCaps);
}


void DrawCrosshair(HDC hDC, int x, int y, LONG xVal, LONG yVal)
{
	Rectangle(hDC, x, y, x + 256, y + 256);
	MoveToEx(hDC, x + xVal - 5 + 128, y + yVal + 128, NULL);
	LineTo(hDC, x + xVal + 5 + 128, y + yVal + 128);
	MoveToEx(hDC, x + xVal + 128, y + yVal - 5 + 128, NULL);
	LineTo(hDC, x + xVal + 128, y + yVal + 5 + 128);
}


LRESULT CALLBACK WindowProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	switch(msg)
	{
	case WM_CREATE:
		{
			//
			// Register for joystick devices
			//

			RAWINPUTDEVICE rid;

			rid.usUsagePage = 1;
			rid.usUsage     = 4;	// Joystick
			
			// RIDEV_INPUTSINK is needed to let this program receive input even if not in foreground/focused
			rid.dwFlags     = RIDEV_INPUTSINK;
			
			rid.hwndTarget  = hWnd;

			if(!RegisterRawInputDevices(&rid, 1, sizeof(RAWINPUTDEVICE)))
				return -1;
		}
		return 0;

	case WM_INPUT:
		{
			//
			// Get the pointer to the raw device data, process it and update the window
			//

			PRAWINPUT pRawInput;
			UINT      bufferSize=0;
			HANDLE    hHeap;

			GetRawInputData((HRAWINPUT)lParam, RID_INPUT, NULL, &bufferSize, sizeof(RAWINPUTHEADER));

			hHeap     = GetProcessHeap();
			pRawInput = (PRAWINPUT)HeapAlloc(hHeap, 0, bufferSize);
			if(!pRawInput)
				return 0;

			GetRawInputData((HRAWINPUT)lParam, RID_INPUT, pRawInput, &bufferSize, sizeof(RAWINPUTHEADER));
			ParseRawInput(pRawInput);

			HeapFree(hHeap, 0, pRawInput);

			InvalidateRect(hWnd, NULL, TRUE);
			UpdateWindow(hWnd);
		}
		return 0;

	case WM_PAINT:
		{
			//
			// Draw the buttons and axis-values
			//

			PAINTSTRUCT ps;
			HDC         hDC;

			hDC = BeginPaint(hWnd, &ps);
			SetBkMode(hDC, TRANSPARENT);

			DrawCrosshair(hDC, 50, 50, lAxisZ, 0);
			TCHAR text[256];
			swprintf_s(text, 256, L"Throttle: %d", lAxisZ);
			TextOut(hDC, 10, 10, text, wcslen(text));

			EndPaint(hWnd, &ps);
		}
		return 0;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	}

	return DefWindowProc(hWnd, msg, wParam, lParam);
}


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
	HWND hWnd;
	MSG msg;
	WNDCLASSEX wcex;

	struct sockaddr_in si_other;
	int s, slen = sizeof(si_other);
	char buf[BUFLEN];
	char message[BUFLEN];
	WSADATA wsa;

	//Initialise winsock

	if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
	{
		exit(EXIT_FAILURE);
	}
	

	//create socket
	if ((s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) == SOCKET_ERROR)
	{
		exit(EXIT_FAILURE);
	}

	//setup address structure
	memset((char*)&si_other, 0, sizeof(si_other));
	si_other.sin_family = AF_INET;
	si_other.sin_port = htons(PORT);
	si_other.sin_addr.S_un.S_addr = inet_addr(SERVER);

	//
	// Register window class
	//

	wcex.cbSize        = sizeof(WNDCLASSEX);
	wcex.cbClsExtra    = 0;
	wcex.cbWndExtra    = 0;
	wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
	wcex.hCursor       = LoadCursor(NULL, IDC_ARROW);
	wcex.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
	wcex.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);
	wcex.hInstance     = hInstance;
	wcex.lpfnWndProc   = WindowProc;
	wcex.lpszClassName = WC_MAINFRAME;
	wcex.lpszMenuName  = NULL;
	wcex.style         = CS_HREDRAW | CS_VREDRAW;

	if(!RegisterClassEx(&wcex))
		return -1;

	//
	// Create window
	//

	hWnd = CreateWindow(WC_MAINFRAME, TEXT("Rumblebutt Raw Input to UDP"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 400, 400, NULL, NULL, hInstance, NULL);
	ShowWindow(hWnd, nShowCmd);
	UpdateWindow(hWnd);

	//
	// Message loop
	//

	while(GetMessage(&msg, NULL, 0, 0))
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);

		int sendThrottle;
		
		// The values -127 to 127 are divided into 4 cases 
		// Use this if throttle has backward gear, stop, and forward 
		if (lAxisZ > 84) {
			// Backwards 
			sendThrottle = 20 + (lAxisZ - 84)*2;
		}
		else if (lAxisZ > 74) {
			// No thrust, no rumble
			// Close to full stop send nothing to eliminate high pitched sound from chair
			sendThrottle = 0;
		}
		else if (lAxisZ > 0) {
			// Forward positiv
			sendThrottle = 75 - lAxisZ;
		}
		else {
			// Forward negativ
			sendThrottle = 75 + (lAxisZ*-1);
		}
		
		/* Use this if throttle in game is only forward or you can shift
		if (lAxisZ > 0) {
			// Backwards 42 Values
			sendThrottle = 128 - lAxisZ;
			
			// Close to full stop send nothing to eliminate high pitched sound from chair
			if (sendThrottle < 10) sendThrottle = 0;
		
		}
		else {
			// Forward negativ
			sendThrottle = 126 + (lAxisZ * -1);
		}
    */

		//Send via UDP
		char udpsend[4];
		sprintf_s(udpsend, sizeof(udpsend), "%d", sendThrottle);
		sendto(s, udpsend, strlen(udpsend), 0, (struct sockaddr*)&si_other, slen);


	}

	closesocket(s);
	WSACleanup();

	return (int)msg.wParam;
}

int startWinsock(void)
{
	WSADATA wsa;
	return WSAStartup(MAKEWORD(2, 0), &wsa);
}

Wemos D1 mini UDP Server and PWM Controller

C/C++
It receives UDP packet and controls power to 'rumble pack' via PWM and D4184 Mosfet Module
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
 
// Set WiFi credentials
#define WIFI_SSID "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
#define WIFI_PASS "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

// PWM pin, see pinout for Wemos D1 mini
const int vibrateAllPin = 4;

uint8_t pwmint[1];

WiFiUDP Udp;
unsigned int localUdpPort = 4210;  // local port to listen on
char incomingPacket[32];  // buffer for incoming packets

 
void setup() {
  
  pinMode(vibrateAllPin, OUTPUT);

  // Set value 0
  pwmint[0] = 0;
     
  // Begin WiFi
  WiFi.begin(WIFI_SSID, WIFI_PASS);
 
  // Loop continuously while WiFi is not connected
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(100);
  }
 
  Udp.begin(localUdpPort);

}
 
void loop() {

  //Write pwmint[0]*4 to D4/GPIO 2 ( this factor x4 is a PWM range conversion for the Wemos, may differ with other boards
  analogWrite(vibrateAllPin, pwmint[0]*4);
  
  int packetSize = Udp.parsePacket();
  if (packetSize)
  {
    // receive incoming UDP packets
    int len = Udp.read(incomingPacket, 32);
    if (len > 0)
    {
      incomingPacket[len] = 0;
    }

    
    if (len > 0 ){
     
        pwmint[0]= atoi(incomingPacket);
       
        Udp.beginPacket(Udp.remoteIP(), Udp.remotePort());
        Udp.write(replyPacket);
        Udp.endPacket();
        
    }else{
    
        Udp.beginPacket(Udp.remoteIP(), Udp.remotePort());
        Udp.write(replyPacketError);
        Udp.endPacket();
    }

  }

  delay(200);
}

Credits

abrakhim
0 projects • 0 followers
Contact

Comments

Please log in or sign up to comment.