diff --git a/include/core/configuration.h b/include/core/configuration.h index acd4d00..e6e6718 100644 --- a/include/core/configuration.h +++ b/include/core/configuration.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include diff --git a/include/display/display.h b/include/display/display.h index ce77513..732481b 100644 --- a/include/display/display.h +++ b/include/display/display.h @@ -10,6 +10,7 @@ #endif #include "display/display-images.h" +#include "display/oled-serialization.h" namespace SystemCore { @@ -37,6 +38,7 @@ namespace Display char selectedOption(uint8_t index, uint8_t selectedOptionIndex) { return index == selectedOptionIndex ? '>' : ' '; }; int16_t calculateCenterText(const char *text); + void debugUpdateDisplay(const char *format, ...); void drawHeader(const char *message); void drawBootScreen(); @@ -46,6 +48,7 @@ namespace Display void drawRecallGameHud(); void drawPhaseEvasionGameHud(); void drawChainReactionGameHud(); + void drawReflexGameHud(); void drawDemoGameHud(); void drawCanvasSceneHud(); void drawUnconnectedControllerScreen(); diff --git a/include/display/oled-serialization.h b/include/display/oled-serialization.h new file mode 100644 index 0000000..900e9b1 --- /dev/null +++ b/include/display/oled-serialization.h @@ -0,0 +1,13 @@ +#pragma once + +#ifdef VIRTUALIZATION + +namespace Display +{ + // Send OLED text update to serial port (only if content changed) + // Format: 0xBB 0x66 [2-byte length] [text data] + void sendOledUpdate(const char *text); + void serializeOledText(const char *text); +} + +#endif diff --git a/include/engine/state-manager.h b/include/engine/state-manager.h index 4cd296c..6bec2d1 100644 --- a/include/engine/state-manager.h +++ b/include/engine/state-manager.h @@ -3,6 +3,7 @@ #include "games/recall/state.h" #include "games/phase-evasion/state.h" #include "games/chain-reaction/state.h" +#include "games/reflex/state.h" #include "games/demo/state.h" #include "scenes/canvas/state.h" @@ -23,6 +24,7 @@ namespace Engine GameRecall, GamePhaseEvasion, GameChainReaction, + GameReflex, GameDemo, SceneCanvas, NoControllerConnected, @@ -41,6 +43,7 @@ namespace Engine Recall, PhaseEvasion, ChainReaction, + Reflex, Demo, COUNT }; @@ -64,6 +67,7 @@ namespace Engine recallGameState{ctx}, phaseEvasionGameState{ctx}, chainReactionGameState{ctx}, + reflexGameState{ctx}, systemState{SystemState::Initialize}, userMainMenuChoice{MainMenuSelection::Games}, userGameChoice{GameSelection::Recall}, @@ -95,6 +99,7 @@ namespace Engine Games::Recall::GameState &getRecallGameState() { return recallGameState; } Games::PhaseEvasion::GameState &getPhaseEvasionGameState() { return phaseEvasionGameState; } Games::ChainReaction::GameState &getChainReactionGameState() { return chainReactionGameState; } + Games::Reflex::GameState &getReflexGameState() { return reflexGameState; } Games::Demo::GameState &getDemoGameState() { return demoGameState; } Scenes::Canvas::SceneState &getCanvasSceneState() { return canvasSceneState; } @@ -110,6 +115,7 @@ namespace Engine Games::Recall::GameState recallGameState; Games::PhaseEvasion::GameState phaseEvasionGameState; Games::ChainReaction::GameState chainReactionGameState; + Games::Reflex::GameState reflexGameState; Games::Demo::GameState demoGameState; Scenes::Canvas::SceneState canvasSceneState; diff --git a/include/games/reflex/driver.h b/include/games/reflex/driver.h new file mode 100644 index 0000000..d2c560e --- /dev/null +++ b/include/games/reflex/driver.h @@ -0,0 +1,27 @@ +#pragma once + +#include "engine/layer.h" +#include "engine/timer.h" +#include "core/context-manager.h" +#include "games/reflex/signal.h" + +namespace Games::Reflex +{ + class Driver : public Engine::Layer, private Engine::Timer + { + public: + Driver(SystemCore::ContextManager *ctx); + ~Driver() { state.reset(); } + static constexpr uint16_t signalWidth = 7; + static constexpr float signalSpeed = 1.0f; + void nextEvent() override; + void renderSignal(); + void reset(); + + private: + SystemCore::ContextManager *contextManager; + GameState &state; + + Signal signal; + }; +} \ No newline at end of file diff --git a/include/games/reflex/signal.h b/include/games/reflex/signal.h new file mode 100644 index 0000000..4f23312 --- /dev/null +++ b/include/games/reflex/signal.h @@ -0,0 +1,19 @@ +#pragma once + +#include "core/context-manager.h" +#include "player/player.h" + +namespace Games::Reflex +{ + class Signal : public ::Player::BasePlayer + { + public: + Signal(SystemCore::ContextManager *ctx, uint16_t width, float _speed); + ~Signal() = default; + + void advance() { setPosition(positionPrecise += speed); } + + private: + float speed; + }; +} diff --git a/include/games/reflex/state.h b/include/games/reflex/state.h new file mode 100644 index 0000000..94f00d0 --- /dev/null +++ b/include/games/reflex/state.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +namespace SystemCore +{ + class ContextManager; +} + +namespace Games::Reflex +{ + enum class Actions + { + Startup, + Ready, + ActiveGame, + GameOver + }; + + class GameState + { + public: + GameState(SystemCore::ContextManager *ctx) : contextManager{ctx} {} + + Actions current = Actions::Startup; + static constexpr const char *memoryKeyName = "high_reflex"; + + void reset() {} + + private: + SystemCore::ContextManager *contextManager; + }; +} \ No newline at end of file diff --git a/include/logger.h b/include/logger.h index 3c11c7a..f543ffd 100644 --- a/include/logger.h +++ b/include/logger.h @@ -1,7 +1,5 @@ #pragma once -#include - /* Interesting compiler optimization below, when building with the DEBUG flag on it prints to the serial monitor as normal, but when building in RELEASE diff --git a/src/core/context-manager.cpp b/src/core/context-manager.cpp index b726813..8c57826 100644 --- a/src/core/context-manager.cpp +++ b/src/core/context-manager.cpp @@ -3,6 +3,7 @@ #include "games/recall/driver.h" #include "games/phase-evasion/driver.h" #include "games/chain-reaction/driver.h" +#include "games/reflex/driver.h" #include "games/demo/driver.h" #include "scenes/canvas/driver.h" #include "logger.h" @@ -116,6 +117,9 @@ namespace SystemCore case Engine::GameSelection::ChainReaction: stateManager.setNext(Engine::SystemState::GameChainReaction); break; + case Engine::GameSelection::Reflex: + stateManager.setNext(Engine::SystemState::GameReflex); + break; case Engine::GameSelection::Demo: stateManager.setNext(Engine::SystemState::GameDemo); break; @@ -185,6 +189,10 @@ namespace SystemCore application = new Games::ChainReaction::Driver{this}; logf("Transitioning to Chain Reaction (Game)"); break; + case Engine::SystemState::GameReflex: + application = new Games::Reflex::Driver{this}; + logf("Transitioning to Reflex (Game)"); + break; case Engine::SystemState::GameDemo: application = new Games::Demo::Driver{this}; logf("Transitioning to Demo (Game)"); diff --git a/src/display/display.cpp b/src/display/display.cpp index 364ed72..4ca3ef7 100644 --- a/src/display/display.cpp +++ b/src/display/display.cpp @@ -1,4 +1,5 @@ #include +#include #include "display/display.h" #include "core/context-manager.h" @@ -52,6 +53,9 @@ namespace Display case Engine::SystemState::GameChainReaction: drawChainReactionGameHud(); break; + case Engine::SystemState::GameReflex: + drawReflexGameHud(); + break; case Engine::SystemState::SceneCanvas: drawCanvasSceneHud(); break; @@ -85,6 +89,18 @@ namespace Display display.print(message); } + void OledDisplay::debugUpdateDisplay(const char *format, ...) + { +#ifdef VIRTUALIZATION + char oledBuffer[256] = ""; + va_list args; + va_start(args, format); + vsnprintf(oledBuffer, sizeof(oledBuffer), format, args); + va_end(args); + Display::sendOledUpdate(oledBuffer); +#endif + } + void OledDisplay::drawBootScreen() { display.clearDisplay(); @@ -115,6 +131,9 @@ namespace Display display.print(selectedOption(1, selectedOptionIndex)); display.print(" Scenes"); + debugUpdateDisplay(" Main Menu\n%c Games\n%c Scenes", + selectedOption(0, selectedOptionIndex), selectedOption(1, selectedOptionIndex)); + display.display(); } @@ -140,6 +159,15 @@ namespace Display display.print(nameBuffer); } + char oledBuffer[256] = ""; + int offset = snprintf(oledBuffer, sizeof(oledBuffer), "Games Menu (%d/%d)", currentPage + 1, (numGames + itemsPerPage - 1) / itemsPerPage); + for (int i = startIndex; i < endIndex; ++i) + { + offset += snprintf(oledBuffer + offset, sizeof(oledBuffer) - offset, "\n%c %s", + selectedOption(i, selectedOptionIndex), contextManager->stateManager.printGameName(i)); + } + debugUpdateDisplay("%s", oledBuffer); + display.display(); } @@ -155,6 +183,9 @@ namespace Display sprintf(nameBuffer, "%c %s", selectedOption(0, selectedOptionIndex), contextManager->stateManager.printSceneName(0)); display.print(nameBuffer); + debugUpdateDisplay(" Scenes Menu\n%c %s", + selectedOption(0, selectedOptionIndex), contextManager->stateManager.printSceneName(0)); + display.display(); } @@ -169,6 +200,9 @@ namespace Display display.setCursor(0, 24); display.print("High Score: -"); + debugUpdateDisplay(" Demo\nCurrent Score: %u\nHigh Score: -", + contextManager->stateManager.getDemoGameState().currentScore); + display.display(); } @@ -186,6 +220,8 @@ namespace Display display.setCursor(0, 24); display.printf("High Score: %u", highScore + 1); + debugUpdateDisplay(" Recall\nRound: %u\nHigh Score: %u", round + 1, highScore + 1); + display.display(); } @@ -210,6 +246,9 @@ namespace Display display.setCursor(DISPLAY_WIDTH / 2 + 4, 24); display.printf(" High: %u", highScore); + debugUpdateDisplay(" Phase Evasion\nFlares: %u Total: %u\nGems: %u High: %u", + flaresEvaded, totalScore, gemsCaptured, highScore); + display.display(); } @@ -226,6 +265,20 @@ namespace Display display.setCursor(0, 24); display.printf("Total: %u", reactions); + debugUpdateDisplay(" Chain Reaction\nReactions: %u\nTotal: %u", reactions, reactions); + + display.display(); + } + + void OledDisplay::drawReflexGameHud() + { + display.clearDisplay(); + drawHeader("Reflex"); + + const auto reflexState = contextManager->stateManager.getReflexGameState(); + + debugUpdateDisplay(" Reflex"); + display.display(); } @@ -240,6 +293,8 @@ namespace Display display.setCursor(0, 24); display.print(colorBuffer); + debugUpdateDisplay(" Canvas\nRGB: (%u, %u, %u)", color.r, color.g, color.b); + display.display(); } @@ -259,6 +314,8 @@ namespace Display display.setCursor(startingX, 16); display.print("not connected"); + debugUpdateDisplay(" LumenLab\n%s controller\nnot connected", SystemCore::Configuration::psControllerType().c_str()); + display.display(); } } \ No newline at end of file diff --git a/src/display/oled-serialization.cpp b/src/display/oled-serialization.cpp new file mode 100644 index 0000000..e162adf --- /dev/null +++ b/src/display/oled-serialization.cpp @@ -0,0 +1,52 @@ +#ifdef VIRTUALIZATION + +#include +#include +#include "display/oled-serialization.h" + +namespace Display +{ + static uint32_t simpleHash(const char *str) + { + uint32_t hash = 5381; + int c; + while ((c = *str++)) + hash = ((hash << 5) + hash) + c; + return hash; + } + + static uint32_t previousHash = 0; + + void serializeOledText(const char *text) + { + if (!text) + return; + + size_t len = strlen(text); + if (len > 65535) + len = 65535; + + Serial.write(0xBB); + Serial.write(0x66); + Serial.write((uint8_t)(len >> 8)); + Serial.write((uint8_t)(len & 0xFF)); + + Serial.write(reinterpret_cast(text), len); + } + + void sendOledUpdate(const char *text) + { + if (!text) + return; + + uint32_t currentHash = simpleHash(text); + + if (currentHash != previousHash) + { + previousHash = currentHash; + serializeOledText(text); + } + } +} + +#endif diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 63579e5..a9198a6 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -45,6 +45,7 @@ namespace Engine case SystemState::GameRecall: case SystemState::GamePhaseEvasion: case SystemState::GameChainReaction: + case SystemState::GameReflex: case SystemState::GameDemo: case SystemState::SceneCanvas: contextManager.application->nextEvent(); diff --git a/src/engine/state-manager.cpp b/src/engine/state-manager.cpp index d82dced..8ebff64 100644 --- a/src/engine/state-manager.cpp +++ b/src/engine/state-manager.cpp @@ -51,6 +51,7 @@ namespace Engine "Recall", "Phase Evasion", "Chain Reaction", + "Reflex", "Demo"}; return gameNames[index]; } diff --git a/src/games/reflex/driver.cpp b/src/games/reflex/driver.cpp new file mode 100644 index 0000000..2f29e42 --- /dev/null +++ b/src/games/reflex/driver.cpp @@ -0,0 +1,56 @@ +#include "games/reflex/driver.h" +#include "games/reflex/signal.h" +#include "logger.h" + +namespace Games::Reflex +{ + Driver::Driver(SystemCore::ContextManager *ctx) : contextManager{ctx}, + state{ctx->stateManager.getReflexGameState()}, + signal{ctx, signalWidth, signalSpeed} + { + reset(); + } + + void Driver::nextEvent() + { + switch (state.current) + { + case Actions::Startup: + if (isReady()) + { + reset(); + state.current = Actions::Ready; + wait(1000); + log("Starting new game."); + } + break; + case Actions::Ready: + if (isReady()) + { + state.current = Actions::ActiveGame; + } + renderSignal(); + break; + case Actions::ActiveGame: + signal.advance(); + renderSignal(); + break; + default: + break; + } + } + + void Driver::reset() + { + contextManager->stateManager.displayShouldUpdate = true; + } + + void Driver::renderSignal() + { + for (uint16_t i = 0; i < signal.width; ++i) + { + uint16_t index = (signal.getPosition() + i) % SystemCore::Configuration::numLeds(); + contextManager->leds.buffer[index] = Lights::ColorCode::MenuUnselected; + } + } +} \ No newline at end of file diff --git a/src/games/reflex/signal.cpp b/src/games/reflex/signal.cpp new file mode 100644 index 0000000..e8124f6 --- /dev/null +++ b/src/games/reflex/signal.cpp @@ -0,0 +1,7 @@ +#include "games/reflex/signal.h" + +namespace Games::Reflex +{ + Signal::Signal(SystemCore::ContextManager *ctx, uint16_t width, float _speed) : ::Player::BasePlayer{ctx, width}, + speed{_speed} {} +} \ No newline at end of file diff --git a/src/lights/led-luminance.cpp b/src/lights/led-luminance.cpp index f006dca..e78cc92 100644 --- a/src/lights/led-luminance.cpp +++ b/src/lights/led-luminance.cpp @@ -1,5 +1,3 @@ -#include - #include "lights/led-luminance.h" namespace Lights diff --git a/tools/led_gui/virtualization.py b/tools/led_gui/virtualization.py index fee839f..1bcd01a 100644 --- a/tools/led_gui/virtualization.py +++ b/tools/led_gui/virtualization.py @@ -79,6 +79,8 @@ def __init__(self, **kwargs): 2.0 / self.window_size[1], ) self.led_lock = threading.Lock() + self.oled_text = "" + self.OLED_SYNC_BYTES = b'\xBB\x66' self.serial_thread = threading.Thread(target=self.read_serial, daemon=True) self.serial_thread.start() @@ -104,6 +106,18 @@ def __init__(self, **kwargs): color=(160, 160, 160, 160), width=300 ) + self.oled_label = pyglet.text.Label( + "", + font_name='Consolas', + font_size=16, + x=50, + y=self.window_size[1] - 100, + anchor_x='left', + anchor_y='top', + color=(255, 255, 255, 255), + width=400, + multiline=True + ) def compute_led_positions(self): led_radius = 11 @@ -154,20 +168,45 @@ def normalize_positions(self, raw_positions): def read_serial(self): def process_buffer(): - """Extract complete frames from buffer and update LED colors.""" + """Extract complete OLED and LED frames from buffer.""" nonlocal buffer while True: - idx = buffer.find(self.SYNC_BYTES) - if idx == -1: - break # no sync bytes found yet - if len(buffer) < idx + 2 + self.FRAME_SIZE: - break # incomplete frame - start = idx + 2 - frame = buffer[start:start + self.FRAME_SIZE] - buffer = buffer[start + self.FRAME_SIZE:] - with self.led_lock: - colors = np.frombuffer(frame, dtype=np.uint8).reshape(-1, 3) / 255.0 - self.led_colors[:] = colors + oled_idx = buffer.find(self.OLED_SYNC_BYTES) + led_idx = buffer.find(self.SYNC_BYTES) + + if oled_idx != -1 and (led_idx == -1 or oled_idx < led_idx): + if len(buffer) < oled_idx + 4: + break + + length = (buffer[oled_idx + 2] << 8) | buffer[oled_idx + 3] + + if len(buffer) < oled_idx + 4 + length: + break + + oled_data = buffer[oled_idx + 4:oled_idx + 4 + length] + try: + oled_text = oled_data.decode('utf-8') + with self.led_lock: + self.oled_text = oled_text + logging.info(f"OLED: {repr(oled_text)}") + except UnicodeDecodeError as e: + logging.warning(f"Failed to decode OLED text: {e}") + + buffer = buffer[oled_idx + 4 + length:] + + elif led_idx != -1: + if len(buffer) < led_idx + 2 + self.FRAME_SIZE: + break # incomplete frame + + start = led_idx + 2 + frame = buffer[start:start + self.FRAME_SIZE] + buffer = buffer[start + self.FRAME_SIZE:] + + with self.led_lock: + colors = np.frombuffer(frame, dtype=np.uint8).reshape(-1, 3) / 255.0 + self.led_colors[:] = colors + else: + break buffer = bytearray() while True: @@ -191,10 +230,12 @@ def on_render(self, time, frametime): with self.led_lock: self.vbo_positions.write(self.positions.tobytes()) self.vbo_colors.write(self.led_colors.tobytes()) + self.oled_label.text = self.oled_text self.prog['pixel_size'].value = self.pixel_size self.vao.render(instances=self.num_leds) self.title.draw() self.description.draw() + self.oled_label.draw() def start_led_virtualization(window_size, num_leds, port, baud_rate):