diff --git a/BEMANI_PSX_Adapter/BEMANI_PSX_Adapter.ino b/BEMANI_PSX_Adapter/BEMANI_PSX_Adapter.ino new file mode 100644 index 0000000..fea9a87 --- /dev/null +++ b/BEMANI_PSX_Adapter/BEMANI_PSX_Adapter.ino @@ -0,0 +1,295 @@ +#include "IIDXHID.h" +#include "POPNHID.h" +#include "PsxControllerHwSpi.h" +#include + +//use doublepress hotkeys (select is E1, start is E2, double press select E3, triple press select is E1+E3, double press start is E4, triple press start is E2+E4) +#define WITH_DOUBLECLICK 1 + +//delay in µs between HID reports (note: 1000 for jkoc/popn, will be *15 for uskoc) +#define REPORT_DELAY 1000 + +#define COOLDOWN 4 + +IIDXHID_ IIDXHID; +POPNHID_ POPNHID; + +const byte PIN_PS2_ATT = A2; +PsxControllerHwSpi psx; +bool haveController = false; + +int32_t tt_pos = 0; + +typedef enum +{ + TT_NEUTRAL = 0, + TT_UP, + TT_DOWN, +} tt_direction_t; + +void update_encoder(tt_direction_t dir, uint8_t quantity) { + static tt_direction_t last_state; + static int cooldown = 0; + + if (cooldown > 0) + cooldown--; + + if (dir != last_state || cooldown == 0) + { + if (dir == TT_UP) { + tt_pos += quantity; + } else if (dir == TT_DOWN) { + tt_pos -= quantity; + } + cooldown = COOLDOWN; + } + + last_state = dir; +} + +bool g_popn = false; + +void setup() { + haveController = psx.begin(); + delay(300); + psx.read(); + PsxButtons psxButtons = psx.getButtonWord(); + if ((psxButtons & PSB_PAD_LEFT) && (psxButtons & PSB_PAD_DOWN) && (psxButtons & PSB_PAD_RIGHT)) + { + g_popn = true; + PluggableUSB().plug(&POPNHID); + } else { + PluggableUSB().plug(&IIDXHID); + } +} + +void buttonRead(uint32_t *button_state, tt_direction_t *tt_dir) +{ + PsxButtons psxButtons = 0; +if (!haveController) { + if (psx.begin ()) { + delay (300); + haveController = true; + } + } else { + if (!psx.read ()) { + haveController = false; + } else { + psxButtons = psx.getButtonWord(); + } + } + + uint32_t buttonsState = 0; + + if (g_popn) + { + buttonsState |= !!(psxButtons & PSB_TRIANGLE) << 0; + buttonsState |= !!(psxButtons & PSB_CIRCLE) << 1; + buttonsState |= !!(psxButtons & PSB_R1) << 2; + buttonsState |= !!(psxButtons & PSB_CROSS) << 3; + buttonsState |= !!(psxButtons & PSB_L1) << 4; + buttonsState |= !!(psxButtons & PSB_SQUARE) << 5; + buttonsState |= !!(psxButtons & PSB_R2) << 6; + buttonsState |= !!(psxButtons & PSB_PAD_UP) << 7; + buttonsState |= !!(psxButtons & PSB_L2) << 8; + buttonsState |= !!(psxButtons & PSB_SELECT) << 9; + buttonsState |= !!(psxButtons & PSB_START) << 10; + + *button_state = buttonsState; + return; + } + /* + 1 square + 2 L1 + 3 cross + 4 R1 + 5 circle + 6 L2 + 7 left + sel sel + stt start + tt cw up + tt ccw down + */ + + buttonsState |= !!(psxButtons & PSB_SQUARE) << 0; + buttonsState |= !!(psxButtons & PSB_L1) << 1; + buttonsState |= !!(psxButtons & PSB_CROSS) << 2; + buttonsState |= !!(psxButtons & PSB_R1) << 3; + buttonsState |= !!(psxButtons & PSB_CIRCLE) << 4; + buttonsState |= !!(psxButtons & PSB_L2) << 5; + buttonsState |= !!(psxButtons & PSB_PAD_LEFT) << 6; + buttonsState |= !!(psxButtons & PSB_START) << 8; /* E1 */ + buttonsState |= !!(psxButtons & PSB_SELECT) << 9; /* E2 */ + + if (psxButtons & PSB_PAD_UP) + { + *tt_dir = TT_DOWN; + buttonsState |= ((uint32_t)DPAD_UP) << 16; + } + else if (psxButtons & PSB_PAD_DOWN) + { + *tt_dir = TT_UP; + buttonsState |= ((uint32_t)DPAD_DOWN) << 16; + } + else + { + *tt_dir = TT_NEUTRAL; + buttonsState |= ((uint32_t)DPAD_NEUTRAL) << 16; + } + +#if WITH_DOUBLECLICK == 1 + static uint16_t previous_select = 0; + static bool select_double = false; + static bool select_triple = false; + if (buttonsState >> 9 & 1) + { + if (previous_select) + { + if (previous_select < 18) + { + if (select_double) select_triple = true; + else select_double = true; + } + } + previous_select = 18; + } + else + { + if (previous_select) + { + previous_select--; + if (previous_select == 0) + { + select_double = false; + } + } + if (select_triple) + { + select_double = false; + select_triple = false; + } + } + //change output based on double/triple click + if (select_triple) + { + buttonsState |= (uint32_t)1 << 10; /* E3 */ + } + else if (select_double) + { + buttonsState |= (uint32_t)1 << 10; + buttonsState &= ~((uint32_t)1 << 9); + } + + static uint16_t previous_start = 0; + static bool start_double = false; + static bool start_triple = false; + if (buttonsState >> 8 & 1) + { + if (previous_start) + { + if (previous_start < 18) + { + if (start_double) start_triple = true; + else start_double = true; + } + } + previous_start = 18; + } + else + { + if (previous_start) + { + previous_start--; + if (previous_start == 0) + { + start_double = false; + } + } + if (start_triple) + { + start_double = false; + start_triple = false; + } + } + //change output based on double/triple click + if (start_triple) + { + buttonsState |= (uint32_t)1 << 11; /* E4 */ + } + else if (start_double) + { + buttonsState |= (uint32_t)1 << 11; + buttonsState &= ~((uint32_t)1 << 8); + } +#endif + + *button_state = buttonsState; + +} + +void loop() { + static unsigned long lastReport = 0; + static unsigned long lastPool = 0; + static unsigned long currTime = 0; + static int32_t saved_tt_stop = tt_pos; +static uint32_t curr_button_state; +static tt_direction_t curr_tt_dir; +static int curr_tt_pos; + + currTime = micros(); + + if (g_popn) + { + if ( (currTime - lastReport) >= REPORT_DELAY ) + { + uint32_t button_state = 0; + buttonRead(&button_state, NULL); + POPNHID.sendState(button_state); + lastReport = currTime; + } + return; + + } + + /* IIDX MODE */ + // Limit the encoder from 0 to ENCODER_PPR + if (tt_pos >= ENCODER_PPR) { + tt_pos = 1; + } else if (tt_pos <= 0) { + tt_pos = ENCODER_PPR - 1; + } + + if ( (currTime - lastPool) >= 15*REPORT_DELAY ) /* 15ms for USKOC compatibility */ + { + buttonRead(&curr_button_state, &curr_tt_dir); + lastPool = currTime; + } + + if ( (currTime - lastReport) >= REPORT_DELAY ) + { + if ((curr_button_state >> 9) & 1 || (curr_button_state >> 10) & 1) + { + static uint8_t upd_sel_cooldown = 0; + if (upd_sel_cooldown) upd_sel_cooldown--; + + if (upd_sel_cooldown == 0) + { + update_encoder(curr_tt_dir, 1); + upd_sel_cooldown = 8; + } + } else { + static uint8_t upd_cooldown = 2; + if (upd_cooldown) upd_cooldown--; + + if (upd_cooldown == 0) + { + update_encoder(curr_tt_dir, 1); + upd_cooldown = 2; + } + } + + IIDXHID.send_state(curr_button_state, tt_pos); + lastReport = currTime; + } +} diff --git a/BEMANI_PSX_Adapter/IIDXHID.cpp b/BEMANI_PSX_Adapter/IIDXHID.cpp new file mode 100644 index 0000000..e823d9e --- /dev/null +++ b/BEMANI_PSX_Adapter/IIDXHID.cpp @@ -0,0 +1,249 @@ +#include "IIDXHID.h" + +#define BUTTON_PADDING (8 - (NUMBER_OF_BUTTONS % 8)) +#define LED_PADDING (8 - (NUMBER_OF_LEDS % 8)) + +uint8_t usb_data[128]; +uint16_t lamp_hid_state = 0; + +uint8_t extern led_pins[]; +bool extern hid_reactive_autoswitch; + +/* HID string and device descriptor */ +const DeviceDescriptor PROGMEM USB_DeviceDescriptorIAD = + D_DEVICE(0xEF, 0x02, 0x01, 64, 0x1ccf, 0x8048, 0x100, IMANUFACTURER, IPRODUCT, ISERIAL, 1); +const char* const PROGMEM String_Manufacturer = "Konami Amusement"; +const char* const PROGMEM String_Product = "beatmania IIDX controller premium model"; +const char* const PROGMEM String_Serial = "IIDX"; + +const char* const PROGMEM LEDString_00 = "Button 1"; +const char* const PROGMEM LEDString_01 = "Button 2"; +const char* const PROGMEM LEDString_02 = "Button 3"; +const char* const PROGMEM LEDString_03 = "Button 4"; +const char* const PROGMEM LEDString_04 = "Button 5"; +const char* const PROGMEM LEDString_05 = "Button 6"; +const char* const PROGMEM LEDString_06 = "Button 7"; +const char* const PROGMEM LEDString_07 = "Misc button 1"; +const char* const PROGMEM LEDString_08 = "Misc button 2"; +const char* const PROGMEM LEDString_09 = "Misc button 3"; +const char* const PROGMEM LEDString_10 = "Misc button 4"; +const char* const PROGMEM TTString = "TT Sensitivity"; + +const char* String_indiv[] = { LEDString_00, LEDString_01, LEDString_02, LEDString_03, LEDString_04, LEDString_05, LEDString_06, LEDString_07, LEDString_08, LEDString_09, LEDString_10, TTString }; +uint8_t STRING_ID_Count = 12; + +static const uint8_t PROGMEM hid_report[] = { + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x04, // USAGE (Joystick) + 0xa1, 0x01, // COLLECTION (Application) + + 0x85, 0x05, // REPORT_ID (5) + + // HAT Switch (1 nibble) + 0x05, 0x01, // USAGE_PAGE (HAT) + 0x25, 0x07, // LOGICAL_MAXIMUM (7) + 0x46, 0x3B, 0x01, // + 0x75, 0x04, // REPORT_SIZE (4) + 0x95, 0x01, // REPORT_COUNT (1) + 0x65, 0x14, // UNIT (None) + 0x09, 0x39, // USAGE (57) + 0x81, 0x42, // INPUT (Data,Var,Abs) + + 0x65, 0x00, // UNIT (None) + 0x95, 0x01, // REPORT_COUNT (1) + 0x81, 0x01, // INPUT (Data,Var,Abs) + + /* Buttons */ + 0x05, 0x09, // USAGE_PAGE (Button) + 0x19, 0x01, // USAGE_MINIMUM (Button 1) + 0x29, NUMBER_OF_BUTTONS, // USAGE_MAXIMUM (Button NUMBER_OF_BUTTONS) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x25, 0x01, // LOGICAL_MAXIMUM (1) + 0x75, 0x01, // REPORT_SIZE (1) + 0x95, NUMBER_OF_BUTTONS, // REPORT_COUNT (NUMBER_OF_BUTTONS) + 0x55, 0x00, // UNIT_EXPONENT (0) + 0x65, 0x00, // UNIT (None) + 0x81, 0x02, // INPUT (Data,Var,Abs) + /* Buttons END */ + + /* Buttons padding */ + 0x75, 0x01, // REPORT_SIZE (1) + 0x95, BUTTON_PADDING, // REPORT_COUNT (BUTTON_PADDING) + 0x81, 0x03, // INPUT (Cnst,Var,Abs) + /* Buttons padding END */ + + /* Encoder */ + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x01, // USAGE (Pointer) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x26, 0xFF, 0, // LOGICAL_MAXIMUM (512r) + 0x75, 0x10, // REPORT_SIZE (16) + 0x95, 0x01, // REPORT_COUNT (1) + 0xa1, 0x00, // COLLECTION (Physical) + 0x09, 0x30, // USAGE (X) + 0x81, 0x02, // INPUT (Data,Var,Abs) + 0xc0, // END_COLLECTION + /* Encoder END*/ + + 0x85, 0x04, // REPORT_ID (4) + + /* LEDs begin */ + 0x05, 0x0a, // USAGE_PAGE (Ordinals) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x25, 0x01, // LOGICAL_MAXIMUM (1) + 0x75, 0x01, // REPORT_SIZE (1) + 0x95, NUMBER_OF_LEDS, // REPORT_COUNT (NUMBER_OF_LEDS) + 0xa1, 0x02, // COLLECTION (Logical) + 0x89, 0x04, // STRING_MINIMUM (4) + 0x99, 0x0e, // STRING_MAXIMUM (14) + 0x09, 0x01, // USAGE (Instance 1) + 0x09, 0x02, // USAGE (Instance 2) + 0x09, 0x03, // USAGE (Instance 3) + 0x09, 0x04, // USAGE (Instance 4) + 0x09, 0x05, // USAGE (Instance 5) + 0x09, 0x06, // USAGE (Instance 6) + 0x09, 0x07, // USAGE (Instance 7) + 0x09, 0x08, // USAGE (Instance 8) + 0x09, 0x09, // USAGE (Instance 9) + 0x09, 0x0a, // USAGE (Instance 10) + 0x09, 0x0b, // USAGE (Instance 11) + 0x91, 0x02, // OUTPUT (Data,Var,Abs) + 0xc0, // END_COLLECTION + /* LEDs END */ + + /* LEDs padding */ + 0x75, 0x01, // REPORT_SIZE (1) + 0x95, LED_PADDING, // REPORT_COUNT (BUTTON_PADDING) + 0x91, 0x03, // OUTPUT (Cnst,Var,Abs) + /* LEDs padding END */ + + 0xc0 // END_COLLECTION +}; + +static bool SendControl(uint8_t d) { + return USB_SendControl(0, &d, 1) == 1; +} + +static bool USB_SendStringDescriptor(const char *string_P, uint8_t string_len, uint8_t flags) { + SendControl(2 + string_len * 2); + SendControl(3); + bool pgm = flags & TRANSFER_PGM; + for (uint8_t i = 0; i < string_len; i++) { + bool r = SendControl(pgm ? pgm_read_byte(&string_P[i]) : string_P[i]); + r &= SendControl(0); // high byte + if (!r) { + return false; + } + } + return true; +} + +IIDXHID_::IIDXHID_() : PluggableUSBModule(1, 1, epType) { + epType[0] = EP_TYPE_INTERRUPT_IN; + //PluggableUSB().plug(this); +} + +int IIDXHID_::getInterface(byte* interface_count) { + *interface_count += 1; + HIDDescriptor hid_interface = { + D_INTERFACE(pluggedInterface, 1, USB_DEVICE_CLASS_HUMAN_INTERFACE, HID_SUBCLASS_NONE, HID_PROTOCOL_NONE), + D_HIDREPORT(sizeof(hid_report)), + D_ENDPOINT(USB_ENDPOINT_IN(pluggedEndpoint), USB_ENDPOINT_TYPE_INTERRUPT, USB_EP_SIZE, 0x01) + }; + return USB_SendControl(0, &hid_interface, sizeof(hid_interface)); +} + +int IIDXHID_::getDescriptor(USBSetup& setup) { + if (setup.wValueH == USB_DEVICE_DESCRIPTOR_TYPE) { + return USB_SendControl(TRANSFER_PGM, (const uint8_t*)&USB_DeviceDescriptorIAD, sizeof(USB_DeviceDescriptorIAD)); + } + if (setup.wValueH == USB_STRING_DESCRIPTOR_TYPE) { + if (setup.wValueL == IPRODUCT) { + return USB_SendStringDescriptor(String_Product, strlen(String_Product), 0); + } + else if (setup.wValueL == IMANUFACTURER) { + return USB_SendStringDescriptor(String_Manufacturer, strlen(String_Manufacturer), 0); + } + else if (setup.wValueL == ISERIAL) { + return USB_SendStringDescriptor(String_Serial, strlen(String_Serial), 0); + } + else if (setup.wValueL >= STRING_ID_Base && setup.wValueL < (STRING_ID_Base + STRING_ID_Count)) { + return USB_SendStringDescriptor(String_indiv[setup.wValueL - STRING_ID_Base], strlen(String_indiv[setup.wValueL - STRING_ID_Base]), 0); + } + } + + if (setup.bmRequestType != REQUEST_DEVICETOHOST_STANDARD_INTERFACE) { + return 0; + } + + if (setup.wValueH != HID_REPORT_DESCRIPTOR_TYPE) { + return 0; + } + + if (setup.wIndex != pluggedInterface) { + return 0; + } + + return USB_SendControl(TRANSFER_PGM, hid_report, sizeof(hid_report)); +} + +bool IIDXHID_::setup(USBSetup& setup) { + if (pluggedInterface != setup.wIndex) { + return false; + } + + uint8_t request = setup.bRequest; + uint8_t request_type = setup.bmRequestType; + + if (request_type == REQUEST_DEVICETOHOST_CLASS_INTERFACE) { + return true; + } + + if (request_type == REQUEST_HOSTTODEVICE_CLASS_INTERFACE) { + if (request == HID_SET_REPORT) { + if (setup.wValueH == HID_REPORT_TYPE_OUTPUT) { + USB_RecvControl(usb_data, setup.wLength); + + if (usb_data[0] == 4) { + lamp_hid_state = usb_data[2] << 8 | usb_data[1]; + lastHidUpdate = millis(); + // No need to write lights or update lightmode here (for autoswitch), main loop() will take care of it + } + return true; + } + } + } + + return false; +} + +unsigned long IIDXHID_::getLastHidUpdate() { + return lastHidUpdate; +} + +void IIDXHID_::write_lights(uint32_t button_state, bool hid, bool reactive) { + if (!reactive) { + button_state = 0; + } + + if (hid) { + button_state |= lamp_hid_state; + } + + for (int i = 0; i < NUMBER_OF_LEDS; i++) { + digitalWrite(led_pins[i], ((button_state >> i) & 1)); + } +} + +int IIDXHID_::send_state(uint32_t button_state, int32_t turntable_state) { + uint8_t data[6]; + + data[0] = (uint8_t) 5; + data[1] = (uint8_t) (button_state >> 16) & 0xFF; /* HAT SWITCH */ + data[2] = (uint8_t) (button_state & 0xFF); + data[3] = (uint8_t) (button_state >> 8) & 0xFF; + data[4] = (uint8_t) (turntable_state & 0xFF); + data[5] = (uint8_t) (turntable_state >> 8) & 0xFF; + + return USB_Send(pluggedEndpoint | TRANSFER_RELEASE, data, 6); +} diff --git a/BEMANI_PSX_Adapter/IIDXHID.h b/BEMANI_PSX_Adapter/IIDXHID.h new file mode 100644 index 0000000..b935b5b --- /dev/null +++ b/BEMANI_PSX_Adapter/IIDXHID.h @@ -0,0 +1,30 @@ +#include "HID.h" + +#define NUMBER_OF_LEDS 11 +#define NUMBER_OF_BUTTONS 12 +#define ENCODER_PPR 255 + +#define EPTYPE_DESCRIPTOR_SIZE uint8_t +#define STRING_ID_Base 4 + +#define DPAD_DOWN 0x04 +#define DPAD_UP 0x00 +#define DPAD_NEUTRAL 0x08 + +class IIDXHID_ : public PluggableUSBModule { + public: + IIDXHID_(); + void write_lights(uint32_t button_state, bool hid, bool reactive); + int send_state(uint32_t button_state, int32_t turntable_state); + unsigned long getLastHidUpdate(); + + protected: + EPTYPE_DESCRIPTOR_SIZE epType[1]; + unsigned long lastHidUpdate = 0; + + int getInterface(uint8_t* interface_count); + int getDescriptor(USBSetup& setup); + bool setup(USBSetup& setup); +}; + +extern IIDXHID_ IIDXHID; diff --git a/BEMANI_PSX_Adapter/POPNHID.cpp b/BEMANI_PSX_Adapter/POPNHID.cpp new file mode 100644 index 0000000..724950b --- /dev/null +++ b/BEMANI_PSX_Adapter/POPNHID.cpp @@ -0,0 +1,350 @@ +/* Arduino-HID-Lighting-Library + + This Arduino-HID-Lighting-Library is derived from Arduino-HID-Lighting, whose copyriht owner is mon. + More information about Arduino-HID-Lighting you can find under: + + mon's Arduino-HID-Lighting + https://github.com/mon/Arduino-HID-Lighting + + 2018 (C) Arduino-HID-Lighting-Library, Knuckleslee +*/ +#include "POPNHID.h" + +byte extern LightPins[]; +byte extern DipPins[]; + +/* HID DESCRIPTOR */ +static const byte PROGMEM _hidReportPOPN[] = { + 0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */ + 0x09, 0x05, /* USAGE (Game Pad) */ + 0xa1, 0x01, /* COLLECTION (Application) */ + + /*Buttons */ + 0x85, 0x04, /* REPORT_ID 4 */ + 0x05, 0x09, /* USAGE_PAGE (Button) */ + 0x19, 0x01, /* USAGE_MINIMUM (Button 1) */ + 0x29, 0x0d, /* USAGE_MAXIMUM (Button 13)*/ + 0x15, 0x00, /* LOGICAL_MINIMUM (0) */ + 0x25, 0x01, /* LOGICAL_MAXIMUM (1) */ + 0x95, 0x0b, /* REPORT_COUNT (11) */ + 0x75, 0x01, /* REPORT_SIZE (1) */ + 0x81, 0x02, /* INPUT (Data,Var,Abs) */ + /* Reserved bits */ + 0x95, 0x01, /* REPORT_COUNT (1) */ + 0x75, 0x05, /* REPORT_SIZE (5) */ + 0x81, 0x03, /* INPUT (Cnst,Var,Abs) */ + + /*Lights */ + 0x85, 0x05, /* REPORT_ID 5*/ + 0x15, 0x00, /* LOGICAL_MINIMUM (0) */ + 0x25, 0x01, /* LOGICAL_MAXIMUM (1) */ + /*Led 1 */ + 0x05, 0x0a, /* USAGE_PAGE (Ordinals) */ + 0x09, 0x01, /* USAGE (Instance 1) */ + 0xa1, 0x02, /* COLLECTION (Logical) */ + 0x05, 0x08, /* USAGE_PAGE (LEDs) */ + 0x09, 0x4b, /* USAGE (Generic Indicator 1) */ + 0x79, 0x04, /* STRING INDEX (4) */ + 0x75, 0x01, /* REPORT_SIZE (1) */ + 0x95, 0x01, /* REPORT_COUNT (1) */ + 0x91, 0x02, /* OUTPUT (Data,Var,Abs) */ + 0xc0, /* END_COLLECTION */ + /*Led 2 */ + 0x05, 0x0a, /* USAGE_PAGE (Ordinals) */ + 0x09, 0x02, /* USAGE (Instance 2) */ + 0xa1, 0x02, /* COLLECTION (Logical) */ + 0x05, 0x08, /* USAGE_PAGE (LEDs) */ + 0x09, 0x4b, /* USAGE (Generic Indicator 1) */ + 0x79, 0x05, + 0x75, 0x01, /* REPORT_SIZE (1) */ + 0x95, 0x01, /* REPORT_COUNT (1) */ + 0x91, 0x02, /* OUTPUT (Data,Var,Abs) */ + 0xc0, /* END_COLLECTION */ + /*Led 3 */ + 0x05, 0x0a, /* USAGE_PAGE (Ordinals) */ + 0x09, 0x03, /* USAGE (Instance 3) */ + 0xa1, 0x02, /* COLLECTION (Logical) */ + 0x05, 0x08, /* USAGE_PAGE (LEDs) */ + 0x09, 0x4b, /* USAGE (Generic Indicator 1) */ + 0x79, 0x06, + 0x75, 0x01, /* REPORT_SIZE (1) */ + 0x95, 0x01, /* REPORT_COUNT (1) */ + 0x91, 0x02, /* OUTPUT (Data,Var,Abs) */ + 0xc0, /* END_COLLECTION */ + /*Led 4 */ + 0x05, 0x0a, /* USAGE_PAGE (Ordinals) */ + 0x09, 0x04, /* USAGE (Instance 4) */ + 0xa1, 0x02, /* COLLECTION (Logical) */ + 0x05, 0x08, /* USAGE_PAGE (LEDs) */ + 0x09, 0x4b, /* USAGE (Generic Indicator 1) */ + 0x79, 0x07, + 0x75, 0x01, /* REPORT_SIZE (1) */ + 0x95, 0x01, /* REPORT_COUNT (1) */ + 0x91, 0x02, /* OUTPUT (Data,Var,Abs) */ + 0xc0, /* END_COLLECTION */ + /*Led 5 */ + 0x05, 0x0a, /* USAGE_PAGE (Ordinals) */ + 0x09, 0x05, /* USAGE (Instance 5) */ + 0xa1, 0x02, /* COLLECTION (Logical) */ + 0x05, 0x08, /* USAGE_PAGE (LEDs) */ + 0x09, 0x4b, /* USAGE (Generic Indicator 1) */ + 0x79, 0x08, + 0x75, 0x01, /* REPORT_SIZE (1) */ + 0x95, 0x01, /* REPORT_COUNT (1) */ + 0x91, 0x02, /* OUTPUT (Data,Var,Abs) */ + 0xc0, /* END_COLLECTION */ + /*Led 6 */ + 0x05, 0x0a, /* USAGE_PAGE (Ordinals) */ + 0x09, 0x06, /* USAGE (Instance 6) */ + 0xa1, 0x02, /* COLLECTION (Logical) */ + 0x05, 0x08, /* USAGE_PAGE (LEDs) */ + 0x09, 0x4b, /* USAGE (Generic Indicator 1) */ + 0x79, 0x09, + 0x75, 0x01, /* REPORT_SIZE (1) */ + 0x95, 0x01, /* REPORT_COUNT (1) */ + 0x91, 0x02, /* OUTPUT (Data,Var,Abs) */ + 0xc0, /* END_COLLECTION */ + /*Led 7 */ + 0x05, 0x0a, /* USAGE_PAGE (Ordinals) */ + 0x09, 0x07, /* USAGE (Instance 7) */ + 0xa1, 0x02, /* COLLECTION (Logical) */ + 0x05, 0x08, /* USAGE_PAGE (LEDs) */ + 0x09, 0x4b, /* USAGE (Generic Indicator 1) */ + 0x79, 0x0a, + 0x75, 0x01, /* REPORT_SIZE (1) */ + 0x95, 0x01, /* REPORT_COUNT (1) */ + 0x91, 0x02, /* OUTPUT (Data,Var,Abs) */ + 0xc0, /* END_COLLECTION */ + /*Led 8 */ + 0x05, 0x0a, /* USAGE_PAGE (Ordinals) */ + 0x09, 0x08, /* USAGE (Instance 8) */ + 0xa1, 0x02, /* COLLECTION (Logical) */ + 0x05, 0x08, /* USAGE_PAGE (LEDs) */ + 0x09, 0x4b, /* USAGE (Generic Indicator 1) */ + 0x79, 0x0b, + 0x75, 0x01, /* REPORT_SIZE (1) */ + 0x95, 0x01, /* REPORT_COUNT (1) */ + 0x91, 0x02, /* OUTPUT (Data,Var,Abs) */ + 0xc0, /* END_COLLECTION */ + /*Led 9 */ + 0x05, 0x0a, /* USAGE_PAGE (Ordinals) */ + 0x09, 0x09, /* USAGE (Instance 9) */ + 0xa1, 0x02, /* COLLECTION (Logical) */ + 0x05, 0x08, /* USAGE_PAGE (LEDs) */ + 0x09, 0x4b, /* USAGE (Generic Indicator 1) */ + 0x79, 0x0c, + 0x75, 0x01, /* REPORT_SIZE (1) */ + 0x95, 0x01, /* REPORT_COUNT (1) */ + 0x91, 0x02, /* OUTPUT (Data,Var,Abs) */ + 0xc0, /* END_COLLECTION */ + /* Reserved 23 bits */ + 0x95, 0x01, /* REPORT_COUNT (1) */ + 0x75, 0x17, /* REPORT_SIZE (23) */ + 0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */ + + /*Light mode switch*/ + 0x85, 0x07, /* REPORT_ID 7 */ + 0x05, 0x0a, /* USAGE_PAGE (Ordinals) */ + 0x19, 0x00, /* USAGE_MINIMUM (Button 1) */ + 0x29, 0x04, /* USAGE_MAXIMUM (Button 4)*/ + 0x15, 0x00, /* LOGICAL_MINIMUM (0) */ + 0x25, 0x04, /* LOGICAL_MAXIMUM (4) */ + 0x95, 0x01, /* REPORT_COUNT (1) */ + 0x75, 0x04, /* REPORT_SIZE (4) */ + 0xb1, 0x02, /* FEATURE (Data,Var,Abs) */ + /* Reserved bits */ + 0x95, 0x01, /* REPORT_COUNT (1) */ + 0x75, 0x04, /* REPORT_SIZE (4) */ + 0xb1, 0x03, /* FEATURE (Cnst,Var,Abs) */ + + /*Footer */ + 0xc0 /* END_COLLECTION */ +}; + +static bool SendControl(uint8_t d) +{ + return USB_SendControl(0, &d, 1) == 1; +} + +static bool USB_SendStringDescriptor(const char *string_P, uint8_t string_len, uint8_t flags) { + SendControl(2 + string_len * 2); + SendControl(3); + bool pgm = flags & TRANSFER_PGM; + for (uint8_t i = 0; i < string_len; i++) { + bool r = SendControl(pgm ? pgm_read_byte(&string_P[i]) : string_P[i]); + r &= SendControl(0); // high byte + if (!r) { + return false; + } + } + return true; +} + +/* HID Light descriptor strings */ +const char* const PROGMEM String_Manufacturer = "CrazyRedMachine"; +const char* const PROGMEM String_Product = "Pop'n controller"; +const char* const PROGMEM String_Serial = "POPNHID"; + +const char* const PROGMEM LEDString_00 = "Button 1"; +const char* const PROGMEM LEDString_01 = "Button 2"; +const char* const PROGMEM LEDString_02 = "Button 3"; +const char* const PROGMEM LEDString_03 = "Button 4"; +const char* const PROGMEM LEDString_04 = "Button 5"; +const char* const PROGMEM LEDString_05 = "Button 6"; +const char* const PROGMEM LEDString_06 = "Button 7"; +const char* const PROGMEM LEDString_07 = "Button 8"; +const char* const PROGMEM LEDString_08 = "Button 9"; + +const char* LEDString_indiv[] = {LEDString_00, LEDString_01, LEDString_02, LEDString_03, LEDString_04, LEDString_05, LEDString_06, LEDString_07, LEDString_08}; + +uint8_t STRING_ID_LED_Count = 9; + +const DeviceDescriptor PROGMEM USB_DeviceDescriptorIAD = + D_DEVICE(0xEF, 0x02, 0x01, 64, 0x1ccf, 0x1000, 0x100, IMANUFACTURER, IPRODUCT, ISERIAL, 1); + +/* PluggableUSBModule IMPLEMENTATION */ + +POPNHID_::POPNHID_(void) : PluggableUSBModule(1, 1, epType) { + epType[0] = EP_TYPE_INTERRUPT_IN; + //PluggableUSB().plug(this); +} + +int POPNHID_::getInterface(byte* interfaceCount) { + *interfaceCount += 1; // uses 1 + HIDDescriptor hidInterface = { + D_INTERFACE(pluggedInterface, 1, USB_DEVICE_CLASS_HUMAN_INTERFACE, HID_SUBCLASS_NONE, HID_PROTOCOL_NONE), + D_HIDREPORT(sizeof(_hidReportPOPN)), + D_ENDPOINT(USB_ENDPOINT_IN(pluggedEndpoint), USB_ENDPOINT_TYPE_INTERRUPT, USB_EP_SIZE, USB_EP_BINTERVAL) + }; + return USB_SendControl(0, &hidInterface, sizeof(hidInterface)); +} + +int POPNHID_::getDescriptor(USBSetup& setup) +{ + if (setup.wValueH == USB_DEVICE_DESCRIPTOR_TYPE) { + return USB_SendControl(TRANSFER_PGM, (const uint8_t*)&USB_DeviceDescriptorIAD, sizeof(USB_DeviceDescriptorIAD)); + } + if (setup.wValueH == USB_STRING_DESCRIPTOR_TYPE) { + if (setup.wValueL == IPRODUCT) { + return USB_SendStringDescriptor(String_Product, strlen(String_Product), 0); + } + else if (setup.wValueL == IMANUFACTURER) { + return USB_SendStringDescriptor(String_Manufacturer, strlen(String_Manufacturer), 0); + } + else if (setup.wValueL == ISERIAL) { + return USB_SendStringDescriptor(String_Serial, strlen(String_Serial), 0); + } + else if (setup.wValueL >= STRING_ID_LED_Base && setup.wValueL < (STRING_ID_LED_Base + STRING_ID_LED_Count)) { + return USB_SendStringDescriptor(LEDString_indiv[setup.wValueL - STRING_ID_LED_Base], strlen(LEDString_indiv[setup.wValueL - STRING_ID_LED_Base]), 0); + } + } + // Check if this is a HID Class Descriptor request + if (setup.bmRequestType != REQUEST_DEVICETOHOST_STANDARD_INTERFACE) { + return 0; + } + if (setup.wValueH != HID_REPORT_DESCRIPTOR_TYPE) { + return 0; + } + + // In a HID Class Descriptor wIndex contains the interface number + if (setup.wIndex != pluggedInterface) { + return 0; + } + + return USB_SendControl(TRANSFER_PGM, _hidReportPOPN, sizeof(_hidReportPOPN)); +} + +bool POPNHID_::setup(USBSetup& setup) +{ + if (pluggedInterface != setup.wIndex) { + return false; + } + + byte request = setup.bRequest; + byte requestType = setup.bmRequestType; + + if (requestType == REQUEST_DEVICETOHOST_CLASS_INTERFACE) + { + /* lightmode info */ + if (setup.wValueH == HID_REPORT_TYPE_FEATURE && setup.wValueL == 7) { + uint8_t lm_data[2] = {7, 0x00}; + lm_data[1] = lightMode; + USB_SendControl(0, &lm_data, 2); + return true; + } + + return true; + } + + if (requestType == REQUEST_HOSTTODEVICE_CLASS_INTERFACE) { + if (request == HID_SET_REPORT) { + if (setup.wValueH == HID_REPORT_TYPE_OUTPUT && setup.wLength == 5) { + lastHidUpdate = millis(); + USB_RecvControl(led_data, 5); + return true; + } + else if (setup.wValueH == HID_REPORT_TYPE_FEATURE ) { + /* lightmode switch */ + byte usb_data[5]; + USB_RecvControl(usb_data, setup.wLength); + if (usb_data[0] == 7) + { + mode_data = usb_data[1]; + return true; + } + } + + } + } + + return false; +} + +/* CUSTOM POPN FUNCTIONS */ + +uint8_t POPNHID_::getLightMode() { + return lightMode; +} + +unsigned long POPNHID_::getLastHidUpdate() { + return lastHidUpdate; +} + +uint8_t POPNHID_::setLightMode(uint8_t mode) { + uint8_t effective_mode = mode; + if ((effective_mode > 4)) { + effective_mode = 2; + } + lightMode = effective_mode; + mode_data = lightMode; + return lightMode; +} + +void POPNHID_::updateLightMode() { + uint8_t mode = mode_data; + if (mode != lightMode) { + mode_data = setLightMode(mode); + } +} + +void POPNHID_::updateLeds(uint32_t buttonsState, bool invert) { + uint32_t* bitfield = (uint32_t*) & (led_data[1]); + uint32_t leds = (*bitfield | buttonsState); + if (invert) + leds = ~leds; + for (int i = 0; i < 9; i++) { + if (leds >> i & 1) + digitalWrite(LightPins[i], HIGH); + else + digitalWrite(LightPins[i], LOW); + } + +} + +int POPNHID_::sendState(uint32_t buttonsState) { + uint8_t data[3]; + data[0] = (uint8_t) 4; //report id + data[1] = (uint8_t) (buttonsState & 0xFF); + data[2] = (uint8_t) (buttonsState >> 8) & 0xFF; + return USB_Send(pluggedEndpoint | TRANSFER_RELEASE, data, 3); +} diff --git a/BEMANI_PSX_Adapter/POPNHID.h b/BEMANI_PSX_Adapter/POPNHID.h new file mode 100644 index 0000000..ade31b7 --- /dev/null +++ b/BEMANI_PSX_Adapter/POPNHID.h @@ -0,0 +1,61 @@ +#include "HID.h" + +#define USB_EP_BINTERVAL 1 +#define EPTYPE_DESCRIPTOR_SIZE uint8_t + +#define STRING_ID_LED_Base 4 + +class POPNHID_ : public PluggableUSBModule { + + public: + POPNHID_(void); + + /** + Updates the led status based on led_data (HID report received) and button states + param[in] buttonState bitfield with currently pressed buttons (used to force additional lights for mixed mode) + param[in] invert set to true to invert on/off status (used for invert lightmode) + */ + void updateLeds(uint32_t buttonsState, bool invert); + + /** + Sends the gamepad button states to the PC as an HID report + param[in] buttonsState bitfield with currently pressed buttons + return USB_Send() return value + */ + int sendState(uint32_t buttonsState); + + /** + Changes the lightMode if a received HID report asks for it + */ + void updateLightMode(); + + /** + getter and setter for lightMode protected field. + */ + uint8_t getLightMode(); + uint8_t setLightMode(uint8_t mode); + + /** + getter for lastHidUpdate protected field. + */ + unsigned long getLastHidUpdate(); + + protected: + /* current lightMode (0 = reactive, 1 = HID only, 2 = mixed (HID+reactive auto-switch), 3 = combined (HID+button presses), 4 = combined invert) */ + uint8_t lightMode = 2; + /* timestamp of last received HID report for lightMode 3 */ + unsigned long lastHidUpdate = 0; + /* byte array to receive HID reports from the PC */ + byte led_data[5]; + byte mode_data; + + /* Implementation of the PUSBListNode */ + EPTYPE_DESCRIPTOR_SIZE epType[1]; + uint8_t protocol; + uint8_t idle; + int getInterface(uint8_t* interfaceCount); + int getDescriptor(USBSetup& setup); + bool setup(USBSetup& setup); +}; + +extern POPNHID_ POPNHID; diff --git a/BEMANI_PSX_Adapter/PsxControllerBitBang.h b/BEMANI_PSX_Adapter/PsxControllerBitBang.h new file mode 100644 index 0000000..a49080c --- /dev/null +++ b/BEMANI_PSX_Adapter/PsxControllerBitBang.h @@ -0,0 +1,89 @@ +#include "PsxNewLib.h" +#include + +/** \brief Attention Delay + * + * Time between attention being issued to the controller and the first clock + * edge (us). + */ +const byte ATTN_DELAY = 50; + +/** \brief Clock Period + * + * Inverse of clock frequency, i.e.: time for a *full* clock cycle, from falling + * edge to the next falling edge. + */ +const byte CLK_PERIOD = 40; + +// Must be < CLK_PERIOD / 2 +const byte HOLD_TIME = 2; + + +template +class PsxControllerBitBang: public PsxController { +private: + DigitalPin att; + DigitalPin clk; + DigitalPin cmd; + DigitalPin dat; + +protected: + virtual void attention () override { + att.low (); + delayMicroseconds (ATTN_DELAY); + } + + virtual void noAttention () override { + //~ delayMicroseconds (5); + + cmd.high (); + clk.high (); + att.high (); + delayMicroseconds (ATTN_DELAY); + } + + virtual byte shiftInOut (const byte out) override { + byte in = 0; + + // 1. The clock is held high until a byte is to be sent. + + for (byte i = 0; i < 8; ++i) { + // 2. When the clock edge drops low, the values on the line start to + // change + clk.low (); + + delayMicroseconds (HOLD_TIME); + + if (bitRead (out, i)) { + cmd.high (); + } else { + cmd.low (); + } + + delayMicroseconds (CLK_PERIOD / 2 - HOLD_TIME); + + // 3. When the clock goes from low to high, value are actually read + clk.high (); + + delayMicroseconds (HOLD_TIME); + + if (dat) { + bitSet (in, i); + } + + delayMicroseconds (CLK_PERIOD / 2 - HOLD_TIME); + } + + return in; + } + +public: + virtual boolean begin () override { + att.config (OUTPUT, HIGH); // HIGH -> Controller not selected + cmd.config (OUTPUT, HIGH); + clk.config (OUTPUT, HIGH); + dat.config (INPUT, HIGH); // Enable pull-up + + return PsxController::begin (); + } +}; diff --git a/BEMANI_PSX_Adapter/PsxControllerHwSpi.h b/BEMANI_PSX_Adapter/PsxControllerHwSpi.h new file mode 100644 index 0000000..6a48b05 --- /dev/null +++ b/BEMANI_PSX_Adapter/PsxControllerHwSpi.h @@ -0,0 +1,64 @@ +#include "PsxNewLib.h" +#include +#include + +/** \brief Attention Delay + * + * Time between attention being issued to the controller and the first clock + * edge (us). + */ +const byte ATTN_DELAY = 50; + +// Set up the speed, data order and data mode +static SPISettings spiSettings (250000, LSBFIRST, SPI_MODE3); + +template +class PsxControllerHwSpi: public PsxController { +private: + DigitalPin att; + DigitalPin cmd; + DigitalPin dat; + DigitalPin clk; + +protected: + virtual void attention () override { + att.low (); + + SPI.beginTransaction (spiSettings); + + delayMicroseconds (ATTN_DELAY); + } + + virtual void noAttention () override { + //~ delayMicroseconds (5); + + SPI.endTransaction (); + + // Make sure CMD and CLK sit high + cmd.high (); + clk.high (); + att.high (); + delayMicroseconds (ATTN_DELAY); + } + + virtual byte shiftInOut (const byte out) override { + return SPI.transfer (out); + } + +public: + virtual boolean begin () override { + att.config (OUTPUT, HIGH); // HIGH -> Controller not selected + + /* We need to force these at startup, that's why we need to know which + * pins are used for HW SPI. It's a sort of "start condition" the + * controller needs. + */ + cmd.config (OUTPUT, HIGH); + clk.config (OUTPUT, HIGH); + dat.config (INPUT, HIGH); // Enable pull-up + + SPI.begin (); + + return PsxController::begin (); + } +}; diff --git a/BEMANI_PSX_Adapter/PsxNewLib.cpp b/BEMANI_PSX_Adapter/PsxNewLib.cpp new file mode 100644 index 0000000..a032b03 --- /dev/null +++ b/BEMANI_PSX_Adapter/PsxNewLib.cpp @@ -0,0 +1 @@ +// Congratulations! You've found the end of the rainbow! diff --git a/BEMANI_PSX_Adapter/PsxNewLib.h b/BEMANI_PSX_Adapter/PsxNewLib.h new file mode 100644 index 0000000..587a8ce --- /dev/null +++ b/BEMANI_PSX_Adapter/PsxNewLib.h @@ -0,0 +1,1236 @@ +/******************************************************************************* + * This file is part of PsxNewLib. * + * * + * Copyright (C) 2019-2020 by SukkoPera * + * * + * PsxNewLib is free software: you can redistribute it and/or * + * modify it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * PsxNewLib is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with PsxNewLib. If not, see http://www.gnu.org/licenses. * + ******************************************************************************/ +/** + * \file PsxNewLib.h + * \author SukkoPera + * \date 27 Jan 2020 + * \brief Playstation controller interface library for Arduino + * + * Please refer to the GitHub page and wiki for any information: + * https://github.com/SukkoPera/PsxNewLib + */ + +#ifndef PSXNEWLIB_H_ +#define PSXNEWLIB_H_ + +// Uncomment this to have all byte exchanges logged to serial +//~ #define DUMP_COMMS + +/** \brief Command Inter-Byte Delay (us) + * + * Commands are several bytes long. This is the time to wait between two + * consecutive bytes. + * + * This should actually be done by watching the \a Acknowledge line, but we are + * ignoring it at the moment. + */ +const byte INTER_CMD_BYTE_DELAY = 50; + +/** \brief Command timeout (ms) + * + * Commands are sent to the controller repeatedly, until they succeed or time + * out. This is the length of that timeout. + * + * \sa COMMAND_RETRY_INTERVAL + */ +const unsigned long COMMAND_TIMEOUT = 250; + +/** \brief Command Retry Interval (ms) + * + * When sending a command to the controller, if it does not succeed, it is + * retried after this amount of time. + */ +const unsigned long COMMAND_RETRY_INTERVAL = 10; + +/** \brief Mode switch delay (ms) + * + * After a command has been issued successfully to the controller, this amount + * of time is waited to allow it to complete any internal procedures required to + * execute the command. + * + * \todo This is probably unnecessary. + */ +const unsigned long MODE_SWITCH_DELAY = 500; + + +/** \brief Type that is used to represent a single button in most places + */ +enum PsxButton { + PSB_NONE = 0x0000, + PSB_SELECT = 0x0001, + PSB_L3 = 0x0002, + PSB_R3 = 0x0004, + PSB_START = 0x0008, + PSB_PAD_UP = 0x0010, + PSB_PAD_RIGHT = 0x0020, + PSB_PAD_DOWN = 0x0040, + PSB_PAD_LEFT = 0x0080, + PSB_L2 = 0x0100, + PSB_R2 = 0x0200, + PSB_L1 = 0x0400, + PSB_R1 = 0x0800, + PSB_TRIANGLE = 0x1000, + PSB_CIRCLE = 0x2000, + PSB_CROSS = 0x4000, + PSB_SQUARE = 0x8000 +}; + +/** \brief Type that is used to represent a single button when retrieving + * analog pressure data + * + * \sa getAnalogButton() + */ +enum PsxAnalogButton { + PSAB_PAD_RIGHT = 0, + PSAB_PAD_LEFT = 1, + PSAB_PAD_UP = 2, + PSAB_PAD_DOWN = 3, + PSAB_TRIANGLE = 4, + PSAB_CIRCLE = 5, + PSAB_CROSS = 6, + PSAB_SQUARE = 7, + PSAB_L1 = 8, + PSAB_R1 = 9, + PSAB_L2 = 10, + PSAB_R2 = 11 +}; + +/** \brief Number of digital buttons + * + * Includes \a everything, i.e.: 4 directions, Square, Cross, Circle, Triangle, + * L1/2/3, R1/2/3, Select and Start. + * + * This is the number of entries in #PsxButton. + */ +const byte PSX_BUTTONS_NO = 16; + +/** \brief Type that is used to report button presses + */ +typedef uint16_t PsxButtons; + +/** \brief Size of buffer holding analog button data + * + * This is the size of the array returned by getAnalogButtonData(). + */ +const byte PSX_ANALOG_BTN_DATA_SIZE = 12; + +//! \name Controller Commands +//! @{ +/** \brief Enter Configuration Mode + * + * Command used to enter the controller configuration (also known as \a escape) + * mode + */ +static const byte enter_config[] = {0x01, 0x43, 0x00, 0x01, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A}; +static const byte exit_config[] = {0x01, 0x43, 0x00, 0x00, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A}; +/* These shorter versions of enter_ and exit_config are accepted by all + * controllers I've tested, even in analog mode, EXCEPT SCPH-1200, so let's use + * the longer ones + */ +//~ static byte enter_config[] = {0x01, 0x43, 0x00, 0x01, 0x00}; +//~ static byte exit_config[] = {0x01, 0x43, 0x00, 0x00, 0x00}; + +/** \brief Read Controller Type + * + * Command used to read the controller type. + * + * This does not seem to be 100% reliable, or at least we don't know how to tell + * all the various controllers apart. + */ +static const byte type_read[] = {0x01, 0x45, 0x00, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A}; +static const byte set_mode[] = {0x01, 0x44, 0x00, /* enabled */ 0x01, /* locked */ 0x03, 0x00, 0x00, 0x00, 0x00}; +static const byte enable_rumble[] = {0x01, 0x4D, 0x00, /* motor 1 on */ 0x00, /* motor 2 on*/ 0x01, 0xff, 0xff, 0xff, 0xff}; +static const byte set_pressures[] = {0x01, 0x4F, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00}; + +/** \brief Poll all buttons + * + * Command used to read the status of all buttons. + */ +static const byte poll[] = {0x01, 0x42, 0x00, 0xFF, 0xFF}; +//! @} + +/** \brief Controller Type + * + * This is somehow derived from the reply to the #type_read command. It is NOT + * much trustworthy, so it might be removed in the future. + * + * \sa getControllerType + */ +enum PsxControllerType { + PSCTRL_UNKNOWN = 0, //!< No idea + PSCTRL_DUALSHOCK, //!< DualShock or compatible + PSCTRL_DSWIRELESS, //!< Sony DualShock Wireless + PSCTRL_GUITHERO, //!< Guitar Hero controller +}; + +/** \brief Number of different controller types recognized + * + * This is the number of entries in #PsxControllerType. + */ +const byte PSCTRL_MAX = static_cast (PSCTRL_GUITHERO) + 1; + + +/** \brief Controller Protocol + * + * Identifies the protocol the controller uses to report axes positions and + * button presses. It's quite more reliable than #PsxControllerType, so use this + * if you must. + * + * \sa getProtocol + */ +enum PsxControllerProtocol { + PSPROTO_UNKNOWN = 0, //!< No idea + PSPROTO_DIGITAL, //!< Original controller (SCPH-1010) protocol (8 digital buttons + START + SELECT) + PSPROTO_DUALSHOCK, //!< DualShock (has analog axes) + PSPROTO_DUALSHOCK2, //!< DualShock 2 (has analog axes and buttons) + PSPROTO_FLIGHTSTICK, //!< Green-mode (like DualShock but missing SELECT, L3 and R3) + PSPROTO_NEGCON, //!< Namco neGcon (has 1 analog X axis and analog Square, Circle and L1 buttons) + PSPROTO_JOGCON, //!< Namco Jogcon (Wheel is mapped to analog X axis, half a rotation in each direction) + PSPROTO_GUNCON +}; + +/** \brief Number of different protocols supported + * + * This is the number of entries in #PsxControllerProtocol. + */ +const byte PSPROTO_MAX = static_cast (PSPROTO_JOGCON) + 1; + +/** \brief Analog sticks minimum value + * + * Minimum value reported by analog sticks. This usually means that the stick is + * fully either at the top or left position. Note that some sticks might not get + * fully down to this value. + * + * \sa ANALOG_MAX_VALUE + * \sa ANALOG_IDLE_VALUE + */ +const byte ANALOG_MIN_VALUE = 0U; + +/** \brief Analog sticks maximum value + * + * Maximum value reported by analog sticks. This usually means that the stick is + * fully either at the bottom or right position. Note that some sticks might not + * get fully up to this value. + * + * \sa ANALOGI_MAX_VALUE + * \sa ANALOG_IDLE_VALUE + */ +const byte ANALOG_MAX_VALUE = 255U; + +/** \brief Analog sticks idle value + * + * Value reported when an analog stick is in the (ideal) center position. Note + * that old and worn-out sticks might not self-center perfectly when released, + * so you should never rely on this precise value to be reported. + * + * Also note that the up/down and left/right ranges are off by one, since values + * 0-127 represent up/left and 129-255 mean down/right. The former interval + * contains 128 different values, while the latter only 127. Sometimes you will + * need to take this in consideration. + */ +const byte ANALOG_IDLE_VALUE = 128U; + +/** \brief neGcon I/II-button press threshold + * + * The neGcon does not report digital button press data for its analog buttons, + * so we have to make it up. The Square, Cross digital buttons will be + * reported as pressed when the analog value of the II and I buttons + * (respectively), goes over this threshold. + * + * \sa NEGCON_L_BUTTON_THRESHOLD + */ +const byte NEGCON_I_II_BUTTON_THRESHOLD = 128U; + +/** \brief neGcon L-button press threshold + * + * The neGcon does not report digital button press data for its analog buttons, + * so we have to make it up. The L1 digital button will be reported as pressed + * when the analog value of the L buttons goes over this threshold. + * + * This value has been tuned so that the L button gets digitally triggered at + * about the same point as the non-analog R button. This is done "empirically" + * and might need tuning on a different controller than the one I actually have. + * + * \sa NEGCON_I_II_BUTTON_THRESHOLD + */ +const byte NEGCON_L_BUTTON_THRESHOLD = 240U; + +//! \brief Guncon read status +enum GunconStatus { + //! Guncon data is valid + GUNCON_OK, + + /** "Unexpected light": sensed light during VSYNC (e.g. from a Bulb or + * Sunlight) + */ + GUNCON_UNEXPECTED_LIGHT, + + /** "No light", this can mean either no light sensed at all (not aimed at + * screen, or screen too dark: ERROR) or no light sensed yet (when trying to + * read during rendering: BUSY) + */ + GUNCON_NO_LIGHT, + + /** Data is not valid for some other reason (i.e.: no Guncon, read failed, + * etc...) + */ + GUNCON_OTHER_ERROR +}; + +/** \brief PSX Controller Interface + * + * This is the base class implementing interactions with PSX controllers. It is + * partially abstract, so it is not supposed to be instantiated directly. + */ +class PsxController { +protected: + /** \brief Size of internal communication buffer + * + * This can be sized after the longest command reply (which is 21 bytes for + * 01 42 when in DualShock 2 mode), but we're better safe than sorry. + */ + static const byte BUFFER_SIZE = 32; + + /** \brief Internal communication buffer + * + * This is used to hold replies received from the controller. + */ + byte inputBuffer[BUFFER_SIZE]; + + /** \brief Previous (Digital) Button Status + * + * The individual bits can be identified through #PsxButton. + */ + PsxButtons previousButtonWord; + + /** \brief (Digital) Button Status + * + * The individual bits can be identified through #PsxButton. + */ + PsxButtons buttonWord; + + /** \brief Controller Protocol + * + * The protocol controller data was interpreted with at the last call to + * read() + * + * \sa getProtocol + */ + PsxControllerProtocol protocol; + + //! \name Analog Stick Data + //! @{ + byte lx; //!< Horizontal axis of left stick [0-255, L to R] + byte ly; //!< Vertical axis of left stick [0-255, U to D] + byte rx; //!< Horizontal axis of right stick [0-255, L to R] + byte ry; //!< Vertical axis of right stick [0-255, U to D] + + boolean analogSticksValid; //!< True if the above were valid at the last call to read() + //! @} + + /** \brief Analog Button Data + * + * \todo What's the meaning of every individual byte? + */ + byte analogButtonData[PSX_ANALOG_BTN_DATA_SIZE]; + + /** \brief Analog Button Data Validity + * + * True if the #analogButtonData were valid in last call to read() + */ + boolean analogButtonDataValid; + + /** \brief Rumble feature enabled or disabled. + * + * True if rumble has been turned on with command 0x4d, false otherwise. + * Rumble must be enabled and 7.5v supplied to pin 3! + */ + boolean rumbleEnabled; + + /** \brief requested left motor (motor 1) power. + * + * 0xff for on, 0x00 for off, motor does not support partial activation. + * Rumble must be enabled and 7.5v supplied to pin 3! + */ + byte motor1Level; + + /** \brief requested right motor (motor 2) power. + * + * 0x00 to 0xFF -> 0 to 100% power. + * Rumble must be enabled and 7.5v supplied to pin 3! + */ + byte motor2Level; + + /** \brief Assert the Attention line + * + * This function must be implemented by derived classes and must set the + * Attention line \a low so that the controller will pay attention to what + * we will send. + */ + virtual void attention () = 0; + + /** \brief Deassert the Attention line + * + * This function must be implemented by derived classes and must set the + * Attention line \a high so that the controller will no longer pay + * attention to what we will send. + */ + virtual void noAttention () = 0; + + /** \brief Transfer a single byte to/from the controller + * + * This function must be implemented by derived classes and must transfer + * a single command byte to the controller and read back a single + * data byte. + * + * \param[in] out The command byte to send the controller + * \return The data byte returned by the controller + */ + virtual byte shiftInOut (const byte out) = 0; + + /** \brief Transfer several bytes to/from the controller + * + * This function transfers an array of command bytes to the + * controller and reads back an equally sized array of data bytes. + * + * \param[in] out The command bytes to send the controller + * \param[out] in The data bytes returned by the controller, must be sized + * to hold at least \a len bytes + * \param[in] len The amount of bytes to be exchanged + */ + void shiftInOut (const byte *out, byte *in, const byte len) { +#ifdef DUMP_COMMS + byte inbuf[len]; +#endif + + for (byte i = 0; i < len; ++i) { + byte tmp = shiftInOut (out != NULL ? out[i] : 0x5A); +#ifdef DUMP_COMMS + inbuf[i] = tmp; +#endif + if (in != NULL) { + in[i] = tmp; + } + + delayMicroseconds (INTER_CMD_BYTE_DELAY); // Very important! + } + +#ifdef DUMP_COMMS + Serial.print (F("<-- ")); + for (byte i = 0; i < len; ++i) { + if (out && out[i] < 0x10) + Serial.print (0); + Serial.print (out ? out[i]: 0x5A, HEX); + Serial.print (' '); + } + Serial.println (); + + Serial.print (F("--> ")); + for (byte i = 0; i < len; ++i) { + if (inbuf[i] < 0x10) + Serial.print (0); + Serial.print (inbuf[i], HEX); + Serial.print (' '); + } + Serial.println (); +#endif + } + + /** \brief Transfer several bytes to/from the controller + * + * This function transfers an array of command bytes to the + * controller and reads back the full reply of data bytes. The size + * of the reply is calculated automatically and padding bytes (0x5A) are + * appended to the outgoing message if it is shorter. + * + * The reply is stored in an internal buffer and will be valid until the + * next call to this function, so make sure to save anything if is needed. + * + * \param[out] out The data bytes returned by the controller, must be sized + * to hold at least \a len bytes + * \param[in] len The amount of bytes to be exchanged + * \return A pointer to a buffer containing the reply, whose size can be + * calculated with getReplyLength() + */ + byte *autoShift (const byte *out, const byte len) { + byte *ret = nullptr; + + if (len >= 3 && len <= BUFFER_SIZE) { + // All commands have at least 3 bytes, so shift out those first + shiftInOut (out, inputBuffer, 3); + if (isValidReply (inputBuffer)) { + // Reply is good, get full length + byte replyLen = getReplyLength (inputBuffer); + + // Shift out rest of command + if (len > 3) { + shiftInOut (out + 3, inputBuffer + 3, len - 3); + } + + byte left = replyLen - len + 3; + //~ Serial.print ("len = "); + //~ Serial.print (replyLen); + //~ Serial.print (", left = "); + //~ Serial.println (left); + if (left == 0) { + // The whole reply was gathered + ret = inputBuffer; + } else if (len + left <= BUFFER_SIZE) { + // Part of reply is still missing and we have space for it + shiftInOut (NULL, inputBuffer + len, left); + ret = inputBuffer; + } else { + // Reply incomplete but not enough space provided + } + } + } + + return ret; + } + + /** \brief Get reply length + * + * Calculates the length of a command reply, in bytes + * + * \param[in] buf The buffer containing the reply, must be at least 2 bytes + * long + * \return The calculated length + */ + byte getReplyLength (const byte *buf) const { + return (buf[1] & 0x0F) * 2; + } + + inline boolean isValidReply (const byte *status) { + //~ return status[0] != 0xFF || status[1] != 0xFF || status[2] != 0xFF; + return status[1] != 0xFF && (status[2] == 0x5A || status[2] == 0x00); + //~ return /* status[0] == 0xFF && */ status[1] != 0xFF && status[2] == 0x5A; + } + + // Green Mode controllers + inline boolean isFlightstickReply (const byte *status) { + return (status[1] & 0xF0) == 0x50; + } + + inline boolean isDualShockReply (const byte *status) { + return (status[1] & 0xF0) == 0x70; + } + + inline boolean isDualShock2Reply (const byte *status) { + return status[1] == 0x79; + } + + inline boolean isDigitalReply (const byte *status) { + return (status[1] & 0xF0) == 0x40; + } + + inline boolean isConfigReply (const byte *status) { + return (status[1] & 0xF0) == 0xF0; + } + + inline boolean isNegconReply (const byte *status) { + return status[1] == 0x23; + } + + inline boolean isJogconReply (const byte *status) { + return (status[1] & 0xF0) == 0xE0; + } + + inline boolean isGunconReply (const byte *status) { + return status[1] == 0x63; + } + + +public: + /** \brief Initialize library + * + * This function shall be called before any others, it will initialize the + * communication and return if a supported controller was found. It shall + * also be called to reinitialize the communication whenever the controller + * is unplugged. + * + * Derived classes can override this function if they need to perform + * additional initializations, but shall call it on return. + * + * \return true if a supported controller was found, false otherwise + */ + virtual boolean begin () { + // Start with all analog axes at midway position + lx = ANALOG_IDLE_VALUE; + ly = ANALOG_IDLE_VALUE; + rx = ANALOG_IDLE_VALUE; + ry = ANALOG_IDLE_VALUE; + + analogSticksValid = false; + memset (analogButtonData, 0, sizeof (analogButtonData)); + + protocol = PSPROTO_UNKNOWN; + + rumbleEnabled = false; + motor1Level = 0x00; + motor2Level = 0x00; + + // Some disposable readings to let the controller know we are here + for (byte i = 0; i < 5; ++i) { + read (); + delay (1); + } + + return read (); + } + + //! \name Configuration Mode Functions + //! @{ + + /** \brief Enter Configuration Mode + * + * Some controllers can be configured in several aspects. For instance, + * DualShock controllers can return analog stick data. This function puts + * the controller in configuration mode. + * + * Note that Configuration Mode is sometimes called Escape Mode. + * + * \return true if Configuration Mode was entered successfully + */ + boolean enterConfigMode () { + boolean ret = false; + + unsigned long start = millis (); + do { + attention (); + byte *in = autoShift (enter_config, 4); + noAttention (); + + ret = in != NULL && isConfigReply (in); + + if (!ret) { + delay (COMMAND_RETRY_INTERVAL); + } + } while (!ret && millis () - start <= COMMAND_TIMEOUT); + delay (MODE_SWITCH_DELAY); + + return ret; + } + + /** \brief Enable (or disable) analog sticks + * + * This function enables or disables the analog sticks that were introduced + * with DualShock controllers. When they are enabled, the getLeftAnalog() + * and getRightAnalog() functions can be used to retrieve their positions. + * Also, button presses for L3 and R3 will be available through the + * buttonPressed() and similar functions. + * + * When analog sticks are enabled, the \a ANALOG led will light up (in red) + * on the controller. + * + * Note that on some third-party controllers, when analog sticks are + * disabled the analog levers will "emulate" the D-Pad and possibly the + * []/^/O/X buttons. This does not happen on official Sony controllers. + * + * This function will only work if when the controller is in Configuration + * Mode. + * + * \param[in] enabled true to enable, false to disable + * \param[in] locked If true, the \a ANALOG button on the controller will be + * disabled and the user will not be able to turn off the + * analog sticks. + * \return true if the command was ackowledged by the controller. Note that + * this does not fully guarantee that the analog sticks were enabled + * as this can only be checked after Configuration Mode is exited. + */ + boolean enableAnalogSticks (bool enabled = true, bool locked = false) { + boolean ret = false; + byte out[sizeof (set_mode)]; + + memcpy (out, set_mode, sizeof (set_mode)); + out[3] = enabled ? 0x01 : 0x00; + out[4] = locked ? 0x03 : 0x00; + + unsigned long start = millis (); + byte cnt = 0; + do { + attention (); + byte *in = autoShift (out, 5); + noAttention (); + + /* We can't know if we have successfully enabled analog mode until + * we get out of config mode, so let's just be happy if we get a few + * consecutive valid replies + */ + if (in != nullptr) { + ++cnt; + } + ret = cnt >= 3; + + if (!ret) { + delay (COMMAND_RETRY_INTERVAL); + } + } while (!ret && millis () - start <= COMMAND_TIMEOUT); + delay (MODE_SWITCH_DELAY); + + return ret; + } + + /** \brief Enable (or disable) the vibration capability of the DualShock / DualShock 2 + * + * This function enables or disables the rumble feature of the DualShock / DualShock 2 controllers. + * NOTE that this function does nothing on its own - the vibration on/off must be set using + * setRumble() and the controller will begin to vibrate when the read() function is + * next called. + * + * This function will only work if when the controller is in Configuration + * Mode. + * + * \param[in] enabled true to enable both motors, false to disable them. + * + * \return true if we got bytes back. Eventually we should wait for ACK from the controller. + */ + boolean enableRumble(bool enabled = true) { + boolean ret = true; + byte out[sizeof (enable_rumble)]; + + memcpy (out, enable_rumble, sizeof (enable_rumble)); + out[3] = enabled ? 0x00 : 0xff; + out[4] = enabled ? 0x01 : 0xff; + + unsigned long start = millis (); + byte cnt = 0; + do { + attention (); + byte *in = autoShift (out, 5); + noAttention (); + + /* The real way to check if the command was successful is to wait for ACK. + * Currently the library doesn't support the pin, so I will just assume success. + */ + if (in != nullptr) { + ++cnt; + } + ret = cnt >= 3; + + if (!ret) { + delay (COMMAND_RETRY_INTERVAL); + } + } while (!ret && millis () - start <= COMMAND_TIMEOUT); + delay (MODE_SWITCH_DELAY); + + rumbleEnabled = true; + return ret; + } + + /** \brief Set the requested power output of the rumble motors on DualShock / DualShock 2 controllers. + * + * This function sets internal variables that set the requested motor power of the rumble motors. + * NOTE this does nothing if rumble has not been enabled with enableRumble(), rumble motors will + * activate or deactivate to match the arguments of this function with the next call to read() + * + * \param[in] enabled true to activate motor 1, false to deactivate. + * \param[in] requested motor power of motor 2, where 0x00 to 0xFF corresponds to 0 to 100%. + */ + void setRumble(bool motor1Active = true, byte motor2Power = 0xff) { + motor1Level = motor1Active ? 0xff : 0x00; + motor2Level = motor2Power; + } + + /** \brief Enable (or disable) analog buttons + * + * This function enables or disables the analog buttons that were introduced + * with DualShock 2 controllers. When they are enabled, the + * getAnalogButton() functions can be used to retrieve how deep/strongly + * they are pressed. This applies to the D-Pad buttons, []/^/O/X, L1/2 and + * R1/2 + * + * This function will only work if when the controller is in Configuration + * Mode. + * + * \param[in] enabled true to enable, false to disable + * \return true if the command was ackowledged by the controller. Note that + * this does not fully guarantee that the analog sticks were enabled + * as this can only be checked after Configuration Mode is exited. + */ + boolean enableAnalogButtons (bool enabled = true) { + boolean ret = false; + byte out[sizeof (set_mode)]; + + memcpy (out, set_pressures, sizeof (set_pressures)); + if (!enabled) { + out[3] = 0x00; + out[4] = 0x00; + out[5] = 0x00; + } + + unsigned long start = millis (); + byte cnt = 0; + do { + attention (); + byte *in = autoShift (out, sizeof (set_pressures)); + noAttention (); + + /* We can't know if we have successfully enabled analog mode until + * we get out of config mode, so let's just be happy if we get a few + * consecutive valid replies + */ + if (in != nullptr) { + ++cnt; + } + ret = cnt >= 3; + + if (!ret) { + delay (COMMAND_RETRY_INTERVAL); + } + } while (!ret && millis () - start <= COMMAND_TIMEOUT); + delay (MODE_SWITCH_DELAY); + + return ret; + } + + /** \brief Retrieve the controller type + * + * This function retrieves the controller type. It is not 100% reliable, so + * do not rely on it for anything other than a vague indication (for + * instance, the DualShock SCPH-1200 controller gets reported as the Guitar + * Hero controller...). + * + * This function will only work if when the controller is in Configuration + * Mode. + * + * \return The (tentative) controller type + */ + PsxControllerType getControllerType () { + PsxControllerType ret = PSCTRL_UNKNOWN; + + attention (); + byte *in = autoShift (type_read, 3); + noAttention (); + + if (in != nullptr) { + const byte& controllerType = in[3]; + if (controllerType == 0x03) { + ret = PSCTRL_DUALSHOCK; + //~ } else if (controllerType == 0x01 && in[1] == 0x42) { + //~ return 4; // ??? + } else if (controllerType == 0x01 && in[1] != 0x42) { + ret = PSCTRL_GUITHERO; + } else if (controllerType == 0x0C) { + ret = PSCTRL_DSWIRELESS; + } + } + + return ret; + } + + boolean exitConfigMode () { + boolean ret = false; + + unsigned long start = millis (); + do { + attention (); + //~ shiftInOut (poll, in, sizeof (poll)); + //~ shiftInOut (exit_config, in, sizeof (exit_config)); + byte *in = autoShift (exit_config, 4); + noAttention (); + + ret = in != nullptr && !isConfigReply (in); + + if (!ret) { + delay (COMMAND_RETRY_INTERVAL); + } + } while (!ret && millis () - start <= COMMAND_TIMEOUT); + delay (MODE_SWITCH_DELAY); + + return ret; + } + + //! @} // Configuration Mode Functions + + //! \name Polling Functions + //! @{ + + /** \brief Retrieve the controller protocol + * + * This function retrieves the protocol that was used to interpret + * controller data at the last call to read(). + * + * \return The controller protocol + */ + PsxControllerProtocol getProtocol () const { + return protocol; + } + + /** \brief Poll the controller + * + * This function polls the controller for button and stick data. It self- + * adapts to all the supported controller types and populates internal + * variables with the retrieved information, which can be later accessed + * through the inspection functions. + * + * This function must be called quite often in order to keep the controller + * alive. Most controllers have some kind of watchdog that will reset them + * if they don't get polled at least every so often (like a couple dozen + * times per seconds). + * + * If this function fails repeatedly, it can safely be assumed that the + * controller has been disconnected (or that it is not supported if it + * failed right from the beginning). + * + * \return true if the read was successful, false otherwise + */ + boolean read () { + boolean ret = false; + + analogSticksValid = false; + analogButtonDataValid = false; + + attention (); + byte *in = nullptr; + if(rumbleEnabled) { + byte out[sizeof (poll)]; + memcpy(out, poll, sizeof(poll)); + out[3] = motor1Level; + out[4] = motor2Level; + in = autoShift (out, sizeof(poll)); + } + else { + in = autoShift (poll, 3); + } + noAttention (); + + if (in != NULL) { + if (isConfigReply (in)) { + // We're stuck in config mode, try to get out + exitConfigMode (); + } else { + // We surely have buttons + previousButtonWord = buttonWord; + buttonWord = ((PsxButtons) in[4] << 8) | in[3]; + + // See if we have anything more to read + if (isDualShock2Reply (in)) { + protocol = PSPROTO_DUALSHOCK2; + } else if (isDualShockReply (in)) { + protocol = PSPROTO_DUALSHOCK; + } else if (isFlightstickReply (in)) { + protocol = PSPROTO_FLIGHTSTICK; + } else if (isNegconReply (in)) { + protocol = PSPROTO_NEGCON; + } else if (isJogconReply (in)) { + protocol = PSPROTO_JOGCON; + } else if (isGunconReply (in)) { + protocol = PSPROTO_GUNCON; + } else { + protocol = PSPROTO_DIGITAL; + } + + switch (protocol) { + case PSPROTO_DUALSHOCK2: + // We also have analog button data + analogButtonDataValid = true; + for (int i = 0; i < PSX_ANALOG_BTN_DATA_SIZE; ++i) { + analogButtonData[i] = in[i + 9]; + } + /* Now fall through to DualShock case, the next line + * avoids GCC warning + */ + /* FALLTHRU */ + case PSPROTO_GUNCON: + /* The Guncon uses the same reply format as DualShocks, + * by just falling through we'll end up with: + * - A (Left side) -> Start + * - B (Right side) -> Cross + * - Trigger -> Circle + * - Low byte of HSYNC -> RX + * - High byte of HSYNC -> RY + * - Low byte of VSYNC -> LX + * - High byte of VSYNC -> LY + */ + case PSPROTO_DUALSHOCK: + case PSPROTO_FLIGHTSTICK: + // We have analog stick data + analogSticksValid = true; + rx = in[5]; + ry = in[6]; + lx = in[7]; + ly = in[8]; + break; + case PSPROTO_NEGCON: + // Map the twist axis to X axis of left analog + analogSticksValid = true; + lx = in[5]; + + // Map analog button data to their reasonable counterparts + analogButtonDataValid = true; + analogButtonData[PSAB_CROSS] = in[6]; + analogButtonData[PSAB_SQUARE] = in[7]; + analogButtonData[PSAB_L1] = in[8]; + + // Make up "missing" digital data + if (analogButtonData[PSAB_SQUARE] >= NEGCON_I_II_BUTTON_THRESHOLD) { + buttonWord &= ~PSB_SQUARE; + } + if (analogButtonData[PSAB_CROSS] >= NEGCON_I_II_BUTTON_THRESHOLD) { + buttonWord &= ~PSB_CROSS; + } + if (analogButtonData[PSAB_L1] >= NEGCON_L_BUTTON_THRESHOLD) { + buttonWord &= ~PSB_L1; + } + break; + case PSPROTO_JOGCON: + /* Map the wheel X axis of left analog, half a rotation + * per direction: byte 5 has the wheel position, it is + * 0 at startup, then we have 0xFF down to 0x80 for + * left/CCW, and 0x01 up to 0x80 for right/CW + * + * byte 6 is the number of full CW rotations + * byte 7 is 0 if wheel is still, 1 if it is rotating CW + * and 2 if rotation CCW + * byte 8 seems to stay at 0 + * + * We'll want to cap the movement halfway in each + * direction, for ease of use/implementation. + */ + analogSticksValid = true; + if (in[6] < 0x80) { + // CW up to half + lx = in[5] < 0x80 ? in[5] : (0x80 - 1); + } else { + // CCW down to half + lx = in[5] > 0x80 ? in[5] : (0x80 + 1); + } + + // Bring to the usual 0-255 range + lx += 0x80; + break; + default: + // We are already done + break; + } + + ret = true; + } + } + + return ret; + } + + /** \brief Check if any button has changed state + * + * \return true if any button has changed state with regard to the previous + * call to read(), false otherwise + */ + boolean buttonsChanged () const { + return ((previousButtonWord ^ buttonWord) > 0); + } + + /** \brief Check if a button has changed state + * + * \return true if \a button has changed state with regard to the previous + * call to read(), false otherwise + */ + boolean buttonChanged (const PsxButtons button) const { + return (((previousButtonWord ^ buttonWord) & button) > 0); + } + + /** \brief Check if a button is currently pressed + * + * \param[in] button The button to be checked + * \return true if \a button was pressed in last call to read(), false + * otherwise + */ + boolean buttonPressed (const PsxButton button) const { + return buttonPressed (~buttonWord, button); + } + + /** \brief Check if a button is pressed in a Button Word + * + * \param[in] buttons The button word to check in + * \param[in] button The button to be checked + * \return true if \a button is pressed in \a buttons, false otherwise + */ + boolean buttonPressed (const PsxButtons buttons, const PsxButton button) const { + return ((buttons & static_cast (button)) > 0); + } + + /** \brief Check if a button has just been pressed + * + * \param[in] button The button to be checked + * \return true if \a button was not pressed in the previous call to read() + * and is now, false otherwise + */ + boolean buttonJustPressed (const PsxButton button) const { + return (buttonChanged (button) & buttonPressed (button)); + } + + /** \brief Check if a button has just been released + * + * \param[in] button The button to be checked + * \return true if \a button was pressed in the previous call to read() and + * is not now, false otherwise + */ + boolean buttonJustReleased (const PsxButton button) const { + return (buttonChanged (button) & ((~previousButtonWord & button) > 0)); + } + + /** \brief Check if NO button is pressed in a Button Word + * + * \param[in] buttons The button word to check in + * \return true if all buttons in \a buttons are released, false otherwise + */ + boolean noButtonPressed (const PsxButtons buttons) const { + return buttons == PSB_NONE; + } + + /** \brief Check if NO button is currently pressed + * + * \return true if all buttons were released in the last call to read(), + * false otherwise + */ + boolean noButtonPressed (void) const { + return buttonWord == ~PSB_NONE; + } + + /** \brief Retrieve the Button Word + * + * The button word contains the status of all digital buttons and can be + * retrieved so that it can be inspected later. + * + * \sa buttonPressed + * \sa noButtonPressed + * + * \return the Button Word + */ + PsxButtons getButtonWord () const { + return ~buttonWord; + } + + /** \brief Retrieve button pressure depth/strength + * + * This function will return how deeply/strongly a button is pressed. It + * will only work on DualShock 2 controllers after enabling this feature + * with enableAnalogButtons(). + * + * Note that button pressure depth/strength is only available for the D-Pad + * buttons, []/^/O/X, L1/2 and R1/2. + * + * \param[in] button the button the retrieve the pressure depth/strength of + * \return the pressure depth/strength [0-255, Fully released to fully + * pressed] + */ + byte getAnalogButton (const PsxAnalogButton button) const { + byte ret = 0; + + if (analogButtonDataValid) { + ret = analogButtonData[button]; + //~ } else if (buttonPressed (button)) { // FIXME + //~ // No analog data, assume fully pressed or fully released + //~ ret = 0xFF; + } + + return ret; + } + + /** \brief Retrieve all analog button data + */ + const byte* getAnalogButtonData () const { + return analogButtonDataValid ? analogButtonData : NULL; + } + + /** \brief Retrieve position of the \a left analog stick + * + * This function will return the absolute position of the left analog stick. + * + * Note that not all controllers have analog sticks, in which case this + * function will return false. + * + * \param[in] x A variable where the horizontal position will be stored + * [0-255, L to R] + * \param[in] y A variable where the vertical position will be stored + * [0-255, U to D] + * \return true if the returned position is valid, false otherwise + */ + boolean getLeftAnalog (byte& x, byte& y) const { + x = lx; + y = ly; + + return analogSticksValid; + } + + /** \brief Retrieve position of the \a right analog stick + * + * This function will return the absolute position of the right analog + * stick. + * + * Note that not all controllers have analog sticks, in which case this + * function will return false. + * + * \param[in] x A variable where the horizontal position will be stored + * [0-255, L to R] + * \param[in] y A variable where the vertical position will be stored + * [0-255, U to D] + * \return true if the returned position is valid, false otherwise + */ + boolean getRightAnalog (byte& x, byte& y) { + x = rx; + y = ry; + + return analogSticksValid; + } + + /** \brief Retrieve Guncon X/Y readings + * + * According to the Nocash PSX Specifications, the Guncon returns 16-bit X/Y + * coordinates of the screen it is aimed at. + * + * The coordinates are updated in all frames. The absolute min/max may vary + * from TV set to TV set. + * + * Vertical coordinates are counted in scanlines (ie. equal to pixels). + * Horizontal coordinates are counted in 8MHz units (which would equal a + * resolution of 385 pixels; which can be, for example, converted to 320 + * pixel resolution as X=X*320/385). + * + * Caution: The gun only returns meaningful data when read shortly + * after begin of VBLANK (ie. AFTER rendering, but still BEFORE vsync), so + * make sure to only consider readings returning \a GUNCON_OK; + * + * \sa GunconStatus + */ + GunconStatus getGunconCoordinates (word& x, word& y) const { + GunconStatus status = GUNCON_OTHER_ERROR; + + if (protocol == PSPROTO_GUNCON && analogSticksValid) { + status = GUNCON_OK; + + x = (((word) ry) << 8) | rx; + y = (((word) ly) << 8) | lx; + + if (x == 0x0001) { + if (y == 0x0005) { + status = GUNCON_UNEXPECTED_LIGHT; + } else if (y == 0x000A) { + status = GUNCON_NO_LIGHT; + } + } + } + + return status; + } + + //! @} // Polling Functions +}; + +#endif diff --git a/README.md b/README.md index a51c362..8ac1b8c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,88 @@ -# IIDXPS2toUSB -IIDX PS2 (JKOC/USKOC/KASC) Controller USB Adapter with Infinitas/Ultimate Mobile compatibility +[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/donate?hosted_button_id=WT735CX4UMZ9U) + +# BEMANI PSX Adapter + +USB Adapter for Playstation IIDX and Pop'n Music Controllers + +Generic Playstation to USB adapters are not working correctly with Bemani controllers : + +- IIDX controllers have a digital TT instead of an analog axis, and lack the correct descriptors for Infinitas or Ultimate Mobile compatibility + +- Pop'n Music controllers hold left+down+right constantly and the right yellow button is mapped to up, which confuses most adapters into making the right yellow button a normally closed "down" input. + +This code solves these issues and auto-detects whether a Pop'n Music controller is plugged. + +You can buy a cheap "Dualshock extension cable" or a dualshock breakout board to build an external adapter box, or you can buy a pre-made adapter from me to support my work :) + +## Demo + +https://www.instagram.com/p/CHVLleylqFl/ + +## Acknowledgments + +The PSX pad read code is using [PsxNewLib by SukkoPera](https://github.com/SukkoPera/PsxNewLib/). + +## Features + +Pop'n Music and IIDX controller modes are auto detected (you must plug the PSX controller to the adapter before plugging the USB) + +### Pop'n Music mode + +- Compatible with Pop'n Music Lively natively (wired, **not bluetooth**) + +- All buttons are recognized as simple buttons from 1 to 9 then select and start. + +- It should be compatible with Lively without any remapping, but also with UltimatePopnController's ezusb.dll for other purposes + +### IIDX mode + +- Compatible with Infinitas and Ultimate Mobile natively (wired, **not bluetooth**) + +- Buttons 1 to 7 are mapped to the first 7 buttons, then E1-E4 are mapped to buttons 9-12. + +- The TT is mapped to the X analog axis. + +- Still includes a HAT switch to retrieve the digital TT signals (useful for MAME) + +- Select acts as E1, Start as E2. + +- Double (resp. triple) press Select acts as E3 (resp. E1+E3), double (resp. triple) press Start acts as E4 (resp. E2+E4) + +- **Note**: digital TT signal is lingering a bit too much, which causes inertia on the analog axis. This is a **hardware limitation** from the controller itself. In order to make song wheel selection less painful, holding SELECT allows for very slow increments. This also means that it's important to keep alternating direction when playing a rapid succession of scratch notes to avoid dropped inputs. + +## Building Instructions + +- Download Arduino IDE, +- Build and Flash for your ATMEGA32U4 Board +- Have Fun + +## Pinout + +When looking at the plug of the *controller cable* : +``` + 1 2 3 4 5 6 7 8 9 +------------------------------- +| o o o | o o o | o o o | (at the Controller) +\_____________________________/ + +1 : DAT -> to arduino MISO (on ICSP header) (with 1k ohm pullup resistor*) +2 : CMD -> to arduino MOSI (on ICSP header) +4 : GND -> to arduino GND +5 : 3.3V -> to arduino 3.3V or 5V (tested on Pop'n KASC, minicon, and IIDX JKOC) +6 : Attention -> to arduino digital pin 2 +7 : SCK -> to arduino SCK (on ICSP header) + +* Note: 1k ohm pullup resistor to 5V means you also have to put a 1k ohm resistor between 5V and MISO. +See the demo pic. + + ``` + +## Donation + +If this project helps you and you want to give back, you can help me with my future projects. + +While not necessary, donations are much appreciated and will only go towards funding future github projects (arcade hardware ain't cheap :( ). + +Of course you'll also receive my gratitude and I'll remember you if you post a feature request ;) + +[![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/donate?hosted_button_id=WT735CX4UMZ9U)