0feece9036
Split the emulator script into an xteink package so the CLI and the new web UI share one QEMU driver. The web UI uploads firmware and an SD image, boots them, drives the buttons, and long-polls the panel frame generation so the screen updates on each e-ink refresh.
167 lines
5.4 KiB
Python
167 lines
5.4 KiB
Python
"""Command line front end for the xteink emulator."""
|
|
|
|
import argparse
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from . import emu
|
|
from .web import serve
|
|
|
|
|
|
def state_dir(args):
|
|
return Path(args.state_dir).expanduser().resolve()
|
|
|
|
|
|
def run(args, interactive):
|
|
state = state_dir(args)
|
|
process = emu.launch(
|
|
state,
|
|
args.firmware,
|
|
args.sdcard,
|
|
variant=args.variant,
|
|
power_hold_ms=args.power_hold_ms,
|
|
interactive=interactive,
|
|
)
|
|
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:
|
|
emu.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):
|
|
print(f"stopped PID {emu.shutdown(state_dir(args))}")
|
|
|
|
|
|
def wait(args):
|
|
line = emu.wait_for_log(state_dir(args), args.match, args.timeout)
|
|
print(line, end="" if line.endswith("\n") else "\n")
|
|
|
|
|
|
def button(args):
|
|
state = state_dir(args)
|
|
emu.require_running(state)
|
|
if (args.down or args.up) and len(args.name) != 1:
|
|
raise RuntimeError("--down/--up accept exactly one button")
|
|
|
|
for name in args.name:
|
|
if args.down or args.up:
|
|
emu.set_button(state, name, down=args.down)
|
|
return
|
|
emu.press(state, name, args.samples, args.timeout, args.hold_ms)
|
|
|
|
|
|
def screenshot(args):
|
|
state = state_dir(args)
|
|
emu.require_running(state)
|
|
print(emu.screendump(state, args.output, args.settle_ms))
|
|
|
|
|
|
def web(args):
|
|
serve(state_dir(args), args.host, args.port, args.log_bytes)
|
|
|
|
|
|
def parser():
|
|
root = argparse.ArgumentParser(prog="xteink-emu")
|
|
commands = root.add_subparsers(dest="command", required=True)
|
|
|
|
def add_state(command):
|
|
command.add_argument("--state-dir", default=emu.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("--variant", choices=emu.VARIANTS, default="x3")
|
|
command.add_argument("--power-hold-ms", type=int, default=2000)
|
|
|
|
command = commands.add_parser("start")
|
|
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)
|
|
|
|
command = commands.add_parser("wait")
|
|
add_state(command)
|
|
command.add_argument("--timeout", type=float, required=True, help="seconds")
|
|
command.add_argument("--match", required=True, help="regular expression")
|
|
command.set_defaults(handler=wait)
|
|
|
|
command = commands.add_parser("button")
|
|
add_state(command)
|
|
command.add_argument("name", nargs="+", choices=emu.BUTTONS, help="one or more buttons pressed in order")
|
|
command.add_argument("--hold-ms", type=int, default=500, help="hold time for the power button")
|
|
command.add_argument("--samples", type=int, default=4, help="guest ADC reads required per press and release")
|
|
command.add_argument("--timeout", type=float, default=90, help="seconds to wait for guest ADC sampling")
|
|
button_mode = command.add_mutually_exclusive_group()
|
|
button_mode.add_argument("--down", action="store_true", help="press and hold")
|
|
button_mode.add_argument("--up", action="store_true", help="release")
|
|
command.set_defaults(handler=button)
|
|
|
|
command = commands.add_parser("screenshot")
|
|
add_state(command)
|
|
command.add_argument("--output", required=True)
|
|
command.add_argument("--settle-ms", type=int, default=1500, help="wait for the panel to refresh before capturing")
|
|
command.set_defaults(handler=screenshot)
|
|
|
|
command = commands.add_parser("web")
|
|
add_state(command)
|
|
command.add_argument("--host", default="127.0.0.1")
|
|
command.add_argument("--port", type=int, default=8080)
|
|
command.add_argument("--log-bytes", type=int, default=200_000, help="tail size of the serial log")
|
|
command.set_defaults(handler=web)
|
|
return root
|
|
|
|
|
|
def main():
|
|
args = parser().parse_args()
|
|
if getattr(args, "timeout", 0) < 0:
|
|
raise RuntimeError("--timeout must not be negative")
|
|
if getattr(args, "hold_ms", 0) < 0:
|
|
raise RuntimeError("--hold-ms must not be negative")
|
|
if getattr(args, "samples", 1) < 1:
|
|
raise RuntimeError("--samples must be positive")
|
|
if getattr(args, "settle_ms", 0) < 0:
|
|
raise RuntimeError("--settle-ms must not be negative")
|
|
if getattr(args, "power_hold_ms", 0) < 0:
|
|
raise RuntimeError("--power-hold-ms must not be negative")
|
|
args.handler(args)
|
|
|
|
|
|
def run_cli():
|
|
try:
|
|
main()
|
|
except (RuntimeError, OSError) as error:
|
|
print(f"xteink-emu: {error}", file=sys.stderr)
|
|
sys.exit(1)
|