feat(xteink): add interactive emulator mode
This commit is contained in:
+2
-1
@@ -37,13 +37,14 @@ uv run --script scripts/xteink-emu.py --help
|
||||
|
||||
```sh
|
||||
nix run .#xteink-emu -- start --firmware firmware.bin --sdcard sdcard.img
|
||||
nix run .#xteink-emu -- interactive --firmware firmware.bin --sdcard sdcard.img
|
||||
nix run .#xteink-emu -- wait --timeout 30 --match 'ready'
|
||||
nix run .#xteink-emu -- button bottom-2 --hold-ms 100
|
||||
nix run .#xteink-emu -- screenshot --output screen.png
|
||||
nix run .#xteink-emu -- stop
|
||||
```
|
||||
|
||||
State defaults to `/tmp/xteink-emu-$UID`; pass `--state-dir` to any subcommand for parallel instances. `wait` advances a persistent log cursor after each match. The emulator exposes the open Wi-Fi network `qemu`; guest traffic uses QEMU's user-mode NAT.
|
||||
State defaults to `/tmp/xteink-emu-$UID`; pass `--state-dir` to any subcommand for parallel instances. `interactive` opens the display and maps Left/Right, 1–4, and P to the matching device buttons. Hold P for a long power press and close the window to stop. `wait` advances a persistent log cursor after each match. The emulator exposes the open Wi-Fi network `qemu`; guest traffic uses QEMU's user-mode NAT.
|
||||
|
||||
### WebAssembly
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
buildInputs = with pkgs; [
|
||||
dtc
|
||||
glib
|
||||
gtk3
|
||||
libgcrypt
|
||||
libpng
|
||||
libslirp
|
||||
@@ -66,6 +67,7 @@
|
||||
--without-default-features \
|
||||
--enable-system \
|
||||
--enable-gcrypt \
|
||||
--enable-gtk \
|
||||
--enable-pixman \
|
||||
--enable-png \
|
||||
--enable-slirp \
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
#include "hw/display/xteink_x3_eink.h"
|
||||
#include "hw/sd/sd.h"
|
||||
#include "hw/net/can/esp32c3_twai.h"
|
||||
#include "ui/console.h"
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten/emscripten.h>
|
||||
@@ -795,6 +796,56 @@ static void esp32c3_machine_init(MachineState *machine)
|
||||
}
|
||||
|
||||
|
||||
static void xteink_key_event(DeviceState *dev, QemuConsole *src,
|
||||
InputEvent *evt)
|
||||
{
|
||||
Esp32C3MachineState *ms = ESP32C3_MACHINE(qdev_get_machine());
|
||||
InputKeyEvent *key = evt->u.key.data;
|
||||
int qcode = qemu_input_key_value_to_qcode(key->key);
|
||||
int channel = -1;
|
||||
uint32_t pressed = 0;
|
||||
|
||||
switch (qcode) {
|
||||
case Q_KEY_CODE_LEFT:
|
||||
channel = 2;
|
||||
pressed = 2242;
|
||||
break;
|
||||
case Q_KEY_CODE_RIGHT:
|
||||
channel = 2;
|
||||
pressed = 5;
|
||||
break;
|
||||
case Q_KEY_CODE_1:
|
||||
channel = 1;
|
||||
pressed = 3512;
|
||||
break;
|
||||
case Q_KEY_CODE_2:
|
||||
channel = 1;
|
||||
pressed = 2694;
|
||||
break;
|
||||
case Q_KEY_CODE_3:
|
||||
channel = 1;
|
||||
pressed = 1493;
|
||||
break;
|
||||
case Q_KEY_CODE_4:
|
||||
channel = 1;
|
||||
pressed = 5;
|
||||
break;
|
||||
case Q_KEY_CODE_P:
|
||||
esp32_gpio_set_input_level(&ms->gpio.parent, 3, !key->down);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
qatomic_set(&ms->adc.input[channel], key->down ? pressed : 4095);
|
||||
}
|
||||
|
||||
static const QemuInputHandler xteink_key_handler = {
|
||||
.name = "xteink buttons",
|
||||
.mask = INPUT_EVENT_MASK_KEY,
|
||||
.event = xteink_key_event,
|
||||
};
|
||||
|
||||
static void xteink_machine_init(MachineState *machine)
|
||||
{
|
||||
Esp32C3MachineState *ms = ESP32C3_MACHINE(machine);
|
||||
@@ -814,6 +865,7 @@ static void xteink_machine_init(MachineState *machine)
|
||||
|
||||
DeviceState *panel = ssi_create_peripheral(ms->spi2.bus,
|
||||
ms->x4 ? TYPE_XTEINK_X4_EINK : TYPE_XTEINK_X3_EINK);
|
||||
qemu_input_handler_register(panel, &xteink_key_handler);
|
||||
qdev_connect_gpio_out_named(DEVICE(&ms->gpio), ESP32_GPIO_OUTPUT, 21,
|
||||
qdev_get_gpio_in_named(panel, SSI_GPIO_CS, 0));
|
||||
qdev_connect_gpio_out_named(DEVICE(&ms->gpio), ESP32_GPIO_OUTPUT, 4,
|
||||
|
||||
+60
-22
@@ -123,7 +123,7 @@ def build_flash(firmware, output):
|
||||
output.write_bytes(flash)
|
||||
|
||||
|
||||
def start(args):
|
||||
def run(args, interactive):
|
||||
state = state_dir(args)
|
||||
old_pid = pid_from(state)
|
||||
if process_alive(old_pid):
|
||||
@@ -140,6 +140,7 @@ def start(args):
|
||||
for name in ("pid", "qmp.sock", "serial.log", "qemu.log", "wait.offset"):
|
||||
(state / name).unlink(missing_ok=True)
|
||||
(state / "serial.log").touch()
|
||||
(state / "qemu.log").touch()
|
||||
(state / "wait.offset").write_text("0\n")
|
||||
flash = state / "flash.bin"
|
||||
build_flash(firmware, flash)
|
||||
@@ -153,8 +154,8 @@ def start(args):
|
||||
"-machine", "xteink,variant=x3",
|
||||
"-accel", "tcg",
|
||||
"-L", str(require_env("XTEINK_BIOS")),
|
||||
"-display", "none",
|
||||
"-serial", f"file:{state / 'serial.log'}",
|
||||
"-display", "gtk" if interactive else "none",
|
||||
"-serial", "stdio" if interactive else f"file:{state / 'serial.log'}",
|
||||
"-monitor", "none",
|
||||
"-qmp", f"unix:{state / 'qmp.sock'},server=on,wait=off",
|
||||
"-nic", "user,model=esp32c3_wifi,mac=52:54:00:12:34:56,net=192.168.4.0/24",
|
||||
@@ -163,15 +164,17 @@ def start(args):
|
||||
"-global", "driver=nvram.esp32c3.efuse,property=drive,value=efuse",
|
||||
"-drive", f"file={sdcard},if=sd,format=raw",
|
||||
]
|
||||
qemu_log = (state / "qemu.log").open("ab")
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=qemu_log,
|
||||
stderr=subprocess.STDOUT,
|
||||
start_new_session=True,
|
||||
)
|
||||
qemu_log.close()
|
||||
if interactive:
|
||||
process = subprocess.Popen(command)
|
||||
else:
|
||||
with (state / "qemu.log").open("ab") as qemu_log:
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=qemu_log,
|
||||
stderr=subprocess.STDOUT,
|
||||
start_new_session=True,
|
||||
)
|
||||
(state / "pid").write_text(f"{process.pid}\n")
|
||||
|
||||
deadline = time.monotonic() + 10
|
||||
@@ -188,14 +191,42 @@ def start(args):
|
||||
time.sleep(args.power_hold_ms / 1000)
|
||||
finally:
|
||||
qmp_command(state, "qom-set", {"path": path, "property": prop, "value": released})
|
||||
print(f"started PID {process.pid}")
|
||||
print(f"state: {state}")
|
||||
print(f"serial log: {state / 'serial.log'}")
|
||||
return
|
||||
break
|
||||
except (FileNotFoundError, ConnectionRefusedError, socket.timeout):
|
||||
time.sleep(0.05)
|
||||
process.terminate()
|
||||
raise RuntimeError("QEMU did not open its QMP socket")
|
||||
else:
|
||||
process.terminate()
|
||||
raise RuntimeError("QEMU did not open its QMP socket")
|
||||
|
||||
print(f"started PID {process.pid}")
|
||||
print(f"state: {state}")
|
||||
if not interactive:
|
||||
print(f"serial log: {state / 'serial.log'}")
|
||||
return
|
||||
|
||||
print("keys: Left/Right, 1-4 bottom buttons, P power; close the window to stop")
|
||||
try:
|
||||
returncode = process.wait()
|
||||
except KeyboardInterrupt:
|
||||
if process.poll() is None:
|
||||
try:
|
||||
qmp_command(state, "quit")
|
||||
except OSError:
|
||||
process.terminate()
|
||||
returncode = process.wait()
|
||||
finally:
|
||||
(state / "pid").unlink(missing_ok=True)
|
||||
(state / "qmp.sock").unlink(missing_ok=True)
|
||||
if returncode:
|
||||
raise RuntimeError(f"QEMU exited with status {returncode}")
|
||||
|
||||
|
||||
def start(args):
|
||||
run(args, interactive=False)
|
||||
|
||||
|
||||
def interactive(args):
|
||||
run(args, interactive=True)
|
||||
|
||||
|
||||
def stop(args):
|
||||
@@ -281,13 +312,20 @@ def parser():
|
||||
def add_state(command):
|
||||
command.add_argument("--state-dir", default=default_state_dir())
|
||||
|
||||
def add_run_arguments(command):
|
||||
add_state(command)
|
||||
command.add_argument("--firmware", required=True)
|
||||
command.add_argument("--sdcard", required=True)
|
||||
command.add_argument("--power-hold-ms", type=int, default=2000)
|
||||
|
||||
command = commands.add_parser("start")
|
||||
add_state(command)
|
||||
command.add_argument("--firmware", required=True)
|
||||
command.add_argument("--sdcard", required=True)
|
||||
command.add_argument("--power-hold-ms", type=int, default=2000)
|
||||
add_run_arguments(command)
|
||||
command.set_defaults(handler=start)
|
||||
|
||||
command = commands.add_parser("interactive")
|
||||
add_run_arguments(command)
|
||||
command.set_defaults(handler=interactive)
|
||||
|
||||
command = commands.add_parser("stop")
|
||||
add_state(command)
|
||||
command.set_defaults(handler=stop)
|
||||
|
||||
Reference in New Issue
Block a user