From 3532f90f1ef4d542431e182c9cecbe0816802c3b Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Fri, 23 Jan 2026 17:12:18 +0100 Subject: [PATCH] btn ok --- lib/hal/EmulationUtils.h | 9 ++++- lib/hal/HalDisplay.cpp | 6 ++-- lib/hal/HalInput.cpp | 63 +++++++++++++++++++++++++++-------- lib/hal/HalInput.h | 12 +++++++ lib/hal/HalStorage.h | 2 ++ scripts/emulation/server.py | 51 +++++++++++++++++++++------- scripts/emulation/web_ui.html | 40 ++++++++++++++++++++++ 7 files changed, 154 insertions(+), 29 deletions(-) diff --git a/lib/hal/EmulationUtils.h b/lib/hal/EmulationUtils.h index c4ad48c0..ea86a4b8 100644 --- a/lib/hal/EmulationUtils.h +++ b/lib/hal/EmulationUtils.h @@ -18,12 +18,18 @@ static const char* CMD_FS_STAT = "FS_STAT"; // arg0: path -- return file size static const char* CMD_FS_WRITE = "FS_WRITE"; // arg0: path, arg1: base64-encoded data, arg2: offset, arg3: is inplace (0/1) -- return int64_t bytes written static const char* CMD_FS_MKDIR = "FS_MKDIR"; // arg0: path -- return int64_t: 0=success static const char* CMD_FS_RM = "FS_RM"; // arg0: path -- return int64_t: 0=success +static const char* CMD_BUTTON = "BUTTON"; // arg0: action ("read") -- return int64_t: HalInput state bitmask static std::string base64_encode(const char* buf, unsigned int bufLen); static std::vector base64_decode(const char* encoded_string, unsigned int in_len); +static SemaphoreHandle_t sendMutex = xSemaphoreCreateMutex(); + static void sendCmd(const char* cmd, const char* arg0 = nullptr, const char* arg1 = nullptr, const char* arg2 = nullptr, const char* arg3 = nullptr) { - Serial.printf("[%lu] [EMU] Sending command: %s\n", millis(), cmd); + xSemaphoreTake(sendMutex, portMAX_DELAY); + if (cmd != CMD_BUTTON) { + Serial.printf("[%lu] [EMU] Sending command: %s\n", millis(), cmd); + } Serial.print("$$CMD_"); Serial.print(cmd); if (arg0 != nullptr) { @@ -45,6 +51,7 @@ static void sendCmd(const char* cmd, const char* arg0 = nullptr, const char* arg Serial.print(arg3); } Serial.print("$$\n"); + xSemaphoreGive(sendMutex); } static String recvRespStr() { diff --git a/lib/hal/HalDisplay.cpp b/lib/hal/HalDisplay.cpp index fa8e6c1e..088064b1 100644 --- a/lib/hal/HalDisplay.cpp +++ b/lib/hal/HalDisplay.cpp @@ -83,11 +83,11 @@ void HalDisplay::refreshDisplay(RefreshMode mode, bool turnOffScreen) { Serial.printf("[%lu] [ ] Emulated refresh display with mode %d, turnOffScreen %d\n", millis(), static_cast(mode), turnOffScreen); // emulated delay if (mode == RefreshMode::FAST_REFRESH) { - delay(50); + delay(500); } else if (mode == RefreshMode::HALF_REFRESH) { - delay(800); + delay(1000); } else if (mode == RefreshMode::FULL_REFRESH) { - delay(1500); + delay(2000); } } } diff --git a/lib/hal/HalInput.cpp b/lib/hal/HalInput.cpp index bed3d861..160e826f 100644 --- a/lib/hal/HalInput.cpp +++ b/lib/hal/HalInput.cpp @@ -1,4 +1,5 @@ #include "HalInput.h" +#include "EmulationUtils.h" #include void HalInput::begin() { @@ -11,7 +12,44 @@ void HalInput::update() { #if CROSSPOINT_EMULATED == 0 inputMgr.update(); #else - // TODO + const unsigned long currentTime = millis(); + + EmulationUtils::sendCmd(EmulationUtils::CMD_BUTTON, "read"); + auto res = EmulationUtils::recvRespInt64(); + assert(res >= 0); + + const uint8_t state = static_cast(res); + + // Always clear events first + pressedEvents = 0; + releasedEvents = 0; + + // Debounce + if (state != lastState) { + lastDebounceTime = currentTime; + lastState = state; + } + + static constexpr unsigned long DEBOUNCE_DELAY = 5; + if ((currentTime - lastDebounceTime) > DEBOUNCE_DELAY) { + if (state != currentState) { + // Calculate pressed and released events + pressedEvents = state & ~currentState; + releasedEvents = currentState & ~state; + + // If pressing buttons and wasn't before, start recording time + if (pressedEvents > 0 && currentState == 0) { + buttonPressStart = currentTime; + } + + // If releasing a button and no other buttons being pressed, record finish time + if (releasedEvents > 0 && state == 0) { + buttonPressFinish = currentTime; + } + + currentState = state; + } + } #endif } @@ -19,8 +57,7 @@ bool HalInput::isPressed(uint8_t buttonIndex) const { #if CROSSPOINT_EMULATED == 0 return inputMgr.isPressed(buttonIndex); #else - // TODO - return false; + return currentState & (1 << buttonIndex); #endif } @@ -28,8 +65,7 @@ bool HalInput::wasPressed(uint8_t buttonIndex) const { #if CROSSPOINT_EMULATED == 0 return inputMgr.wasPressed(buttonIndex); #else - // TODO - return false; + return currentState & (1 << buttonIndex); #endif } @@ -37,8 +73,7 @@ bool HalInput::wasAnyPressed() const { #if CROSSPOINT_EMULATED == 0 return inputMgr.wasAnyPressed(); #else - // TODO - return false; + return pressedEvents > 0; #endif } @@ -46,8 +81,7 @@ bool HalInput::wasReleased(uint8_t buttonIndex) const { #if CROSSPOINT_EMULATED == 0 return inputMgr.wasReleased(buttonIndex); #else - // TODO - return false; + return releasedEvents & (1 << buttonIndex); #endif } @@ -55,8 +89,7 @@ bool HalInput::wasAnyReleased() const { #if CROSSPOINT_EMULATED == 0 return inputMgr.wasAnyReleased(); #else - // TODO - return false; + return releasedEvents > 0; #endif } @@ -64,8 +97,12 @@ unsigned long HalInput::getHeldTime() const { #if CROSSPOINT_EMULATED == 0 return inputMgr.getHeldTime(); #else - // TODO - return 0; + // Still hold a button + if (currentState > 0) { + return millis() - buttonPressStart; + } + + return buttonPressFinish - buttonPressStart; #endif } diff --git a/lib/hal/HalInput.h b/lib/hal/HalInput.h index f1dd8ce5..b011c993 100644 --- a/lib/hal/HalInput.h +++ b/lib/hal/HalInput.h @@ -31,8 +31,20 @@ class HalInput { static constexpr uint8_t BTN_UP = 4; static constexpr uint8_t BTN_DOWN = 5; static constexpr uint8_t BTN_POWER = 6; + + private: + // emulation state + uint8_t currentState; + uint8_t lastState; + uint8_t pressedEvents; + uint8_t releasedEvents; + unsigned long lastDebounceTime; + unsigned long buttonPressStart; + unsigned long buttonPressFinish; }; +// TODO @ngxson : this is a trick to avoid changing too many files at once. +// consider refactoring in a dedicated PR later. #if CROSSPOINT_EMULATED == 1 using InputManager = HalInput; #endif diff --git a/lib/hal/HalStorage.h b/lib/hal/HalStorage.h index f5cd82c3..bb351df6 100644 --- a/lib/hal/HalStorage.h +++ b/lib/hal/HalStorage.h @@ -113,6 +113,8 @@ class HalStorage { bool is_emulated = CROSSPOINT_EMULATED; }; +// TODO @ngxson : this is a trick to avoid changing too many files at once. +// consider refactoring in a dedicated PR later. #if CROSSPOINT_EMULATED == 1 #define SdMan HalStorage::getInstance() #endif diff --git a/scripts/emulation/server.py b/scripts/emulation/server.py index d0dd3dfe..f5533dd6 100644 --- a/scripts/emulation/server.py +++ b/scripts/emulation/server.py @@ -205,10 +205,11 @@ async def send_response(response: str): LAST_DISPLAY_BUFFER = None +BUTTON_STATE = 0 async def handle_command(command: str, args: list[str]) -> bool: """Handle a command from the device. Returns True if handled.""" - global sdcard_handler, LAST_DISPLAY_BUFFER + global sdcard_handler, LAST_DISPLAY_BUFFER, BUTTON_STATE try: if command == "DISPLAY": @@ -274,6 +275,16 @@ async def handle_command(command: str, args: list[str]) -> bool: print(f"[Emulator] FS_RM {path} -> {result}", flush=True) await send_response(str(result)) return True + + elif command == "BUTTON": + # arg0: action + action = args[0] + if action != "read": + raise ValueError("Only read action is supported") + # no log (too frequent) + # print(f"[Emulator] BUTTON command received: {action}", flush=True) + await send_response(str(BUTTON_STATE)) + return True else: print(f"[Emulator] Unknown command: {command}", flush=True) @@ -284,7 +295,9 @@ async def handle_command(command: str, args: list[str]) -> bool: return False -def process_message(message: str, for_ui: bool) -> str: +def process_message(message: str, for_ui: bool) -> str | None: + if message.startswith("$$CMD_BUTTON:"): + return None # Too frequent, ignore if message.startswith("$$CMD_"): if for_ui: return message @@ -338,18 +351,23 @@ async def read_stream(stream: asyncio.StreamReader, stream_type: str): if parsed: command, args = parsed await handle_command(command, args) - # Still broadcast to UI for visibility - await broadcast_message(stream_type, decoded_line) + if command == "DISPLAY": + await broadcast_message(stream_type, decoded_line) continue + to_stdio = process_message(decoded_line, for_ui=False) + to_ws = process_message(decoded_line, for_ui=True) + # Forward to parent process - if stream_type == "stdout": - print(process_message(decoded_line, for_ui=False), flush=True) - else: - print(process_message(decoded_line, for_ui=False), file=sys.stderr, flush=True) + if to_stdio is not None: + if stream_type == "stdout": + print(to_stdio, flush=True) + else: + print(to_stdio, file=sys.stderr, flush=True) # Broadcast to WebSocket clients - await broadcast_message(stream_type, process_message(decoded_line, for_ui=True)) + if to_ws is not None: + await broadcast_message(stream_type, to_ws) except Exception as e: print(f"Error reading {stream_type}: {e}", file=sys.stderr) @@ -436,16 +454,25 @@ async def websocket_handler(websocket: WebSocketServerProtocol): "data": f"$$CMD_DISPLAY:{LAST_DISPLAY_BUFFER}$$" })) - # Handle incoming messages (for stdin forwarding) + # Handle incoming messages (for stdin forwarding and events) async for message in websocket: try: data = json.loads(message) - if data.get("type") == "stdin" and qemu_process and qemu_process.stdin: + msg_type = data.get("type") + + if msg_type == "stdin" and qemu_process and qemu_process.stdin: input_data = data.get("data", "") qemu_process.stdin.write((input_data + "\n").encode()) await qemu_process.stdin.drain() + elif msg_type == "button_state": + global BUTTON_STATE + BUTTON_STATE = int(data.get("state", "0")) + print(f"[WebUI] Button state updated to {BUTTON_STATE}", flush=True) + else: + # Print any other messages for debugging + print(f"[WebUI Message] {message}", flush=True) except json.JSONDecodeError: - pass + print(f"[WebUI] Invalid JSON received: {message}", file=sys.stderr) except Exception as e: print(f"Error handling client message: {e}", file=sys.stderr) diff --git a/scripts/emulation/web_ui.html b/scripts/emulation/web_ui.html index 2d1741a0..2c5b0408 100644 --- a/scripts/emulation/web_ui.html +++ b/scripts/emulation/web_ui.html @@ -153,6 +153,46 @@ ctx.putImageData(imageData, 0, 0); } + let btn0Pressed = false; + let btn1Pressed = false; + let btn2Pressed = false; + let btn3Pressed = false; + let btnUpPressed = false; + let btnDownPressed = false; + let btnPowerPressed = false; + + let buttonState = 0; + function sendButtonState() { + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ + type: 'button_state', + state: buttonState + })); + appendLog('info', `Sent button state: ${buttonState}`); + } else { + appendLog('error', 'WebSocket not connected'); + } + } + + function setupButtonHandler(buttonId, bitIndex) { + const button = document.getElementById(buttonId); + button.onmousedown = () => { + buttonState ^= (1 << bitIndex); + sendButtonState(); + }; + button.onmouseup = () => { + buttonState &= ~(1 << bitIndex); + sendButtonState(); + }; + } + setupButtonHandler('btn0', 0); + setupButtonHandler('btn1', 1); + setupButtonHandler('btn2', 2); + setupButtonHandler('btn3', 3); + setupButtonHandler('btnU', 4); + setupButtonHandler('btnD', 5); + setupButtonHandler('btnP', 6); + function connect() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws`;