diff --git a/.github/scripts/get-pr-details.sh b/.github/scripts/get-pr-details.sh index 339ac25..feb2233 100644 --- a/.github/scripts/get-pr-details.sh +++ b/.github/scripts/get-pr-details.sh @@ -1,35 +1,16 @@ - #!/usr/bin/env bash - set -euo pipefail -echo "Retrieving PR information..." - -if [ -z "${GITHUB_TOKEN:-}" ]; then - echo "GITHUB_TOKEN not set" - exit 1 -fi - -OWNER="${GITHUB_REPOSITORY%%/*}" -REPO="${GITHUB_REPOSITORY##*/}" -SHA="${GITHUB_SHA}" - -PRS_JSON=$(curl -s \ - -H "Authorization: Bearer $GITHUB_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - "https://api.github.com/repos/$OWNER/$REPO/commits/$SHA/pulls") - -PR_COUNT=$(echo "$PRS_JSON" | jq length) - -if [ "$PR_COUNT" -eq 0 ]; then - echo "No PR associated with this commit." +if [ -z "${PR_NUMBER:-}" ]; then + echo "No PR number provided, skipping release notes." echo "title=" >> "$GITHUB_OUTPUT" echo "body=" >> "$GITHUB_OUTPUT" exit 0 fi -TITLE=$(echo "$PRS_JSON" | jq -r '.[0].title // ""') -BODY=$(echo "$PRS_JSON" | jq -r '.[0].body // ""') +PR_JSON=$(gh pr view "$PR_NUMBER" --json title,body) +TITLE=$(echo "$PR_JSON" | jq -r '.title // ""') +BODY=$(echo "$PR_JSON" | jq -r '.body // ""') { echo "title<> "$GITHUB_OUTPUT" -echo "PR details captured." -echo "- Title = $TITLE." -echo "- Description = $BODY." \ No newline at end of file +echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT" \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9aed8c4..b3bd1e1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -34,7 +34,7 @@ jobs: run: pip install --upgrade platformio - name: Build LumenLab - run: pio run + run: pio run -e release - name: Run tests - run: pio test --environment native + run: pio test -e native diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 2661443..df51570 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -7,6 +7,7 @@ on: permissions: contents: write + pull-requests: write concurrency: group: release-main @@ -22,13 +23,51 @@ jobs: with: fetch-depth: 0 - - name: Cache pip and PlatformIO + - name: Determine PR number + id: pr + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_NUMBER=$(gh pr list \ + --state merged \ + --base main \ + --json number,mergeCommit \ + --jq '.[] | select(.mergeCommit.oid=="'"$GITHUB_SHA"'") | .number') + if [[ -z "$PR_NUMBER" ]]; then + echo "No PR associated with this commit; skipping release." + echo "run_release=false" >> $GITHUB_OUTPUT + else + echo "Found PR #$PR_NUMBER" + echo "run_release=true" >> $GITHUB_OUTPUT + echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + fi + + - name: Determine next version id from PR labels + id: semver + run: | + PR_NUMBER=${{ steps.pr.outputs.pr_number }} + export PR_NUMBER + bash .github/scripts/compute-semver.sh + echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Get release description notes from PR + id: pr_details + run: bash .github/scripts/get-pr-details.sh + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ steps.semver.outputs.pr_number }} + + - name: Cache PlatformIO & Arduino CLI uses: actions/cache@v4 with: path: | ~/.cache/pip ~/.platformio/.cache - key: ${{ runner.os }}-pio-${{ hashFiles('**/platformio.ini') }} + ~/.arduino15 + key: ${{ runner.os }}-build-tools-${{ hashFiles('**/platformio.ini') }} - name: Setup Python uses: actions/setup-python@v5 @@ -38,26 +77,65 @@ jobs: - name: Install PlatformIO run: pip install --upgrade platformio - - name: Determine next SemVer from PR labels - id: semver - run: bash .github/scripts/compute-semver.sh - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Get release description notes from PR - id: pr_details - run: bash .github/scripts/get-pr-details.sh - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Run tests run: pio test --environment native - - name: Build release + - name: Build PS3 Firmware (PlatformIO) run: pio run -e release env: LUMENLAB_VERSION: ${{ steps.semver.outputs.tag }} + - name: Install Arduino CLI + uses: arduino/setup-arduino-cli@v1 + + - name: Setup Arduino CLI config + run: | + cat > arduino-cli.yaml < lumenlab.ino + arduino-cli compile --fqbn esp32-bluepad32:esp32:uPesy_wroom \ + --build-property compiler.cpp.extra_flags="-Iinclude -Isrc -Os -DRELEASE -std=gnu++2a -DLUMENLAB_VERSION=\"${LUMENLAB_VERSION}\"" \ + --clean --output-dir build + env: + LUMENLAB_VERSION: ${{ steps.semver.outputs.tag }} + + - name: Package PS3 Firmware ZIP + run: | + cp .pio/build/release/firmware.bin ./build/ps3-firmware.bin + cp .pio/build/release/bootloader.bin ./build/ps3-bootloader.bin + cp .pio/build/release/partitions.bin ./build/ps3-partitions.bin + zip -j ./build/ps3-firmware.zip \ + ./build/ps3-firmware.bin \ + ./build/ps3-bootloader.bin \ + ./build/ps3-partitions.bin + + + - name: Package PS4 Firmware ZIP + working-directory: build + run: | + mv ./lumenlab.ino.bin ./ps4-firmware.bin + mv ./lumenlab.ino.bootloader.bin ./ps4-bootloader.bin + mv ./lumenlab.ino.partitions.bin ./ps4-partitions.bin + zip -j ./ps4-firmware.zip \ + ./ps4-firmware.bin \ + ./ps4-bootloader.bin \ + ./ps4-partitions.bin + - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: @@ -65,8 +143,7 @@ jobs: name: ${{ steps.semver.outputs.tag }} - ${{ steps.pr_details.outputs.title }} body: ${{ steps.pr_details.outputs.body }} files: | - .pio/build/release/firmware.bin - .pio/build/release/bootloader.bin - .pio/build/release/partitions.bin + ./build/ps3-firmware.zip + ./build/ps4-firmware.zip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 6d77523..16bbdbf 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ tools/.venv activate-virtual-env.* __pycache__ +lumenlab.ino +build *.drl *.gko diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 3711b1d..bc1d53c 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -39,7 +39,7 @@ } }, { - "label": "LumenLab - Upload (Virtualization)", + "label": "LumenLab - Upload (PS3 Virtualization)", "type": "shell", "command": "platformio", "args": [ @@ -63,6 +63,24 @@ "panel": "shared" } }, + { + "label": "LumenLab - Upload (PS4 Virtualization)", + "type": "shell", + "command": "echo \"\" > lumenlab.ino && export LUMENLAB_VERSION='v0.0.0' && echo $LUMENLAB_VERSION && arduino-cli compile --fqbn esp32-bluepad32:esp32:uPesy_wroom --build-property compiler.cpp.extra_flags=\"-Iinclude -Isrc -Os -DVIRTUALIZATION -std=gnu++2a -DLUMENLAB_VERSION=\\\"${LUMENLAB_VERSION}\\\"\" --output-dir build && ../lumenlab-installer/Firmware/esptool.exe --chip esp32 --baud 921600 write_flash -z 0x1000 build/lumenlab.ino.bootloader.bin 0x8000 build/lumenlab.ino.partitions.bin 0x10000 build/lumenlab.ino.bin", + "problemMatcher": [ + "$platformio" + ], + "options": { + "statusbar": { + "hide": true + } + }, + "presentation": { + "reveal": "always", + "focus": true, + "panel": "shared" + } + }, { "label": "PlatformIO: Upload", "type": "shell", @@ -179,6 +197,42 @@ "focus": true, "panel": "shared" } - } + }, + { + "label": "Upload and Start Virtualization (PS3)", + "dependsOrder": "sequence", + "dependsOn": [ + "LumenLab - Upload (PS3 Virtualization)", + "Start Virtualization" + ], + "options": { + "statusbar": { + "hide": true + } + }, + "presentation": { + "reveal": "always", + "focus": true, + "panel": "shared" + } + }, + { + "label": "Upload and Start Virtualization (PS4)", + "dependsOrder": "sequence", + "dependsOn": [ + "LumenLab - Upload (PS4 Virtualization)", + "Start Virtualization" + ], + "options": { + "statusbar": { + "hide": true + } + }, + "presentation": { + "reveal": "always", + "focus": true, + "panel": "shared" + } + }, ] } \ No newline at end of file diff --git a/include/core/configuration.h b/include/core/configuration.h index ba283ff..acd4d00 100644 --- a/include/core/configuration.h +++ b/include/core/configuration.h @@ -10,16 +10,19 @@ namespace SystemCore { public: static const String &version() { return _version; } + static const String &psControllerType() { return _psControllerType; } static const String &macAddress() { return _macAddress; } static uint16_t numLeds() { return _numLeds; } static uint32_t serialBaud() { return _serialBaud; } static const std::array &recallBoundaries() { return _recallBoundaries; } + static constexpr uint8_t ledDimmerGpio = 34; // set from PCB design, should not be configurable static void load(::Preferences &memory); private: static String _version; + static String _psControllerType; static String _macAddress; static uint16_t _numLeds; static uint32_t _serialBaud; diff --git a/include/core/context-manager.h b/include/core/context-manager.h index 3098325..59cdbc7 100644 --- a/include/core/context-manager.h +++ b/include/core/context-manager.h @@ -4,7 +4,8 @@ #include "engine/layer.h" #include "engine/state-manager.h" -#include "player/controller.h" +#include "player/ps3-controller.h" +#include "player/ps4-controller.h" #include "lights/led-strip.h" #include "display/display.h" #include "display/menu-navigation.h" @@ -21,12 +22,17 @@ namespace SystemCore Engine::Layer *application = nullptr; Engine::StateManager stateManager; - Player::Controller controller; +#ifdef USE_PS3 + Player::Ps3Controller controller; +#else + Player::Ps4Controller controller; +#endif Lights::LedStrip leds; Display::OledDisplay display; Preferences memory; Display::MenuTileNavigation menuNav; + void initializeSystemMemory(); void navigateMainMenu(); void navigateGameMenu(); void navigateSceneMenu(); diff --git a/include/engine/state-manager.h b/include/engine/state-manager.h index 192846c..7728106 100644 --- a/include/engine/state-manager.h +++ b/include/engine/state-manager.h @@ -62,7 +62,8 @@ namespace Engine recallGameState{ctx}, systemState{SystemState::Initialize}, userMainMenuChoice{MainMenuSelection::Games}, - userGameChoice{GameSelection::Recall} + userGameChoice{GameSelection::Recall}, + userSceneChoice{SceneSelection::Canvas} { } diff --git a/include/games/phase-evasion/state.h b/include/games/phase-evasion/state.h index 4427cfe..dd2f687 100644 --- a/include/games/phase-evasion/state.h +++ b/include/games/phase-evasion/state.h @@ -32,7 +32,7 @@ namespace Games::PhaseEvasion void loadHighScore(); uint16_t calculateTotalScore() const; void checkHighScore(); - void updateHighScore(); + void updateHighScore(uint16_t newHighScore); private: SystemCore::ContextManager *contextManager; diff --git a/include/lights/led-buffer.h b/include/lights/led-buffer.h index aee169f..ec7b2ef 100644 --- a/include/lights/led-buffer.h +++ b/include/lights/led-buffer.h @@ -3,14 +3,15 @@ #include #include "core/configuration.h" +#include "lights/color.h" namespace Lights { class LedBuffer { public: - LedBuffer() : leds{new Color[SystemCore::Configuration::numLeds()]} {} - ~LedBuffer() { delete leds; } + LedBuffer() : leds{new Color[maxLEDs]} {} + ~LedBuffer() { delete[] leds; } LedBuffer(LedBuffer &&other) = delete; LedBuffer &operator=(LedBuffer &&other) = delete; LedBuffer(const LedBuffer &other) = delete; @@ -26,5 +27,6 @@ namespace Lights private: Color *leds; + static constexpr uint16_t maxLEDs = 600; }; } \ No newline at end of file diff --git a/include/player/controller-properties.h b/include/player/controller-properties.h new file mode 100644 index 0000000..3884fba --- /dev/null +++ b/include/player/controller-properties.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +namespace Player +{ + struct AnalogStick + { + int8_t x = 0; + int8_t y = 0; + }; + + enum class ControllerButton : uint8_t + { + Cross, + Circle, + Triangle, + Square, + Up, + Down, + Left, + Right, + L1, + L2, + L3, + R1, + R2, + R3, + Start, + Select, + Ps + }; +} \ No newline at end of file diff --git a/include/player/controller.h b/include/player/controller.h index a05e0c1..197ff33 100644 --- a/include/player/controller.h +++ b/include/player/controller.h @@ -1,89 +1,58 @@ #pragma once -#include +#include +#include + +#include "player/controller-properties.h" namespace Player { - - struct AnalogStick - { - int8_t x = 0; - int8_t y = 0; - }; - - enum class ControllerButton : uint8_t - { - Cross, - Circle, - Triangle, - Square, - Up, - Down, - Left, - Right, - L1, - L2, - L3, - R1, - R2, - R3, - Start, - Select, - Ps - }; - class Controller { public: - Controller() { instance = this; } - void begin(String macAddress); - - uint8_t cross() { return instance->controller.data.analog.button.cross; } - uint8_t circle() { return instance->controller.data.analog.button.circle; } - uint8_t triangle() { return instance->controller.data.analog.button.triangle; } - uint8_t square() { return instance->controller.data.analog.button.square; } - - uint8_t l1() { return instance->controller.data.analog.button.l1; } - uint8_t l2() { return instance->controller.data.analog.button.l2; } - uint8_t r1() { return instance->controller.data.analog.button.r1; } - uint8_t r2() { return instance->controller.data.analog.button.r2; } - - uint8_t up() { return instance->controller.data.analog.button.up; } - uint8_t down() { return instance->controller.data.analog.button.down; } - uint8_t left() { return instance->controller.data.analog.button.left; } - uint8_t right() { return instance->controller.data.analog.button.right; } - - uint8_t start() { return instance->controller.data.button.start; } - uint8_t ps() { return instance->controller.data.button.ps; } - - AnalogStick leftAnalog(); - AnalogStick rightAnalog(); - - const uint8_t rawButtonState(const ControllerButton button) const; - const bool wasPressed(const ControllerButton button) const; - const bool wasPressedAndReleased(const ControllerButton button) const; - const bool isConnected() const { return instance->connection; } - void poll(); - bool isDown(const ControllerButton button) const; + virtual ~Controller() = default; + virtual void begin(String macAddress) = 0; + + virtual uint8_t cross() = 0; + virtual uint8_t circle() = 0; + virtual uint8_t triangle() = 0; + virtual uint8_t square() = 0; + + virtual uint8_t l1() = 0; + virtual uint8_t l2() = 0; + virtual uint8_t r1() = 0; + virtual uint8_t r2() = 0; + + virtual uint8_t up() = 0; + virtual uint8_t down() = 0; + virtual uint8_t left() = 0; + virtual uint8_t right() = 0; + + virtual uint8_t start() = 0; + virtual uint8_t ps() = 0; + + virtual AnalogStick leftAnalog() = 0; + virtual AnalogStick rightAnalog() = 0; + + virtual const uint8_t rawButtonState(const ControllerButton button) const = 0; + virtual const bool wasPressed(const ControllerButton button) const = 0; + virtual const bool wasPressedAndReleased(const ControllerButton button) const = 0; + virtual const bool isConnected() const = 0; + virtual void poll() = 0; + virtual bool isDown(const ControllerButton button) const = 0; void reset(); - private: - Ps3Controller controller; + protected: uint32_t buttonDebouceEvent[17]; static constexpr uint32_t buttonDebounceThreshold = 30; bool buttonLastState[17] = {0}; bool buttonPressedEvent[17] = {0}; bool buttonReleasedEvent[17] = {0}; - static Controller *instance; bool connection = false; + uint32_t ignoreEventsUntil = 0; - void handleOnConnect(); + virtual void handleOnConnect() = 0; int filterDeadZone(int8_t value, int deadZone = 3); - static void onConnect() - { - if (instance) - instance->handleOnConnect(); - } }; } \ No newline at end of file diff --git a/include/player/ps3-controller.h b/include/player/ps3-controller.h new file mode 100644 index 0000000..a3f73fe --- /dev/null +++ b/include/player/ps3-controller.h @@ -0,0 +1,56 @@ +#ifdef USE_PS3 +#pragma once + +#include +#include "player/controller.h" + +namespace Player +{ + + class Ps3Controller : public Controller + { + public: + Ps3Controller() : controller(&Ps3) { instance = this; } + + void begin(String macAddress) override; + uint8_t cross() override { return instance->controller->data.analog.button.cross; } + uint8_t circle() override { return instance->controller->data.analog.button.circle; } + uint8_t triangle() override { return instance->controller->data.analog.button.triangle; } + uint8_t square() override { return instance->controller->data.analog.button.square; } + + uint8_t l1() override { return instance->controller->data.analog.button.l1; } + uint8_t l2() override { return instance->controller->data.analog.button.l2; } + uint8_t r1() override { return instance->controller->data.analog.button.r1; } + uint8_t r2() override { return instance->controller->data.analog.button.r2; } + + uint8_t up() override { return instance->controller->data.analog.button.up; } + uint8_t down() override { return instance->controller->data.analog.button.down; } + uint8_t left() override { return instance->controller->data.analog.button.left; } + uint8_t right() override { return instance->controller->data.analog.button.right; } + + uint8_t start() override { return instance->controller->data.button.start; } + uint8_t ps() override { return instance->controller->data.button.ps; } + + AnalogStick leftAnalog() override; + AnalogStick rightAnalog() override; + + const uint8_t rawButtonState(const ControllerButton button) const; + const bool wasPressed(const ControllerButton button) const; + const bool wasPressedAndReleased(const ControllerButton button) const; + const bool isConnected() const { return instance->connection; } + void poll(); + bool isDown(const ControllerButton button) const; + + private: + ::Ps3Controller *controller; + static Ps3Controller *instance; + + void handleOnConnect() override; + static void onConnect() + { + if (instance) + instance->handleOnConnect(); + } + }; +} +#endif \ No newline at end of file diff --git a/include/player/ps4-controller.h b/include/player/ps4-controller.h new file mode 100644 index 0000000..e1913d2 --- /dev/null +++ b/include/player/ps4-controller.h @@ -0,0 +1,63 @@ +#if !defined(USE_PS3) + +#pragma once + +#include + +#include "player/controller.h" +#include "player/controller-properties.h" + +namespace Player +{ + class Ps4Controller : public Controller + { + public: + Ps4Controller() : controller(nullptr) { instance = this; } + void begin(String macAddress) {}; + + uint8_t cross() { return 5; } // instance->controller.data.button.cross; } + uint8_t circle() { return 5; } // instance->controller.data.button.circle; } + uint8_t triangle() { return 5; } // instance->controller.data.button.triangle; } + uint8_t square() { return 5; } // instance->controller.data.button.square; } + + uint8_t l1() { return 5; } // instance->controller.data.button.l1; } + uint8_t l2() { return 5; } // instance->controller.data.button.l2; } + uint8_t r1() { return 5; } // instance->controller.data.button.r1; } + uint8_t r2() { return 5; } // instance->controller.data.button.r2; } + + uint8_t up() { return 5; } // instance->controller.data.button.up; } + uint8_t down() { return 5; } // instance->controller.data.button.down; } + uint8_t left() { return 5; } // instance->controller.data.button.left; } + uint8_t right() { return 5; } // instance->controller.data.button.right; } + + uint8_t start() { return 5; } // instance->controller.data.button.options; } + uint8_t select() { return 5; } // instance->controller.data.button.share; } + uint8_t ps() { return 5; } // instance->controller.data.button.ps; } + + AnalogStick leftAnalog() { return {0, 0}; } + AnalogStick rightAnalog() { return {0, 0}; } + + const uint8_t rawButtonState(const ControllerButton button) const override { return 0; } + const bool wasPressed(const ControllerButton button) const override { return 0; } + const bool wasPressedAndReleased(const ControllerButton button) const override { return 0; } + const bool isConnected() const { return instance->connection; } + void poll() {} + bool isDown(const ControllerButton button) const { return false; } + void reset() {} + + private: + ::Controller *controller; + + static Ps4Controller *instance; + bool connection = false; + uint32_t ignoreEventsUntil = 0; + void handleOnConnect() override {}; + static void onConnect() + { + if (instance) + instance->handleOnConnect(); + } + }; +} + +#endif \ No newline at end of file diff --git a/lumenlab.code-workspace b/lumenlab.code-workspace index aea313b..aaa602e 100644 --- a/lumenlab.code-workspace +++ b/lumenlab.code-workspace @@ -63,7 +63,13 @@ "cinttypes": "cpp", "typeinfo": "cpp", "any": "cpp", - "variant": "cpp" + "variant": "cpp", + "format": "cpp", + "forward_list": "cpp", + "xhash": "cpp", + "xstring": "cpp", + "xtree": "cpp", + "xutility": "cpp" }, "terminal.integrated.profiles.windows": { "PowerShell": { @@ -131,12 +137,12 @@ }, { "text": "$(debug-alt)", - "tooltip": "PlatformIO: Upload (Virtualization)", + "tooltip": "PlatformIO: Upload (PS3 Virtualization)", "commands": [ { "id": "workbench.action.tasks.runTask", "args": [ - "Upload and Start Virtualization" + "Upload and Start Virtualization (PS3)" ] }, ] @@ -199,7 +205,19 @@ "id": "platformio-ide.serialMonitor" }, ] - } + }, + { + "text": "$(debug-alt)", + "tooltip": "PlatformIO: Upload (PS4 Virtualization)", + "commands": [ + { + "id": "workbench.action.tasks.runTask", + "args": [ + "Upload and Start Virtualization (PS4)" + ] + }, + ] + }, ], } } \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 3db7412..f363e07 100644 --- a/platformio.ini +++ b/platformio.ini @@ -6,7 +6,7 @@ default_envs = debug [env] upload_speed = 921600 monitor_speed = 921600 -lib_deps = +lib_deps = jvpernis/PS3 Controller Host@^1.1.0 fastled/FastLED@^3.10.2 adafruit/Adafruit SSD1306@^2.5.15 @@ -16,17 +16,9 @@ lib_deps = platform = espressif32 board = upesy_wroom framework = arduino -; debug_tool = esp-prog -; upload_protocol = esp-prog -; debug_init_break = tbreak setup -; debug_extra_cmds = -; set remote hardware-breakpoint-limit 2 -; set remote hardware-watchpoint-limit 1 -; debug_speed = 1000 - build_type = debug build_unflags = -std=gnu++11 -build_flags = -g -DVIRTUALIZATION -std=gnu++2a +build_flags = -g -DVIRTUALIZATION -DUSE_PS3 -std=gnu++2a [env:debug] platform = espressif32 @@ -34,7 +26,7 @@ board = upesy_wroom framework = arduino build_type = debug build_unflags = -std=gnu++11 -build_flags = -g -DDEBUG -std=gnu++2a +build_flags = -g -DDEBUG -DUSE_PS3 -std=gnu++2a [env:release] platform = espressif32 @@ -45,6 +37,7 @@ build_unflags = -std=gnu++11 build_flags = -Os -DRELEASE + -DUSE_PS3 -std=gnu++2a -DLUMENLAB_VERSION=\"${sysenv.LUMENLAB_VERSION}\" @@ -52,8 +45,20 @@ build_flags = platform = native test_framework = googletest build_unflags = -std=gnu++11 -build_flags = +build_flags = -I.pio/libdeps/native/googletest/googletest/include/gtest -I.pio/libdeps/native/googletest/googlemock/include/gmock -pthread + -DUSE_PS3 -std=gnu++2a + +#### +# To explore later: ESP-Prog debugging +#### +; debug_tool = esp-prog +; upload_protocol = esp-prog +; debug_init_break = tbreak setup +; debug_extra_cmds = +; set remote hardware-breakpoint-limit 2 +; set remote hardware-watchpoint-limit 1 +; debug_speed = 1000 \ No newline at end of file diff --git a/src/core/configuration.cpp b/src/core/configuration.cpp index 36137ac..383e4ea 100644 --- a/src/core/configuration.cpp +++ b/src/core/configuration.cpp @@ -4,10 +4,16 @@ #ifndef LUMENLAB_VERSION #define LUMENLAB_VERSION "v999.999.999" #endif +#ifdef USE_PS3 +#define PS_CONTROLLER_TYPE "PS3" +#else +#define PS_CONTROLLER_TYPE "PS4" +#endif namespace SystemCore { String Configuration::_version; + String Configuration::_psControllerType; String Configuration::_macAddress; uint16_t Configuration::_numLeds; uint32_t Configuration::_serialBaud; @@ -16,6 +22,7 @@ namespace SystemCore void Configuration::load(::Preferences &memory) { _version = LUMENLAB_VERSION; + _psControllerType = PS_CONTROLLER_TYPE; _macAddress = memory.getString("macAddress", "00:00:00:00:00:00"); _numLeds = static_cast(memory.getString("numLeds", "300").toInt()); _serialBaud = static_cast(memory.getString("serialBaud", "921600").toInt()); diff --git a/src/core/context-manager.cpp b/src/core/context-manager.cpp index 76823af..0e7fe00 100644 --- a/src/core/context-manager.cpp +++ b/src/core/context-manager.cpp @@ -1,4 +1,5 @@ #include "core/context-manager.h" +#include "player/controller-properties.h" #include "games/demo/driver.h" #include "games/recall/driver.h" #include "games/phase-evasion/driver.h" @@ -16,7 +17,7 @@ namespace SystemCore #endif } - ContextManager::~ContextManager() + ContextManager::~ContextManager() { if (application) { @@ -25,6 +26,13 @@ namespace SystemCore } } + void ContextManager::initializeSystemMemory() + { + SystemCore::Configuration::load(memory); + stateManager.getPhaseEvasionGameState().loadHighScore(); + stateManager.getRecallGameState().loadHighScore(); + } + void ContextManager::checkExitRequest() { if (controller.wasPressedAndReleased(Player::ControllerButton::Ps)) @@ -33,6 +41,7 @@ namespace SystemCore stateManager.setNextUserMenuChoice(Engine::MainMenuSelection::Games); stateManager.setNextUserGameChoice(Engine::GameSelection::Recall); stateManager.setNextUserSceneChoice(Engine::SceneSelection::Canvas); + stateManager.displayShouldUpdate = true; log("Exiting to Main Menu."); } } diff --git a/src/display/display.cpp b/src/display/display.cpp index 000f937..8368b8c 100644 --- a/src/display/display.cpp +++ b/src/display/display.cpp @@ -226,9 +226,13 @@ namespace Display display.clearDisplay(); drawHeader("LumenLab"); - auto startingX = calculateCenterText("PS3 controller"); + char controllerTypeBuffer[32] = ""; + sprintf(controllerTypeBuffer, "%s controller", SystemCore::Configuration::psControllerType); + + auto startingX = calculateCenterText(controllerTypeBuffer); display.setCursor(startingX, 8); - display.print("PS3 controller"); + display.print(controllerTypeBuffer); + startingX = calculateCenterText("not connected"); display.setCursor(startingX, 16); display.print("not connected"); diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 0e0a549..f8bde2f 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -68,11 +68,7 @@ namespace Engine void GameEngine::initializeEngine() { - SystemCore::Configuration::load(contextManager.memory); - - contextManager.stateManager.getPhaseEvasionGameState().loadHighScore(); - contextManager.stateManager.getRecallGameState().loadHighScore(); - contextManager.controller.begin(SystemCore::Configuration::macAddress()); + contextManager.initializeSystemMemory(); // If debugging, ensure serial connection is stable before setting up components #if defined(VIRTUALIZATION) || defined(DEBUG) @@ -87,22 +83,31 @@ namespace Engine } log("Serial connection established."); log("Printing environment variables."); - logf("version = %s", SystemCore::Configuration::version()); - logf("macAddress = %s", SystemCore::Configuration::macAddress()); + logf("version = %s", SystemCore::Configuration::version().c_str()); + logf("controller_type = %s", SystemCore::Configuration::psControllerType().c_str()); + logf("macAddress = %s", SystemCore::Configuration::macAddress().c_str()); logf("numLeds = %u", SystemCore::Configuration::numLeds()); logf("serialBaud = %u", SystemCore::Configuration::serialBaud()); logf("boundary_1 = %u", SystemCore::Configuration::recallBoundaries()[0]); logf("boundary_2 = %u", SystemCore::Configuration::recallBoundaries()[1]); logf("boundary_3 = %u", SystemCore::Configuration::recallBoundaries()[2]); + logf("Phase Evasion high score: %u", contextManager.stateManager.getPhaseEvasionGameState().highScore); + logf("Recall high score: %u", contextManager.stateManager.getRecallGameState().highScore); +#endif +#ifdef RELEASE + log("NVS memory namespace: lumenlab"); +#else + log("NVS memory namespace: lumenlab-dev"); #endif - log("Connecting to PS3 controller"); + contextManager.controller.begin(SystemCore::Configuration::macAddress()); + logf("Connecting to %s controller", SystemCore::Configuration::psControllerType); - // twenty second attempt to connect to PS3 controller + // twenty second attempt to connect to PS3/PS4 controller int reattempt = 0; while (!contextManager.controller.isConnected() && reattempt < 80) { - log(" Searching for PS3 controller..."); + logf(" Searching for %s controller...", SystemCore::Configuration::psControllerType); ++reattempt; delay(250); } @@ -118,7 +123,6 @@ namespace Engine contextManager.stateManager.displayIsVisible = true; contextManager.controller.reset(); contextManager.controller.poll(); - lastRender = micros(); log("Startup process completed. Transitioning to Main Menu"); } @@ -129,17 +133,21 @@ namespace Engine { if (contextManager.controller.isConnected()) { + contextManager.controller.reset(); + contextManager.controller.poll(); + contextManager.stateManager.setNext(SystemState::MenuHome); contextManager.stateManager.setNextUserMenuChoice(MainMenuSelection::Games); contextManager.stateManager.setNextUserGameChoice(GameSelection::Recall); contextManager.stateManager.setNextUserSceneChoice(SceneSelection::Canvas); - lastRender = micros(); + contextManager.stateManager.displayShouldUpdate = true; contextManager.stateManager.displayIsVisible = true; - log("PS3 controller connected. Transitioning to Main Menu"); + logf("%s controller connected. Transitioning to Main Menu", SystemCore::Configuration::psControllerType); + contextManager.controller.reset(); return; } - for (uint16_t i = 0; i <= SystemCore::Configuration::numLeds(); ++i) + for (uint16_t i = 0; i < SystemCore::Configuration::numLeds(); ++i) { float phase = std::cos((2 * M_PI * i / SystemCore::Configuration::numLeds()) + (2 * M_PI * disconnectedLedPhaseShift / SystemCore::Configuration::numLeds())) * 127 + 128; contextManager.leds.buffer[i] = {static_cast(std::floor(phase)), 0, 0}; diff --git a/src/games/phase-evasion/driver.cpp b/src/games/phase-evasion/driver.cpp index e26e448..a8ec1be 100644 --- a/src/games/phase-evasion/driver.cpp +++ b/src/games/phase-evasion/driver.cpp @@ -199,7 +199,7 @@ namespace Games::PhaseEvasion void Driver::checkIfHighScore() { - GameState state = contextManager->stateManager.getPhaseEvasionGameState(); + GameState &state = contextManager->stateManager.getPhaseEvasionGameState(); state.checkHighScore(); } @@ -219,7 +219,7 @@ namespace Games::PhaseEvasion void Driver::gameOver() { - for (uint16_t i = 0; i <= SystemCore::Configuration::numLeds(); ++i) + for (uint16_t i = 0; i < SystemCore::Configuration::numLeds(); ++i) { float offset = std::cos((2.0f * M_PI * i / SystemCore::Configuration::numLeds()) - (2.0f * M_PI * static_cast(gameOverPhaseShift) / SystemCore::Configuration::numLeds())); float phase = offset * 127.5 + 127.5; diff --git a/src/games/phase-evasion/state.cpp b/src/games/phase-evasion/state.cpp index f9f3981..71ba490 100644 --- a/src/games/phase-evasion/state.cpp +++ b/src/games/phase-evasion/state.cpp @@ -21,17 +21,18 @@ namespace Games::PhaseEvasion void GameState::checkHighScore() { - if (calculateTotalScore() > highScore) + const auto currentScore = calculateTotalScore(); + if (currentScore >= highScore) { - updateHighScore(); + updateHighScore(currentScore); + highScore = currentScore; contextManager->stateManager.displayShouldUpdate = true; } } - void GameState::updateHighScore() + void GameState::updateHighScore(uint16_t newHighScore) { - highScore = calculateTotalScore(); - contextManager->memory.putUInt(memoryKeyName, highScore); - logf("High score updated: %u", highScore); + contextManager->memory.putUInt(memoryKeyName, newHighScore); + logf("High score updated: %u", newHighScore); } } \ No newline at end of file diff --git a/src/games/recall/driver.cpp b/src/games/recall/driver.cpp index be2c80c..249ea74 100644 --- a/src/games/recall/driver.cpp +++ b/src/games/recall/driver.cpp @@ -1,5 +1,5 @@ #include "esp_system.h" - +#include "player/controller-properties.h" #include "games/recall/driver.h" #include "common.h" #include "logger.h" @@ -251,7 +251,7 @@ namespace Games::Recall contextManager->stateManager.displayShouldUpdate = true; } - for (uint16_t i = 0; i <= SystemCore::Configuration::numLeds(); ++i) + for (uint16_t i = 0; i < SystemCore::Configuration::numLeds(); ++i) { float phase = std::cos((2.0f * M_PI * i / SystemCore::Configuration::numLeds()) + (2.0f * M_PI * gameOverLedPhaseShift / SystemCore::Configuration::numLeds())) * 127 + 128; contextManager->leds.buffer[i] = {static_cast(std::floor(phase)), 0, 0}; diff --git a/src/player/controller.cpp b/src/player/controller.cpp index d81336a..ab22f0e 100644 --- a/src/player/controller.cpp +++ b/src/player/controller.cpp @@ -1,161 +1,8 @@ -#include -#include -#include #include "player/controller.h" -#include "logger.h" #include "common.h" namespace Player { - // Following Singleton pattern. Only one instance of the game controller can exist - Controller *Controller::instance = nullptr; - - void Controller::begin(String macAddress) - { - instance->controller.begin(macAddress.c_str()); - instance->controller.attachOnConnect(&Controller::onConnect); - } - - AnalogStick Controller::leftAnalog() - { - AnalogStick joystick; - auto &analog = instance->controller.data.analog.stick; - - joystick.x = filterDeadZone(analog.lx); - joystick.y = filterDeadZone(analog.ly); - - return joystick; - } - - AnalogStick Controller::rightAnalog() - { - AnalogStick joystick; - auto &analog = instance->controller.data.analog.stick; - - joystick.x = filterDeadZone(analog.rx); - joystick.y = filterDeadZone(analog.ry); - - return joystick; - } - - const uint8_t Controller::rawButtonState(const ControllerButton button) const - { - if (!instance->connection) - return 0; - - switch (button) - { - case ControllerButton::Cross: - return instance->controller.data.analog.button.cross; - case ControllerButton::Circle: - return instance->controller.data.analog.button.circle; - case ControllerButton::Triangle: - return instance->controller.data.analog.button.triangle; - case ControllerButton::Square: - return instance->controller.data.analog.button.square; - case ControllerButton::Up: - return instance->controller.data.analog.button.up; - case ControllerButton::Down: - return instance->controller.data.analog.button.down; - case ControllerButton::Left: - return instance->controller.data.analog.button.left; - case ControllerButton::Right: - return instance->controller.data.analog.button.right; - case ControllerButton::L1: - return instance->controller.data.analog.button.l1; - case ControllerButton::L2: - return instance->controller.data.analog.button.l2; - case ControllerButton::L3: - return instance->controller.data.button.l3; - case ControllerButton::R1: - return instance->controller.data.analog.button.r1; - case ControllerButton::R2: - return instance->controller.data.analog.button.r2; - case ControllerButton::R3: - return instance->controller.data.button.r3; - case ControllerButton::Start: - return instance->controller.data.button.start; - case ControllerButton::Select: - return instance->controller.data.button.select; - case ControllerButton::Ps: - return instance->controller.data.button.ps; - default: - return 0; - } - } - - const bool Controller::wasPressed(const ControllerButton button) const - { - uint32_t idx = static_cast(button); - return instance->buttonPressedEvent[idx]; - } - - const bool Controller::wasPressedAndReleased(const ControllerButton button) const - { - uint32_t idx = static_cast(button); - return instance->buttonReleasedEvent[idx]; - } - - bool Controller::isDown(const ControllerButton button) const - { - uint32_t idx = static_cast(button); - return instance->buttonLastState[idx]; - } - - void Controller::reset() - { - for (uint32_t i = 0; i < arraySize(buttonPressedEvent); ++i) - { - buttonPressedEvent[i] = false; - buttonReleasedEvent[i] = false; - buttonLastState[i] = rawButtonState(static_cast(i)); - buttonDebouceEvent[i] = 0; - } - } - - void Controller::poll() - { - for (uint32_t i = 0; i < arraySize(buttonPressedEvent); ++i) - { - buttonPressedEvent[i] = false; - buttonReleasedEvent[i] = false; - } - - for (uint32_t i = 0; i < arraySize(buttonLastState); ++i) - { - auto btn = static_cast(i); - bool raw = rawButtonState(btn) != 0; - - uint32_t now = millis(); - if (raw && !buttonLastState[i]) - { - if (now - buttonDebouceEvent[i] > buttonDebounceThreshold) - { - buttonPressedEvent[i] = true; - buttonLastState[i] = true; - buttonDebouceEvent[i] = now; - } - } - else if (!raw && buttonLastState[i]) - { - if (now - buttonDebouceEvent[i] > buttonDebounceThreshold) - { - buttonReleasedEvent[i] = true; - buttonLastState[i] = false; - buttonDebouceEvent[i] = now; - } - } - } - } - - void Controller::handleOnConnect() - { - log("Game controller connected successfully."); - instance->connection = true; - instance->reset(); - instance->poll(); - } - int Controller::filterDeadZone(int8_t value, int deadZone) { if (std::abs(value) <= deadZone) @@ -173,4 +20,15 @@ namespace Player int output = static_cast(std::round(scaled * static_cast(maxRaw))); return static_cast(sign * output); } + + void Controller::reset() + { + for (uint32_t i = 0; i < arraySize(buttonPressedEvent); ++i) + { + buttonPressedEvent[i] = false; + buttonReleasedEvent[i] = false; + buttonLastState[i] = rawButtonState(static_cast(i)); + buttonDebouceEvent[i] = 0; + } + } } \ No newline at end of file diff --git a/src/player/ps3-controller.cpp b/src/player/ps3-controller.cpp new file mode 100644 index 0000000..ef14e97 --- /dev/null +++ b/src/player/ps3-controller.cpp @@ -0,0 +1,164 @@ +#ifdef USE_PS3 + +#include +#include +#include "player/ps3-controller.h" +#include "logger.h" +#include "common.h" + +#include + +namespace Player +{ + // Following Singleton pattern. Only one instance of the game controller can exist + Ps3Controller *Ps3Controller::instance = nullptr; + + void Ps3Controller::begin(String macAddress) + { + instance->controller->begin(macAddress.c_str()); + instance->controller->attachOnConnect(&Ps3Controller::onConnect); + } + + AnalogStick Ps3Controller::leftAnalog() + { + AnalogStick joystick; + auto &analog = instance->controller->data.analog.stick; + + joystick.x = filterDeadZone(analog.lx); + joystick.y = filterDeadZone(analog.ly); + + return joystick; + } + + AnalogStick Ps3Controller::rightAnalog() + { + AnalogStick joystick; + auto &analog = instance->controller->data.analog.stick; + + joystick.x = filterDeadZone(analog.rx); + joystick.y = filterDeadZone(analog.ry); + + return joystick; + } + + const uint8_t Ps3Controller::rawButtonState(const ControllerButton button) const + { + if (!instance->connection) + return 0; + + switch (button) + { + case ControllerButton::Cross: + return instance->controller->data.analog.button.cross; + case ControllerButton::Circle: + return instance->controller->data.analog.button.circle; + case ControllerButton::Triangle: + return instance->controller->data.analog.button.triangle; + case ControllerButton::Square: + return instance->controller->data.analog.button.square; + case ControllerButton::Up: + return instance->controller->data.analog.button.up; + case ControllerButton::Down: + return instance->controller->data.analog.button.down; + case ControllerButton::Left: + return instance->controller->data.analog.button.left; + case ControllerButton::Right: + return instance->controller->data.analog.button.right; + case ControllerButton::L1: + return instance->controller->data.analog.button.l1; + case ControllerButton::L2: + return instance->controller->data.analog.button.l2; + case ControllerButton::L3: + return instance->controller->data.button.l3; + case ControllerButton::R1: + return instance->controller->data.analog.button.r1; + case ControllerButton::R2: + return instance->controller->data.analog.button.r2; + case ControllerButton::R3: + return instance->controller->data.button.r3; + case ControllerButton::Start: + return instance->controller->data.button.start; + case ControllerButton::Select: + return instance->controller->data.button.select; + case ControllerButton::Ps: + return instance->controller->data.button.ps; + default: + return 0; + } + } + + const bool Ps3Controller::wasPressed(const ControllerButton button) const + { + uint32_t idx = static_cast(button); + return instance->buttonPressedEvent[idx]; + } + + const bool Ps3Controller::wasPressedAndReleased(const ControllerButton button) const + { + uint32_t idx = static_cast(button); + return instance->buttonReleasedEvent[idx]; + } + + bool Ps3Controller::isDown(const ControllerButton button) const + { + uint32_t idx = static_cast(button); + return instance->buttonLastState[idx]; + } + + void Ps3Controller::poll() + { + for (uint32_t i = 0; i < arraySize(buttonPressedEvent); ++i) + { + buttonPressedEvent[i] = false; + buttonReleasedEvent[i] = false; + } + + if (millis() < ignoreEventsUntil) + { + for (uint32_t i = 0; i < arraySize(buttonLastState); ++i) + { + auto btn = static_cast(i); + bool raw = rawButtonState(btn) != 0; + buttonLastState[i] = raw; + buttonDebouceEvent[i] = millis(); + } + return; + } + + for (uint32_t i = 0; i < arraySize(buttonLastState); ++i) + { + auto btn = static_cast(i); + bool raw = rawButtonState(btn) != 0; + + uint32_t now = millis(); + if (raw && !buttonLastState[i]) + { + if (now - buttonDebouceEvent[i] > buttonDebounceThreshold) + { + buttonPressedEvent[i] = true; + buttonLastState[i] = true; + buttonDebouceEvent[i] = now; + } + } + else if (!raw && buttonLastState[i]) + { + if (now - buttonDebouceEvent[i] > buttonDebounceThreshold) + { + buttonReleasedEvent[i] = true; + buttonLastState[i] = false; + buttonDebouceEvent[i] = now; + } + } + } + } + + void Ps3Controller::handleOnConnect() + { + log("Game controller connected successfully."); + instance->connection = true; + instance->ignoreEventsUntil = millis() + 200; + instance->reset(); + instance->poll(); + } +} +#endif \ No newline at end of file diff --git a/src/player/ps4-controller.cpp b/src/player/ps4-controller.cpp new file mode 100644 index 0000000..ffd87d7 --- /dev/null +++ b/src/player/ps4-controller.cpp @@ -0,0 +1,8 @@ +#if !defined(USE_PS3) +#include "player/ps4-controller.h" + +namespace Player +{ + Ps4Controller *Ps4Controller::instance = nullptr; +} +#endif \ No newline at end of file diff --git a/tools/setup_dev_env.py b/tools/setup_dev_env.py index fab4644..ebb643f 100644 --- a/tools/setup_dev_env.py +++ b/tools/setup_dev_env.py @@ -6,7 +6,7 @@ def setup_python_virtual_environment(): - tools_dir = Path("tools") + tools_dir = Path(".") venv_dir = tools_dir / ".venv" print(f"\n\nCreating a python virtual environment in {venv_dir}...")