mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-05 23:27:38 +03:00
Compare commits
5 Commits
ace58b813b
...
90e93abff9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90e93abff9 | ||
|
|
3532f90f1e | ||
|
|
72c2da971d | ||
|
|
fa05eba8a1 | ||
|
|
5783faed33 |
@ -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() {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
122
lib/hal/HalInput.cpp
Normal file
122
lib/hal/HalInput.cpp
Normal file
@ -0,0 +1,122 @@
|
||||
#include "HalInput.h"
|
||||
#include "EmulationUtils.h"
|
||||
#include <esp_sleep.h>
|
||||
|
||||
void HalInput::begin() {
|
||||
#if CROSSPOINT_EMULATED == 0
|
||||
inputMgr.begin();
|
||||
#endif
|
||||
}
|
||||
|
||||
void HalInput::update() {
|
||||
#if CROSSPOINT_EMULATED == 0
|
||||
inputMgr.update();
|
||||
#else
|
||||
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
|
||||
}
|
||||
|
||||
bool HalInput::isPressed(uint8_t buttonIndex) const {
|
||||
#if CROSSPOINT_EMULATED == 0
|
||||
return inputMgr.isPressed(buttonIndex);
|
||||
#else
|
||||
return currentState & (1 << buttonIndex);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool HalInput::wasPressed(uint8_t buttonIndex) const {
|
||||
#if CROSSPOINT_EMULATED == 0
|
||||
return inputMgr.wasPressed(buttonIndex);
|
||||
#else
|
||||
return currentState & (1 << buttonIndex);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool HalInput::wasAnyPressed() const {
|
||||
#if CROSSPOINT_EMULATED == 0
|
||||
return inputMgr.wasAnyPressed();
|
||||
#else
|
||||
return pressedEvents > 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool HalInput::wasReleased(uint8_t buttonIndex) const {
|
||||
#if CROSSPOINT_EMULATED == 0
|
||||
return inputMgr.wasReleased(buttonIndex);
|
||||
#else
|
||||
return releasedEvents & (1 << buttonIndex);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool HalInput::wasAnyReleased() const {
|
||||
#if CROSSPOINT_EMULATED == 0
|
||||
return inputMgr.wasAnyReleased();
|
||||
#else
|
||||
return releasedEvents > 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
unsigned long HalInput::getHeldTime() const {
|
||||
#if CROSSPOINT_EMULATED == 0
|
||||
return inputMgr.getHeldTime();
|
||||
#else
|
||||
// Still hold a button
|
||||
if (currentState > 0) {
|
||||
return millis() - buttonPressStart;
|
||||
}
|
||||
|
||||
return buttonPressFinish - buttonPressStart;
|
||||
#endif
|
||||
}
|
||||
|
||||
void startDeepSleep(InputManager& inputMgr) {
|
||||
#if CROSSPOINT_EMULATED == 0
|
||||
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
|
||||
while (inputMgr.isPressed(InputManager::BTN_POWER)) {
|
||||
delay(50);
|
||||
inputMgr.update();
|
||||
}
|
||||
// Enter Deep Sleep
|
||||
esp_deep_sleep_start();
|
||||
#else
|
||||
Serial.println("[ ] GPIO wakeup setup skipped in emulation mode.");
|
||||
#endif
|
||||
}
|
||||
52
lib/hal/HalInput.h
Normal file
52
lib/hal/HalInput.h
Normal file
@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#if CROSSPOINT_EMULATED == 0
|
||||
#include <InputManager.h>
|
||||
#endif
|
||||
|
||||
class HalInput {
|
||||
#if CROSSPOINT_EMULATED == 0
|
||||
InputManager inputMgr;
|
||||
#endif
|
||||
|
||||
public:
|
||||
HalInput() = default;
|
||||
void begin();
|
||||
|
||||
void update();
|
||||
bool isPressed(uint8_t buttonIndex) const;
|
||||
bool wasPressed(uint8_t buttonIndex) const;
|
||||
bool wasAnyPressed() const;
|
||||
bool wasReleased(uint8_t buttonIndex) const;
|
||||
bool wasAnyReleased() const;
|
||||
unsigned long getHeldTime() const;
|
||||
|
||||
// Button indices
|
||||
static constexpr uint8_t BTN_BACK = 0;
|
||||
static constexpr uint8_t BTN_CONFIRM = 1;
|
||||
static constexpr uint8_t BTN_LEFT = 2;
|
||||
static constexpr uint8_t BTN_RIGHT = 3;
|
||||
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
|
||||
|
||||
void startDeepSleep(InputManager& inputMgr);
|
||||
@ -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
|
||||
|
||||
@ -3,21 +3,42 @@ FROM debian:12
|
||||
WORKDIR /root
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG ESP_IDF_VERSION=v5.5.2
|
||||
ARG ESP_QEMU_VERSION=esp-develop-9.2.2-20250817
|
||||
|
||||
# Update container
|
||||
RUN apt-get update && apt-get upgrade -y
|
||||
|
||||
# Install dependencies
|
||||
RUN apt-get install git wget flex bison gperf python3 python3-pip python3-setuptools python3-venv cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0 \
|
||||
libgcrypt20 libglib2.0-0 libpixman-1-0 libsdl2-2.0-0 libslirp0 -y
|
||||
RUN apt-get install -y git wget flex bison gperf python3 python3-pip python3-setuptools python3-venv cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0 \
|
||||
libgcrypt20 libglib2.0-0 libpixman-1-0 libsdl2-2.0-0 libslirp0 \
|
||||
libglib2.0-dev libpixman-1-dev libgcrypt20-dev libslirp-dev libsdl2-dev
|
||||
|
||||
# Install ESP-IDF
|
||||
ENV PATH="~/.local/bin:${PATH}"
|
||||
RUN mkdir -p ~/esp && cd ~/esp && git clone --quiet --recursive https://github.com/espressif/esp-idf.git && ln -s /usr/bin/python3 /usr/bin/python
|
||||
RUN mkdir -p ~/esp && \
|
||||
cd ~/esp && git clone --recursive https://github.com/espressif/esp-idf.git -b ${ESP_IDF_VERSION} --depth 1 -j $(nproc) && \
|
||||
ln -s /usr/bin/python3 /usr/bin/python
|
||||
RUN cd ~/esp/esp-idf && export IDF_GITHUB_ASSETS="dl.espressif.com/github_assets" && ./install.sh && /bin/bash -c "source ./export.sh"
|
||||
|
||||
# Install qemu
|
||||
RUN cd ~/esp/esp-idf && python tools/idf_tools.py install qemu-riscv32
|
||||
RUN cd ~/esp/esp-idf && python tools/idf_tools.py install qemu-riscv32 qemu-xtensa
|
||||
|
||||
# Or, build qemu from source
|
||||
# RUN git clone https://github.com/espressif/qemu.git -b ${ESP_QEMU_VERSION} --depth 1 \
|
||||
# && cd qemu \
|
||||
# && mkdir -p build \
|
||||
# && cd build \
|
||||
# && ../configure --target-list=riscv32-softmmu \
|
||||
# --enable-gcrypt \
|
||||
# --enable-slirp \
|
||||
# --enable-debug \
|
||||
# --enable-sdl \
|
||||
# --disable-strip --disable-user \
|
||||
# --disable-capstone --disable-vnc \
|
||||
# --disable-gtk \
|
||||
# && make -j $(nproc) vga=no \
|
||||
# && make install
|
||||
|
||||
RUN echo "alias idf='source /root/esp/esp-idf/export.sh'" >> .bashrc
|
||||
|
||||
|
||||
@ -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)
|
||||
@ -380,7 +398,7 @@ async def spawn_qemu():
|
||||
"-nographic",
|
||||
"-M", "esp32c3",
|
||||
"-drive", "file=flash.bin,if=mtd,format=raw",
|
||||
"-global", "driver=timer.esp32c3.timg,property=wdt_disable,value=true", # got panic if we don't disable WDT, why?
|
||||
# "-global", "driver=timer.esp32c3.timg,property=wdt_disable,value=true", # got panic if we don't disable WDT, why?
|
||||
]
|
||||
|
||||
# Get working directory from environment or use /tmp
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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`;
|
||||
|
||||
@ -3,4 +3,16 @@
|
||||
|
||||
#define BAT_GPIO0 0 // Battery voltage
|
||||
|
||||
static BatteryMonitor battery(BAT_GPIO0);
|
||||
class Battery {
|
||||
public:
|
||||
uint16_t readPercentage() const {
|
||||
#if CROSSPOINT_EMULATED == 0
|
||||
static const BatteryMonitor hwBattery = BatteryMonitor(BAT_GPIO0);
|
||||
return hwBattery.readPercentage();
|
||||
#else
|
||||
return 100;
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
static Battery battery;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <InputManager.h>
|
||||
#include <HalInput.h>
|
||||
|
||||
class MappedInputManager {
|
||||
public:
|
||||
|
||||
11
src/main.cpp
11
src/main.cpp
@ -2,7 +2,7 @@
|
||||
#include <HalDisplay.h>
|
||||
#include <Epub.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <InputManager.h>
|
||||
#include <HalInput.h>
|
||||
#include <HalStorage.h>
|
||||
#include <SPI.h>
|
||||
#include <builtinFonts/all.h>
|
||||
@ -184,8 +184,7 @@ void verifyWakeupLongPress() {
|
||||
if (abort) {
|
||||
// Button released too early. Returning to sleep.
|
||||
// IMPORTANT: Re-arm the wakeup trigger before sleeping again
|
||||
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||
esp_deep_sleep_start();
|
||||
startDeepSleep(inputManager);
|
||||
}
|
||||
}
|
||||
|
||||
@ -208,11 +207,7 @@ void enterDeepSleep() {
|
||||
einkDisplay.deepSleep();
|
||||
Serial.printf("[%lu] [ ] Power button press calibration value: %lu ms\n", millis(), t2 - t1);
|
||||
Serial.printf("[%lu] [ ] Entering deep sleep.\n", millis());
|
||||
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
|
||||
waitForPowerRelease();
|
||||
// Enter Deep Sleep
|
||||
esp_deep_sleep_start();
|
||||
startDeepSleep(inputManager);
|
||||
}
|
||||
|
||||
void onGoHome();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user