From ee7bf6e5a0575d52a4bd283f0c94c212adea66ca Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Sun, 17 Jan 2021 20:17:32 +0100 Subject: [PATCH] PS5 controller is now working via USB --- PS5Parser.cpp | 160 +++++++++++++++ PS5Parser.h | 405 +++++++++++++++++++++++++++++++++++++ PS5Trigger.cpp | 94 +++++++++ PS5Trigger.h | 168 +++++++++++++++ PS5USB.h | 154 ++++++++++++++ controllerEnums.h | 6 + examples/PS5USB/PS5USB.ino | 146 +++++++++++++ keywords.txt | 2 + 8 files changed, 1135 insertions(+) create mode 100644 PS5Parser.cpp create mode 100644 PS5Parser.h create mode 100644 PS5Trigger.cpp create mode 100644 PS5Trigger.h create mode 100644 PS5USB.h create mode 100644 examples/PS5USB/PS5USB.ino diff --git a/PS5Parser.cpp b/PS5Parser.cpp new file mode 100644 index 00000000..b59d32c6 --- /dev/null +++ b/PS5Parser.cpp @@ -0,0 +1,160 @@ +/* Copyright (C) 2021 Kristian Sloth Lauszus. All rights reserved. + + This software may be distributed and modified under the terms of the GNU + General Public License version 2 (GPL2) as published by the Free Software + Foundation and appearing in the file GPL2.TXT included in the packaging of + this file. Please note that GPL2 Section 2[b] requires that all works based + on this software must also be made publicly available under the terms of + the GPL2 ("Copyleft"). + + Contact information + ------------------- + + Kristian Sloth Lauszus + Web : https://lauszus.com + e-mail : lauszus@gmail.com + + Thanks to Joseph Duchesne for the initial code. + Based on Ludwig Füchsl's https://github.com/Ohjurot/DualSense-Windows PS5 port. + */ + +#include "PS5Parser.h" + +enum DPADEnum { + DPAD_UP = 0x0, + DPAD_UP_RIGHT = 0x1, + DPAD_RIGHT = 0x2, + DPAD_RIGHT_DOWN = 0x3, + DPAD_DOWN = 0x4, + DPAD_DOWN_LEFT = 0x5, + DPAD_LEFT = 0x6, + DPAD_LEFT_UP = 0x7, + DPAD_OFF = 0x8, +}; + +// To enable serial debugging see "settings.h" +//#define PRINTREPORT // Uncomment to print the report send by the PS5 Controller + +bool PS5Parser::checkDpad(ButtonEnum b) { + switch (b) { + case UP: + return ps5Data.btn.dpad == DPAD_LEFT_UP || ps5Data.btn.dpad == DPAD_UP || ps5Data.btn.dpad == DPAD_UP_RIGHT; + case RIGHT: + return ps5Data.btn.dpad == DPAD_UP_RIGHT || ps5Data.btn.dpad == DPAD_RIGHT || ps5Data.btn.dpad == DPAD_RIGHT_DOWN; + case DOWN: + return ps5Data.btn.dpad == DPAD_RIGHT_DOWN || ps5Data.btn.dpad == DPAD_DOWN || ps5Data.btn.dpad == DPAD_DOWN_LEFT; + case LEFT: + return ps5Data.btn.dpad == DPAD_DOWN_LEFT || ps5Data.btn.dpad == DPAD_LEFT || ps5Data.btn.dpad == DPAD_LEFT_UP; + default: + return false; + } +} + +bool PS5Parser::getButtonPress(ButtonEnum b) { + if (b <= LEFT) // Dpad + return checkDpad(b); + else + return ps5Data.btn.val & (1UL << pgm_read_byte(&PS5_BUTTONS[(uint8_t)b])); +} + +bool PS5Parser::getButtonClick(ButtonEnum b) { + uint32_t mask = 1UL << pgm_read_byte(&PS5_BUTTONS[(uint8_t)b]); + bool click = buttonClickState.val & mask; + buttonClickState.val &= ~mask; // Clear "click" event + return click; +} + +uint8_t PS5Parser::getAnalogButton(ButtonEnum b) { + if (b == L2) // These are the only analog buttons on the controller + return ps5Data.trigger[0]; + else if (b == R2) + return ps5Data.trigger[1]; + return 0; +} + +uint8_t PS5Parser::getAnalogHat(AnalogHatEnum a) { + return ps5Data.hatValue[(uint8_t)a]; +} + +void PS5Parser::Parse(uint8_t len, uint8_t *buf) { + if (len > 1 && buf) { +#ifdef PRINTREPORT + Notify(PSTR("\r\n"), 0x80); + for (uint8_t i = 0; i < len; i++) { + D_PrintHex (buf[i], 0x80); + Notify(PSTR(" "), 0x80); + } +#endif + + if (buf[0] == 0x01) // Check report ID + memcpy(&ps5Data, buf + 1, min((uint8_t)(len - 1), MFK_CASTUINT8T sizeof(ps5Data))); + else if (buf[0] == 0x11) { // This report is send via Bluetooth, it has an offset of 2 compared to the USB data + if (len < 4) { +#ifdef DEBUG_USB_HOST + Notify(PSTR("\r\nReport is too short: "), 0x80); + D_PrintHex (len, 0x80); +#endif + return; + } + memcpy(&ps5Data, buf + 3, min((uint8_t)(len - 3), MFK_CASTUINT8T sizeof(ps5Data))); + } else { +#ifdef DEBUG_USB_HOST + Notify(PSTR("\r\nUnknown report id: "), 0x80); + D_PrintHex (buf[0], 0x80); +#endif + return; + } + + if (ps5Data.btn.val != oldButtonState.val) { // Check if anything has changed + buttonClickState.val = ps5Data.btn.val & ~oldButtonState.val; // Update click state variable + oldButtonState.val = ps5Data.btn.val; + + // The DPAD buttons does not set the different bits, but set a value corresponding to the buttons pressed, we will simply set the bits ourself + uint8_t newDpad = 0; + if (checkDpad(UP)) + newDpad |= 1 << UP; + if (checkDpad(RIGHT)) + newDpad |= 1 << RIGHT; + if (checkDpad(DOWN)) + newDpad |= 1 << DOWN; + if (checkDpad(LEFT)) + newDpad |= 1 << LEFT; + if (newDpad != oldDpad) { + buttonClickState.dpad = newDpad & ~oldDpad; // Override values + oldDpad = newDpad; + } + } + } + message_counter++; + + if (ps5Output.reportChanged || leftTrigger.reportChanged || rightTrigger.reportChanged) + sendOutputReport(&ps5Output); // Send output report +} + + +void PS5Parser::Reset() { + uint8_t i; + for (i = 0; i < sizeof(ps5Data.hatValue); i++) + ps5Data.hatValue[i] = 127; // Center value + ps5Data.btn.val = 0; + oldButtonState.val = 0; + for (i = 0; i < sizeof(ps5Data.trigger); i++) + ps5Data.trigger[i] = 0; + for (i = 0; i < sizeof(ps5Data.xy.finger)/sizeof(ps5Data.xy.finger[0]); i++) + ps5Data.xy.finger[i].touching = 1; // The bit is cleared if the finger is touching the touchpad + + ps5Data.btn.dpad = DPAD_OFF; + oldButtonState.dpad = DPAD_OFF; + buttonClickState.dpad = 0; + oldDpad = 0; + + leftTrigger.Reset(); + rightTrigger.Reset(); + + ps5Output.bigRumble = ps5Output.smallRumble = 0; + ps5Output.microphoneLed = 0; + ps5Output.disableLeds = 0; + ps5Output.playerLeds = 0; + ps5Output.r = ps5Output.g = ps5Output.b = 0; + ps5Output.reportChanged = false; +}; diff --git a/PS5Parser.h b/PS5Parser.h new file mode 100644 index 00000000..c299b700 --- /dev/null +++ b/PS5Parser.h @@ -0,0 +1,405 @@ +/* Copyright (C) 2021 Kristian Sloth Lauszus. All rights reserved. + + This software may be distributed and modified under the terms of the GNU + General Public License version 2 (GPL2) as published by the Free Software + Foundation and appearing in the file GPL2.TXT included in the packaging of + this file. Please note that GPL2 Section 2[b] requires that all works based + on this software must also be made publicly available under the terms of + the GPL2 ("Copyleft"). + + Contact information + ------------------- + + Kristian Sloth Lauszus + Web : https://lauszus.com + e-mail : lauszus@gmail.com + + Thanks to Joseph Duchesne for the initial code. + Based on Ludwig Füchsl's https://github.com/Ohjurot/DualSense-Windows PS5 port. + */ + +#ifndef _ps5parser_h_ +#define _ps5parser_h_ + +#include "Usb.h" +#include "controllerEnums.h" +#include "PS5Trigger.h" + +/** Buttons on the controller */ +const uint8_t PS5_BUTTONS[] PROGMEM = { + UP, // UP + RIGHT, // RIGHT + DOWN, // DOWN + LEFT, // LEFT + + 0x0C, // SHARE + 0x0D, // OPTIONS + 0x0E, // L3 + 0x0F, // R3 + + 0x0A, // L2 + 0x0B, // R2 + 0x08, // L1 + 0x09, // R1 + + 0x07, // TRIANGLE + 0x06, // CIRCLE + 0x05, // CROSS + 0x04, // SQUARE + + 0x10, // PS + 0x11, // TOUCHPAD + 0x12, // MICROPHONE +}; + +union PS5Buttons { + struct { + uint8_t dpad : 4; + uint8_t square : 1; + uint8_t cross : 1; + uint8_t circle : 1; + uint8_t triangle : 1; + + uint8_t l1 : 1; + uint8_t r1 : 1; + uint8_t l2 : 1; + uint8_t r2 : 1; + uint8_t share : 1; + uint8_t menu : 1; + uint8_t l3 : 1; + uint8_t r3 : 1; + + uint8_t ps : 1; + uint8_t touchpad : 1; + uint8_t mic : 1; + uint8_t dummy : 5; + } __attribute__((packed)); + uint32_t val : 24; +} __attribute__((packed)); + +struct ps5TouchpadXY { + struct { + uint8_t counter : 7; // Increments every time a finger is touching the touchpad + uint8_t touching : 1; // The top bit is cleared if the finger is touching the touchpad + uint16_t x : 12; + uint16_t y : 12; + } __attribute__((packed)) finger[2]; // 0 = first finger, 1 = second finger +} __attribute__((packed)); + +union PS5Status { + struct { + // first byte + uint8_t headphone : 1; + uint8_t dummy : 2; // Seems to change when a jack is plugged in. First bit stays on when a mic is plugged in. + uint8_t usb : 1; // charging + uint8_t dummy2: 4; + + // second byte + uint8_t mic : 1; + uint8_t dummy4 : 3; + } __attribute__((packed)); + uint16_t val; +} __attribute__((packed)); + +struct PS5Data { + /* Button and joystick values */ + uint8_t hatValue[4]; // 0-3 bytes + uint8_t trigger[2]; // 4-5 + + uint8_t dummy; // 6 unknown + + PS5Buttons btn; // 7-9 + + uint8_t dummy2[5]; // 0xA-0xD unknown + + /* Gyro and accelerometer values */ + int16_t gyroX, gyroZ, gyroY; // 0x0F - 0x14 + int16_t accX, accZ, accY; // 0x15-0x1A + + uint8_t dummy3[5]; // 0x1B - 0x1F unknown + + // 0x20 - 0x23 touchpad point 1 + // 0x24 - 0x27 touchpad point 2 + ps5TouchpadXY xy; + + uint8_t dummy4; //0x28 unknown + + uint8_t rightTriggerFeedback; // 0x29 + uint8_t leftTriggerFeedback; // 0x2A + + uint8_t dummy5[10]; // 0x2B - 0x34 unknown + + // status bytes 0x35-0x36 + PS5Status status; +} __attribute__((packed)); + +struct PS5Output { + uint8_t bigRumble, smallRumble; // Rumble + uint8_t microphoneLed; + uint8_t disableLeds; + uint8_t playerLeds; + uint8_t r, g, b; // RGB for lightbar + bool reportChanged; // The data is send when data is received from the controller +} __attribute__((packed)); + +/** This class parses all the data sent by the PS5 controller */ +class PS5Parser { +public: + PS5Trigger leftTrigger; + PS5Trigger rightTrigger; + + /** Constructor for the PS5Parser class. */ + PS5Parser() : leftTrigger(), rightTrigger() { + Reset(); + }; + + /** @name PS5 Controller functions */ + /** + * getButtonPress(ButtonEnum b) will return true as long as the button is held down. + * + * While getButtonClick(ButtonEnum b) will only return it once. + * + * So you instance if you need to increase a variable once you would use getButtonClick(ButtonEnum b), + * but if you need to drive a robot forward you would use getButtonPress(ButtonEnum b). + * @param b ::ButtonEnum to read. + * @return getButtonPress(ButtonEnum b) will return a true as long as a button is held down, while getButtonClick(ButtonEnum b) will return true once for each button press. + */ + bool getButtonPress(ButtonEnum b); + bool getButtonClick(ButtonEnum b); + /**@}*/ + /** @name PS5 Controller functions */ + /** + * Used to get the analog value from button presses. + * @param b The ::ButtonEnum to read. + * The supported buttons are: + * ::L2 and ::R2. + * @return Analog value in the range of 0-255. + */ + uint8_t getAnalogButton(ButtonEnum b); + + /** + * Used to read the analog joystick. + * @param a ::LeftHatX, ::LeftHatY, ::RightHatX, and ::RightHatY. + * @return Return the analog value in the range of 0-255. + */ + uint8_t getAnalogHat(AnalogHatEnum a); + + /** + * Get the x-coordinate of the touchpad. Position 0 is in the top left. + * @param finger 0 = first finger, 1 = second finger. If omitted, then 0 will be used. + */ + uint16_t getX(uint8_t finger = 0) { + return ps5Data.xy.finger[finger].x; + }; + + /** + * Get the y-coordinate of the touchpad. Position 0 is in the top left. + * @param finger 0 = first finger, 1 = second finger. If omitted, then 0 will be used. + * @return Returns the y-coordinate of the finger. + */ + uint16_t getY(uint8_t finger = 0) { + return ps5Data.xy.finger[finger].y; + }; + + /** + * Returns whenever the user is toucing the touchpad. + * @param finger 0 = first finger, 1 = second finger. If omitted, then 0 will be used. + * @return Returns true if the specific finger is touching the touchpad. + */ + bool isTouching(uint8_t finger = 0) { + return !(ps5Data.xy.finger[finger].touching); // The bit is cleared when a finger is touching the touchpad + }; + + /** + * This counter increments every time a finger touches the touchpad. + * @param finger 0 = first finger, 1 = second finger. If omitted, then 0 will be used. + * @return Return the value of the counter, note that it is only a 7-bit value. + */ + uint8_t getTouchCounter(uint8_t finger = 00) { + return ps5Data.xy.finger[finger].counter; + }; + + /** + * Get the angle of the controller calculated using the accelerometer. + * @param a Either ::Pitch or ::Roll. + * @return Return the angle in the range of 0-360. + */ + float getAngle(AngleEnum a) { + if (a == Pitch) + return (atan2f(ps5Data.accY, ps5Data.accZ) + PI) * RAD_TO_DEG; + else + return (atan2f(ps5Data.accX, ps5Data.accZ) + PI) * RAD_TO_DEG; + }; + + /** + * Used to get the raw values from the 3-axis gyroscope and 3-axis accelerometer inside the PS5 controller. + * @param s The sensor to read. + * @return Returns the raw sensor reading. + */ + int16_t getSensor(SensorEnum s) { + switch(s) { + case gX: + return ps5Data.gyroX; + case gY: + return ps5Data.gyroY; + case gZ: + return ps5Data.gyroZ; + case aX: + return ps5Data.accX; + case aY: + return ps5Data.accY; + case aZ: + return ps5Data.accZ; + default: + return 0; + } + }; + + /** + * Return the battery level of the PS5 controller. + * @return The battery level in the range 0-15. + */ + /*uint8_t getBatteryLevel() { + return ps5Data.status.battery; + };*/ + + /** + * Use this to check if an USB cable is connected to the PS5 controller. + * @return Returns true if an USB cable is connected. + */ + bool getUsbStatus() { + return ps5Data.status.usb; + }; + + /** + * Use this to check if an audio jack cable is connected to the PS5 controller. + * @return Returns true if an audio jack cable is connected. + */ + bool getAudioStatus() { + return ps5Data.status.headphone; + }; + + /** + * Use this to check if a microphone is connected to the PS5 controller. + * @return Returns true if a microphone is connected. + */ + bool getMicStatus() { + return ps5Data.status.mic; + }; + + /** Turn both rumble and the LEDs off. */ + void setAllOff() { + setRumbleOff(); + setLedOff(); + }; + + /** Set rumble off. */ + void setRumbleOff() { + setRumbleOn(0, 0); + }; + + /** + * Turn on rumble. + * @param mode Either ::RumbleHigh or ::RumbleLow. + */ + void setRumbleOn(RumbleEnum mode) { + if (mode == RumbleLow) + setRumbleOn(0x00, 0xFF); + else + setRumbleOn(0xFF, 0x00); + }; + + /** + * Turn on rumble. + * @param bigRumble Value for big motor. + * @param smallRumble Value for small motor. + */ + void setRumbleOn(uint8_t bigRumble, uint8_t smallRumble) { + ps5Output.bigRumble = bigRumble; + ps5Output.smallRumble = smallRumble; + ps5Output.reportChanged = true; + }; + + /** Turn all LEDs off. */ + void setLedOff() { + setLed(0, 0, 0); + }; + + /** + * Use this to set the color using RGB values. + * @param r,g,b RGB value. + */ + void setLed(uint8_t r, uint8_t g, uint8_t b) { + ps5Output.r = r; + ps5Output.g = g; + ps5Output.b = b; + ps5Output.reportChanged = true; + }; + + /** + * Use this to set the color using the predefined colors in ::ColorsEnum. + * @param color The desired color. + */ + void setLed(ColorsEnum color) { + setLed((uint8_t)(color >> 16), (uint8_t)(color >> 8), (uint8_t)(color)); + }; + + /** Turn all player LEDs off. */ + void setPlayerLedOff() { + setPlayerLed(0); + } + + /** + * Use this to set five player LEDs. + * @param mask Bit mask to set the five player LEDs. The first 5 bits represent a LED each. + */ + void setPlayerLed(uint8_t mask) { + ps5Output.playerLeds = mask; + } + + /** Use to turn the microphone LED off. */ + void setMicLedOff() { + setMicLed(0); + } + + /** + * Use this to turn the microphone LED on/off. + * @param on Turn the microphone LED on/off. + */ + void setMicLed(bool on) { + ps5Output.microphoneLed = on ? 1 : 0; + } + + /** Get the incoming message count. */ + uint16_t getMessageCounter(){ + return message_counter; + } + +protected: + /** + * Used to parse data sent from the PS5 controller. + * @param len Length of the data. + * @param buf Pointer to the data buffer. + */ + void Parse(uint8_t len, uint8_t *buf); + + /** Used to reset the different buffers to their default values */ + void Reset(); + + /** + * Send the output to the PS5 controller. This is implemented in PS5BT.h and PS5USB.h. + * @param output Pointer to PS5Output buffer; + */ + virtual void sendOutputReport(PS5Output *output) = 0; + + +private: + bool checkDpad(ButtonEnum b); // Used to check PS5 DPAD buttons + + PS5Data ps5Data; + PS5Buttons oldButtonState, buttonClickState; + PS5Output ps5Output; + uint8_t oldDpad; + uint16_t message_counter = 0; +}; +#endif diff --git a/PS5Trigger.cpp b/PS5Trigger.cpp new file mode 100644 index 00000000..534158ab --- /dev/null +++ b/PS5Trigger.cpp @@ -0,0 +1,94 @@ +/** + * @file PS5Trigger.cpp + * @author Ludwig Füchsl, adapted for USB_Host_Library SAMD by Joseph Duchesne + * @brief Based on Ludwig Füchsl's DualSense Windows driver https://github.com/Ohjurot/DualSense-Windows + * @date 2020-11-25 + * + * @copyright Copyright (c) 2020 Ludwig Füchsl + * + * MIT License + * + * 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, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * 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. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * Minor updates by Kristian Sloth Lauszus. + */ + +#include "PS5Trigger.h" + +void PS5Trigger::processTrigger(uint8_t* buffer) { + // Switch on effect + switch (data.effectType) { + // Continious + case EffectType::ContinuousResitance: + // Mode + buffer[0x00] = 0x01; + // Parameters + buffer[0x01] = data.Continuous.startPosition; + buffer[0x02] = data.Continuous.force; + + break; + + // Section + case EffectType::SectionResitance: + // Mode + buffer[0x00] = 0x02; + // Parameters + buffer[0x01] = data.Section.startPosition; + buffer[0x02] = data.Section.endPosition; + + break; + + // EffectEx + case EffectType::EffectEx: + // Mode + buffer[0x00] = 0x02 | 0x20 | 0x04; + // Parameters + buffer[0x01] = 0xFF - data.EffectEx.startPosition; + // Keep flag + if (data.EffectEx.keepEffect) + buffer[0x02] = 0x02; + // Forces + buffer[0x04] = data.EffectEx.beginForce; + buffer[0x05] = data.EffectEx.middleForce; + buffer[0x06] = data.EffectEx.endForce; + // Frequency + buffer[0x09] = data.EffectEx.frequency / 2; + if(buffer[0x09] < 1) buffer[0x09] = 1; // minimum frequency + + break; + + // Calibrate + case EffectType::Calibrate: + // Mode + buffer[0x00] = 0xFC; + + break; + + // No resistance / default + case EffectType::NoResitance: + default: + // All zero + buffer[0x00] = 0x00; + buffer[0x01] = 0x00; + buffer[0x02] = 0x00; + + break; + } + reportChanged = false; +} diff --git a/PS5Trigger.h b/PS5Trigger.h new file mode 100644 index 00000000..6c1fc024 --- /dev/null +++ b/PS5Trigger.h @@ -0,0 +1,168 @@ +/** + * @file PS5Trigger.h + * @author Ludwig Füchsl, adapted for USB_Host_Library SAMD by Joseph Duchesne + * @brief Based on Ludwig Füchsl's DualSense Windows driver https://github.com/Ohjurot/DualSense-Windows + * @version 0.1 + * @date 2020-11-25 + * + * @copyright Copyright (c) 2020 Ludwig Füchsl + * + * MIT License + * + * 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, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * 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. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * Minor updates by Kristian Sloth Lauszus. + */ + +#ifndef _ps5trigger_h_ +#define _ps5trigger_h_ + +#include + +class PS5Trigger { +private: + // Type of trigger effect + typedef enum _EffectType : uint8_t { + NoResitance = 0x00, // No resistance is applied + ContinuousResitance = 0x01, // Continuous Resitance is applied + SectionResitance = 0x02, // Seciton resistance is appleyed + EffectEx = 0x26, // Extended trigger effect + Calibrate = 0xFC, // Calibrate triggers + } EffectType; + + // Trigger effect + typedef struct _EffectData { + // Trigger effect type + EffectType effectType; + + // Union for effect parameters + union { + // Union one raw data + uint8_t _u1_raw[6]; + + // For type == ContinuousResitance + struct { + uint8_t startPosition; // Start position of resistance + uint8_t force; // Force of resistance + uint8_t _pad[4]; // PAD / UNUSED + } __attribute__((packed)) Continuous; + + // For type == SectionResitance + struct { + uint8_t startPosition; // Start position of resistance + uint8_t endPosition; // End position of resistance (>= start) + uint8_t _pad[4]; // PAD / UNUSED + } __attribute__((packed)) Section; + + // For type == EffectEx + struct { + uint8_t startPosition; // Position at witch the effect starts + bool keepEffect; // Wher the effect should keep playing when trigger goes beyond 255 + uint8_t beginForce; // Force applied when trigger >= (255 / 2) + uint8_t middleForce; // Force applied when trigger <= (255 / 2) + uint8_t endForce; // Force applied when trigger is beyond 255 + uint8_t frequency; // Vibration frequency of the trigger + } __attribute__((packed)) EffectEx; + } __attribute__((packed)); + } __attribute__((packed)) EffectData; + + EffectData data; + +public: + bool reportChanged = false; + + /** + * @brief Apply the trigger data to a PS5 update buffer + * + * @param buffer The buffer at the start offset for this trigger data + */ + void processTrigger(uint8_t* buffer); + + /** + * Clear force feedback on trigger without report changed + */ + void Reset() { + data.effectType = EffectType::NoResitance; + + reportChanged = false; + }; + + /** + * Clear force feedback on trigger + */ + void clearTriggerForce() { + data.effectType = EffectType::NoResitance; + + reportChanged = true; + }; + + /** + * Set continuous force feedback on trigger + * @param start 0-255 trigger pull to start resisting + * @param force The force amount + */ + void setTriggerForce(uint8_t start, uint8_t force) { + if (force == 0) + data.effectType = EffectType::NoResitance; + else { + data.effectType = EffectType::ContinuousResitance; + data.Continuous.startPosition = start; + data.Continuous.force = force; + } + + reportChanged = true; + }; + + /** + * Set section force feedback on trigger + * @param start trigger pull to start resisting + * @param end trigger pull to stop resisting + */ + void setTriggerForceSection(uint8_t start, uint8_t end) { + data.effectType = EffectType::SectionResitance; + data.Section.startPosition = start; + data.Section.endPosition = end; + + reportChanged = true; + }; + + /** + * Set effect force feedback on trigger + * @param start trigger pull to start resisting + * @param keep Keep effect active after max trigger pull + * @param begin_force 0-255 force at start position + * @param mid_force 0-255 force half way between start and max pull + * @param end_force 0-255 force at max pull + * @param frequency Vibration frequency of the trigger + */ + void setTriggerForceEffect(uint8_t start, bool keep, uint8_t begin_force, uint8_t mid_force, uint8_t end_force, uint8_t frequency) { + data.effectType = EffectType::SectionResitance; + data.EffectEx.startPosition = start; + data.EffectEx.keepEffect = keep; + data.EffectEx.beginForce = begin_force; + data.EffectEx.middleForce = mid_force; + data.EffectEx.endForce = end_force; + data.EffectEx.frequency = frequency; + + reportChanged = true; + }; + +}; + +#endif diff --git a/PS5USB.h b/PS5USB.h new file mode 100644 index 00000000..a7ce9dfc --- /dev/null +++ b/PS5USB.h @@ -0,0 +1,154 @@ +/* Copyright (C) 2021 Kristian Sloth Lauszus. All rights reserved. + + This software may be distributed and modified under the terms of the GNU + General Public License version 2 (GPL2) as published by the Free Software + Foundation and appearing in the file GPL2.TXT included in the packaging of + this file. Please note that GPL2 Section 2[b] requires that all works based + on this software must also be made publicly available under the terms of + the GPL2 ("Copyleft"). + + Contact information + ------------------- + + Kristian Sloth Lauszus + Web : https://lauszus.com + e-mail : lauszus@gmail.com + + Thanks to Joseph Duchesne for the initial port. Data structure mapping partially based + on values from Ludwig Füchsl's https://github.com/Ohjurot/DualSense-Windows + */ + +#ifndef _ps5usb_h_ +#define _ps5usb_h_ + +#include "hiduniversal.h" +#include "PS5Parser.h" + +#define PS5_VID 0x054C // Sony Corporation +#define PS5_PID 0x0CE6 // PS5 Controller + +/** + * This class implements support for the PS5 controller via USB. + * It uses the HIDUniversal class for all the USB communication. + */ +class PS5USB : public HIDUniversal, public PS5Parser { +public: + /** + * Constructor for the PS5USB class. + * @param p Pointer to the USB class instance. + */ + PS5USB(USB *p) : + HIDUniversal(p) { + PS5Parser::Reset(); + }; + + /** + * Used to check if a PS5 controller is connected. + * @return Returns true if it is connected. + */ + bool connected() { + return HIDUniversal::isReady() && HIDUniversal::VID == PS5_VID && HIDUniversal::PID == PS5_PID; + }; + + /** + * Used to call your own function when the device is successfully initialized. + * @param funcOnInit Function to call. + */ + void attachOnInit(void (*funcOnInit)(void)) { + pFuncOnInit = funcOnInit; + }; + +protected: + /** @name HIDUniversal implementation */ + /** + * Used to parse USB HID data. + * @param hid Pointer to the HID class. + * @param is_rpt_id Only used for Hubs. + * @param len The length of the incoming data. + * @param buf Pointer to the data buffer. + */ + virtual void ParseHIDData(USBHID *hid __attribute__((unused)), bool is_rpt_id __attribute__((unused)), uint8_t len, uint8_t *buf) { + if (HIDUniversal::VID == PS5_VID && HIDUniversal::PID == PS5_PID) + PS5Parser::Parse(len, buf); + }; + + /** + * Called when a device is successfully initialized. + * Use attachOnInit(void (*funcOnInit)(void)) to call your own function. + * This is useful for instance if you want to set the LEDs in a specific way. + */ + virtual uint8_t OnInitSuccessful() { + if (HIDUniversal::VID == PS5_VID && HIDUniversal::PID == PS5_PID) { + PS5Parser::Reset(); + if (pFuncOnInit) + pFuncOnInit(); // Call the user function + else + setLed(Blue); + }; + return 0; + }; + /**@}*/ + + /** @name PS5Parser implementation */ + virtual void sendOutputReport(PS5Output *output) { // Source: https://github.com/chrippa/ds4drv + // PS4 Source: https://github.com/chrippa/ds4drv + // PS5 values from https://www.reddit.com/r/gamedev/comments/jumvi5/dualsense_haptics_leds_and_more_hid_output_report/ + // and Ludwig Füchsl's https://github.com/Ohjurot/DualSense-Windows + uint8_t buf[48]; + memset(buf, 0, sizeof(buf)); + + buf[0x00] = 0x02; // report type + buf[0x01] = 0xFF; // feature flags 1 + buf[0x02]= 0xF7; // feature flags 2 + buf[0x03] = output->smallRumble; // Small Rumble + buf[0x04] = output->bigRumble; // Big rumble + + // 5-7 headphone, speaker, mic volume, audio flags + + buf[0x09] = (uint8_t)output->microphoneLed; + + // 0x0A mute flags + + // Adaptive Triggers: 0x0B-0x14 right, 0x15 unknown, 0x16-0x1F left + rightTrigger.processTrigger(&buf[0x0B]); // right + leftTrigger.processTrigger(&buf[0x16]); // left + + // 0x20-0x24 unknown + // 0x25 trigger motor effect strengths + // 0x26 speaker volume + + // player LEDs + buf[0x27] = 0x03; // led brightness, pulse + buf[0x2A] = output->disableLeds ? 0x01 : 0x2; // led pulse option + // buf[0x2B] LED brightness, 0 = full, 1= medium, 2 = low + buf[0x2C] = output->playerLeds; // 5 white player LEDs + + // lightbar + buf[0x2D] = output->r; // Red + buf[0x2E] = output->g; // Green + buf[0x2F] = output->b; // Blue + + output->reportChanged = false; + + // The PS5 console actually set the four last bytes to a CRC32 checksum, but it seems like it is actually not needed + + pUsb->outTransfer(bAddress, epInfo[ hidInterfaces[0].epIndex[epInterruptOutIndex] ].epAddr, sizeof(buf), buf); + }; + /**@}*/ + + /** @name USBDeviceConfig implementation */ + /** + * Used by the USB core to check what this driver support. + * @param vid The device's VID. + * @param pid The device's PID. + * @return Returns true if the device's VID and PID matches this driver. + */ + virtual bool VIDPIDOK(uint16_t vid, uint16_t pid) { + return (vid == PS5_VID && pid == PS5_PID); + }; + /**@}*/ + +private: + void (*pFuncOnInit)(void); // Pointer to function called in onInit() +}; +#endif diff --git a/controllerEnums.h b/controllerEnums.h index c1a5a497..66963ee4 100644 --- a/controllerEnums.h +++ b/controllerEnums.h @@ -150,6 +150,7 @@ enum ButtonEnum { MENU = 5, /**@}*/ + /**@{*/ /** PS Buzz controllers */ RED = 0, YELLOW = 1, @@ -157,6 +158,11 @@ enum ButtonEnum { ORANGE = 3, BLUE = 4, /**@}*/ + + /**@{*/ + /** PS5 buttons */ + MICROPHONE = 18, + /**@}*/ }; /** Joysticks on the PS3 and Xbox controllers. */ diff --git a/examples/PS5USB/PS5USB.ino b/examples/PS5USB/PS5USB.ino new file mode 100644 index 00000000..3f008fc8 --- /dev/null +++ b/examples/PS5USB/PS5USB.ino @@ -0,0 +1,146 @@ +/* + Example sketch for the PS5 USB library - developed by Kristian Sloth Lauszus + For more information visit the Github repository: github.com/felis/USB_Host_Shield_2.0 or + send me an e-mail: lauszus@gmail.com + */ + +#include + +// Satisfy the IDE, which needs to see the include statment in the ino too. +#ifdef dobogusinclude +#include +#endif +#include + +USB Usb; +PS5USB PS5(&Usb); + +bool printAngle, printTouch; +uint16_t lastMessageCounter = -1; +uint8_t player_led_mask = 0; +bool microphone_led = false; + +void setup() { + Serial.begin(115200); +#if !defined(__MIPSEL__) + while (!Serial); // Wait for serial port to connect - used on Leonardo, Teensy and other boards with built-in USB CDC serial connection +#endif + if (Usb.Init() == -1) { + Serial.print(F("\r\nOSC did not start")); + while (1); // Halt + } + Serial.print(F("\r\nPS5 USB Library Started")); +} + +void loop() { + Usb.Task(); + + if (PS5.connected() && lastMessageCounter != PS5.getMessageCounter()) { + lastMessageCounter = PS5.getMessageCounter(); + + if (PS5.getAnalogHat(LeftHatX) > 137 || PS5.getAnalogHat(LeftHatX) < 117 || PS5.getAnalogHat(LeftHatY) > 137 || PS5.getAnalogHat(LeftHatY) < 117 || PS5.getAnalogHat(RightHatX) > 137 || PS5.getAnalogHat(RightHatX) < 117 || PS5.getAnalogHat(RightHatY) > 137 || PS5.getAnalogHat(RightHatY) < 117) { + Serial.print(F("\r\nLeftHatX: ")); + Serial.print(PS5.getAnalogHat(LeftHatX)); + Serial.print(F("\tLeftHatY: ")); + Serial.print(PS5.getAnalogHat(LeftHatY)); + Serial.print(F("\tRightHatX: ")); + Serial.print(PS5.getAnalogHat(RightHatX)); + Serial.print(F("\tRightHatY: ")); + Serial.print(PS5.getAnalogHat(RightHatY)); + } + + if (PS5.getAnalogButton(L2) || PS5.getAnalogButton(R2)) { // These are the only analog buttons on the PS5 controller + Serial.print(F("\r\nL2: ")); + Serial.print(PS5.getAnalogButton(L2)); + Serial.print(F("\tR2: ")); + Serial.print(PS5.getAnalogButton(R2)); + } + + // Set the left trigger to resist at the right trigger's level + PS5.leftTrigger.setTriggerForce(PS5.getAnalogButton(R2), 255); + + if (PS5.getButtonClick(PS)) + Serial.print(F("\r\nPS")); + if (PS5.getButtonClick(TRIANGLE)) { + Serial.print(F("\r\nTriangle")); + PS5.setRumbleOn(RumbleLow); + } + if (PS5.getButtonClick(CIRCLE)) { + Serial.print(F("\r\nCircle")); + PS5.setRumbleOn(RumbleHigh); + } + if (PS5.getButtonClick(CROSS)) { + Serial.print(F("\r\nCross")); + + // Set the player LEDs + player_led_mask = (player_led_mask << 1) | 1; + if (player_led_mask > 0x1F) + player_led_mask = 0; + PS5.setPlayerLed(player_led_mask); // The bottom 5 bits set player LEDs + } + if (PS5.getButtonClick(SQUARE)) { + Serial.print(F("\r\nSquare")); + PS5.setRumbleOff(); + } + + if (PS5.getButtonClick(UP)) { + Serial.print(F("\r\nUp")); + PS5.setLed(Red); + } if (PS5.getButtonClick(RIGHT)) { + Serial.print(F("\r\nRight")); + PS5.setLed(Blue); + } if (PS5.getButtonClick(DOWN)) { + Serial.print(F("\r\nDown")); + PS5.setLed(Yellow); + } if (PS5.getButtonClick(LEFT)) { + Serial.print(F("\r\nLeft")); + PS5.setLed(Green); + } + + if (PS5.getButtonClick(L1)) + Serial.print(F("\r\nL1")); + if (PS5.getButtonClick(L3)) + Serial.print(F("\r\nL3")); + if (PS5.getButtonClick(R1)) + Serial.print(F("\r\nR1")); + if (PS5.getButtonClick(R3)) + Serial.print(F("\r\nR3")); + + if (PS5.getButtonClick(SHARE)) + Serial.print(F("\r\nShare")); + if (PS5.getButtonClick(OPTIONS)) { + Serial.print(F("\r\nOptions")); + printAngle = !printAngle; + } + if (PS5.getButtonClick(TOUCHPAD)) { + Serial.print(F("\r\nTouchpad")); + printTouch = !printTouch; + } + if (PS5.getButtonClick(MICROPHONE)) { + Serial.print(F("\r\nMicrophone")); + microphone_led = !microphone_led; + PS5.setMicLed(microphone_led); + } + + if (printAngle) { // Print angle calculated using the accelerometer only + Serial.print(F("\r\nPitch: ")); + Serial.print(PS5.getAngle(Pitch)); + Serial.print(F("\tRoll: ")); + Serial.print(PS5.getAngle(Roll)); + } + + if (printTouch) { // Print the x, y coordinates of the touchpad + if (PS5.isTouching(0) || PS5.isTouching(1)) // Print newline and carriage return if any of the fingers are touching the touchpad + Serial.print(F("\r\n")); + for (uint8_t i = 0; i < 2; i++) { // The touchpad track two fingers + if (PS5.isTouching(i)) { // Print the position of the finger if it is touching the touchpad + Serial.print(F("X")); Serial.print(i + 1); Serial.print(F(": ")); + Serial.print(PS5.getX(i)); + Serial.print(F("\tY")); Serial.print(i + 1); Serial.print(F(": ")); + Serial.print(PS5.getY(i)); + Serial.print(F("\t")); + } + } + } + } +} diff --git a/keywords.txt b/keywords.txt index 2513e98e..0f3ca523 100644 --- a/keywords.txt +++ b/keywords.txt @@ -135,6 +135,8 @@ SHARE LITERAL1 OPTIONS LITERAL1 TOUCHPAD LITERAL1 +MICROPHONE LITERAL1 + LeftHatX LITERAL1 LeftHatY LITERAL1 RightHatX LITERAL1