This commit is contained in:
Xuan Son Nguyen 2026-01-23 17:12:18 +01:00
parent 72c2da971d
commit 3532f90f1e
7 changed files with 154 additions and 29 deletions

View File

@ -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<uint8_t> 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() {

View File

@ -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<int>(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);
}
}
}

View File

@ -1,4 +1,5 @@
#include "HalInput.h"
#include "EmulationUtils.h"
#include <esp_sleep.h>
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<uint8_t>(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
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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`;