feat(xteink): support direct emulator workflow

This commit is contained in:
2026-07-24 17:12:12 -04:00
parent fe35cb68d6
commit 1ede8fe42c
6 changed files with 41 additions and 9 deletions
+1
View File
@@ -1,6 +1,7 @@
/GNUmakefile
/build/
/build-native/
/dist/
/subprojects/.wraplock
/.cache/
/.vscode/
+6 -2
View File
@@ -26,12 +26,16 @@ quiet-command-run = $(if $(V),,$(if $2,printf " %-7s %s\n" $2 $3 && ))$1
quiet-@ = $(if $(V),,@)
quiet-command = $(quiet-@)$(call quiet-command-run,$1,$2,$3)
UNCHECKED_GOALS := TAGS gtags cscope ctags dist \
UNCHECKED_GOALS := TAGS gtags cscope ctags dist xteink \
help check-help print-% \
docker docker-% lcitool-refresh vm-help vm-test vm-build-%
all:
.PHONY: all clean distclean recurse-all dist msi FORCE
.PHONY: all clean distclean recurse-all dist msi xteink FORCE
xteink:
@mkdir -p dist
nix build .#qemu-native-xteink --builders '' --out-link dist/qemu-native
# Don't try to regenerate Makefile or configure
# We don't generate any of them
+3 -2
View File
@@ -26,10 +26,11 @@ nix develop
ninja -C build qemu-system-riscv32
```
Or build the native xteink target directly:
Or build the native xteink target into `dist/qemu-native` for direct script use:
```sh
nix build .#qemu-native-xteink
make xteink
uv run --script scripts/xteink-emu.py --help
```
### Agent emulator
+1 -1
View File
@@ -94,7 +94,7 @@
export XTEINK_QEMU=${qemu-native-xteink}/bin/qemu-system-riscv32
export XTEINK_BIOS=${qemu-native-xteink}/share/qemu
export XTEINK_BOOTLOADER=${./scripts/assets/esp32c3-bootloader.bin}
export XTEINK_PARTITIONS=${./scripts/assets/crosspoint-partitions.bin}
export XTEINK_PARTITIONS=${./scripts/assets/xteink-partitions.bin}
exec uv run --script ${./scripts/xteink-emu.py} "$@"
'';
};
+30 -4
View File
@@ -20,6 +20,13 @@ FLASH_SIZE = 16 * 1024 * 1024
APP_OFFSET = 0x10000
PARTITIONS_OFFSET = 0x8000
RELEASED_ADC = 4095
PROJECT_ROOT = Path(__file__).resolve().parent.parent
DEFAULT_PATHS = {
"XTEINK_QEMU": PROJECT_ROOT / "dist/qemu-native/bin/qemu-system-riscv32",
"XTEINK_BIOS": PROJECT_ROOT / "dist/qemu-native/share/qemu",
"XTEINK_BOOTLOADER": PROJECT_ROOT / "scripts/assets/esp32c3-bootloader.bin",
"XTEINK_PARTITIONS": PROJECT_ROOT / "scripts/assets/xteink-partitions.bin",
}
BUTTONS = {
"left": ("/machine/adc", "adci[2]", 2242, RELEASED_ADC),
"right": ("/machine/adc", "adci[2]", 5, RELEASED_ADC),
@@ -40,10 +47,10 @@ def state_dir(args):
def require_env(name):
value = os.environ.get(name)
if not value:
raise RuntimeError(f"{name} is not set; run xteink-emu through its Nix app")
return Path(value)
path = Path(os.environ.get(name, DEFAULT_PATHS[name]))
if not path.exists():
raise RuntimeError(f"{path} does not exist; run 'make xteink'")
return path
def pid_from(state):
@@ -174,6 +181,13 @@ def start(args):
raise RuntimeError(f"QEMU exited during startup\n{message}")
try:
qmp_command(state, "query-status")
if args.power_hold_ms:
path, prop, pressed, released = BUTTONS["power"]
qmp_command(state, "qom-set", {"path": path, "property": prop, "value": pressed})
try:
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'}")
@@ -238,6 +252,12 @@ def button(args):
state = state_dir(args)
require_running(state)
path, prop, pressed, released = BUTTONS[args.name]
if args.down:
qmp_command(state, "qom-set", {"path": path, "property": prop, "value": pressed})
return
if args.up:
qmp_command(state, "qom-set", {"path": path, "property": prop, "value": released})
return
qmp_command(state, "qom-set", {"path": path, "property": prop, "value": pressed})
try:
time.sleep(args.hold_ms / 1000)
@@ -265,6 +285,7 @@ def parser():
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.set_defaults(handler=start)
command = commands.add_parser("stop")
@@ -281,6 +302,9 @@ def parser():
add_state(command)
command.add_argument("name", choices=BUTTONS)
command.add_argument("--hold-ms", type=int, default=100)
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")
@@ -296,6 +320,8 @@ def main():
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, "power_hold_ms", 0) < 0:
raise RuntimeError("--power-hold-ms must not be negative")
args.handler(args)