diff --git a/src/CMakeData-arch.cmake b/src/CMakeData-arch.cmake index dd76fa30d8..f388a0b72b 100644 --- a/src/CMakeData-arch.cmake +++ b/src/CMakeData-arch.cmake @@ -227,17 +227,20 @@ if(NOT APPLE) endif() else() # Unix/Linux TODO: Linux HAVE_PARALLEL_PORT if(LINUX) + list(APPEND SMDATA_LINK_LIB "udev") list(APPEND SMDATA_ARCH_LIGHTS_SRC "arch/Lights/LightsDriver_Linux_PIUIO.cpp" "arch/Lights/LightsDriver_Linux_PIUIO_Leds.cpp" "arch/Lights/LightsDriver_Linux_PIUIOBTN_Leds.cpp" "arch/Lights/LightsDriver_Linux_ITGIO.cpp" + "arch/Lights/LightsDriver_Linux_stac.cpp" "arch/Lights/LightsDriver_LinuxWeedTech.cpp") list(APPEND SMDATA_ARCH_LIGHTS_HPP "arch/Lights/LightsDriver_Linux_PIUIO.h" "arch/Lights/LightsDriver_Linux_PIUIO_Leds.h" "arch/Lights/LightsDriver_Linux_PIUIOBTN_Leds.h" "arch/Lights/LightsDriver_Linux_ITGIO.h" + "arch/Lights/LightsDriver_Linux_stac.h" "arch/Lights/LightsDriver_LinuxWeedTech.h") if(WITH_PARALLEL_PORT) list(APPEND SMDATA_ARCH_LIGHTS_SRC diff --git a/src/arch/Lights/LightsDriver_Linux_stac.cpp b/src/arch/Lights/LightsDriver_Linux_stac.cpp new file mode 100644 index 0000000000..f5e187f623 --- /dev/null +++ b/src/arch/Lights/LightsDriver_Linux_stac.cpp @@ -0,0 +1,286 @@ +#include "global.h" +#include +#if defined(HAVE_UNISTD_H) +#include +#endif +#include +#include + +#if defined(HAVE_FCNTL_H) +#include +#endif + +#include +#include "LightsDriver_Linux_stac.h" +#include "GameState.h" +#include "Game.h" +#include "RageLog.h" + +#include +#include +#include +#include + +REGISTER_LIGHTS_DRIVER_CLASS2(stac, Linux_stac); + +StacDevice::StacDevice(uint8_t pn) +{ + memset(outputBuffer, 0x00, sizeof(outputBuffer)); + + deviceVID = STAC_VID; + playerNumber = pn; + + switch (playerNumber) + { + case 1: + devicePID = STAC_PID_P1; + break; + + case 2: + devicePID = STAC_PID_P2; + break; + + default: + LOG->Warn("STAC: Invalid player number instantiated, no PID set."); + break; + } +} + +//function adapted from example in: +//https://web.archive.org/web/20190312223426/http://www.signal11.us/oss/udev/ +//allows us to find the /dev/hidraw device that matches the Snek usb VID and PID. +void StacDevice::FindDevice() +{ + struct udev *udev; + struct udev_enumerate *enumerate; + struct udev_list_entry *devices, *dev_list_entry; + struct udev_device *dev; + + const char *devVID; + const char *devPID; + + bool found = false; + + /* Create the udev object */ + udev = udev_new(); + if (!udev) + { + LOG->Warn("Can't create udev"); + return; + } + + /* Create a list of the devices in the 'hidraw' subsystem. */ + enumerate = udev_enumerate_new(udev); + udev_enumerate_add_match_subsystem(enumerate, "hidraw"); + udev_enumerate_scan_devices(enumerate); + devices = udev_enumerate_get_list_entry(enumerate); + /* For each item enumerated, print out its information. + udev_list_entry_foreach is a macro which expands to + a loop. The loop will be executed for each member in + devices, setting dev_list_entry to a list entry + which contains the device's path in /sys. */ + udev_list_entry_foreach(dev_list_entry, devices) + { + const char *path; + + /* Get the filename of the /sys entry for the device + and create a udev_device object (dev) representing it */ + path = udev_list_entry_get_name(dev_list_entry); + dev = udev_device_new_from_syspath(udev, path); + + devicePath = udev_device_get_devnode(dev); + + /* The device pointed to by dev contains information about + the hidraw device. In order to get information about the + USB device, get the parent device with the + subsystem/devtype pair of "usb"/"usb_device". This will + be several levels up the tree, but the function will find + it.*/ + dev = udev_device_get_parent_with_subsystem_devtype( + dev, + "usb", + "usb_device"); + + if (!dev) + { + LOG->Warn("Unable to find parent usb device."); + return; + } + + devVID = udev_device_get_sysattr_value(dev, "idVendor"); + devPID = udev_device_get_sysattr_value(dev, "idProduct"); + + udev_device_unref(dev); + + if (strcmp(devVID, deviceVID) == 0 && + strcmp(devPID, devicePID) == 0) + { + found = true; + break; + } + } + + /* Free the enumerator object */ + udev_enumerate_unref(enumerate); + + udev_unref(udev); + + if (!found) + { + devicePath = nullptr; + } +} + +void StacDevice::Connect() +{ + devicePath = nullptr; + fd = -1; + + FindDevice(); + + if (devicePath == nullptr) + { + LOG->Warn("stac pn%d not found. Please ensure device is connected to system.", playerNumber); + } + else + { + fd = open(devicePath, O_RDWR | O_NONBLOCK); + + if (!IsConnected()) + { + LOG->Warn("Could not open stac pn%d at %s. Check device permissions or udev rules. errno: %d: %s", playerNumber, devicePath, errno, strerror(errno)); + } + else + { + LOG->Info("Successfully opened stac pn%d at: %s", playerNumber, devicePath); + + //ensure the device, when connected, gets a fresh state. + newState = true; + } + } +} + +void StacDevice::Close() +{ + if (fd >= 0) + { + close(fd); + + LOG->Info("Closed stac pn%d at: %s", playerNumber, devicePath); + + fd = -1; + } +} + +void StacDevice::SetInBuffer(int index, bool lightState) +{ + //the first byte is the report ID, so we offset it here to adjust. + uint8_t index_offset = index + 1; + + //each index in the array represents a single light, + //the light will turn on for any value that isn't 0x00 + uint8_t val = lightState ? 0xFF : 0x00; + + //ensure the index is valid and the light value has changed. + if (index_offset < STAC_HIDREPORT_SIZE && outputBuffer[index_offset] != val) + { + outputBuffer[index_offset] = val; + + //signal the loop to push the new buffer to the device. + newState = true; + } +} + +void StacDevice::PushBufferToDevice() +{ + //first check to make sure we have *new* data to send to the device. + if (newState) + { + //Always ensure there is a report ID before writing, + //otherwise linux will complain and throw errors. + outputBuffer[0] = STAC_REPORT_ID; + + int res = write(fd, outputBuffer, STAC_HIDREPORT_SIZE); + + if (res < 0) + { + LOG->Warn("stac pn%d Write Error: %d: %s", playerNumber, errno, strerror(errno)); + + //close on error write to signal a reconnect. + Close(); + } + + newState = false; + } +} + +LightsDriver_Linux_stac::LightsDriver_Linux_stac() +{ + //attempt to connect on game launch + stacPlayer1->Connect(); + stacPlayer2->Connect(); + + //we keep track of if the device has *ever* been connected to signal + //if we want to try to reconnect. + //because if there is no P2 stac connected, then we don't want to go + //searching for something that was *never* there. + haveSeenP1 = stacPlayer1->IsConnected(); + haveSeenP2 = stacPlayer2->IsConnected(); +} + +LightsDriver_Linux_stac::~LightsDriver_Linux_stac() +{ + stacPlayer1->Close(); + stacPlayer2->Close(); + + delete stacPlayer1; + delete stacPlayer2; +} + +void LightsDriver_Linux_stac::HandleState(const LightsState *ls, StacDevice *dev, GameController ctrlNum) +{ + //check to see which game we are running as it can change during gameplay. + const InputScheme *pInput = &GAMESTATE->GetCurrentGame()->m_InputScheme; + RString sInputName = pInput->m_szName; + + if (sInputName.EqualsNoCase("dance")) + { + dev->SetInBuffer(STAC_LIGHTINDEX_BTN1, ls->m_bGameButtonLights[ctrlNum][DANCE_BUTTON_UP]); + dev->SetInBuffer(STAC_LIGHTINDEX_BTN2, ls->m_bGameButtonLights[ctrlNum][DANCE_BUTTON_DOWN]); + dev->SetInBuffer(STAC_LIGHTINDEX_BTN3, ls->m_bGameButtonLights[ctrlNum][DANCE_BUTTON_LEFT]); + dev->SetInBuffer(STAC_LIGHTINDEX_BTN4, ls->m_bGameButtonLights[ctrlNum][DANCE_BUTTON_RIGHT]); + } + else if (sInputName.EqualsNoCase("pump")) + { + dev->SetInBuffer(STAC_LIGHTINDEX_BTN1, ls->m_bGameButtonLights[ctrlNum][PUMP_BUTTON_UPLEFT]); + dev->SetInBuffer(STAC_LIGHTINDEX_BTN2, ls->m_bGameButtonLights[ctrlNum][PUMP_BUTTON_UPRIGHT]); + dev->SetInBuffer(STAC_LIGHTINDEX_BTN3, ls->m_bGameButtonLights[ctrlNum][PUMP_BUTTON_CENTER]); + dev->SetInBuffer(STAC_LIGHTINDEX_BTN4, ls->m_bGameButtonLights[ctrlNum][PUMP_BUTTON_DOWNLEFT]); + dev->SetInBuffer(STAC_LIGHTINDEX_BTN5, ls->m_bGameButtonLights[ctrlNum][PUMP_BUTTON_DOWNRIGHT]); + } + + dev->PushBufferToDevice(); +} + +void LightsDriver_Linux_stac::Set(const LightsState *ls) +{ + if (stacPlayer1->IsConnected()) + { + HandleState(ls, stacPlayer1, GameController_1); + } + else if (haveSeenP1) + { + //only attempt to connect again if it was connected on launch. + stacPlayer1->Connect(); + } + + if (stacPlayer2->IsConnected()) + { + HandleState(ls, stacPlayer2, GameController_2); + } + else if (haveSeenP2) + { + //only attempt to connect again if it was connected on launch. + stacPlayer2->Connect(); + } +} diff --git a/src/arch/Lights/LightsDriver_Linux_stac.h b/src/arch/Lights/LightsDriver_Linux_stac.h new file mode 100644 index 0000000000..a6dc1f9d4a --- /dev/null +++ b/src/arch/Lights/LightsDriver_Linux_stac.h @@ -0,0 +1,120 @@ +/* LightsDriver_Linux_stac: Control lights for the stac by icedragon.io via direct hidraw writes. */ + +#ifndef LightsDriver_Linux_stac_H +#define LightsDriver_Linux_stac_H + +/* + * -------------------------- NOTE -------------------------- + * + * This driver needs user read/write access to the icedragon.io stac. + * This can be achieved by using a udev rule like this: + * + * (player 1 then player 2) + * SUBSYSTEMS=="usb", ATTRS{idVendor}=="04d8", ATTRS{idProduct}=="eb5b", OWNER="dance", GROUP="dance", MODE="0660" + * SUBSYSTEMS=="usb", ATTRS{idVendor}=="04d8", ATTRS{idProduct}=="eb5a", OWNER="dance", GROUP="dance", MODE="0660" + * + * Refer to your distrobution's documentation on how to properly apply a udev rule. + * + * -------------------------- NOTE -------------------------- + */ + +#include "arch/Lights/LightsDriver.h" + +//static information about the device(s) in question. +#define STAC_VID "04d8" +#define STAC_PID_P1 "ea4b" +#define STAC_PID_P2 "ea4a" +#define STAC_NUMOF_LIGHTS 5 + +//the first byte of the buffer is a static report id. +#define STAC_HIDREPORT_SIZE (STAC_NUMOF_LIGHTS + 1) +#define STAC_REPORT_ID 0x01 + +//all indicies contain their respective 573 pinouts +enum StacLightIndex +{ + STAC_LIGHTINDEX_BTN1 = 0, + STAC_LIGHTINDEX_BTN2 = 1, + STAC_LIGHTINDEX_BTN3 = 2, + STAC_LIGHTINDEX_BTN4 = 3, + STAC_LIGHTINDEX_BTN5 = 4, + STAC_LIGHTINDEX_MAX +}; + +class StacDevice +{ +public: + const char *deviceVID; + const char *devicePID; + + const char *devicePath; + int fd = -1; + + uint8_t playerNumber = 0; + bool newState = false; + + uint8_t outputBuffer[STAC_HIDREPORT_SIZE]; + + StacDevice(uint8_t pn); + + void FindDevice(); + void Connect(); + void Close(); + + bool IsConnected() + { + //a zero/positive file descriptor is a valid one. + //and if we have opened it, then we are connected. + //if there is an error, then we close it. + return (fd >= 0); + } + + void SetInBuffer(int index, bool lightState); + + void PushBufferToDevice(); +}; + +class LightsDriver_Linux_stac : public LightsDriver +{ +private: + StacDevice *stacPlayer1 = new StacDevice(1); + StacDevice *stacPlayer2 = new StacDevice(2); + + bool haveSeenP1 = false; + bool haveSeenP2 = false; + + void HandleState(const LightsState *ls, StacDevice *dev, GameController ctrlNum); + +public: + LightsDriver_Linux_stac(); + virtual ~LightsDriver_Linux_stac(); + + virtual void Set(const LightsState *ls); +}; + +#endif + +/* + * (c) 2021 StepMania team + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, provided that the above + * copyright notice(s) and this permission notice appear in all copies of + * the Software and that both the above copyright notice(s) and this + * permission notice appear in supporting documentation. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF + * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS + * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT + * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS + * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ \ No newline at end of file