Compare commits
35 Commits
725845d0d8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e0e65218a | |||
| 09616c2093 | |||
| 21de913b7e | |||
| 1174ef0e5d | |||
| 0feece9036 | |||
| 4cfbdfbd15 | |||
| d613b079ef | |||
| 55d801ca9e | |||
| 5721bd8102 | |||
| 5906b67287 | |||
| 0da29e8871 | |||
| cc539e448f | |||
| 5950fa2086 | |||
| 1ede8fe42c | |||
| fe35cb68d6 | |||
| ee101c8698 | |||
| 598b5e66fd | |||
| c1fc905cf2 | |||
| ec4c6cc95a | |||
| 29cab65aff | |||
| e4a6de07c6 | |||
| cd3f083fb0 | |||
| 346c789851 | |||
| ab9c5823ee | |||
| b6d67a89bb | |||
| 9e9184949d | |||
| a18a2991c6 | |||
| f46ca3b386 | |||
| 59a198dcf8 | |||
| b3ffe97fb3 | |||
| 4dd54253ab | |||
| 1473b7ab10 | |||
| e45c1e2db1 | |||
| e80c3c7088 | |||
| 3c064ae6c9 |
@@ -1,6 +1,7 @@
|
||||
/GNUmakefile
|
||||
/build/
|
||||
/build-native/
|
||||
/dist/
|
||||
/subprojects/.wraplock
|
||||
/.cache/
|
||||
/.vscode/
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
name: test-xteink-firmware
|
||||
description: "Build and interactively test Xteink ESP32-C3 firmware in the native emulator. Use when validating firmware UI, buttons, screenshots, serial output, SD behavior, or networking."
|
||||
---
|
||||
|
||||
# Test Xteink Firmware
|
||||
|
||||
## Overview
|
||||
|
||||
Build firmware for an Xteink device, boot it in the native emulator, and validate behavior through serial logs, physical controls, screenshots, SD state, and network access.
|
||||
|
||||
Resolve script paths relative to this skill directory. `scripts/xteink-emu.sh` stores the qemu-xteink checkout location, builds native QEMU when missing, and runs the emulator. If its location is unset, run the `variable.sh --set` command printed by the wrapper after asking the user for the checkout path.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Inspect the firmware project and use its documented build command. Locate the resulting ESP32-C3 application binary; for PlatformIO this is commonly:
|
||||
|
||||
```sh
|
||||
pio run
|
||||
export XTEINK_FIRMWARE=.pio/build/default/firmware.bin
|
||||
export XTEINK_EMU_STATE=/tmp/xteink-firmware-$UID
|
||||
export XTEINK_EMU_SD=/tmp/xteink-firmware-$UID.img
|
||||
```
|
||||
|
||||
2. Create a persistent FAT32 SD image once, unless the test requires a fresh card:
|
||||
|
||||
```sh
|
||||
truncate -s 64M "$XTEINK_EMU_SD"
|
||||
mkfs.fat -F 32 "$XTEINK_EMU_SD"
|
||||
```
|
||||
|
||||
3. Start a clean emulator process while preserving SD contents:
|
||||
|
||||
```sh
|
||||
scripts/xteink-emu.sh stop --state-dir "$XTEINK_EMU_STATE" || true
|
||||
scripts/xteink-emu.sh start \
|
||||
--state-dir "$XTEINK_EMU_STATE" \
|
||||
--firmware "$XTEINK_FIRMWARE" \
|
||||
--sdcard "$XTEINK_EMU_SD"
|
||||
```
|
||||
|
||||
`start` simulates a two-second power-button hold for cold boot. Override `--power-hold-ms` when testing power behavior.
|
||||
|
||||
4. Observe serial behavior with either a persistent regex cursor or the raw log:
|
||||
|
||||
```sh
|
||||
scripts/xteink-emu.sh wait \
|
||||
--state-dir "$XTEINK_EMU_STATE" --timeout 30 --match 'pattern'
|
||||
tail -f "$XTEINK_EMU_STATE/serial.log"
|
||||
```
|
||||
|
||||
5. Drive the physical controls:
|
||||
|
||||
```sh
|
||||
scripts/xteink-emu.sh button --state-dir "$XTEINK_EMU_STATE" bottom-2
|
||||
scripts/xteink-emu.sh button --state-dir "$XTEINK_EMU_STATE" power --down
|
||||
scripts/xteink-emu.sh button --state-dir "$XTEINK_EMU_STATE" power --up
|
||||
```
|
||||
|
||||
Available buttons are `left`, `right`, `bottom-1` through `bottom-4`, and `power`. Use the labels rendered by the current screen as the authoritative mapping. Allow the e-ink panel to refresh after state-changing input.
|
||||
|
||||
6. Capture and inspect the screen after meaningful transitions:
|
||||
|
||||
```sh
|
||||
scripts/xteink-emu.sh screenshot \
|
||||
--state-dir "$XTEINK_EMU_STATE" --output /tmp/xteink-screen.png
|
||||
```
|
||||
|
||||
7. For network tests, connect the firmware to the single open Wi-Fi network named `qemu`, then validate the feature's observable result.
|
||||
|
||||
## Diagnose Failures
|
||||
|
||||
- Firmware serial output and emulator messages: `$XTEINK_EMU_STATE/serial.log`
|
||||
- Assembled flash image: `$XTEINK_EMU_STATE/flash.bin`
|
||||
- Resolve crash addresses against the exact ELF produced with the tested binary, using the project's RISC-V `addr2line` tool.
|
||||
- Preserve the SD image when reproducing persistence bugs; replace it when testing first-boot behavior.
|
||||
|
||||
## Cleanup
|
||||
|
||||
```sh
|
||||
scripts/xteink-emu.sh stop --state-dir "$XTEINK_EMU_STATE"
|
||||
```
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
# Skill-local variable store. Values live in <skill-dir>/.vars/<NAME>.
|
||||
#
|
||||
# Usage:
|
||||
# variable.sh --get NAME # prints value to stdout, exits 0
|
||||
# # or prints a self-explaining hint to
|
||||
# # stderr and exits 2 if unset.
|
||||
# variable.sh --set NAME VALUE # writes value, exits 0.
|
||||
#
|
||||
# Callers should treat a non-zero exit as fatal; the stderr message tells
|
||||
# the caller (agent or user) exactly how to populate the missing value.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
STORE="$SKILL_DIR/.vars"
|
||||
SELF="$0"
|
||||
|
||||
usage() {
|
||||
cat >&2 <<EOF
|
||||
Usage:
|
||||
$SELF --get NAME
|
||||
$SELF --set NAME VALUE
|
||||
EOF
|
||||
exit 2
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
--get)
|
||||
[[ $# -eq 2 ]] || usage
|
||||
name="$2"
|
||||
file="$STORE/$name"
|
||||
if [[ ! -f "$file" ]]; then
|
||||
cat >&2 <<EOF
|
||||
$SELF: $name is not set.
|
||||
Ask the user for the value, then set it:
|
||||
$SELF --set $name <value>
|
||||
EOF
|
||||
exit 2
|
||||
fi
|
||||
cat "$file"
|
||||
;;
|
||||
--set)
|
||||
[[ $# -eq 3 ]] || usage
|
||||
name="$2"; value="$3"
|
||||
[[ "$name" =~ ^[A-Z][A-Z0-9_]*$ ]] || {
|
||||
echo >&2 "$SELF: invalid name '$name' (must match [A-Z][A-Z0-9_]*)"
|
||||
exit 2
|
||||
}
|
||||
mkdir -p "$STORE"
|
||||
# Self-ignore the store so values never get committed, even if the
|
||||
# skill root lacks a .gitignore entry for .vars/.
|
||||
[[ -f "$STORE/.gitignore" ]] || printf '*\n' > "$STORE/.gitignore"
|
||||
printf '%s' "$value" > "$STORE/$name"
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
QEMU_REPO="$("$SCRIPT_DIR/variable.sh" --get XTEINK_QEMU_REPO)"
|
||||
EMU="$QEMU_REPO/scripts/xteink-emu.py"
|
||||
QEMU="$QEMU_REPO/dist/qemu-native/bin/qemu-system-riscv32"
|
||||
|
||||
if [[ ! -f "$EMU" ]]; then
|
||||
echo >&2 "$QEMU_REPO is not a qemu-xteink checkout (missing scripts/xteink-emu.py)"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ ! -x "$QEMU" ]]; then
|
||||
make -C "$QEMU_REPO" xteink
|
||||
fi
|
||||
|
||||
exec "$EMU" "$@"
|
||||
@@ -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
|
||||
|
||||
@@ -26,6 +26,31 @@ nix develop
|
||||
ninja -C build qemu-system-riscv32
|
||||
```
|
||||
|
||||
Or build the native xteink target into `dist/qemu-native` for direct script use:
|
||||
|
||||
```sh
|
||||
make xteink
|
||||
uv run --script scripts/xteink-emu.py --help
|
||||
```
|
||||
|
||||
### Agent emulator
|
||||
|
||||
```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 -- memory
|
||||
nix run .#xteink-emu -- memory --region iram --dump iram.bin
|
||||
nix run .#xteink-emu -- stop
|
||||
nix run .#xteink-emu -- web --host 127.0.0.1 --port 8080
|
||||
```
|
||||
|
||||
The code lives in `scripts/xteink/` (`emu.py` drives QEMU, `cli.py` is the command line, `web/` is the browser UI); `scripts/xteink-emu.py` is the entry point. `web` serves a page that uploads firmware and an SD image, boots them, drives the buttons, streams the panel and serial log, and browses or edits the SD card through mtools. Card edits stop the emulator first, since QEMU caches the image; use *Apply & Reboot* to bring it back. `memory` reports how much of each RAM window the guest has written and dumps a region to a file; the web UI shows the same table under its *Memory* tab. QEMU zeroes RAM at reset, so the number is a high-water mark, not live heap — the firmware's own heap logging is authoritative for that. The screen redraws off the panel's `frame-generation` counter, so it updates on each real e-ink refresh rather than on a timer.
|
||||
|
||||
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
|
||||
|
||||
Driven by the parent repo, not here:
|
||||
|
||||
Generated
+61
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1784796856,
|
||||
"narHash": "sha256-wWFrV5/Qbm+lyt5x20E/bSbfJiGKMo4RCxZV8cl/WZI=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e2587caef70cea85dd97d7daab492899902dbf5d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -7,13 +7,108 @@
|
||||
};
|
||||
|
||||
outputs =
|
||||
{ self, nixpkgs, flake-utils }:
|
||||
{ self
|
||||
, nixpkgs
|
||||
, flake-utils
|
||||
,
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
keycodemapdb = pkgs.fetchFromGitLab {
|
||||
owner = "qemu-project";
|
||||
repo = "keycodemapdb";
|
||||
rev = "f5772a62ec52591ff6870b7e8ef32482371f22c6";
|
||||
hash = "sha256-GbZ5mrUYLXMi0IX4IZzles0Oyc095ij2xAsiLNJwfKQ=";
|
||||
};
|
||||
qemu-native-xteink = pkgs.stdenv.mkDerivation {
|
||||
pname = "qemu-native-xteink";
|
||||
version = pkgs.lib.strings.trim (builtins.readFile ./VERSION);
|
||||
src = pkgs.lib.cleanSource ./.;
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
bison
|
||||
flex
|
||||
meson
|
||||
ninja
|
||||
perl
|
||||
pkg-config
|
||||
python3
|
||||
];
|
||||
buildInputs = with pkgs; [
|
||||
dtc
|
||||
glib
|
||||
gtk3
|
||||
libgcrypt
|
||||
libpng
|
||||
libslirp
|
||||
pixman
|
||||
zlib
|
||||
];
|
||||
|
||||
dontUseMesonConfigure = true;
|
||||
|
||||
postPatch = ''
|
||||
cp -r ${keycodemapdb} subprojects/keycodemapdb
|
||||
chmod -R u+w subprojects/keycodemapdb
|
||||
sed -i "/subdir('fp')/d" tests/meson.build
|
||||
substituteInPlace meson.build --replace-fail "static: true" "static: false"
|
||||
'';
|
||||
|
||||
configurePhase = ''
|
||||
runHook preConfigure
|
||||
export PKG_CONFIG_PATH=${pkgs.libslirp}/lib/pkgconfig''${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}
|
||||
./configure \
|
||||
--prefix=$out \
|
||||
--target-list=riscv32-softmmu \
|
||||
--with-devices-riscv32=xteink \
|
||||
--with-coroutine=ucontext \
|
||||
--without-default-features \
|
||||
--enable-system \
|
||||
--enable-gcrypt \
|
||||
--enable-gtk \
|
||||
--enable-pixman \
|
||||
--enable-png \
|
||||
--enable-slirp \
|
||||
--disable-werror \
|
||||
--disable-docs \
|
||||
--disable-tools
|
||||
runHook postConfigure
|
||||
'';
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
ninja -C build qemu-system-riscv32
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
ninja -C build install
|
||||
runHook postInstall
|
||||
'';
|
||||
};
|
||||
xteink-emu = pkgs.writeShellApplication {
|
||||
name = "xteink-emu";
|
||||
runtimeInputs = [ pkgs.uv pkgs.mtools pkgs.dosfstools ];
|
||||
text = ''
|
||||
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/xteink-partitions.bin}
|
||||
exec uv run --script ${./scripts}/xteink-emu.py "$@"
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
packages.qemu-native-xteink = qemu-native-xteink;
|
||||
packages.xteink-emu = xteink-emu;
|
||||
apps.xteink-emu = {
|
||||
type = "app";
|
||||
program = "${xteink-emu}/bin/xteink-emu";
|
||||
};
|
||||
|
||||
# Native build/test shell. The wasm target is built by the parent
|
||||
# xteink-web-emulator repo via its emsdk Docker toolchain, not here.
|
||||
#
|
||||
@@ -23,16 +118,17 @@
|
||||
devShells.default = pkgs.mkShell {
|
||||
inputsFrom = [ pkgs.qemu ];
|
||||
packages = with pkgs; [
|
||||
gnumake
|
||||
dosfstools
|
||||
git
|
||||
ninja
|
||||
meson
|
||||
pkg-config
|
||||
gnumake
|
||||
libgcrypt
|
||||
libslirp
|
||||
pixman
|
||||
dosfstools
|
||||
meson
|
||||
mtools
|
||||
ninja
|
||||
pixman
|
||||
pkg-config
|
||||
uv
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,9 +26,11 @@ static uint64_t esp32c3_adc_read(void *opaque, hwaddr addr, unsigned size)
|
||||
return ADC_DONE;
|
||||
}
|
||||
if (addr == ADC_DATA1 || addr == ADC_DATA2) {
|
||||
return s->channel < ESP32C3_ADC_CHANNELS
|
||||
? MIN(qatomic_read(&s->input[s->channel]), ADC_MAX)
|
||||
: ADC_MAX;
|
||||
if (s->channel >= ESP32C3_ADC_CHANNELS) {
|
||||
return ADC_MAX;
|
||||
}
|
||||
qatomic_inc(&s->samples[s->channel]);
|
||||
return MIN(qatomic_read(&s->input[s->channel]), ADC_MAX);
|
||||
}
|
||||
return s->regs[addr / 4];
|
||||
}
|
||||
@@ -60,6 +62,7 @@ static void esp32c3_adc_reset_hold(Object *obj, ResetType type)
|
||||
memset(s->regs, 0, sizeof(s->regs));
|
||||
for (int i = 0; i < ESP32C3_ADC_CHANNELS; i++) {
|
||||
s->input[i] = ADC_MAX;
|
||||
s->samples[i] = 0;
|
||||
}
|
||||
s->channel = 0;
|
||||
}
|
||||
@@ -75,6 +78,8 @@ static void esp32c3_adc_init(Object *obj)
|
||||
for (int i = 0; i < ESP32C3_ADC_CHANNELS; i++) {
|
||||
object_property_add_uint32_ptr(obj, "adci[*]", &s->input[i],
|
||||
OBJ_PROP_FLAG_READWRITE);
|
||||
object_property_add_uint32_ptr(obj, "adcsamples[*]", &s->samples[i],
|
||||
OBJ_PROP_FLAG_READ);
|
||||
}
|
||||
esp32c3_adc_reset_hold(obj, RESET_TYPE_COLD);
|
||||
}
|
||||
|
||||
@@ -11,11 +11,21 @@
|
||||
#include "hw/irq.h"
|
||||
#include "hw/display/xteink_x3_eink.h"
|
||||
|
||||
/* Frame Counter - Both the wasm front end and the QMP web UI poll this to redraw only on a real
|
||||
* panel refresh. Only one panel is instantiated per machine, so a single counter suffices. */
|
||||
static uint32_t xteink_frame_generation;
|
||||
|
||||
static void xteink_frame_generation_init(Object *obj)
|
||||
{
|
||||
object_property_add_uint32_ptr(obj, "frame-generation",
|
||||
&xteink_frame_generation,
|
||||
OBJ_PROP_FLAG_READ);
|
||||
}
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten/emscripten.h>
|
||||
|
||||
static QemuConsole *xteink_wasm_console;
|
||||
static uint32_t xteink_wasm_generation;
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE uintptr_t xteink_wasm_framebuffer(void)
|
||||
{
|
||||
@@ -51,17 +61,18 @@ EMSCRIPTEN_KEEPALIVE int xteink_wasm_stride(void)
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE uint32_t xteink_wasm_frame_generation(void)
|
||||
{
|
||||
return qatomic_read(&xteink_wasm_generation);
|
||||
return qatomic_read(&xteink_frame_generation);
|
||||
}
|
||||
|
||||
static void xteink_wasm_frame_updated(QemuConsole *console)
|
||||
static void xteink_frame_updated(QemuConsole *console)
|
||||
{
|
||||
xteink_wasm_console = console;
|
||||
qatomic_inc(&xteink_wasm_generation);
|
||||
qatomic_inc(&xteink_frame_generation);
|
||||
}
|
||||
#else
|
||||
static void xteink_wasm_frame_updated(QemuConsole *console)
|
||||
static void xteink_frame_updated(QemuConsole *console)
|
||||
{
|
||||
qatomic_inc(&xteink_frame_generation);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -220,7 +231,7 @@ static void xteink_x3_eink_render(XteinkX3EinkState *s)
|
||||
}
|
||||
}
|
||||
|
||||
xteink_wasm_frame_updated(s->console);
|
||||
xteink_frame_updated(s->console);
|
||||
dpy_gfx_update(s->console, 0, 0, XTEINK_X3_HEIGHT, XTEINK_X3_WIDTH);
|
||||
}
|
||||
|
||||
@@ -326,6 +337,7 @@ static void xteink_x3_eink_init(Object *obj)
|
||||
{
|
||||
XteinkX3EinkState *s = XTEINK_X3_EINK(obj);
|
||||
|
||||
xteink_frame_generation_init(obj);
|
||||
s->console = graphic_console_init(DEVICE(s), 0,
|
||||
&xteink_x3_eink_graphics_ops, s);
|
||||
dpy_gfx_replace_surface(s->console,
|
||||
@@ -378,7 +390,7 @@ static void xteink_x4_eink_render(XteinkX4EinkState *s)
|
||||
DisplaySurface *surface = qemu_console_surface(s->console);
|
||||
memset(surface_data(surface), 0xff,
|
||||
surface_stride(surface) * surface_height(surface));
|
||||
xteink_wasm_frame_updated(s->console);
|
||||
xteink_frame_updated(s->console);
|
||||
dpy_gfx_update(s->console, 0, 0, 480, 800);
|
||||
}
|
||||
|
||||
@@ -412,6 +424,7 @@ static void xteink_x4_eink_init(Object *obj)
|
||||
{
|
||||
XteinkX4EinkState *s = XTEINK_X4_EINK(obj);
|
||||
|
||||
xteink_frame_generation_init(obj);
|
||||
s->console = graphic_console_init(DEVICE(s), 0,
|
||||
&xteink_x4_eink_graphics_ops, s);
|
||||
dpy_gfx_replace_surface(s->console, qemu_create_displaysurface(480, 800));
|
||||
|
||||
@@ -27,6 +27,22 @@
|
||||
#define GPIO_ENABLE_W1TS 0x24
|
||||
#define GPIO_ENABLE_W1TC 0x28
|
||||
#define GPIO_IN 0x3c
|
||||
#define GPIO_STATUS 0x44
|
||||
#define GPIO_STATUS_W1TS 0x48
|
||||
#define GPIO_STATUS_W1TC 0x4c
|
||||
#define GPIO_PCPU_INT 0x5c
|
||||
|
||||
#define GPIO_PIN_INT_TYPE_SHIFT 7
|
||||
#define GPIO_PIN_INT_TYPE_MASK 0x7
|
||||
#define GPIO_PIN_INT_ENA_SHIFT 13
|
||||
#define GPIO_PIN_INT_ENA_CPU0 BIT(0)
|
||||
|
||||
#define GPIO_INTR_DISABLE 0
|
||||
#define GPIO_INTR_POSEDGE 1
|
||||
#define GPIO_INTR_NEGEDGE 2
|
||||
#define GPIO_INTR_ANYEDGE 3
|
||||
#define GPIO_INTR_LOW_LEVEL 4
|
||||
#define GPIO_INTR_HIGH_LEVEL 5
|
||||
|
||||
static void esp32_gpio_drive_outputs(Esp32GpioState *s)
|
||||
{
|
||||
@@ -38,6 +54,41 @@ static void esp32_gpio_drive_outputs(Esp32GpioState *s)
|
||||
}
|
||||
}
|
||||
|
||||
static uint32_t esp32_gpio_enabled_interrupts(Esp32GpioState *s)
|
||||
{
|
||||
uint32_t enabled = 0;
|
||||
|
||||
for (int pin = 0; pin < ESP32_GPIO_COUNT; pin++) {
|
||||
uint32_t interrupt_enable = s->pin_config[pin] >> GPIO_PIN_INT_ENA_SHIFT;
|
||||
if (interrupt_enable & GPIO_PIN_INT_ENA_CPU0) {
|
||||
enabled |= BIT(pin);
|
||||
}
|
||||
}
|
||||
return enabled;
|
||||
}
|
||||
|
||||
static void esp32_gpio_update_irq(Esp32GpioState *s)
|
||||
{
|
||||
qemu_set_irq(s->irq, !!(s->interrupt_status &
|
||||
esp32_gpio_enabled_interrupts(s)));
|
||||
}
|
||||
|
||||
static void esp32_gpio_latch_level_interrupts(Esp32GpioState *s)
|
||||
{
|
||||
uint32_t levels = qatomic_read(&s->input_level);
|
||||
|
||||
for (int pin = 0; pin < ESP32_GPIO_COUNT; pin++) {
|
||||
uint32_t type = extract32(s->pin_config[pin],
|
||||
GPIO_PIN_INT_TYPE_SHIFT,
|
||||
3);
|
||||
bool level = !!(levels & BIT(pin));
|
||||
if ((type == GPIO_INTR_LOW_LEVEL && !level) ||
|
||||
(type == GPIO_INTR_HIGH_LEVEL && level)) {
|
||||
s->interrupt_status |= BIT(pin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void esp32_gpio_set_input_level(Esp32GpioState *s, int pin, bool level)
|
||||
{
|
||||
uint32_t old_level;
|
||||
@@ -47,6 +98,18 @@ void esp32_gpio_set_input_level(Esp32GpioState *s, int pin, bool level)
|
||||
old_level = qatomic_read(&s->input_level);
|
||||
new_level = deposit32(old_level, pin, 1, level);
|
||||
} while (qatomic_cmpxchg(&s->input_level, old_level, new_level) != old_level);
|
||||
|
||||
bool old_pin_level = !!(old_level & BIT(pin));
|
||||
uint32_t type = extract32(s->pin_config[pin],
|
||||
GPIO_PIN_INT_TYPE_SHIFT, 3);
|
||||
if ((!old_pin_level && level && type == GPIO_INTR_POSEDGE) ||
|
||||
(old_pin_level && !level && type == GPIO_INTR_NEGEDGE) ||
|
||||
(old_pin_level != level && type == GPIO_INTR_ANYEDGE) ||
|
||||
(!level && type == GPIO_INTR_LOW_LEVEL) ||
|
||||
(level && type == GPIO_INTR_HIGH_LEVEL)) {
|
||||
s->interrupt_status |= BIT(pin);
|
||||
}
|
||||
esp32_gpio_update_irq(s);
|
||||
}
|
||||
|
||||
static void esp32_gpio_set_input(void *opaque, int pin, int level)
|
||||
@@ -89,7 +152,15 @@ static uint64_t esp32_gpio_read(void *opaque, hwaddr addr, unsigned int size)
|
||||
return s->strap_mode;
|
||||
case GPIO_IN:
|
||||
return qatomic_read(&s->input_level);
|
||||
case GPIO_STATUS:
|
||||
return s->interrupt_status;
|
||||
case GPIO_PCPU_INT:
|
||||
return s->interrupt_status & esp32_gpio_enabled_interrupts(s);
|
||||
default:
|
||||
if (addr >= s->pin_config_base &&
|
||||
addr < s->pin_config_base + sizeof(s->pin_config)) {
|
||||
return s->pin_config[(addr - s->pin_config_base) / 4];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -99,6 +170,14 @@ static void esp32_gpio_write(void *opaque, hwaddr addr,
|
||||
{
|
||||
Esp32GpioState *s = ESP32_GPIO(opaque);
|
||||
|
||||
if (addr >= s->pin_config_base &&
|
||||
addr < s->pin_config_base + sizeof(s->pin_config)) {
|
||||
s->pin_config[(addr - s->pin_config_base) / 4] = value;
|
||||
esp32_gpio_latch_level_interrupts(s);
|
||||
esp32_gpio_update_irq(s);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (addr) {
|
||||
case GPIO_OUT:
|
||||
s->output_level = value;
|
||||
@@ -118,6 +197,19 @@ static void esp32_gpio_write(void *opaque, hwaddr addr,
|
||||
case GPIO_ENABLE_W1TC:
|
||||
s->output_enable &= ~value;
|
||||
break;
|
||||
case GPIO_STATUS:
|
||||
s->interrupt_status = value;
|
||||
esp32_gpio_update_irq(s);
|
||||
return;
|
||||
case GPIO_STATUS_W1TS:
|
||||
s->interrupt_status |= value;
|
||||
esp32_gpio_update_irq(s);
|
||||
return;
|
||||
case GPIO_STATUS_W1TC:
|
||||
s->interrupt_status &= ~value;
|
||||
esp32_gpio_latch_level_interrupts(s);
|
||||
esp32_gpio_update_irq(s);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
@@ -137,7 +229,10 @@ static void esp32_gpio_reset_hold(Object *obj, ResetType type)
|
||||
s->input_level = UINT32_MAX;
|
||||
s->output_level = 0;
|
||||
s->output_enable = 0;
|
||||
s->interrupt_status = 0;
|
||||
memset(s->pin_config, 0, sizeof(s->pin_config));
|
||||
esp32_gpio_drive_outputs(s);
|
||||
esp32_gpio_update_irq(s);
|
||||
}
|
||||
|
||||
static void esp32_gpio_realize(DeviceState *dev, Error **errp)
|
||||
@@ -151,6 +246,7 @@ static void esp32_gpio_init(Object *obj)
|
||||
|
||||
/* Set the default value for the strap_mode property */
|
||||
object_property_set_int(obj, "strap_mode", ESP32_STRAP_MODE_FLASH_BOOT, &error_fatal);
|
||||
s->pin_config_base = 0x88;
|
||||
|
||||
memory_region_init_io(&s->iomem, obj, &uart_ops, s,
|
||||
TYPE_ESP32_GPIO, 0x1000);
|
||||
|
||||
@@ -22,8 +22,11 @@
|
||||
|
||||
static void esp32c3_gpio_init(Object *obj)
|
||||
{
|
||||
Esp32GpioState *s = ESP32_GPIO(obj);
|
||||
|
||||
/* Set the default value for the property */
|
||||
object_property_set_int(obj, "strap_mode", ESP32C3_STRAP_MODE_FLASH_BOOT, &error_fatal);
|
||||
s->pin_config_base = 0x74;
|
||||
}
|
||||
|
||||
/* If we need to override any function from the parent (reset, realize, ...), it shall be done
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
|
||||
#include "qemu/osdep.h"
|
||||
#include "qemu/log.h"
|
||||
#include "qemu/error-report.h"
|
||||
#include "qemu/guest-random.h"
|
||||
#include "qapi/error.h"
|
||||
#include "hw/hw.h"
|
||||
#include "hw/sysbus.h"
|
||||
#include "hw/misc/esp32_fe.h"
|
||||
|
||||
#define DEBUG 0
|
||||
|
||||
static uint64_t esp32_fe_read(void *opaque, hwaddr addr, unsigned int size)
|
||||
{
|
||||
uint32_t r = 0;
|
||||
Esp32FeState *s = ESP32_FE(opaque);
|
||||
r=s->mem[addr/4];
|
||||
switch(addr){
|
||||
case 0x174:
|
||||
case 0x7C:
|
||||
r=0xffffffff;
|
||||
break;
|
||||
}
|
||||
if(DEBUG) printf("esp32_fe_read 0x%04lx= 0x%08x\n",(unsigned long) addr,r);
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
static void esp32_fe_write(void *opaque, hwaddr addr, uint64_t value,
|
||||
unsigned int size) {
|
||||
Esp32FeState *s = ESP32_FE(opaque);
|
||||
if(DEBUG) printf("esp32_fe_write 0x%04lx= 0x%08lx\n",(unsigned long) addr, (unsigned long) value);
|
||||
|
||||
s->mem[addr/4]=(uint32_t)value;
|
||||
}
|
||||
|
||||
static const MemoryRegionOps esp32_fe_ops = {
|
||||
.read = esp32_fe_read,
|
||||
.write = esp32_fe_write,
|
||||
.endianness = DEVICE_LITTLE_ENDIAN,
|
||||
};
|
||||
|
||||
static void esp32_fe_init(Object *obj)
|
||||
{
|
||||
Esp32FeState *s = ESP32_FE(obj);
|
||||
SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
|
||||
|
||||
memory_region_init_io(&s->iomem, obj, &esp32_fe_ops, s,
|
||||
TYPE_ESP32_FE, 0x1000);
|
||||
sysbus_init_mmio(sbd, &s->iomem);
|
||||
memset(s->mem,0,sizeof(s->mem));
|
||||
}
|
||||
|
||||
|
||||
static const TypeInfo esp32_fe_info = {
|
||||
.name = TYPE_ESP32_FE,
|
||||
.parent = TYPE_SYS_BUS_DEVICE,
|
||||
.instance_size = sizeof(Esp32FeState),
|
||||
.instance_init = esp32_fe_init,
|
||||
};
|
||||
|
||||
static void esp32_fe_register_types(void)
|
||||
{
|
||||
type_register_static(&esp32_fe_info);
|
||||
}
|
||||
|
||||
type_init(esp32_fe_register_types)
|
||||
@@ -0,0 +1,71 @@
|
||||
#include "qemu/osdep.h"
|
||||
#include "qemu/log.h"
|
||||
#include "qemu/error-report.h"
|
||||
#include "qemu/guest-random.h"
|
||||
#include "qapi/error.h"
|
||||
#include "hw/hw.h"
|
||||
#include "hw/sysbus.h"
|
||||
#include "hw/misc/esp32_phya.h"
|
||||
|
||||
#define DEBUG 0
|
||||
|
||||
static uint32_t *g_mem;
|
||||
|
||||
static uint64_t esp32_phya_read(void *opaque, hwaddr addr, unsigned int size)
|
||||
{
|
||||
uint32_t r = 0;
|
||||
Esp32PhyaState *s = ESP32_PHYA(opaque);
|
||||
r=s->mem[addr/4];
|
||||
if(DEBUG) printf("esp32_phya_read 0x%04lx= 0x%08x\n",(unsigned long) addr,r);
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
void Esp32_WLAN_Set_Packet_Status(const uint32_t state){
|
||||
if(g_mem){
|
||||
g_mem[0x270/4] = state; //ESP32
|
||||
g_mem[0x320/4] = state; //ESP32C3
|
||||
}
|
||||
}
|
||||
|
||||
static void esp32_phya_write(void *opaque, hwaddr addr, uint64_t value,
|
||||
unsigned int size) {
|
||||
Esp32PhyaState *s = ESP32_PHYA(opaque);
|
||||
|
||||
if(DEBUG) printf("esp32_phya_write 0x%04lx= 0x%08lx\n",(unsigned long) addr, (unsigned long) value);
|
||||
|
||||
s->mem[addr/4]=(uint32_t)value;
|
||||
}
|
||||
|
||||
static const MemoryRegionOps esp32_phya_ops = {
|
||||
.read = esp32_phya_read,
|
||||
.write = esp32_phya_write,
|
||||
.endianness = DEVICE_LITTLE_ENDIAN,
|
||||
};
|
||||
|
||||
static void esp32_phya_init(Object *obj)
|
||||
{
|
||||
Esp32PhyaState *s = ESP32_PHYA(obj);
|
||||
SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
|
||||
|
||||
memory_region_init_io(&s->iomem, obj, &esp32_phya_ops, s,
|
||||
TYPE_ESP32_PHYA, 0x1000);
|
||||
sysbus_init_mmio(sbd, &s->iomem);
|
||||
memset(s->mem,0,sizeof(s->mem));
|
||||
g_mem = s->mem;
|
||||
}
|
||||
|
||||
|
||||
static const TypeInfo esp32_phya_info = {
|
||||
.name = TYPE_ESP32_PHYA,
|
||||
.parent = TYPE_SYS_BUS_DEVICE,
|
||||
.instance_size = sizeof(Esp32PhyaState),
|
||||
.instance_init = esp32_phya_init,
|
||||
};
|
||||
|
||||
static void esp32_phya_register_types(void)
|
||||
{
|
||||
type_register_static(&esp32_phya_info);
|
||||
}
|
||||
|
||||
type_init(esp32_phya_register_types)
|
||||
@@ -0,0 +1,787 @@
|
||||
/**
|
||||
* QEMU WLAN access point emulation
|
||||
*
|
||||
* Copyright (c) 2008 Clemens Kolbitsch
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
* Modifications:
|
||||
* 2008-February-24 Clemens Kolbitsch :
|
||||
* New implementation based on ne2000.c
|
||||
* 18/01/22 Martin Johnson : Modified for esp32 wifi emulation
|
||||
* 08/12/23 lcgamboa@yahoo.com : Modified for esp32c3, espnow and SoftAp wifi emulation
|
||||
*/
|
||||
|
||||
|
||||
#include "qemu/osdep.h"
|
||||
#include "net/net.h"
|
||||
#include "qemu/timer.h"
|
||||
|
||||
#include "hw/irq.h"
|
||||
#include "hw/misc/esp32_wifi.h"
|
||||
#include "hw/misc/esp32_phya.h"
|
||||
#include "esp32_wlan.h"
|
||||
#include "esp32_wlan_packet.h"
|
||||
|
||||
// 50ms between beacons
|
||||
#define BEACON_TIME 500000000
|
||||
#define INTER_FRAME_TIME 5000000
|
||||
#define WAIT_ACK_TIMEOUT 10000000
|
||||
#define DEBUG 0
|
||||
#define ENABLE_BEACON 1
|
||||
#define DEBUG_DUMPFRAMES 0
|
||||
#define DEBUG_WIRESHARK_IMPORT 0
|
||||
|
||||
// color defines
|
||||
#define BLACK 0
|
||||
#define RED 1
|
||||
#define GREEN 2
|
||||
#define YELLOW 3
|
||||
#define BLUE 4
|
||||
#define MAGNETA 5
|
||||
#define CYAN 6
|
||||
#define WHITE 7
|
||||
#define ANSI_DEFAULT() printf("\033[0m")
|
||||
#define ANSI_COLOR(f, b) printf("\033[%d;%dm", (f) + 30, (b) + 40)
|
||||
#define ANSI_FG_LCOLOR(f) printf("\033[0;%dm", (f) + 30)
|
||||
#define ANSI_FG_HCOLOR(f) printf("\033[1;%dm", (f) + 30)
|
||||
|
||||
access_point_info access_points[] = {
|
||||
{"qemu", 1, -25, {0x10, 0x01, 0x00, 0xc4, 0x0a, 0x51}},
|
||||
};
|
||||
|
||||
int nb_aps=sizeof(access_points)/sizeof(access_point_info);
|
||||
|
||||
static void Esp32_WLAN_beacon_timer(void *opaque)
|
||||
{
|
||||
struct mac80211_frame *frame;
|
||||
Esp32WifiState *s = (Esp32WifiState *)opaque;
|
||||
// only send a beacon if we are an access point
|
||||
if((ENABLE_BEACON)&&(s->mode == Esp32_Mode_Station)){
|
||||
if(s->ap_state!=Esp32_WLAN__STATE_STA_ASSOCIATED) {
|
||||
for(int i=0;i<nb_aps;i++){
|
||||
int ap = (i + s->beacon_ap)%nb_aps;
|
||||
if (access_points[ap].channel==esp32_wifi_channel) {
|
||||
memcpy(s->ap_macaddr,access_points[ap].mac_address,6);
|
||||
frame = Esp32_WLAN_create_beacon_frame(&access_points[ap]);
|
||||
Esp32_WLAN_init_ap_frame(s, frame);
|
||||
Esp32_WLAN_insert_frame(s, frame);
|
||||
break;
|
||||
}
|
||||
}
|
||||
s->beacon_ap=(s->beacon_ap+1)%nb_aps;
|
||||
}
|
||||
}
|
||||
timer_mod(s->beacon_timer, qemu_clock_get_ns(QEMU_CLOCK_REALTIME) + BEACON_TIME);
|
||||
}
|
||||
|
||||
static void Esp32_WLAN_inject_timer(void *opaque)
|
||||
{
|
||||
Esp32WifiState *s = (Esp32WifiState *)opaque;
|
||||
struct mac80211_frame *frame;
|
||||
|
||||
frame = s->inject_queue;
|
||||
if (frame) {
|
||||
// remove from queue
|
||||
s->inject_queue_size--;
|
||||
s->inject_queue = frame->next_frame;
|
||||
Esp32_sendFrame(s, (void *)frame, frame->frame_length,frame->signal_strength);
|
||||
free(frame);
|
||||
}
|
||||
if (s->inject_queue_size > 0) {
|
||||
// there are more packets... schedule
|
||||
// the timer for sending them as well
|
||||
timer_mod(s->inject_timer, qemu_clock_get_ns(QEMU_CLOCK_REALTIME) + INTER_FRAME_TIME);
|
||||
} else {
|
||||
// we wait until a new packet schedules
|
||||
// us again
|
||||
s->inject_timer_running = 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void Esp32_WLAN_Wait_ACk_timer(void *opaque)
|
||||
{
|
||||
Esp32WifiState *s = (Esp32WifiState *)opaque;
|
||||
|
||||
Esp32_WLAN_frame_delivered(s);
|
||||
}
|
||||
|
||||
static void macprint(const uint8_t *p, const char * name) {
|
||||
printf("%s: %02x:%02x:%02x:%02x:%02x:%02x\n",name, p[0],p[1],p[2],p[3],p[4],p[5]);
|
||||
}
|
||||
|
||||
static void buffer_print(uint8_t * b, unsigned size){
|
||||
for(int i=0;i < size ;i++) {
|
||||
if((i%16)==0) printf("%04x: ",i);
|
||||
printf("%02x ",b[i]);
|
||||
if((i%16)==15) printf("\n");
|
||||
}
|
||||
if((size%16)!=0)printf("\n");
|
||||
}
|
||||
|
||||
static void infoprint(struct mac80211_frame *frame) {
|
||||
if(DEBUG_DUMPFRAMES) {
|
||||
#if (DEBUG_WIRESHARK_IMPORT == 0 )
|
||||
printf("\nFrame Info type:%d subtype:%d flags:%d duration:%d length:%d\n",frame->frame_control.type,
|
||||
frame->frame_control.sub_type,frame->frame_control.flags,frame->duration_id, frame->frame_length);
|
||||
|
||||
printf("protocol_version: %i\n",frame->frame_control.protocol_version);
|
||||
printf("type: %i ",frame->frame_control.type );
|
||||
switch (frame->frame_control.type)
|
||||
{
|
||||
case IEEE80211_TYPE_MGT:
|
||||
printf("MGT\n");
|
||||
break;
|
||||
case IEEE80211_TYPE_CTL:
|
||||
printf("CTL\n");
|
||||
break;
|
||||
case IEEE80211_TYPE_DATA:
|
||||
printf("DATA\n");
|
||||
break;
|
||||
}
|
||||
printf("subtype: %i ",frame->frame_control.sub_type);
|
||||
switch (frame->frame_control.type)
|
||||
{
|
||||
case IEEE80211_TYPE_MGT:
|
||||
switch (frame->frame_control.sub_type)
|
||||
{
|
||||
case IEEE80211_TYPE_MGT_SUBTYPE_BEACON:
|
||||
printf("BEACON\n");
|
||||
break;
|
||||
case IEEE80211_TYPE_MGT_SUBTYPE_ACTION:
|
||||
printf("ACTION\n");
|
||||
break;
|
||||
case IEEE80211_TYPE_MGT_SUBTYPE_PROBE_REQ:
|
||||
printf("PROBE_REQ\n");
|
||||
break;
|
||||
case IEEE80211_TYPE_MGT_SUBTYPE_PROBE_RESP:
|
||||
printf("PROBE_RESP\n");
|
||||
break;
|
||||
case IEEE80211_TYPE_MGT_SUBTYPE_AUTHENTICATION:
|
||||
printf("AUTHENTICATION\n");
|
||||
break;
|
||||
case IEEE80211_TYPE_MGT_SUBTYPE_DEAUTHENTICATION:
|
||||
printf("DEAUTHENTICATION\n");
|
||||
break;
|
||||
case IEEE80211_TYPE_MGT_SUBTYPE_ASSOCIATION_REQ:
|
||||
printf("ASSOCIATION_REQ\n");
|
||||
break;
|
||||
case IEEE80211_TYPE_MGT_SUBTYPE_ASSOCIATION_RESP:
|
||||
printf("ASSOCIATION_RESP\n");
|
||||
break;
|
||||
case IEEE80211_TYPE_MGT_SUBTYPE_DISASSOCIATION:
|
||||
printf("DISASSOCIATION\n");
|
||||
break;
|
||||
default:
|
||||
printf("---\n");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case IEEE80211_TYPE_CTL:
|
||||
switch (frame->frame_control.sub_type)
|
||||
{
|
||||
case IEEE80211_TYPE_CTL_SUBTYPE_ACK:
|
||||
printf("ACK\n");
|
||||
break;
|
||||
default:
|
||||
printf("---\n");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case IEEE80211_TYPE_DATA:
|
||||
switch (frame->frame_control.sub_type)
|
||||
{
|
||||
case IEEE80211_TYPE_DATA_SUBTYPE_DATA:
|
||||
printf("DATA\n");
|
||||
break;
|
||||
default:
|
||||
printf("---\n");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
printf("flags: %i\n",frame->frame_control.flags);
|
||||
printf("duration_id: %i\n",frame->duration_id);
|
||||
macprint(frame->destination_address,"destination");
|
||||
macprint(frame->source_address,"source");
|
||||
macprint(frame->bssid_address,"bssid");
|
||||
printf("fragment_number: %u\n",frame->sequence_control.fragment_number);
|
||||
printf("sequence_number : %u\n",frame->sequence_control.sequence_number);
|
||||
|
||||
switch (frame->frame_control.type)
|
||||
{
|
||||
case IEEE80211_TYPE_MGT:
|
||||
switch (frame->frame_control.sub_type)
|
||||
{
|
||||
case IEEE80211_TYPE_MGT_SUBTYPE_BEACON:
|
||||
printf("beacon timestamp : %lu\n",(unsigned long) frame->beacon_info.timestamp);
|
||||
printf("beacon interval : %u\n",frame->beacon_info.interval);
|
||||
printf("beacon capability : %u\n",frame->beacon_info.capability);
|
||||
break;
|
||||
default:
|
||||
if(frame->frame_length > IEEE80211_HEADER_SIZE)
|
||||
buffer_print((uint8_t *)frame->data_and_fcs, frame->frame_length - IEEE80211_HEADER_SIZE);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case IEEE80211_TYPE_CTL:
|
||||
switch (frame->frame_control.sub_type)
|
||||
{
|
||||
default:
|
||||
if(frame->frame_length > IEEE80211_HEADER_SIZE)
|
||||
buffer_print((uint8_t *)frame->data_and_fcs, frame->frame_length - IEEE80211_HEADER_SIZE);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case IEEE80211_TYPE_DATA:
|
||||
switch (frame->frame_control.sub_type)
|
||||
{
|
||||
default:
|
||||
printf("EtherType 0x%02X%02X\n",frame->data_and_fcs[6],frame->data_and_fcs[7]);
|
||||
if( frame->data_and_fcs[6]==8 && frame->data_and_fcs[7]==0x06) {
|
||||
printf("ARP\n");
|
||||
arp_header_t *header=(arp_header_t *)&frame->data_and_fcs[8];
|
||||
printf("Operation %i\n", header->operation>>8);
|
||||
}
|
||||
if( frame->data_and_fcs[6]==8 && frame->data_and_fcs[7]==0x00) {
|
||||
printf("IPV4\n");
|
||||
ip_header_t *header=(ip_header_t *)&frame->data_and_fcs[8];
|
||||
printf("Protocol: 0x%04X\n",header->protocol);
|
||||
switch (header->protocol)
|
||||
{
|
||||
case 0x11:
|
||||
printf("UDP\n");
|
||||
break;
|
||||
case 0x06:
|
||||
printf("TCP\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if( frame->data_and_fcs[6]==8 && frame->data_and_fcs[7]==0xdd) {
|
||||
printf("IPV6\n");
|
||||
}
|
||||
if(frame->frame_length > IEEE80211_HEADER_SIZE)
|
||||
buffer_print((uint8_t *)frame->data_and_fcs, frame->frame_length - IEEE80211_HEADER_SIZE);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
printf("frame_length : %u\n", frame->frame_length);
|
||||
#else
|
||||
uint8_t *b=(uint8_t *)frame;
|
||||
for(int i=0;i<frame->frame_length;i++) {
|
||||
if((i%16)==0) printf("\n%04x: ",i);
|
||||
printf("%02x ",b[i]);
|
||||
}
|
||||
printf("\n");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void Esp32_WLAN_insert_frame(Esp32WifiState *s, struct mac80211_frame *frame)
|
||||
{
|
||||
struct mac80211_frame *i_frame;
|
||||
|
||||
insertCRC(frame);
|
||||
if(DEBUG) {
|
||||
ANSI_FG_HCOLOR(GREEN);
|
||||
printf("---------------\n>IN Send Frame type:%d subtype:%d channel:%d ap_state:%d time:%ld \n",frame->frame_control.type,frame->frame_control.sub_type,esp32_wifi_channel,s->ap_state,(unsigned long) qemu_clock_get_ns(QEMU_CLOCK_REALTIME));
|
||||
ANSI_DEFAULT();
|
||||
}
|
||||
infoprint(frame);
|
||||
s->inject_queue_size++;
|
||||
i_frame = s->inject_queue;
|
||||
if (!i_frame) {
|
||||
s->inject_queue = frame;
|
||||
} else {
|
||||
while (i_frame->next_frame) {
|
||||
i_frame = i_frame->next_frame;
|
||||
}
|
||||
i_frame->next_frame = frame;
|
||||
}
|
||||
|
||||
if (!s->inject_timer_running) {
|
||||
// if the injection timer is not
|
||||
// running currently, let's schedule
|
||||
// one run...
|
||||
s->inject_timer_running = 1;
|
||||
timer_mod(s->inject_timer, qemu_clock_get_ns(QEMU_CLOCK_REALTIME) + INTER_FRAME_TIME);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static _Bool Esp32_WLAN_can_receive(NetClientState *ncs)
|
||||
{
|
||||
Esp32WifiState *s = qemu_get_nic_opaque(ncs);
|
||||
/*
|
||||
if (s->ap_state != Esp32_WLAN__STATE_ASSOCIATED && s->ap_state != Esp32_WLAN__STATE_STA_ASSOCIATED) {
|
||||
// we are currently not connected
|
||||
// to the access point
|
||||
return 0;
|
||||
}
|
||||
*/
|
||||
if (s->inject_queue_size > Esp32_WLAN__MAX_INJECT_QUEUE_SIZE) {
|
||||
// overload, please give me some time...
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static ssize_t Esp32_WLAN_receive(NetClientState *ncs,
|
||||
const uint8_t *buf, size_t size)
|
||||
{
|
||||
Esp32WifiState *s = qemu_get_nic_opaque(ncs);
|
||||
struct mac80211_frame *frame;
|
||||
if (!Esp32_WLAN_can_receive(ncs)) {
|
||||
// this should not happen, but in
|
||||
// case it does, let's simply drop
|
||||
// the packet
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!s) {
|
||||
return -1;
|
||||
}
|
||||
/*
|
||||
* A 802.3 packet comes from the qemu network. The
|
||||
* access points turns it into a 802.11 frame and
|
||||
* forwards it to the wireless device
|
||||
*/
|
||||
frame = Esp32_WLAN_create_data_packet(s, buf, size);
|
||||
if (frame) {
|
||||
if(s->mode == Esp32_Mode_Station){
|
||||
memcpy(s->ap_macaddr,s->associated_ap_macaddr,6);
|
||||
|
||||
if(s->ap_state==Esp32_WLAN__STATE_STA_ASSOCIATED) {
|
||||
frame->frame_control.flags=1;
|
||||
// if it's an arp request put the correct reply mac address in the packet
|
||||
if( frame->data_and_fcs[6]==8 && frame->data_and_fcs[7]==6) {
|
||||
memcpy(frame->data_and_fcs+16,s->macaddr,6);
|
||||
}
|
||||
}
|
||||
}
|
||||
else{
|
||||
memcpy(frame->destination_address, s->softap_macaddr,6);
|
||||
if(s->ap_state==Esp32_WLAN__STATE_STA_ASSOCIATED) {
|
||||
frame->frame_control.flags=1;
|
||||
// if it's an arp request put the correct reply mac address in the packet
|
||||
if( frame->data_and_fcs[6]==8 && frame->data_and_fcs[7]==6) {
|
||||
memcpy(frame->data_and_fcs+16,s->ap_macaddr,6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(((buf[12] & 0xE0) == IEEE80211_ENCAPSULATED)&&(buf[13] == ((IEEE80211_TYPE_MGT << 4)|IEEE80211_TYPE_MGT_SUBTYPE_ACTION)))
|
||||
{
|
||||
unsigned long ethernet_frame_size;
|
||||
unsigned char ethernet_frame[1518];
|
||||
struct mac80211_frame *reply = NULL;
|
||||
|
||||
//discard packages from others channels
|
||||
if((buf[12] & 0x0F) != esp32_wifi_channel) return -1;
|
||||
//discard packages originated from the same mac address
|
||||
if(!memcmp(&buf[6],s->macaddr,6)) return -1;
|
||||
//check destination
|
||||
if((memcmp(&buf[0],BROADCAST,6))&&(memcmp(&buf[0],s->macaddr,6))) return -1;
|
||||
|
||||
//action frame
|
||||
Esp32_WLAN_init_ap_frame(s, frame);
|
||||
memcpy(frame->data_and_fcs, buf+14, size-14);
|
||||
memcpy(frame->destination_address, &buf[0], 6);
|
||||
memcpy(frame->source_address, &buf[6], 6);
|
||||
memset(frame->bssid_address, 0xff, 6);
|
||||
frame->frame_control.type= IEEE80211_TYPE_MGT;
|
||||
frame->frame_control.sub_type= IEEE80211_TYPE_MGT_SUBTYPE_ACTION;
|
||||
frame->frame_control.flags= ((buf[12] & 0xF0) == IEEE80211_ENCAPSULATED_PROTECTED) ? 0x40 : 00;
|
||||
frame->duration_id= 0 ;
|
||||
frame->frame_length-=12;
|
||||
Esp32_WLAN_insert_frame(s, frame);
|
||||
|
||||
|
||||
//if destination is broadcast, don´t send ack
|
||||
if(!memcmp(&buf[0],BROADCAST,6)) return size;
|
||||
|
||||
//Send Ack
|
||||
reply = Esp32_WLAN_create_ack();
|
||||
memcpy(reply->destination_address, &buf[6], 6);
|
||||
memcpy(reply->source_address, s->macaddr, 6);
|
||||
memset(reply->bssid_address, 0xff, 6);
|
||||
reply->duration_id=0;
|
||||
|
||||
if(DEBUG) {
|
||||
ANSI_FG_HCOLOR(RED);
|
||||
macprint(reply->destination_address,"---------------\n<OUT ACK send: dest ");
|
||||
ANSI_DEFAULT();
|
||||
}
|
||||
infoprint(reply);
|
||||
/*
|
||||
* The access point uses the 802.11 frame
|
||||
* and sends a 802.3 frame into the network...
|
||||
* This packet is then understandable by
|
||||
* qemu-slirp
|
||||
*
|
||||
* If we ever want the access point to offer
|
||||
* some services, it can be added here!!
|
||||
*/
|
||||
// ethernet header type
|
||||
ethernet_frame[12] = IEEE80211_ENCAPSULATED | esp32_wifi_channel;
|
||||
ethernet_frame[13] = ((IEEE80211_TYPE_CTL << 4)|IEEE80211_TYPE_CTL_SUBTYPE_ACK);
|
||||
|
||||
memcpy(ðernet_frame[0], reply->destination_address, 6);
|
||||
memcpy(ðernet_frame[6], reply->source_address, 6);
|
||||
|
||||
// add size of ethernet header
|
||||
ethernet_frame_size = 14;
|
||||
/*
|
||||
* Send 802.3 frame
|
||||
*/
|
||||
qemu_send_packet(qemu_get_queue(s->nic), ethernet_frame, ethernet_frame_size);
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
if(((buf[12] & 0xF0)== IEEE80211_ENCAPSULATED)&&(buf[13] == ((IEEE80211_TYPE_CTL << 4)|IEEE80211_TYPE_CTL_SUBTYPE_ACK)))
|
||||
{
|
||||
|
||||
//discard packages from others channels
|
||||
if((buf[12] & 0x0F) != esp32_wifi_channel) return -1;
|
||||
//discard packages originated from the same mac address
|
||||
if(!memcmp(&buf[6],s->macaddr,6)) return -1;
|
||||
//check destination
|
||||
if(memcmp(&buf[0],s->macaddr,6)) return -1;
|
||||
|
||||
Esp32_WLAN_Set_Packet_Status(ESP32_PHYA_ACK);
|
||||
timer_mod_anticipate(s->wait_ack_timer, qemu_clock_get_ns(QEMU_CLOCK_REALTIME));
|
||||
|
||||
Esp32_WLAN_init_ap_frame(s, frame);
|
||||
memcpy(frame->destination_address, &buf[0], 6);
|
||||
memcpy(frame->source_address, &buf[6], 6);
|
||||
memset(frame->bssid_address, 0xff, 6);
|
||||
frame->frame_control.type= IEEE80211_TYPE_CTL;
|
||||
frame->frame_control.sub_type= IEEE80211_TYPE_CTL_SUBTYPE_ACK;
|
||||
frame->frame_control.flags= 0;
|
||||
frame->duration_id= 0 ;
|
||||
frame->frame_length = 10;
|
||||
|
||||
if(DEBUG) {
|
||||
ANSI_FG_HCOLOR(GREEN);
|
||||
macprint(&buf[6],"---------------\n>IN ACK received: source");
|
||||
ANSI_DEFAULT();
|
||||
}
|
||||
infoprint(frame);
|
||||
//ACK is not inserted in DMA
|
||||
return size;
|
||||
}
|
||||
Esp32_WLAN_init_ap_frame(s, frame);
|
||||
Esp32_WLAN_insert_frame(s, frame);
|
||||
}
|
||||
return size;
|
||||
}
|
||||
static void Esp32_WLAN_cleanup(NetClientState *ncs) { }
|
||||
|
||||
static NetClientInfo net_info = {
|
||||
.type = NET_CLIENT_DRIVER_NIC,
|
||||
.size = sizeof(NICState),
|
||||
.can_receive = Esp32_WLAN_can_receive,
|
||||
.receive = Esp32_WLAN_receive,
|
||||
.cleanup = Esp32_WLAN_cleanup,
|
||||
};
|
||||
|
||||
void Esp32_WLAN_reset_ap(Esp32WifiState *s) {
|
||||
esp32_wifi_channel = 0;
|
||||
s->ap_state = Esp32_WLAN__STATE_NOT_AUTHENTICATED;
|
||||
s->beacon_ap=0;
|
||||
s->mode = Esp32_Mode_Station;
|
||||
memcpy(s->ap_macaddr,(uint8_t[]){0x10,0x13,0x46,0xbf,0x31,0x50},sizeof(s->ap_macaddr));
|
||||
|
||||
s->inject_timer_running = 0;
|
||||
s->inject_sequence_number = 0;
|
||||
|
||||
s->inject_queue = NULL;
|
||||
s->inject_queue_size = 0;
|
||||
}
|
||||
|
||||
void Esp32_WLAN_setup_ap(DeviceState *dev,Esp32WifiState *s) {
|
||||
|
||||
Esp32_WLAN_reset_ap(s);
|
||||
|
||||
s->beacon_timer = timer_new_ns(QEMU_CLOCK_REALTIME, Esp32_WLAN_beacon_timer, s);
|
||||
timer_mod(s->beacon_timer, qemu_clock_get_ns(QEMU_CLOCK_REALTIME)+BEACON_TIME);
|
||||
|
||||
// setup the timer but only schedule
|
||||
// it when necessary...
|
||||
s->inject_timer = timer_new_ns(QEMU_CLOCK_REALTIME, Esp32_WLAN_inject_timer, s);
|
||||
|
||||
s->wait_ack_timer = timer_new_ns(QEMU_CLOCK_REALTIME, Esp32_WLAN_Wait_ACk_timer, s);
|
||||
|
||||
qemu_macaddr_default_if_unset(&s->conf.macaddr);
|
||||
memcpy(s->macaddr, s->conf.macaddr.a, sizeof(s->macaddr));
|
||||
s->nic = qemu_new_nic(&net_info, &s->conf,
|
||||
object_get_typename(OBJECT(s)), dev->id,
|
||||
&dev->mem_reentrancy_guard, s);
|
||||
qemu_format_nic_info_str(qemu_get_queue(s->nic), s->macaddr);
|
||||
}
|
||||
|
||||
static void send_single_frame(Esp32WifiState *s, struct mac80211_frame *frame, struct mac80211_frame *reply) {
|
||||
reply->sequence_control.sequence_number = s->inject_sequence_number++ +0x730;
|
||||
reply->signal_strength=-10;
|
||||
|
||||
if(frame) {
|
||||
memcpy(reply->destination_address, frame->source_address, 6);
|
||||
if(s->mode == Esp32_Mode_Station){
|
||||
memcpy(reply->source_address, s->macaddr, 6);
|
||||
}
|
||||
else{
|
||||
memcpy(reply->source_address, s->ap_macaddr, 6);
|
||||
}
|
||||
memcpy(reply->bssid_address, frame->source_address, 6);
|
||||
}
|
||||
|
||||
Esp32_WLAN_insert_frame(s, reply);
|
||||
}
|
||||
|
||||
void Esp32_WLAN_handle_frame(Esp32WifiState *s, struct mac80211_frame *frame)
|
||||
{
|
||||
struct mac80211_frame *reply = NULL;
|
||||
static access_point_info dummy_ap={0};
|
||||
static char ssid[64];
|
||||
unsigned long ethernet_frame_size;
|
||||
unsigned char ethernet_frame[1518];
|
||||
|
||||
|
||||
if(DEBUG){
|
||||
if((ENABLE_BEACON)||!((frame->frame_control.type == IEEE80211_TYPE_MGT)&&(frame->frame_control.sub_type == IEEE80211_TYPE_MGT_SUBTYPE_BEACON))){
|
||||
ANSI_FG_HCOLOR(RED);
|
||||
printf("-------------------------\n<OUT Handle Frame type:%d subtype:%d channel:%d ap_state:%d time:%ld \n",frame->frame_control.type,
|
||||
frame->frame_control.sub_type,esp32_wifi_channel,s->ap_state,(unsigned long) qemu_clock_get_ns(QEMU_CLOCK_REALTIME));
|
||||
ANSI_DEFAULT();
|
||||
}
|
||||
}
|
||||
if((ENABLE_BEACON)||!((frame->frame_control.type == IEEE80211_TYPE_MGT)&&(frame->frame_control.sub_type == IEEE80211_TYPE_MGT_SUBTYPE_BEACON))){
|
||||
infoprint(frame);
|
||||
}
|
||||
access_point_info *ap_info=0;
|
||||
|
||||
if(s->mode == Esp32_Mode_Station){
|
||||
for(int i=0;i<nb_aps;i++)
|
||||
if(access_points[i].channel==esp32_wifi_channel){
|
||||
ap_info=&access_points[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Esp32_WLAN_Set_Packet_Status(ESP32_PHYA_ACK);
|
||||
if(frame->frame_control.type == IEEE80211_TYPE_MGT) {
|
||||
switch(frame->frame_control.sub_type) {
|
||||
case IEEE80211_TYPE_MGT_SUBTYPE_BEACON:
|
||||
if(s->ap_state==Esp32_WLAN__STATE_NOT_AUTHENTICATED || s->ap_state==Esp32_WLAN__STATE_AUTHENTICATED) {
|
||||
strncpy(ssid,(char *)frame->data_and_fcs+14,frame->data_and_fcs[13]);
|
||||
if(DEBUG) printf("beacon from %s\n",ssid);
|
||||
dummy_ap.ssid=ssid;
|
||||
dummy_ap.channel = esp32_wifi_channel;
|
||||
dummy_ap.sigstrength= -23;
|
||||
memcpy(dummy_ap.mac_address,s->ap_macaddr,6) ;
|
||||
memcpy(s->softap_macaddr,frame->bssid_address,6);
|
||||
s->mode = Esp32_Mode_SoftAP;
|
||||
s->ap_state=Esp32_WLAN__STATE_STA_NOT_AUTHENTICATED;
|
||||
send_single_frame(s,frame,Esp32_WLAN_create_probe_request(&dummy_ap));
|
||||
}
|
||||
break;
|
||||
case IEEE80211_TYPE_MGT_SUBTYPE_ACTION:
|
||||
if(DEBUG) printf("action\n");
|
||||
/*
|
||||
* The access point uses the 802.11 frame
|
||||
* and sends a 802.3 frame into the network...
|
||||
* This packet is then understandable by
|
||||
* qemu-slirp
|
||||
*
|
||||
* If we ever want the access point to offer
|
||||
* some services, it can be added here!!
|
||||
*/
|
||||
// ethernet header type
|
||||
ethernet_frame[12] = ((frame->frame_control.flags & 0x40) ? IEEE80211_ENCAPSULATED_PROTECTED : IEEE80211_ENCAPSULATED)| esp32_wifi_channel;
|
||||
ethernet_frame[13] = ((IEEE80211_TYPE_MGT << 4)|IEEE80211_TYPE_MGT_SUBTYPE_ACTION);
|
||||
|
||||
memcpy(ðernet_frame[0], frame->destination_address, 6);
|
||||
memcpy(ðernet_frame[6], s->macaddr, 6);
|
||||
|
||||
// add packet content
|
||||
ethernet_frame_size = frame->frame_length -IEEE80211_HEADER_SIZE;
|
||||
|
||||
if (ethernet_frame_size > sizeof(ethernet_frame)) {
|
||||
ethernet_frame_size = sizeof(ethernet_frame);
|
||||
}
|
||||
memcpy(ðernet_frame[14], &frame->data_and_fcs[0], ethernet_frame_size);
|
||||
// add size of ethernet header
|
||||
ethernet_frame_size += 14;
|
||||
/*
|
||||
* Send 802.3 frame
|
||||
*/
|
||||
qemu_send_packet(qemu_get_queue(s->nic), ethernet_frame, ethernet_frame_size);
|
||||
|
||||
//if destination is not broadcast wait for ack
|
||||
if(memcmp(ðernet_frame[0],BROADCAST,6)){
|
||||
Esp32_WLAN_Set_Packet_Status(ESP32_PHYA_NACK);
|
||||
timer_mod(s->wait_ack_timer, qemu_clock_get_ns(QEMU_CLOCK_REALTIME) + WAIT_ACK_TIMEOUT);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case IEEE80211_TYPE_MGT_SUBTYPE_PROBE_RESP:
|
||||
ap_info=&dummy_ap;
|
||||
strncpy(ssid,(char *)frame->data_and_fcs+14,frame->data_and_fcs[13]);
|
||||
if(DEBUG) printf("probe resp from %s\n",ssid);
|
||||
dummy_ap.ssid=ssid;
|
||||
s->ap_state=Esp32_WLAN__STATE_STA_NOT_AUTHENTICATED;
|
||||
send_single_frame(s,frame,Esp32_WLAN_create_deauthentication());
|
||||
send_single_frame(s,frame,Esp32_WLAN_create_authentication_request());
|
||||
break;
|
||||
case IEEE80211_TYPE_MGT_SUBTYPE_ASSOCIATION_RESP:
|
||||
if(DEBUG) printf("assoc resp\n");
|
||||
mac80211_frame *frame1=Esp32_WLAN_create_dhcp_discover(s);
|
||||
memcpy(frame1->bssid_address,BROADCAST,6);
|
||||
memcpy(frame1->source_address,frame->destination_address,6);
|
||||
memcpy(frame1->destination_address,frame->source_address,6);
|
||||
send_single_frame(s,0,frame1);
|
||||
s->ap_state=Esp32_WLAN__STATE_STA_DHCP;
|
||||
break;
|
||||
case IEEE80211_TYPE_MGT_SUBTYPE_DISASSOCIATION:
|
||||
DEBUG_PRINT_AP(("Received disassociation!\n"));
|
||||
send_single_frame(s,frame,Esp32_WLAN_create_disassociation());
|
||||
if (s->ap_state == Esp32_WLAN__STATE_ASSOCIATED || s->ap_state == Esp32_WLAN__STATE_STA_ASSOCIATED) {
|
||||
s->ap_state = Esp32_WLAN__STATE_AUTHENTICATED;
|
||||
}
|
||||
break;
|
||||
case IEEE80211_TYPE_MGT_SUBTYPE_DEAUTHENTICATION:
|
||||
DEBUG_PRINT_AP(("Received deauthentication!\n"));
|
||||
//reply = Esp32_WLAN_create_authentication_response(ap_info);
|
||||
if (s->ap_state == Esp32_WLAN__STATE_AUTHENTICATED) {
|
||||
s->ap_state = Esp32_WLAN__STATE_NOT_AUTHENTICATED;
|
||||
}
|
||||
break;
|
||||
case IEEE80211_TYPE_MGT_SUBTYPE_AUTHENTICATION:
|
||||
DEBUG_PRINT_AP(("Received authentication!\n"));
|
||||
if(frame->data_and_fcs[2]==2) { // response
|
||||
send_single_frame(s,frame,Esp32_WLAN_create_association_request(&dummy_ap));
|
||||
}
|
||||
break;
|
||||
}
|
||||
if(ap_info) {
|
||||
memcpy(s->ap_macaddr,ap_info->mac_address,6);
|
||||
switch(frame->frame_control.sub_type) {
|
||||
case IEEE80211_TYPE_MGT_SUBTYPE_PROBE_REQ:
|
||||
DEBUG_PRINT_AP(("Received probe request!\n"));
|
||||
reply = Esp32_WLAN_create_probe_response(ap_info);
|
||||
break;
|
||||
case IEEE80211_TYPE_MGT_SUBTYPE_AUTHENTICATION:
|
||||
DEBUG_PRINT_AP(("Received authentication req!\n"));
|
||||
if(frame->data_and_fcs[2]==1) { // request
|
||||
reply = Esp32_WLAN_create_authentication_response(ap_info);
|
||||
if (s->ap_state == Esp32_WLAN__STATE_NOT_AUTHENTICATED) {
|
||||
s->ap_state = Esp32_WLAN__STATE_AUTHENTICATED;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case IEEE80211_TYPE_MGT_SUBTYPE_ASSOCIATION_REQ:
|
||||
DEBUG_PRINT_AP(("Received association request!\n"));
|
||||
reply = Esp32_WLAN_create_association_response(ap_info);
|
||||
if (s->ap_state == Esp32_WLAN__STATE_AUTHENTICATED) {
|
||||
s->ap_state = Esp32_WLAN__STATE_ASSOCIATED;
|
||||
memcpy(s->associated_ap_macaddr,s->ap_macaddr,6);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (reply) {
|
||||
reply->signal_strength=ap_info->sigstrength;
|
||||
memcpy(reply->destination_address, frame->source_address, 6);
|
||||
Esp32_WLAN_init_ap_frame(s, reply);
|
||||
Esp32_WLAN_insert_frame(s, reply);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((frame->frame_control.type == IEEE80211_TYPE_DATA) &&
|
||||
(frame->frame_control.sub_type == IEEE80211_TYPE_DATA_SUBTYPE_DATA)) {
|
||||
if(s->ap_state == Esp32_WLAN__STATE_STA_DHCP) {
|
||||
dhcp_request_t *req=(dhcp_request_t *)&frame->data_and_fcs[8];
|
||||
// check for a dhcp offer
|
||||
if(req->dhcp.bp_options[0]==0x35 && req->dhcp.bp_options[2]==0x2) {
|
||||
mac80211_frame *frame1=Esp32_WLAN_create_dhcp_request(s,req->dhcp.yiaddr);
|
||||
memcpy(frame1->bssid_address,BROADCAST,6);
|
||||
memcpy(frame1->source_address,s->ap_macaddr,6);
|
||||
memcpy(frame1->destination_address,frame->source_address,6);
|
||||
send_single_frame(s,0,frame1);
|
||||
//memcpy(s->ap_macaddr,(uint8_t[]){0x10,0x01,0x00,0xc4,0x0a,0x25},sizeof(s->ap_macaddr));
|
||||
//memcpy(s->associated_ap_macaddr,s->ap_macaddr,sizeof(s->ap_macaddr));
|
||||
memcpy(s->associated_ap_macaddr,s->macaddr,sizeof(s->macaddr));
|
||||
s->ap_state=Esp32_WLAN__STATE_STA_ASSOCIATED;
|
||||
if(DEBUG) printf("Dhcp received addr %d.%d.%d.%d\n",req->dhcp.yiaddr[0],req->dhcp.yiaddr[1],req->dhcp.yiaddr[2],req->dhcp.yiaddr[3]);
|
||||
}
|
||||
} else if (s->ap_state == Esp32_WLAN__STATE_ASSOCIATED || s->ap_state == Esp32_WLAN__STATE_STA_ASSOCIATED) {
|
||||
/*
|
||||
* The access point uses the 802.11 frame
|
||||
* and sends a 802.3 frame into the network...
|
||||
* This packet is then understandable by
|
||||
* qemu-slirp
|
||||
*
|
||||
* If we ever want the access point to offer
|
||||
* some services, it can be added here!!
|
||||
*/
|
||||
// ethernet header type
|
||||
ethernet_frame[12] = frame->data_and_fcs[6];
|
||||
ethernet_frame[13] = frame->data_and_fcs[7];
|
||||
|
||||
// the new originator of the packet is
|
||||
// the access point
|
||||
if(s->ap_state == Esp32_WLAN__STATE_ASSOCIATED)
|
||||
memcpy(ðernet_frame[6], s->ap_macaddr, 6);
|
||||
else
|
||||
memcpy(ðernet_frame[6], s->macaddr, 6);
|
||||
|
||||
if (ethernet_frame[12] == 0x08 && ethernet_frame[13] == 0x06) {
|
||||
// for arp request, we use a broadcast
|
||||
memset(ðernet_frame[0], 0xff, 6);
|
||||
} else {
|
||||
// otherwise we forward the packet to
|
||||
// where it really belongs
|
||||
memcpy(ðernet_frame[0], frame->destination_address, 6);
|
||||
}
|
||||
|
||||
// add packet content
|
||||
ethernet_frame_size = frame->frame_length - IEEE80211_HEADER_SIZE - 4 - 8;
|
||||
|
||||
// for some reason, the packet is 22 bytes too small (??)
|
||||
ethernet_frame_size += 22;
|
||||
if (ethernet_frame_size > sizeof(ethernet_frame)) {
|
||||
ethernet_frame_size = sizeof(ethernet_frame);
|
||||
}
|
||||
memcpy(ðernet_frame[14], &frame->data_and_fcs[8], ethernet_frame_size);
|
||||
// add size of ethernet header
|
||||
ethernet_frame_size += 14;
|
||||
/*
|
||||
* Send 802.3 frame
|
||||
*/
|
||||
qemu_send_packet(qemu_get_queue(s->nic), ethernet_frame, ethernet_frame_size);
|
||||
}
|
||||
}
|
||||
Esp32_WLAN_frame_delivered(s);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* QEMU WLAN device emulation
|
||||
*
|
||||
* Copyright (c) 2008 Clemens Kolbitsch
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
* Modifications:
|
||||
* 2008-February-24 Clemens Kolbitsch :
|
||||
* New implementation based on ne2000.c
|
||||
* 18/1/22 Martin Johnson : Modified for esp32 wifi emulation
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef esp32_wlan_h
|
||||
#define esp32_wlan_h 1
|
||||
|
||||
|
||||
//#define DEBUG_Esp32_WLAN
|
||||
|
||||
#ifdef DEBUG_Esp32_WLAN
|
||||
#define DEBUG_PRINT_AP(x) \
|
||||
do { \
|
||||
struct timeval __tt; \
|
||||
gettimeofday(&__tt, NULL); \
|
||||
printf("%u:%u ", (unsigned)__tt.tv_sec, (unsigned)__tt.tv_usec); \
|
||||
printf x ;\
|
||||
} while (0)
|
||||
#else
|
||||
#define DEBUG_PRINT_AP(x)
|
||||
#endif
|
||||
|
||||
|
||||
#define IEEE80211_IDLE 0xff
|
||||
|
||||
#define IEEE80211_TYPE_MGT 0x00
|
||||
#define IEEE80211_TYPE_CTL 0x01
|
||||
#define IEEE80211_TYPE_DATA 0x02
|
||||
|
||||
#define IEEE80211_ENCAPSULATED 0xA0
|
||||
#define IEEE80211_ENCAPSULATED_PROTECTED 0xB0
|
||||
|
||||
#define IEEE80211_TYPE_MGT_SUBTYPE_BEACON 0x08
|
||||
#define IEEE80211_TYPE_MGT_SUBTYPE_ACTION 0x0d
|
||||
#define IEEE80211_TYPE_MGT_SUBTYPE_PROBE_REQ 0x04
|
||||
#define IEEE80211_TYPE_MGT_SUBTYPE_PROBE_RESP 0x05
|
||||
#define IEEE80211_TYPE_MGT_SUBTYPE_AUTHENTICATION 0x0b
|
||||
#define IEEE80211_TYPE_MGT_SUBTYPE_DEAUTHENTICATION 0x0c
|
||||
#define IEEE80211_TYPE_MGT_SUBTYPE_ASSOCIATION_REQ 0x00
|
||||
#define IEEE80211_TYPE_MGT_SUBTYPE_ASSOCIATION_RESP 0x01
|
||||
#define IEEE80211_TYPE_MGT_SUBTYPE_DISASSOCIATION 0x0a
|
||||
|
||||
#define IEEE80211_TYPE_CTL_SUBTYPE_ACK 0x0d
|
||||
|
||||
#define IEEE80211_TYPE_DATA_SUBTYPE_DATA 0x00
|
||||
|
||||
#define IEEE80211_BEACON_PARAM_SSID 0x00
|
||||
#define IEEE80211_BEACON_PARAM_RATES 0x01
|
||||
#define IEEE80211_BEACON_PARAM_CHANNEL 0x03
|
||||
#define IEEE80211_BEACON_PARAM_EXTENDED_RATES 0x32
|
||||
#define IEEE80211_BEACON_PARAM_TIM 0x05
|
||||
|
||||
|
||||
#define IEEE80211_HEADER_SIZE 24
|
||||
|
||||
typedef struct beacon_info_t {
|
||||
uint64_t timestamp;
|
||||
uint16_t interval;
|
||||
uint16_t capability;
|
||||
} QEMU_PACKED beacon_info_t;
|
||||
|
||||
typedef uint8_t macaddr_t[6];
|
||||
|
||||
#define BROADCAST (uint8_t[]){0xff,0xff,0xff,0xff,0xff,0xff}
|
||||
|
||||
typedef struct mac80211_frame {
|
||||
struct mac80211_frame_control {
|
||||
unsigned protocol_version : 2;
|
||||
unsigned type : 2;
|
||||
unsigned sub_type : 4;
|
||||
unsigned flags:8;
|
||||
} QEMU_PACKED frame_control;
|
||||
uint16_t duration_id;
|
||||
macaddr_t destination_address;
|
||||
macaddr_t source_address;
|
||||
macaddr_t bssid_address;
|
||||
struct mac80211_sequence_control {
|
||||
unsigned fragment_number : 4;
|
||||
unsigned sequence_number : 12;
|
||||
} QEMU_PACKED sequence_control;
|
||||
// variable length, 2312 byte plus 4 byte frame-checksum
|
||||
union {
|
||||
uint8_t data_and_fcs[2316];
|
||||
beacon_info_t beacon_info;
|
||||
};
|
||||
unsigned int frame_length;
|
||||
int signal_strength;
|
||||
int pos;
|
||||
struct mac80211_frame *next_frame;
|
||||
} QEMU_PACKED mac80211_frame;
|
||||
|
||||
typedef struct access_point_info {
|
||||
const char *ssid;
|
||||
int channel;
|
||||
int sigstrength;
|
||||
macaddr_t mac_address;
|
||||
} access_point_info;
|
||||
|
||||
enum esp32_ap_state {
|
||||
Esp32_WLAN__STATE_NOT_AUTHENTICATED,
|
||||
Esp32_WLAN__STATE_AUTHENTICATED,
|
||||
Esp32_WLAN__STATE_ASSOCIATED,
|
||||
Esp32_WLAN__STATE_STA_NOT_AUTHENTICATED,
|
||||
Esp32_WLAN__STATE_STA_AUTHENTICATED,
|
||||
Esp32_WLAN__STATE_STA_ASSOCIATED,
|
||||
Esp32_WLAN__STATE_STA_DHCP,
|
||||
};
|
||||
|
||||
#define Esp32_WLAN__MAX_INJECT_QUEUE_SIZE 20
|
||||
|
||||
typedef struct {
|
||||
signed rssi:8; /**< Received Signal Strength Indicator(RSSI) of packet. unit: dBm */
|
||||
unsigned rate:5; /**< PHY rate encoding of the packet. Only valid for non HT(11bg) packet */
|
||||
unsigned :1; /**< reserved */
|
||||
unsigned sig_mode:2; /**< 0: non HT(11bg) packet; 1: HT(11n) packet; 3: VHT(11ac) packet */
|
||||
unsigned legacy_length:12; /**< copy of the length */
|
||||
unsigned damatch0:1; /* destination matches address0 */
|
||||
unsigned damatch1:1; /* destination matches address1 */
|
||||
unsigned bssidmatch0:1; /* bssid matches address0 */
|
||||
unsigned bssidmatch1:1; /* bssid matches address1 */
|
||||
unsigned mcs:7; /**< Modulation Coding Scheme. If is HT(11n) packet, shows the modulation, range from 0 to 76(MSC0 ~ MCS76) */
|
||||
unsigned cwb:1; /**< Channel Bandwidth of the packet. 0: 20MHz; 1: 40MHz */
|
||||
unsigned :16; /**< reserved */
|
||||
unsigned smoothing:1; /**< reserved */
|
||||
unsigned not_sounding:1; /**< reserved */
|
||||
unsigned :1; /**< reserved */
|
||||
unsigned aggregation:1; /**< Aggregation. 0: MPDU packet; 1: AMPDU packet */
|
||||
unsigned stbc:2; /**< Space Time Block Code(STBC). 0: non STBC packet; 1: STBC packet */
|
||||
unsigned fec_coding:1; /**< Flag is set for 11n packets which are LDPC */
|
||||
unsigned sgi:1; /**< Short Guide Interval(SGI). 0: Long GI; 1: Short GI */
|
||||
signed noise_floor:8; /**< noise floor of Radio Frequency Module(RF). unit: 0.25dBm*/
|
||||
unsigned ampdu_cnt:8; /**< ampdu cnt */
|
||||
unsigned channel:4; /**< primary channel on which this packet is received */
|
||||
unsigned secondary_channel:4; /**< secondary channel on which this packet is received. 0: none; 1: above; 2: below */
|
||||
unsigned :8; /**< reserved */
|
||||
unsigned timestamp:32; /**< timestamp. The local time when this packet is received. It is precise only if modem sleep or light sleep is not enabled. unit: microsecond */
|
||||
unsigned :32; /**< reserved */
|
||||
unsigned :31; /**< reserved */
|
||||
unsigned ant:1; /**< antenna number from which this packet is received. 0: WiFi antenna 0; 1: WiFi antenna 1 */
|
||||
unsigned sig_len:12; /**< length of packet including Frame Check Sequence(FCS) */
|
||||
unsigned sig_len_copy:12; /**< another copy of the length */
|
||||
unsigned rx_state:8; /**< state of the packet. 0: no error; others: error numbers which are not public */
|
||||
} QEMU_PACKED wifi_pkt_rx_ctrl_t;
|
||||
|
||||
|
||||
typedef struct {
|
||||
signed rssi:8; /**< Received Signal Strength Indicator(RSSI) of packet. unit: dBm */
|
||||
unsigned rate:5; /**< PHY rate encoding of the packet. Only valid for non HT(11bg) packet */
|
||||
unsigned :1; /**< reserved */
|
||||
unsigned sig_mode:2; /**< 0: non HT(11bg) packet; 1: HT(11n) packet; 3: VHT(11ac) packet */
|
||||
unsigned legacy_length:12; /**< copy of the length */
|
||||
unsigned damatch0:1; /* destination matches address0 */
|
||||
unsigned damatch1:1; /* destination matches address1 */
|
||||
unsigned bssidmatch0:1; /* bssid matches address0 */
|
||||
unsigned bssidmatch1:1; /* bssid matches address1 */
|
||||
unsigned mcs:7; /**< Modulation Coding Scheme. If is HT(11n) packet, shows the modulation, range from 0 to 76(MSC0 ~ MCS76) */
|
||||
unsigned cwb:1; /**< Channel Bandwidth of the packet. 0: 20MHz; 1: 40MHz */
|
||||
unsigned :16; /**< reserved */
|
||||
unsigned smoothing:1; /**< reserved */
|
||||
unsigned not_sounding:1; /**< reserved */
|
||||
unsigned :1; /**< reserved */
|
||||
unsigned aggregation:1; /**< Aggregation. 0: MPDU packet; 1: AMPDU packet */
|
||||
unsigned stbc:2; /**< Space Time Block Code(STBC). 0: non STBC packet; 1: STBC packet */
|
||||
unsigned fec_coding:1; /**< Flag is set for 11n packets which are LDPC */
|
||||
unsigned sgi:1; /**< Short Guide Interval(SGI). 0: Long GI; 1: Short GI */
|
||||
unsigned :8; /**< reserved */
|
||||
unsigned ampdu_cnt:8; /**< ampdu cnt */
|
||||
unsigned channel:4; /**< primary channel on which this packet is received */
|
||||
unsigned secondary_channel:4; /**< secondary channel on which this packet is received. 0: none; 1: above; 2: below */
|
||||
unsigned :8; /**< reserved */
|
||||
unsigned timestamp:32; /**< timestamp. The local time when this packet is received. It is precise only if modem sleep or light sleep is not enabled. unit: microsecond */
|
||||
unsigned :32; /**< reserved */
|
||||
signed noise_floor:8; /**< noise floor of Radio Frequency Module(RF). unit: dBm*/
|
||||
unsigned :24; /**< reserved */
|
||||
unsigned :32; /**< reserved */
|
||||
unsigned :31; /**< reserved */
|
||||
unsigned ant:1; /**< antenna number from which this packet is received. 0: WiFi antenna 0; 1: WiFi antenna 1 */
|
||||
unsigned :32; /**< reserved */
|
||||
unsigned :32; /**< reserved */
|
||||
unsigned :32; /**< reserved */
|
||||
unsigned sig_len:12; /**< length of packet including Frame Check Sequence(FCS) */
|
||||
unsigned sig_len_copy:12; /**< another copy of the length */
|
||||
unsigned rx_state:8; /**< state of the packet. 0: no error; others: error numbers which are not public */
|
||||
} QEMU_PACKED wifi_pkt_rx_ctrl_c3_t;
|
||||
|
||||
extern int esp32_wifi_channel;
|
||||
|
||||
typedef uint8_t ip4_t[4];
|
||||
|
||||
#define DHCP_CHADDR_LEN 16
|
||||
#define DHCP_SNAME_LEN 64
|
||||
#define DHCP_FILE_LEN 128
|
||||
|
||||
typedef struct ip_header_t {
|
||||
uint8_t version_size;
|
||||
uint8_t dscp_ecn;
|
||||
uint8_t len_h;
|
||||
uint8_t len_l;
|
||||
uint32_t z;
|
||||
uint8_t ttl;
|
||||
uint8_t protocol;
|
||||
uint16_t checksum;
|
||||
ip4_t source_ip;
|
||||
ip4_t dest_ip;
|
||||
}ip_header_t ;
|
||||
|
||||
typedef struct udp_header_t {
|
||||
uint8_t src_port_h;
|
||||
uint8_t src_port_l;
|
||||
uint8_t dest_port_h;
|
||||
uint8_t dest_port_l;
|
||||
uint8_t len_h;
|
||||
uint8_t len_l;
|
||||
uint16_t checksum;
|
||||
} udp_header_t;
|
||||
|
||||
typedef struct dhcp_t {
|
||||
uint8_t opcode;
|
||||
uint8_t htype;
|
||||
uint8_t hlen;
|
||||
uint8_t hops;
|
||||
uint32_t xid;
|
||||
uint16_t secs;
|
||||
uint16_t flags;
|
||||
ip4_t ciaddr;
|
||||
ip4_t yiaddr;
|
||||
ip4_t siaddr;
|
||||
ip4_t giaddr;
|
||||
uint8_t chaddr[DHCP_CHADDR_LEN];
|
||||
char bp_sname[DHCP_SNAME_LEN];
|
||||
char bp_file[DHCP_FILE_LEN];
|
||||
uint32_t magic_cookie;
|
||||
uint8_t bp_options[0];
|
||||
} dhcp_t;
|
||||
|
||||
typedef struct dhcp_request_t {
|
||||
ip_header_t ipheader;
|
||||
udp_header_t udpheader;
|
||||
dhcp_t dhcp;
|
||||
} dhcp_request_t;
|
||||
|
||||
typedef struct arp_header_t {
|
||||
uint16_t htype;
|
||||
uint16_t ptype;
|
||||
uint8_t hlen;
|
||||
uint8_t plen;
|
||||
uint16_t operation;
|
||||
uint8_t sender_mac[6];
|
||||
uint8_t sender_ip[4];
|
||||
uint8_t target_mac[6];
|
||||
uint8_t target_ip[4];
|
||||
}arp_header_t;
|
||||
|
||||
#endif // esp32_wlan_h
|
||||
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* QEMU WLAN access point emulation
|
||||
*
|
||||
* Copyright (c) 2008 Clemens Kolbitsch
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
* Modifications:
|
||||
* 2008-February-24 Clemens Kolbitsch :
|
||||
* New implementation based on ne2000.c
|
||||
* 18/1/22 Martin Johnson : Modified for esp32 wifi emulation
|
||||
*/
|
||||
|
||||
#include "qemu/osdep.h"
|
||||
|
||||
#include "net/net.h"
|
||||
|
||||
#include "esp32_wlan.h"
|
||||
#include "esp32_wlan_packet.h"
|
||||
|
||||
// the frame checksum isn't used so just put zero in there.
|
||||
void insertCRC(mac80211_frame *frame) {
|
||||
unsigned long crc;
|
||||
unsigned char *fcs = (unsigned char *)frame;
|
||||
crc = 0;
|
||||
memcpy(fcs+frame->frame_length, &crc, 4);
|
||||
frame->frame_length += 4;
|
||||
}
|
||||
|
||||
static void add_byte(mac80211_frame *frame, uint8_t data) {
|
||||
frame->data_and_fcs[frame->pos++]=data;
|
||||
frame->frame_length=IEEE80211_HEADER_SIZE+frame->pos;
|
||||
}
|
||||
|
||||
static void add_data(mac80211_frame *frame, int len,uint8_t *data) {
|
||||
for(int i=0;i<len;i++)
|
||||
add_byte(frame,data[i]);
|
||||
}
|
||||
|
||||
static void add_tag(mac80211_frame *frame, int tag, int len, unsigned char bytes[]) {
|
||||
add_byte(frame,tag);
|
||||
add_byte(frame,len);
|
||||
add_data(frame,len,bytes);
|
||||
}
|
||||
|
||||
void Esp32_WLAN_init_ap_frame(Esp32WifiState *s, mac80211_frame *frame) {
|
||||
frame->sequence_control.sequence_number = s->inject_sequence_number++;
|
||||
if(s->mode == Esp32_Mode_Station){
|
||||
if(s->ap_state == Esp32_WLAN__STATE_STA_ASSOCIATED)
|
||||
memcpy(frame->source_address, s->macaddr, 6);
|
||||
else
|
||||
memcpy(frame->source_address, s->ap_macaddr, 6);
|
||||
memcpy(frame->bssid_address, s->ap_macaddr, 6);
|
||||
}
|
||||
else{
|
||||
if(s->ap_state == Esp32_WLAN__STATE_STA_ASSOCIATED)
|
||||
memcpy(frame->source_address, s->ap_macaddr, 6);
|
||||
else
|
||||
memcpy(frame->source_address, s->softap_macaddr, 6);
|
||||
memcpy(frame->bssid_address, s->softap_macaddr, 6);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static mac80211_frame *new_frame(unsigned type, unsigned subtype) {
|
||||
mac80211_frame *frame = (mac80211_frame *)malloc(sizeof(mac80211_frame));
|
||||
frame->next_frame = NULL;
|
||||
frame->frame_control.protocol_version = 0;
|
||||
frame->frame_control.type = type;
|
||||
frame->frame_control.sub_type = subtype;
|
||||
frame->frame_control.flags = 0;
|
||||
frame->duration_id = 314;
|
||||
frame->sequence_control.fragment_number = 0;
|
||||
frame->pos=0;
|
||||
return frame;
|
||||
}
|
||||
|
||||
static void add_rates(mac80211_frame *frame) {
|
||||
add_tag(frame,IEEE80211_BEACON_PARAM_RATES,8,(uint8_t[]){0x8b,0x96,0x82,0x84,0x0c,0x18,0x30,0x60});
|
||||
add_tag(frame,IEEE80211_BEACON_PARAM_EXTENDED_RATES,4,(uint8_t[]){0x6c,0x12,0x24,0x48});
|
||||
}
|
||||
|
||||
static void add_ssid(mac80211_frame *frame, const char *ssid) {
|
||||
add_tag(frame,IEEE80211_BEACON_PARAM_SSID,strlen(ssid),(uint8_t *)ssid);
|
||||
}
|
||||
|
||||
mac80211_frame *Esp32_WLAN_create_beacon_frame(access_point_info *ap) {
|
||||
mac80211_frame *frame=new_frame(IEEE80211_TYPE_MGT,IEEE80211_TYPE_MGT_SUBTYPE_BEACON);
|
||||
frame->signal_strength=ap->sigstrength;
|
||||
memcpy(frame->destination_address,BROADCAST,6);
|
||||
frame->beacon_info.timestamp=qemu_clock_get_ns(QEMU_CLOCK_REALTIME)/1000;
|
||||
frame->beacon_info.interval=1000;
|
||||
frame->beacon_info.capability=1;
|
||||
frame->pos=12;
|
||||
add_ssid(frame,ap->ssid);
|
||||
add_rates(frame);
|
||||
add_tag(frame,IEEE80211_BEACON_PARAM_CHANNEL,1,(uint8_t[]){ap->channel});
|
||||
add_tag(frame,IEEE80211_BEACON_PARAM_TIM,4,(uint8_t[]){4,1,3,0,0});
|
||||
return frame;
|
||||
}
|
||||
|
||||
static uint16_t in_cksum(uint16_t *addr, int len) {
|
||||
int sum = 0;
|
||||
uint16_t answer = 0;
|
||||
uint16_t *w = addr;
|
||||
int nleft = len;
|
||||
while (nleft > 1) {
|
||||
sum += *w++;
|
||||
nleft -= 2;
|
||||
}
|
||||
/* mop up an odd byte, if necessary */
|
||||
if (nleft == 1) {
|
||||
*(uint8_t *)(&answer) = *(uint8_t *) w;
|
||||
sum += answer;
|
||||
}
|
||||
sum = (sum >> 16) + (sum & 0xffff); /* add hi 16 to low 16 */
|
||||
sum += (sum >> 16); /* add carry */
|
||||
answer = ~sum; /* truncate to 16 bits */
|
||||
return (answer);
|
||||
}
|
||||
|
||||
static mac80211_frame *Esp32_WLAN_create_dhcp_frame(Esp32WifiState *s, int cmd_size, uint8_t dhcp_commands[]) {
|
||||
mac80211_frame *frame=new_frame(IEEE80211_TYPE_DATA,IEEE80211_TYPE_DATA_SUBTYPE_DATA);
|
||||
frame->frame_control.flags=1;
|
||||
add_data(frame,8,(uint8_t[]){ 0xaa, 0xaa ,0x03 ,00 ,00 ,00 ,8 ,00});
|
||||
dhcp_request_t req={
|
||||
{.version_size=0x45,.ttl=0xff,.protocol=0x11,.dest_ip={0xff,0xff,0xff,0xff}},
|
||||
{.src_port_l=0x44,.dest_port_l=0x43},
|
||||
{.htype=1,.hlen=6,.xid=0x1d3d00,.chaddr={0,0,0,0,0,0},
|
||||
.magic_cookie=0x63538263}
|
||||
};
|
||||
memcpy(req.dhcp.chaddr, s->macaddr, 6);
|
||||
int len=sizeof(req)+cmd_size;
|
||||
req.ipheader.len_h=len>>8;
|
||||
req.ipheader.len_l=len&0xff;
|
||||
len=len-sizeof(ip_header_t);
|
||||
req.udpheader.len_h=len>>8;
|
||||
req.udpheader.len_l=len&0xff;
|
||||
req.ipheader.checksum=in_cksum((void *)&req.ipheader,sizeof(ip_header_t));
|
||||
add_data(frame,sizeof(req),(uint8_t *)&req);
|
||||
add_data(frame,cmd_size,dhcp_commands);
|
||||
return frame;
|
||||
}
|
||||
|
||||
mac80211_frame *Esp32_WLAN_create_dhcp_request(Esp32WifiState *s, uint8_t *ip) {
|
||||
uint8_t dhcp_commands[]={
|
||||
0x35, 1, 3,
|
||||
0x39, 2 ,5 ,0xdc ,
|
||||
0x32, 4, ip[0],ip[1],ip[2],ip[3],
|
||||
0x3d, 0x07, 0x01, 0x3c, 0x61, 0x05, 0x0d, 0x99, 0x24,
|
||||
0x37, 0x04, 0x01, 0x03, 0x1c, 0x06,
|
||||
0xff, 0, 0
|
||||
};
|
||||
return Esp32_WLAN_create_dhcp_frame(s, sizeof(dhcp_commands),dhcp_commands);
|
||||
}
|
||||
|
||||
mac80211_frame *Esp32_WLAN_create_dhcp_discover(Esp32WifiState *s) {
|
||||
uint8_t dhcp_commands[]={
|
||||
0x35, 1, 1,
|
||||
0x39, 2 ,5 ,0xdc ,
|
||||
0x0c ,0x09 ,0x65 ,0x73 ,0x70 ,0x72 ,0x65 ,0x73 ,0x73 ,0x69 ,0x66 ,
|
||||
0x3d ,0x07, 0x01 ,0x3c ,0x61 ,0x05 ,0x0d ,0x99 ,0x24 ,
|
||||
0x37 ,0x04 ,0x01 ,0x03 ,0x1c ,0x06 ,
|
||||
0xff, 0,0
|
||||
};
|
||||
return Esp32_WLAN_create_dhcp_frame(s,sizeof(dhcp_commands),dhcp_commands);
|
||||
}
|
||||
|
||||
mac80211_frame *Esp32_WLAN_create_association_request(access_point_info *ap) {
|
||||
mac80211_frame *frame=new_frame(IEEE80211_TYPE_MGT,IEEE80211_TYPE_MGT_SUBTYPE_ASSOCIATION_REQ);
|
||||
add_data(frame,4,(uint8_t []){0x21,4,3,0});
|
||||
add_ssid(frame,ap->ssid);
|
||||
add_rates(frame);
|
||||
return frame;
|
||||
}
|
||||
|
||||
mac80211_frame *Esp32_WLAN_create_ack(void) {
|
||||
mac80211_frame *frame=new_frame(IEEE80211_TYPE_CTL,IEEE80211_TYPE_CTL_SUBTYPE_ACK);
|
||||
frame->frame_length=10;
|
||||
return frame;
|
||||
}
|
||||
|
||||
mac80211_frame *Esp32_WLAN_create_probe_response(access_point_info *ap) {
|
||||
mac80211_frame *frame=new_frame(IEEE80211_TYPE_MGT,IEEE80211_TYPE_MGT_SUBTYPE_PROBE_RESP);
|
||||
frame->beacon_info.timestamp=qemu_clock_get_ns(QEMU_CLOCK_REALTIME)/1000;
|
||||
frame->beacon_info.interval=1000;
|
||||
frame->beacon_info.capability=1;
|
||||
frame->pos=12;
|
||||
add_ssid(frame,ap->ssid);
|
||||
add_rates(frame);
|
||||
add_tag(frame,IEEE80211_BEACON_PARAM_CHANNEL,1,(uint8_t[]){ap->channel});
|
||||
return frame;
|
||||
}
|
||||
|
||||
mac80211_frame *Esp32_WLAN_create_probe_request(access_point_info *ap) {
|
||||
mac80211_frame *frame=new_frame(IEEE80211_TYPE_MGT,IEEE80211_TYPE_MGT_SUBTYPE_PROBE_REQ);
|
||||
memcpy(frame->destination_address,BROADCAST,6);
|
||||
memcpy(frame->bssid_address,BROADCAST,6);
|
||||
add_ssid(frame,ap->ssid);
|
||||
add_tag(frame,IEEE80211_BEACON_PARAM_CHANNEL,1,(uint8_t[]){ap->channel});
|
||||
add_rates(frame);
|
||||
return frame;
|
||||
}
|
||||
|
||||
mac80211_frame *Esp32_WLAN_create_authentication_response(access_point_info *ap) {
|
||||
mac80211_frame *frame=new_frame(IEEE80211_TYPE_MGT,IEEE80211_TYPE_MGT_SUBTYPE_AUTHENTICATION);
|
||||
/*
|
||||
* Fixed params... typical AP params (6 byte)
|
||||
*
|
||||
* They include
|
||||
* - Authentication Algorithm (here: Open System)
|
||||
* - Authentication SEQ
|
||||
* - Status code (successful 0x0)
|
||||
*/
|
||||
add_data(frame,6,(uint8_t []){0,0,2,0,0,0});
|
||||
add_ssid(frame,ap->ssid);
|
||||
return frame;
|
||||
}
|
||||
|
||||
mac80211_frame *Esp32_WLAN_create_authentication_request(void) {
|
||||
mac80211_frame *frame=new_frame(IEEE80211_TYPE_MGT,IEEE80211_TYPE_MGT_SUBTYPE_AUTHENTICATION);
|
||||
/*
|
||||
* Fixed params... typical AP params (6 byte)
|
||||
*
|
||||
* They include
|
||||
* - Authentication Algorithm (here: Open System)
|
||||
* - Authentication SEQ
|
||||
* - Status code (successful 0x0)
|
||||
*/
|
||||
//frame->duration_id=0x13a;
|
||||
add_data(frame,6,(uint8_t []){0,0,1,0,0,0});
|
||||
//add_data(frame,11,(uint8_t []){0,0,0,0,0,0,0,0,0,0,0});
|
||||
//add_ssid(frame,ap->ssid);
|
||||
return frame;
|
||||
}
|
||||
|
||||
mac80211_frame *Esp32_WLAN_create_deauthentication(void) {
|
||||
mac80211_frame *frame=new_frame(IEEE80211_TYPE_MGT,IEEE80211_TYPE_MGT_SUBTYPE_DEAUTHENTICATION);
|
||||
/*
|
||||
* Insert reason code:
|
||||
* "Deauthentication because sending STA is leaving"
|
||||
*/
|
||||
add_data(frame,2,(uint8_t []){3,0});
|
||||
return frame;
|
||||
}
|
||||
|
||||
mac80211_frame *Esp32_WLAN_create_association_response(access_point_info *ap) {
|
||||
mac80211_frame *frame=new_frame(IEEE80211_TYPE_MGT,IEEE80211_TYPE_MGT_SUBTYPE_ASSOCIATION_RESP);
|
||||
/*
|
||||
* Fixed params... typical AP params (6 byte)
|
||||
*
|
||||
* They include
|
||||
* - Capability Information
|
||||
* - Status code (successful 0x0)
|
||||
* - Association ID
|
||||
*/
|
||||
add_data(frame,6,(uint8_t []){33,4,0,0,1,0xc0});
|
||||
add_ssid(frame,ap->ssid);
|
||||
add_rates(frame);
|
||||
return frame;
|
||||
}
|
||||
|
||||
mac80211_frame *Esp32_WLAN_create_disassociation(void) {
|
||||
mac80211_frame *frame=new_frame(IEEE80211_TYPE_MGT,IEEE80211_TYPE_MGT_SUBTYPE_DISASSOCIATION);
|
||||
/*
|
||||
* Insert reason code:
|
||||
* "Disassociation because sending STA is leaving"
|
||||
*/
|
||||
add_data(frame,2,(uint8_t []){3,0});
|
||||
return frame;
|
||||
}
|
||||
|
||||
mac80211_frame *Esp32_WLAN_create_data_packet(Esp32WifiState *s, const uint8_t *buf, int size) {
|
||||
mac80211_frame *frame=new_frame(IEEE80211_TYPE_DATA,IEEE80211_TYPE_DATA_SUBTYPE_DATA);
|
||||
|
||||
frame->frame_control.flags = 0x2; /* from station back to station via AP */
|
||||
frame->duration_id = 44;
|
||||
/* send message to wlan-device */
|
||||
if(s->ap_state == Esp32_WLAN__STATE_STA_ASSOCIATED) {
|
||||
memcpy(frame->destination_address, s->ap_macaddr, 6);
|
||||
frame->frame_control.flags = 0x2;
|
||||
}
|
||||
else
|
||||
memcpy(frame->destination_address, s->macaddr, 6);
|
||||
/* LLC */
|
||||
add_data(frame,6,(uint8_t[]){ 0xaa, 0xaa ,0x03 ,0 ,0 ,0});
|
||||
memcpy(frame->data_and_fcs+6, buf+12, size-12);
|
||||
frame->frame_length = IEEE80211_HEADER_SIZE + size-6;
|
||||
return frame;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* QEMU WLAN access point emulation
|
||||
*
|
||||
* Copyright (c) 2008 Clemens Kolbitsch
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
* Modifications:
|
||||
* 2008-February-24 Clemens Kolbitsch :
|
||||
* New implementation based on ne2000.c
|
||||
* 18/1/22 Martin Johnson : Modified for esp32 wifi emulation
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef esp32_wlan_packet_h
|
||||
#define esp32_wlan_packet_h 1
|
||||
|
||||
#include "esp32_wlan.h"
|
||||
#include "hw/misc/esp32_wifi.h"
|
||||
|
||||
void Esp32_WLAN_init_ap_frame(Esp32WifiState *s, struct mac80211_frame *frame);
|
||||
int Esp32_WLAN_dumpFrame(struct mac80211_frame *frame, int frame_len, char *filename);
|
||||
void Esp32_WLAN_insert_frame(Esp32WifiState *s, struct mac80211_frame *frame);
|
||||
struct mac80211_frame *Esp32_WLAN_create_beacon_frame(access_point_info *ap);
|
||||
struct mac80211_frame *Esp32_WLAN_create_probe_response(access_point_info *ap);
|
||||
struct mac80211_frame *Esp32_WLAN_create_probe_request(access_point_info *ap);
|
||||
struct mac80211_frame *Esp32_WLAN_create_authentication_request(void);
|
||||
struct mac80211_frame *Esp32_WLAN_create_authentication_response(access_point_info *ap);
|
||||
struct mac80211_frame *Esp32_WLAN_create_deauthentication(void);
|
||||
struct mac80211_frame *Esp32_WLAN_create_association_request(access_point_info *ap);
|
||||
struct mac80211_frame *Esp32_WLAN_create_association_response(access_point_info *ap);
|
||||
struct mac80211_frame *Esp32_WLAN_create_disassociation(void);
|
||||
struct mac80211_frame *Esp32_WLAN_create_data_reply(Esp32WifiState *s, struct mac80211_frame *incoming);
|
||||
struct mac80211_frame *Esp32_WLAN_create_data_packet(Esp32WifiState *s, const uint8_t *buf, int size);
|
||||
struct mac80211_frame *Esp32_WLAN_create_ack(void);
|
||||
struct mac80211_frame *Esp32_WLAN_create_dhcp_discover(Esp32WifiState *s);
|
||||
struct mac80211_frame *Esp32_WLAN_create_dhcp_request(Esp32WifiState *s, uint8_t *ip);
|
||||
void insertCRC(mac80211_frame *frame);
|
||||
|
||||
#endif // esp32_wlan_packet_h
|
||||
@@ -0,0 +1,87 @@
|
||||
#include "qemu/osdep.h"
|
||||
#include "qemu/log.h"
|
||||
#include "qemu/error-report.h"
|
||||
#include "qemu/guest-random.h"
|
||||
#include "qapi/error.h"
|
||||
#include "hw/hw.h"
|
||||
#include "hw/sysbus.h"
|
||||
#include "hw/misc/esp32c3_ana.h"
|
||||
|
||||
#define DEBUG 0
|
||||
|
||||
int esp32_wifi_channel = 0;
|
||||
|
||||
static uint64_t esp32c3_ana_read(void *opaque, hwaddr addr, unsigned int size)
|
||||
{
|
||||
Esp32C3AnaState *s = ESP32C3_ANA(opaque);
|
||||
uint32_t r = s->mem[addr/4];
|
||||
switch(addr) {
|
||||
case 0x00:
|
||||
case 0x48:
|
||||
r= 0x00FFFFFF;
|
||||
break;
|
||||
case 0x50:
|
||||
r=0x07000000 | s->mem[addr/4];
|
||||
break;
|
||||
case 0x04:
|
||||
r= 0xFDFFFFFF;
|
||||
break;
|
||||
case 0x44:
|
||||
case 0x4C:
|
||||
case 0xC4:
|
||||
r=0xFFFFFFFF;
|
||||
break;
|
||||
}
|
||||
if(DEBUG) printf("esp32c3_ana_read 0x%04lx= 0x%08x\n",(unsigned long) addr,r);
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
static void esp32c3_ana_write(void *opaque, hwaddr addr, uint64_t value,
|
||||
unsigned int size) {
|
||||
Esp32C3AnaState *s = ESP32C3_ANA(opaque);
|
||||
|
||||
if(DEBUG) printf("esp32c3_ana_write 0x%04lx= 0x%08lx\n",(unsigned long) addr, (unsigned long) value);
|
||||
|
||||
if(addr == 0x150) {
|
||||
int v= (value & 0x0FF00000) >> 0x14;
|
||||
esp32_wifi_channel=(v-7)/5;
|
||||
if(esp32_wifi_channel > 14) esp32_wifi_channel = 14;
|
||||
//printf("wifi channel:0x%08x ",(int)value);
|
||||
//printf("===========> esp32_wifi_channel %i \n", esp32_wifi_channel);
|
||||
}
|
||||
|
||||
s->mem[addr/4]=value;
|
||||
}
|
||||
|
||||
static const MemoryRegionOps esp32c3_ana_ops = {
|
||||
.read = esp32c3_ana_read,
|
||||
.write = esp32c3_ana_write,
|
||||
.endianness = DEVICE_LITTLE_ENDIAN,
|
||||
};
|
||||
|
||||
static void esp32c3_ana_init(Object *obj)
|
||||
{
|
||||
Esp32C3AnaState *s = ESP32C3_ANA(obj);
|
||||
SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
|
||||
|
||||
memory_region_init_io(&s->iomem, obj, &esp32c3_ana_ops, s,
|
||||
TYPE_ESP32C3_ANA, 0x1000);
|
||||
sysbus_init_mmio(sbd, &s->iomem);
|
||||
memset(s->mem,0,sizeof(s->mem));
|
||||
}
|
||||
|
||||
|
||||
static const TypeInfo esp32c3_ana_info = {
|
||||
.name = TYPE_ESP32C3_ANA,
|
||||
.parent = TYPE_SYS_BUS_DEVICE,
|
||||
.instance_size = sizeof(Esp32C3AnaState),
|
||||
.instance_init = esp32c3_ana_init,
|
||||
};
|
||||
|
||||
static void esp32c3_ana_register_types(void)
|
||||
{
|
||||
type_register_static(&esp32c3_ana_info);
|
||||
}
|
||||
|
||||
type_init(esp32c3_ana_register_types)
|
||||
+42
-9
@@ -12,31 +12,62 @@
|
||||
#include "qemu/module.h"
|
||||
#include "qemu/error-report.h"
|
||||
#include "hw/hw.h"
|
||||
#include "hw/irq.h"
|
||||
#include "hw/sysbus.h"
|
||||
#include "hw/misc/esp32c3_jtag.h"
|
||||
|
||||
|
||||
/* USB Serial JTAG Console Tap - The firmware routes ESP-IDF console output (incl. panic dumps) through this peripheral. EP1 is the byte FIFO; EP1_CONF bit1 (SERIAL_IN_EP_DATA_FREE) must read set so the driver believes the TX FIFO can always accept a byte. */
|
||||
#define ESP32C3_JTAG_EP1_REG 0x00
|
||||
#define ESP32C3_JTAG_EP1_CONF_REG 0x04
|
||||
#define ESP32C3_JTAG_EP1_REG 0x00
|
||||
#define ESP32C3_JTAG_EP1_CONF_REG 0x04
|
||||
#define ESP32C3_JTAG_INT_RAW_REG 0x08
|
||||
#define ESP32C3_JTAG_INT_ST_REG 0x0c
|
||||
#define ESP32C3_JTAG_INT_ENA_REG 0x10
|
||||
#define ESP32C3_JTAG_IN_EP_DATA_FREE (1u << 1)
|
||||
#define ESP32C3_JTAG_SOF_INT (1u << 1)
|
||||
#define ESP32C3_JTAG_SERIAL_IN_EMPTY (1u << 3)
|
||||
|
||||
/* ponytail: interrupts are permanently pending - the tap has no real FIFO or USB host, so SOF and SERIAL_IN_EMPTY are always true and INT_CLR is a no-op. Model real raw/clear state if a driver ever needs an edge. */
|
||||
static uint32_t esp32c3_jtag_raw_interrupts(void)
|
||||
{
|
||||
return ESP32C3_JTAG_SOF_INT | ESP32C3_JTAG_SERIAL_IN_EMPTY;
|
||||
}
|
||||
|
||||
static void esp32c3_jtag_update_irq(ESP32C3UsbJtagState *s)
|
||||
{
|
||||
qemu_set_irq(s->irq, !!(s->int_ena & esp32c3_jtag_raw_interrupts()));
|
||||
}
|
||||
|
||||
static uint64_t esp32c3_jtag_read(void *opaque, hwaddr addr, unsigned int size)
|
||||
{
|
||||
ESP32C3UsbJtagState *s = ESP32C3_JTAG(opaque);
|
||||
(void) s;
|
||||
if (addr == ESP32C3_JTAG_EP1_CONF_REG) {
|
||||
switch (addr) {
|
||||
case ESP32C3_JTAG_EP1_CONF_REG:
|
||||
return ESP32C3_JTAG_IN_EP_DATA_FREE;
|
||||
case ESP32C3_JTAG_INT_RAW_REG:
|
||||
return esp32c3_jtag_raw_interrupts();
|
||||
case ESP32C3_JTAG_INT_ST_REG:
|
||||
return s->int_ena & esp32c3_jtag_raw_interrupts();
|
||||
case ESP32C3_JTAG_INT_ENA_REG:
|
||||
return s->int_ena;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void esp32c3_jtag_write(void *opaque, hwaddr addr, uint64_t value, unsigned int size)
|
||||
{
|
||||
ESP32C3UsbJtagState *s = ESP32C3_JTAG(opaque);
|
||||
(void) s;
|
||||
if (addr == ESP32C3_JTAG_EP1_REG) {
|
||||
switch (addr) {
|
||||
case ESP32C3_JTAG_EP1_REG:
|
||||
fputc((int)(value & 0xff), stderr);
|
||||
break;
|
||||
case ESP32C3_JTAG_INT_ENA_REG:
|
||||
s->int_ena = value;
|
||||
esp32c3_jtag_update_irq(s);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,8 +79,9 @@ static const MemoryRegionOps esp32c3_jtag_ops = {
|
||||
|
||||
static void esp32c3_jtag_reset_hold(Object *obj, ResetType type)
|
||||
{
|
||||
(void) obj;
|
||||
(void) type;
|
||||
ESP32C3UsbJtagState *s = ESP32C3_JTAG(obj);
|
||||
s->int_ena = 0;
|
||||
esp32c3_jtag_update_irq(s);
|
||||
}
|
||||
|
||||
static void esp32c3_jtag_realize(DeviceState *dev, Error **errp)
|
||||
@@ -66,6 +98,7 @@ static void esp32c3_jtag_init(Object *obj)
|
||||
memory_region_init_io(&s->iomem, obj, &esp32c3_jtag_ops, s,
|
||||
TYPE_ESP32C3_JTAG, ESP32C3_JTAG_REGS_SIZE);
|
||||
sysbus_init_mmio(sbd, &s->iomem);
|
||||
sysbus_init_irq(sbd, &s->irq);
|
||||
}
|
||||
|
||||
static void esp32c3_jtag_class_init(ObjectClass *klass, void *data)
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/* ESP32C3 unimp/dummy peripheral handler
|
||||
*/
|
||||
|
||||
#include "qemu/osdep.h"
|
||||
#include "qemu/log.h"
|
||||
#include "qemu/error-report.h"
|
||||
#include "qapi/error.h"
|
||||
#include "hw/hw.h"
|
||||
#include "hw/sysbus.h"
|
||||
#include "hw/registerfields.h"
|
||||
#include "hw/boards.h"
|
||||
#include "hw/misc/esp32c3_pwrmng.h"
|
||||
#include "hw/misc/esp32c3_reg.h"
|
||||
|
||||
|
||||
#define DEBUG 0
|
||||
|
||||
static uint64_t esp32c3_pwrmng_read(void *opaque, hwaddr addr, unsigned int size)
|
||||
{
|
||||
static bool init = false;
|
||||
Esp32c3PwrMngState *s = ESP32C3_PWR_MANAGER(opaque);
|
||||
|
||||
uint32_t r = s->mem[addr/4];
|
||||
|
||||
switch(addr){
|
||||
case 0x07C:
|
||||
/* Return a random 32-bit value */
|
||||
if (!init) {
|
||||
srand(time(NULL));
|
||||
init = true;
|
||||
}
|
||||
r = rand();
|
||||
break;
|
||||
case 0x128: //DMA enabled
|
||||
r = 0 ;
|
||||
break;
|
||||
case 0x118: //PWR int events
|
||||
r = 0 ;
|
||||
break;
|
||||
}
|
||||
|
||||
if(DEBUG) printf("esp32_pwrmng_read 0x%04lx= 0x%08x\n",(unsigned long) addr,r);
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
static void esp32c3_pwrmng_write(void *opaque, hwaddr addr,
|
||||
uint64_t value, unsigned int size)
|
||||
{
|
||||
Esp32c3PwrMngState *s = ESP32C3_PWR_MANAGER(opaque);
|
||||
|
||||
if(DEBUG) printf("esp32_pwrmng_write 0x%04lx= 0x%08lx\n",(unsigned long) addr, (unsigned long) value);
|
||||
|
||||
s->mem[addr/4]=value;
|
||||
}
|
||||
|
||||
static const MemoryRegionOps esp32c3_pwrmng_ops = {
|
||||
.read = esp32c3_pwrmng_read,
|
||||
.write = esp32c3_pwrmng_write,
|
||||
.endianness = DEVICE_LITTLE_ENDIAN,
|
||||
};
|
||||
|
||||
static void esp32c3_pwrmng_init(Object *obj)
|
||||
{
|
||||
Esp32c3PwrMngState *s = ESP32C3_PWR_MANAGER(obj);
|
||||
SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
|
||||
|
||||
memory_region_init_io(&s->iomem, obj, &esp32c3_pwrmng_ops, s, TYPE_ESP32C3_PWR_MANAGER, 0x1000);
|
||||
sysbus_init_mmio(sbd, &s->iomem);
|
||||
}
|
||||
|
||||
static const TypeInfo esp32c3_pwrmng_info = {
|
||||
.name = TYPE_ESP32C3_PWR_MANAGER,
|
||||
.parent = TYPE_SYS_BUS_DEVICE,
|
||||
.instance_size = sizeof(Esp32c3PwrMngState),
|
||||
.instance_init = esp32c3_pwrmng_init,
|
||||
};
|
||||
|
||||
static void esp32c3_pwrmng_register_types(void)
|
||||
{
|
||||
type_register_static(&esp32c3_pwrmng_info);
|
||||
}
|
||||
|
||||
type_init(esp32c3_pwrmng_register_types)
|
||||
@@ -0,0 +1,245 @@
|
||||
#include "qemu/osdep.h"
|
||||
#include "qemu/log.h"
|
||||
#include "qemu/error-report.h"
|
||||
#include "qemu/guest-random.h"
|
||||
#include "qapi/error.h"
|
||||
#include "sysemu/sysemu.h"
|
||||
#include "hw/hw.h"
|
||||
#include "hw/irq.h"
|
||||
#include "hw/sysbus.h"
|
||||
#include "hw/misc/esp32c3_wifi.h"
|
||||
#include "exec/address-spaces.h"
|
||||
#include "esp32_wlan_packet.h"
|
||||
#include "hw/qdev-properties.h"
|
||||
#include "hw/misc/esp32c3_reg.h"
|
||||
|
||||
#define DEBUG 0
|
||||
|
||||
extern access_point_info access_points[];
|
||||
extern int nb_aps;
|
||||
|
||||
static uint64_t esp32C3_wifi_read(void *opaque, hwaddr addr, unsigned int size)
|
||||
{
|
||||
|
||||
Esp32WifiState *s = ESP32_WIFI(opaque);
|
||||
uint32_t r = s->mem[addr/4];
|
||||
|
||||
switch(addr) {
|
||||
case A_C3_WIFI_DMA_IN_STATUS:
|
||||
r = 0;
|
||||
break;
|
||||
case A_C3_WIFI_DMA_INT_STATUS:
|
||||
case A_C3_WIFI_DMA_INT_CLR:
|
||||
r = s->raw_interrupt;
|
||||
break;
|
||||
case A_C3_WIFI_STATUS:
|
||||
case A_C3_WIFI_DMA_OUT_STATUS:
|
||||
r = 1;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (DEBUG) printf("esp32C3_wifi_read 0x%04lx= 0x%08x\n",(unsigned long) addr,r);
|
||||
|
||||
return r;
|
||||
}
|
||||
static void set_interrupt(Esp32WifiState *s,int e) {
|
||||
s->raw_interrupt |= e;
|
||||
qemu_set_irq(s->irq, 1);
|
||||
}
|
||||
|
||||
void Esp32_WLAN_frame_delivered(Esp32WifiState *s){
|
||||
s->raw_interrupt |= 0x80;
|
||||
qemu_set_irq(s->irq, 1);
|
||||
}
|
||||
|
||||
static void esp32C3_wifi_write(void *opaque, hwaddr addr, uint64_t value,
|
||||
unsigned int size) {
|
||||
Esp32WifiState *s = ESP32_WIFI(opaque);
|
||||
if(DEBUG) printf("esp32C3_wifi_write 0x%04lx= 0x%08lx\n",(unsigned long) addr, (unsigned long) value);
|
||||
|
||||
switch (addr) {
|
||||
case A_C3_WIFI_DMA_INLINK: {
|
||||
uint32_t offset = value & 0xfffff;
|
||||
s->dma_inlink_address = offset ? 0x3fc00000 | offset : 0;
|
||||
s->mem[0x90 / sizeof(uint32_t)] = s->dma_inlink_address;
|
||||
break;
|
||||
}
|
||||
case A_C3_WIFI_DMA_INT_CLR:
|
||||
s->raw_interrupt &= ~value;
|
||||
if (s->raw_interrupt == 0) {
|
||||
qemu_set_irq(s->irq, 0);
|
||||
}
|
||||
break;
|
||||
case A_C3_WIFI_DMA_OUTLINK:
|
||||
if ((value & 0xc0000000) && (value & 0xfffff)) {
|
||||
mac80211_frame frame;
|
||||
dma_list_item item;
|
||||
unsigned memaddr = 0x3fc00000 | (value & 0xfffff);
|
||||
address_space_read(&address_space_memory, memaddr,
|
||||
MEMTXATTRS_UNSPECIFIED, &item, sizeof(item));
|
||||
if (item.length > sizeof(frame)) {
|
||||
break;
|
||||
}
|
||||
address_space_read(&address_space_memory, item.address,
|
||||
MEMTXATTRS_UNSPECIFIED, &frame, item.length);
|
||||
frame.frame_length = item.length;
|
||||
frame.next_frame = NULL;
|
||||
Esp32_WLAN_handle_frame(s, &frame);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
s->mem[addr/4]=value;
|
||||
}
|
||||
|
||||
static int match_mac_address(uint8_t *a1,uint8_t *a2) {
|
||||
if(!memcmp(a1,a2,6)) return 1;
|
||||
if(!memcmp(a1,BROADCAST,6)) return 1;
|
||||
return 0;
|
||||
}
|
||||
// frame from ap to esp32
|
||||
void Esp32_sendFrame(Esp32WifiState *s, mac80211_frame *frame,int length, int signal_strength) {
|
||||
int packet_channel = esp32_wifi_channel;
|
||||
|
||||
if (s->dma_inlink_address == 0) {
|
||||
return;
|
||||
}
|
||||
if (frame->frame_control.type == IEEE80211_TYPE_MGT &&
|
||||
frame->frame_control.sub_type == IEEE80211_TYPE_MGT_SUBTYPE_BEACON) {
|
||||
int pos = 12;
|
||||
int data_length = length - IEEE80211_HEADER_SIZE;
|
||||
|
||||
while (pos + 2 <= data_length) {
|
||||
uint8_t tag = frame->data_and_fcs[pos];
|
||||
uint8_t tag_length = frame->data_and_fcs[pos + 1];
|
||||
|
||||
if (tag == IEEE80211_BEACON_PARAM_CHANNEL && tag_length) {
|
||||
packet_channel = frame->data_and_fcs[pos + 2];
|
||||
break;
|
||||
}
|
||||
pos += 2 + tag_length;
|
||||
}
|
||||
}
|
||||
if (packet_channel == 0) {
|
||||
const uint8_t *address = frame->frame_control.type == IEEE80211_TYPE_DATA
|
||||
? frame->bssid_address : frame->source_address;
|
||||
|
||||
for (int i = 0; i < nb_aps; i++) {
|
||||
if (memcmp(access_points[i].mac_address, address, 6) == 0) {
|
||||
packet_channel = access_points[i].channel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
uint8_t header[sizeof(wifi_pkt_rx_ctrl_c3_t)+length];
|
||||
memset(header,0,sizeof(header));
|
||||
wifi_pkt_rx_ctrl_c3_t *pkt=(wifi_pkt_rx_ctrl_c3_t *)header;
|
||||
*pkt=(wifi_pkt_rx_ctrl_c3_t){
|
||||
.rssi=(signal_strength+(rand()%10)+96),
|
||||
.rate=11,
|
||||
.sig_len=length,
|
||||
.sig_len_copy=length,
|
||||
.legacy_length=length,
|
||||
.noise_floor=-97,
|
||||
.channel=packet_channel,
|
||||
.timestamp=qemu_clock_get_ns(QEMU_CLOCK_REALTIME)/1000,
|
||||
};
|
||||
// These 4 bits are set if the mac addresses previously stored at 0x40 and 0x48
|
||||
// match the destination or bssid addresses in the frame
|
||||
if(match_mac_address(frame->destination_address,(uint8_t *)s->mem+0x40))
|
||||
pkt->damatch0=1;
|
||||
if(match_mac_address(frame->destination_address,(uint8_t *)s->mem+0x48))
|
||||
pkt->damatch1=1;
|
||||
if(match_mac_address(frame->bssid_address,(uint8_t *)s->mem+0x40))
|
||||
pkt->bssidmatch0=1;
|
||||
if(match_mac_address(frame->bssid_address,(uint8_t *)s->mem+0x48))
|
||||
pkt->bssidmatch1=1;
|
||||
pkt->damatch0 = 1;
|
||||
pkt->bssidmatch0 = 1;
|
||||
|
||||
memcpy(header+sizeof(wifi_pkt_rx_ctrl_c3_t),frame,length);
|
||||
length+=sizeof(wifi_pkt_rx_ctrl_c3_t);
|
||||
// do a DMA transfer from the hardware to esp32 memory
|
||||
dma_list_item item;
|
||||
address_space_read(&address_space_memory, s->dma_inlink_address,
|
||||
MEMTXATTRS_UNSPECIFIED, &item, sizeof(item));
|
||||
address_space_write(&address_space_memory, item.address,
|
||||
MEMTXATTRS_UNSPECIFIED, header, length);
|
||||
item.length = length;
|
||||
item.eof = 1;
|
||||
address_space_write(&address_space_memory, s->dma_inlink_address,
|
||||
MEMTXATTRS_UNSPECIFIED, &item, sizeof(uint32_t));
|
||||
s->mem[0x8c / sizeof(uint32_t)] = item.next;
|
||||
s->mem[0x90 / sizeof(uint32_t)] = s->dma_inlink_address;
|
||||
if (item.next) {
|
||||
s->dma_inlink_address = item.next;
|
||||
} else {
|
||||
uint32_t base = s->mem[A_C3_WIFI_DMA_INLINK / sizeof(uint32_t)];
|
||||
uint32_t offset = base & 0xfffff;
|
||||
s->dma_inlink_address = offset ? 0x3fc00000 | offset : 0;
|
||||
}
|
||||
set_interrupt(s, 0x1004024);
|
||||
}
|
||||
|
||||
static const MemoryRegionOps esp32C3_wifi_ops = {
|
||||
.read = esp32C3_wifi_read,
|
||||
.write = esp32C3_wifi_write,
|
||||
.endianness = DEVICE_LITTLE_ENDIAN,
|
||||
};
|
||||
|
||||
static void esp32c3_wifi_reset(DeviceState *dev)
|
||||
{
|
||||
Esp32WifiState *s = ESP32_WIFI(dev);
|
||||
|
||||
s->dma_inlink_address=0;
|
||||
memset(s->mem,0,sizeof(s->mem));
|
||||
Esp32_WLAN_reset_ap(s);
|
||||
}
|
||||
|
||||
static void esp32C3_wifi_realize(DeviceState *dev, Error **errp)
|
||||
{
|
||||
Esp32WifiState *s = ESP32_WIFI(dev);
|
||||
SysBusDevice *sbd = SYS_BUS_DEVICE(dev);
|
||||
s->dma_inlink_address=0;
|
||||
|
||||
memory_region_init_io(&s->iomem, OBJECT(dev), &esp32C3_wifi_ops, s,
|
||||
TYPE_ESP32_WIFI, 0x1000);
|
||||
sysbus_init_mmio(sbd, &s->iomem);
|
||||
sysbus_init_irq(sbd, &s->irq);
|
||||
memset(s->mem,0,sizeof(s->mem));
|
||||
Esp32_WLAN_setup_ap(dev, s);
|
||||
|
||||
}
|
||||
static Property esp32C3_wifi_properties[] = {
|
||||
DEFINE_NIC_PROPERTIES(Esp32WifiState, conf),
|
||||
DEFINE_PROP_END_OF_LIST(),
|
||||
};
|
||||
|
||||
static void esp32C3_wifi_class_init(ObjectClass *klass, void *data)
|
||||
{
|
||||
DeviceClass *dc = DEVICE_CLASS(klass);
|
||||
|
||||
dc->realize = esp32C3_wifi_realize;
|
||||
device_class_set_legacy_reset(dc, esp32c3_wifi_reset);
|
||||
set_bit(DEVICE_CATEGORY_NETWORK, dc->categories);
|
||||
dc->desc = "Esp32C3 WiFi";
|
||||
device_class_set_props(dc, esp32C3_wifi_properties);
|
||||
}
|
||||
|
||||
|
||||
static const TypeInfo esp32C3_wifi_info = {
|
||||
.name = TYPE_ESP32_WIFI,
|
||||
.parent = TYPE_SYS_BUS_DEVICE,
|
||||
.instance_size = sizeof(Esp32WifiState),
|
||||
.class_init = esp32C3_wifi_class_init,
|
||||
};
|
||||
|
||||
static void esp32C3_wifi_register_types(void)
|
||||
{
|
||||
type_register_static(&esp32C3_wifi_info);
|
||||
}
|
||||
|
||||
type_init(esp32C3_wifi_register_types)
|
||||
+8
-1
@@ -155,7 +155,14 @@ system_ss.add(when: 'CONFIG_RISCV_ESP32C3', if_true: files(
|
||||
'esp32c3_jtag.c',
|
||||
'esp32c3_rtc_cntl.c',
|
||||
'esp_hmac.c',
|
||||
'esp32c3_hmac.c'
|
||||
'esp32c3_hmac.c',
|
||||
'esp32c3_wifi.c',
|
||||
'esp32_wifi_ap.c',
|
||||
'esp32_wlan_packet.c',
|
||||
'esp32c3_ana.c',
|
||||
'esp32_phya.c',
|
||||
'esp32_fe.c',
|
||||
'esp32c3_pwrmng.c'
|
||||
))
|
||||
|
||||
system_ss.add(when: 'CONFIG_XTENSA_ESP32S3', if_true: files(
|
||||
|
||||
+135
-26
@@ -51,11 +51,17 @@
|
||||
#include "hw/misc/esp32c3_ds.h"
|
||||
#include "hw/misc/esp32c3_xts_aes.h"
|
||||
#include "hw/misc/esp32c3_jtag.h"
|
||||
#include "hw/misc/esp32c3_wifi.h"
|
||||
#include "hw/misc/esp32c3_ana.h"
|
||||
#include "hw/misc/esp32_phya.h"
|
||||
#include "hw/misc/esp32_fe.h"
|
||||
#include "hw/misc/esp32c3_pwrmng.h"
|
||||
#include "hw/dma/esp32c3_gdma.h"
|
||||
#include "hw/display/esp_rgb.h"
|
||||
#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>
|
||||
@@ -76,6 +82,7 @@ struct Esp32C3MachineState {
|
||||
EspRISCVCPU soc;
|
||||
BusState periph_bus;
|
||||
MemoryRegion iomem;
|
||||
uint32_t syscon[0x1000 / sizeof(uint32_t)];
|
||||
bool xteink;
|
||||
bool x4;
|
||||
|
||||
@@ -183,12 +190,19 @@ static bool addr_in_range(hwaddr addr, hwaddr start, hwaddr end)
|
||||
|
||||
static uint64_t esp32c3_io_read(void *opaque, hwaddr addr, unsigned int size)
|
||||
{
|
||||
if (addr_in_range(addr + ESP32C3_IO_START_ADDR, DR_REG_RTC_I2C_BASE, DR_REG_RTC_I2C_BASE + 0x100)) {
|
||||
return (uint32_t) 0xffffff;
|
||||
} else if (addr + ESP32C3_IO_START_ADDR == DR_REG_SYSCON_BASE + A_SYSCON_ORIGIN_REG) {
|
||||
Esp32C3MachineState *ms = opaque;
|
||||
hwaddr absolute = addr + ESP32C3_IO_START_ADDR;
|
||||
|
||||
if (addr_in_range(absolute, DR_REG_NRX_BASE - 0xc00,
|
||||
DR_REG_BB_BASE + 0x1000)) {
|
||||
return UINT32_MAX;
|
||||
} else if (addr_in_range(absolute, DR_REG_RTC_I2C_BASE,
|
||||
DR_REG_RTC_I2C_BASE + 0x100)) {
|
||||
return 0xffffff;
|
||||
} else if (absolute == DR_REG_SYSCON_BASE + A_SYSCON_ORIGIN_REG) {
|
||||
/* Return "QEMU" as a 32-bit value */
|
||||
return 0x51454d55;
|
||||
} else if (addr + ESP32C3_IO_START_ADDR == DR_REG_SYSCON_BASE + A_SYSCON_RND_DATA_REG) {
|
||||
} else if (absolute == DR_REG_SYSCON_BASE + A_SYSCON_RND_DATA_REG) {
|
||||
/* Return a random 32-bit value */
|
||||
static bool init = false;
|
||||
if (!init) {
|
||||
@@ -196,8 +210,11 @@ static uint64_t esp32c3_io_read(void *opaque, hwaddr addr, unsigned int size)
|
||||
init = true;
|
||||
}
|
||||
return rand();
|
||||
} else if (addr + ESP32C3_IO_START_ADDR == DR_REG_ASSIST_DEBUG_BASE + A_ASSIST_DEBUG_CORE_0_DEBUG_MODE_REG) {
|
||||
} else if (absolute == DR_REG_ASSIST_DEBUG_BASE + A_ASSIST_DEBUG_CORE_0_DEBUG_MODE_REG) {
|
||||
return 0;
|
||||
} else if (addr_in_range(absolute, DR_REG_SYSCON_BASE,
|
||||
DR_REG_SYSCON_BASE + sizeof(ms->syscon))) {
|
||||
return ms->syscon[(absolute - DR_REG_SYSCON_BASE) / sizeof(uint32_t)];
|
||||
} else {
|
||||
#if ESP32C3_IO_WARNING
|
||||
warn_report("[ESP32-C3] Unsupported read to $%08lx\n", ESP32C3_IO_START_ADDR + addr);
|
||||
@@ -208,8 +225,16 @@ static uint64_t esp32c3_io_read(void *opaque, hwaddr addr, unsigned int size)
|
||||
|
||||
static void esp32c3_io_write(void *opaque, hwaddr addr, uint64_t value, unsigned int size)
|
||||
{
|
||||
Esp32C3MachineState *ms = opaque;
|
||||
hwaddr absolute = addr + ESP32C3_IO_START_ADDR;
|
||||
|
||||
if (addr_in_range(absolute, DR_REG_SYSCON_BASE,
|
||||
DR_REG_SYSCON_BASE + sizeof(ms->syscon))) {
|
||||
ms->syscon[(absolute - DR_REG_SYSCON_BASE) / sizeof(uint32_t)] = value;
|
||||
return;
|
||||
}
|
||||
#if ESP32C3_IO_WARNING
|
||||
warn_report("[ESP32-C3] Unsupported write $%08lx = %08lx\n", ESP32C3_IO_START_ADDR + addr, value);
|
||||
warn_report("[ESP32-C3] Unsupported write $%08lx = %08lx\n", absolute, value);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -271,34 +296,60 @@ static void esp32c3_init_spi_flash(Esp32C3MachineState *ms, BlockBackend* blk)
|
||||
}
|
||||
|
||||
|
||||
static void esp32c3_map_radio_device(const char *type, hwaddr address)
|
||||
{
|
||||
DeviceState *device = qdev_new(type);
|
||||
SysBusDevice *sbd = SYS_BUS_DEVICE(device);
|
||||
|
||||
sysbus_realize_and_unref(sbd, &error_fatal);
|
||||
memory_region_add_subregion_overlap(get_system_memory(), address,
|
||||
sysbus_mmio_get_region(sbd, 0), 0);
|
||||
}
|
||||
|
||||
static bool esp32c3_init_wifi(Esp32C3MachineState *ms)
|
||||
{
|
||||
DeviceState *wifi = qemu_create_nic_device(TYPE_ESP32C3_WIFI, false, NULL);
|
||||
SysBusDevice *sbd;
|
||||
|
||||
if (!wifi) {
|
||||
return false;
|
||||
}
|
||||
|
||||
esp32c3_map_radio_device(TYPE_ESP32C3_ANA, DR_REG_RTC_I2C_BASE);
|
||||
esp32c3_map_radio_device(TYPE_ESP32_PHYA, DR_REG_PHYA_BASE);
|
||||
esp32c3_map_radio_device(TYPE_ESP32_FE, DR_REG_FE_BASE);
|
||||
esp32c3_map_radio_device(TYPE_ESP32C3_PWR_MANAGER, DR_REG_PWR_MANAGER_BASE);
|
||||
|
||||
sbd = SYS_BUS_DEVICE(wifi);
|
||||
sysbus_realize_and_unref(sbd, &error_fatal);
|
||||
memory_region_add_subregion_overlap(get_system_memory(), DR_REG_WIFI_BASE,
|
||||
sysbus_mmio_get_region(sbd, 0), 0);
|
||||
sysbus_connect_irq(sbd, 0,
|
||||
qdev_get_gpio_in(DEVICE(&ms->intmatrix),
|
||||
ETS_WIFI_MAC_INTR_SOURCE));
|
||||
return true;
|
||||
}
|
||||
|
||||
static void esp32c3_init_openeth(Esp32C3MachineState *ms)
|
||||
{
|
||||
MemoryRegion* mr = NULL;
|
||||
SysBusDevice* sbd = NULL;
|
||||
DeviceState *open_eth_dev = qemu_create_nic_device("open_eth", true, NULL);
|
||||
SysBusDevice *sbd;
|
||||
|
||||
MemoryRegion* sys_mem = get_system_memory();
|
||||
|
||||
/* Create a new OpenCores Ethernet component */
|
||||
DeviceState* open_eth_dev = qemu_create_nic_device("open_eth", true, NULL);
|
||||
if (!open_eth_dev) {
|
||||
return;
|
||||
}
|
||||
|
||||
ms->eth = open_eth_dev;
|
||||
|
||||
sbd = SYS_BUS_DEVICE(open_eth_dev);
|
||||
sysbus_realize(sbd, &error_fatal);
|
||||
|
||||
/* OpenCores Ethernet has two memory regions: one for registers and one for descriptors,
|
||||
* we need to provide one I/O range for each of them */
|
||||
mr = sysbus_mmio_get_region(sbd, 0);
|
||||
memory_region_add_subregion_overlap(sys_mem, DR_REG_EMAC_BASE, mr, 0);
|
||||
mr = sysbus_mmio_get_region(sbd, 1);
|
||||
memory_region_add_subregion_overlap(sys_mem, DR_REG_EMAC_BASE + 0x400, mr, 0);
|
||||
|
||||
memory_region_add_subregion_overlap(get_system_memory(), DR_REG_EMAC_BASE,
|
||||
sysbus_mmio_get_region(sbd, 0), 0);
|
||||
memory_region_add_subregion_overlap(get_system_memory(),
|
||||
DR_REG_EMAC_BASE + 0x400,
|
||||
sysbus_mmio_get_region(sbd, 1), 0);
|
||||
sysbus_connect_irq(sbd, 0,
|
||||
qdev_get_gpio_in(DEVICE(&ms->intmatrix), ETS_ETH_MAC_INTR_SOURCE));
|
||||
|
||||
qdev_get_gpio_in(DEVICE(&ms->intmatrix),
|
||||
ETS_ETH_MAC_INTR_SOURCE));
|
||||
}
|
||||
|
||||
|
||||
@@ -420,7 +471,7 @@ static void esp32c3_machine_init(MachineState *machine)
|
||||
qdev_realize(DEVICE(&ms->soc), NULL, &error_fatal);
|
||||
|
||||
memory_region_init_io(&ms->iomem, OBJECT(&ms->soc), &esp32c3_io_ops,
|
||||
NULL, "esp32c3.iomem", 0xd1000);
|
||||
ms, "esp32c3.iomem", 0xd1000);
|
||||
memory_region_add_subregion(sys_mem, ESP32C3_IO_START_ADDR, &ms->iomem);
|
||||
|
||||
|
||||
@@ -495,14 +546,17 @@ static void esp32c3_machine_init(MachineState *machine)
|
||||
}
|
||||
}
|
||||
|
||||
/* Initialize OpenCores Ethernet controller now sicne it requires the interrupt matrix */
|
||||
esp32c3_init_openeth(ms);
|
||||
if (!esp32c3_init_wifi(ms)) {
|
||||
esp32c3_init_openeth(ms);
|
||||
}
|
||||
|
||||
/* USB Serial JTAG realization */
|
||||
{
|
||||
sysbus_realize(SYS_BUS_DEVICE(&ms->jtag), &error_fatal);
|
||||
MemoryRegion *mr = sysbus_mmio_get_region(SYS_BUS_DEVICE(&ms->jtag), 0);
|
||||
memory_region_add_subregion_overlap(sys_mem, DR_REG_USB_SERIAL_JTAG_BASE, mr, 0);
|
||||
sysbus_connect_irq(SYS_BUS_DEVICE(&ms->jtag), 0,
|
||||
qdev_get_gpio_in(intmatrix_dev, ETS_USB_SERIAL_JTAG_INTR_SOURCE));
|
||||
}
|
||||
|
||||
/* RTC CNTL realization */
|
||||
@@ -567,6 +621,8 @@ static void esp32c3_machine_init(MachineState *machine)
|
||||
sysbus_realize(SYS_BUS_DEVICE(&ms->gpio), &error_fatal);
|
||||
MemoryRegion *mr = sysbus_mmio_get_region(SYS_BUS_DEVICE(&ms->gpio), 0);
|
||||
memory_region_add_subregion_overlap(sys_mem, DR_REG_GPIO_BASE, mr, 0);
|
||||
sysbus_connect_irq(SYS_BUS_DEVICE(&ms->gpio), 0,
|
||||
qdev_get_gpio_in(intmatrix_dev, ETS_GPIO_INTR_SOURCE));
|
||||
}
|
||||
|
||||
/* (Extmem) Cache realization */
|
||||
@@ -744,6 +800,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);
|
||||
@@ -763,6 +869,9 @@ 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);
|
||||
object_property_add_alias(OBJECT(machine), "frame-generation",
|
||||
OBJECT(panel), "frame-generation");
|
||||
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,
|
||||
|
||||
@@ -13,5 +13,7 @@ struct ESP32C3AdcState {
|
||||
MemoryRegion iomem;
|
||||
uint32_t regs[ESP32C3_ADC_IO_SIZE / sizeof(uint32_t)];
|
||||
uint32_t input[ESP32C3_ADC_CHANNELS];
|
||||
/* Sample counters let a host driver hold a button until the guest has actually read it. */
|
||||
uint32_t samples[ESP32C3_ADC_CHANNELS];
|
||||
uint8_t channel;
|
||||
};
|
||||
|
||||
@@ -28,6 +28,9 @@ typedef struct Esp32GpioState {
|
||||
uint32_t input_level;
|
||||
uint32_t output_level;
|
||||
uint32_t output_enable;
|
||||
uint32_t interrupt_status;
|
||||
uint32_t pin_config[ESP32_GPIO_COUNT];
|
||||
hwaddr pin_config_base;
|
||||
} Esp32GpioState;
|
||||
|
||||
typedef struct Esp32GpioClass {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "hw/hw.h"
|
||||
#include "hw/sysbus.h"
|
||||
|
||||
|
||||
#define TYPE_ESP32_FE "misc.esp32.fe"
|
||||
#define ESP32_FE(obj) OBJECT_CHECK(Esp32FeState, (obj), TYPE_ESP32_FE)
|
||||
//OBJECT_CHECK(Esp32FeState, (obj), TYPE_ESP32_FE)
|
||||
//(Esp32FeState *)(obj)
|
||||
|
||||
typedef struct Esp32FeState {
|
||||
SysBusDevice parent_obj;
|
||||
MemoryRegion iomem;
|
||||
uint32_t mem[1024];
|
||||
} Esp32FeState;
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "hw/hw.h"
|
||||
#include "hw/sysbus.h"
|
||||
|
||||
|
||||
#define TYPE_ESP32_PHYA "misc.esp32.phya"
|
||||
#define ESP32_PHYA(obj) OBJECT_CHECK(Esp32PhyaState, (obj), TYPE_ESP32_PHYA)
|
||||
|
||||
typedef struct Esp32PhyaState {
|
||||
SysBusDevice parent_obj;
|
||||
MemoryRegion iomem;
|
||||
uint32_t mem[1024];
|
||||
} Esp32PhyaState;
|
||||
|
||||
#define ESP32_PHYA_NACK 0x00002300
|
||||
#define ESP32_PHYA_ACK 0x0
|
||||
|
||||
void Esp32_WLAN_Set_Packet_Status(const uint32_t state);
|
||||
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
|
||||
#include "hw/hw.h"
|
||||
#include "hw/registerfields.h"
|
||||
#include "hw/sysbus.h"
|
||||
//#include "hw/misc/esp32_reg.h"
|
||||
#include "sysemu/sysemu.h"
|
||||
#include "net/net.h"
|
||||
|
||||
#define TYPE_ESP32_WIFI "esp32c3_wifi"
|
||||
#define ESP32_WIFI(obj) OBJECT_CHECK(Esp32WifiState, (obj), TYPE_ESP32_WIFI)
|
||||
|
||||
typedef struct dma_list_item {
|
||||
unsigned size:12;
|
||||
unsigned length:12;
|
||||
unsigned :6;
|
||||
unsigned eof:1;
|
||||
unsigned owner:1;
|
||||
uint32_t address;
|
||||
uint32_t next;
|
||||
} QEMU_PACKED dma_list_item;
|
||||
|
||||
typedef struct Esp32WifiState {
|
||||
SysBusDevice parent_obj;
|
||||
MemoryRegion iomem;
|
||||
int raw_interrupt;
|
||||
qemu_irq irq;
|
||||
uint32_t mem[1024];
|
||||
int dma_inlink_address;
|
||||
uint32_t ap_state;
|
||||
int inject_queue_size;
|
||||
struct mac80211_frame *inject_queue;
|
||||
int inject_timer_running;
|
||||
unsigned int inject_sequence_number;
|
||||
int beacon_ap;
|
||||
|
||||
hwaddr receive_queue_address;
|
||||
uint32_t receive_queue_count;
|
||||
NICConf conf;
|
||||
NICState *nic;
|
||||
// various timers
|
||||
QEMUTimer *beacon_timer;
|
||||
QEMUTimer *inject_timer;
|
||||
QEMUTimer *wait_ack_timer;
|
||||
uint8_t ipaddr[4];
|
||||
uint8_t macaddr[6];
|
||||
|
||||
uint8_t ap_ipaddr[4];
|
||||
uint8_t ap_macaddr[6];
|
||||
|
||||
uint8_t associated_ap_macaddr[6];
|
||||
|
||||
uint8_t softap_macaddr[6];
|
||||
|
||||
uint8_t mode;
|
||||
|
||||
} Esp32WifiState;
|
||||
|
||||
#define Esp32_Mode_Station 0
|
||||
#define Esp32_Mode_SoftAP 1
|
||||
|
||||
|
||||
void Esp32_WLAN_handle_frame(Esp32WifiState *s, struct mac80211_frame *frame);
|
||||
void Esp32_WLAN_setup_ap(DeviceState *dev,Esp32WifiState *s);
|
||||
void Esp32_WLAN_reset_ap(Esp32WifiState *s);
|
||||
void Esp32_sendFrame(Esp32WifiState *s, struct mac80211_frame *frame,int length, int signal_strength);
|
||||
void Esp32_WLAN_frame_delivered(Esp32WifiState *s);
|
||||
|
||||
REG32(WIFI_DMA_IN_STATUS, 0x84);
|
||||
REG32(WIFI_DMA_INLINK, 0x88);
|
||||
REG32(WIFI_DMA_INT_STATUS, 0xc48);
|
||||
REG32(WIFI_DMA_INT_CLR, 0xc4c);
|
||||
REG32(WIFI_STATUS, 0xcc8);
|
||||
REG32(WIFI_DMA_OUTLINK, 0xd20);
|
||||
REG32(WIFI_DMA_OUT_STATUS, 0xd24);
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "hw/hw.h"
|
||||
#include "hw/sysbus.h"
|
||||
|
||||
|
||||
#define TYPE_ESP32C3_ANA "misc.esp32c3.ana"
|
||||
#define ESP32C3_ANA(obj) OBJECT_CHECK(Esp32C3AnaState, (obj), TYPE_ESP32C3_ANA)
|
||||
|
||||
typedef struct Esp32C3AnaState {
|
||||
SysBusDevice parent_obj;
|
||||
MemoryRegion iomem;
|
||||
uint32_t mem[1024];
|
||||
} Esp32C3AnaState;
|
||||
|
||||
|
||||
@@ -22,5 +22,7 @@
|
||||
typedef struct ESP32C3UsbJtagState {
|
||||
SysBusDevice parent_object;
|
||||
MemoryRegion iomem;
|
||||
qemu_irq irq;
|
||||
uint32_t int_ena;
|
||||
} ESP32C3UsbJtagState;
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "hw/sysbus.h"
|
||||
#include "hw/hw.h"
|
||||
#include "hw/registerfields.h"
|
||||
|
||||
#define TYPE_ESP32C3_PWR_MANAGER "misc.esp32c3.pwrmng"
|
||||
#define ESP32C3_PWR_MANAGER(obj) OBJECT_CHECK(Esp32c3PwrMngState, (obj), TYPE_ESP32C3_PWR_MANAGER)
|
||||
|
||||
|
||||
typedef struct Esp32c3PwrMngState {
|
||||
SysBusDevice parent_obj;
|
||||
MemoryRegion iomem;
|
||||
uint32_t mem[1024];
|
||||
} Esp32c3PwrMngState;
|
||||
|
||||
@@ -42,6 +42,9 @@
|
||||
#define DR_REG_APB_CTRL_BASE 0x60026000 /* Old name for SYSCON, to be removed */
|
||||
#define DR_REG_TWAI_BASE 0x6002B000
|
||||
#define DR_REG_I2S0_BASE 0x6002D000
|
||||
#define DR_REG_WIFI_BASE 0x60033000
|
||||
#define DR_REG_PHYA_BASE 0x60034000
|
||||
#define DR_REG_PWR_MANAGER_BASE 0x60035000
|
||||
#define DR_REG_APB_SARADC_BASE 0x60040000
|
||||
#define DR_REG_USB_SERIAL_JTAG_BASE 0x60043000
|
||||
#define DR_REG_AES_XTS_BASE 0x600CC000
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "hw/hw.h"
|
||||
#include "hw/registerfields.h"
|
||||
#include "hw/sysbus.h"
|
||||
#include "sysemu/sysemu.h"
|
||||
#include "net/net.h"
|
||||
#include "esp32_wifi.h"
|
||||
|
||||
#define TYPE_ESP32C3_WIFI TYPE_ESP32_WIFI
|
||||
|
||||
|
||||
REG32(C3_WIFI_DMA_IN_STATUS, 0x84);
|
||||
REG32(C3_WIFI_DMA_INLINK, 0x88);
|
||||
REG32(C3_WIFI_DMA_INT_STATUS, 0xc3C);
|
||||
REG32(C3_WIFI_DMA_INT_CLR, 0xc40);
|
||||
REG32(C3_WIFI_STATUS, 0xcb0);
|
||||
REG32(C3_WIFI_DMA_OUTLINK, 0xd08);
|
||||
REG32(C3_WIFI_DMA_OUT_STATUS, 0xd14);
|
||||
Binary file not shown.
Binary file not shown.
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
from xteink.cli import run_cli
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_cli()
|
||||
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
import json
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
state, mode, text = sys.argv[1:4]
|
||||
buttons = {
|
||||
"up": ("/machine/adc", "adci[2]", 2242, 4095),
|
||||
"down": ("/machine/adc", "adci[2]", 5, 4095),
|
||||
"left": ("/machine/adc", "adci[1]", 1493, 4095),
|
||||
"right": ("/machine/adc", "adci[1]", 5, 4095),
|
||||
"select": ("/machine/adc", "adci[1]", 2694, 4095),
|
||||
}
|
||||
rows = ["1234567890", "qwertyuiop", "asdfghjkl", "zxcvbnm"]
|
||||
widths = [10, 10, 9, 9, 6 if mode == "url" else 5]
|
||||
keys = {char: (row, col) for row, chars in enumerate(rows) for col, char in enumerate(chars)}
|
||||
if mode == "url":
|
||||
keys.update({":": (4, 1), "/": (4, 2), ".": (4, 3)})
|
||||
ok = (4, 5)
|
||||
else:
|
||||
ok = (4, 4)
|
||||
|
||||
sock = socket.socket(socket.AF_UNIX)
|
||||
sock.connect(f"{state}/qmp.sock")
|
||||
stream = sock.makefile("rw")
|
||||
json.loads(stream.readline())
|
||||
|
||||
def command(execute, arguments=None):
|
||||
payload = {"execute": execute}
|
||||
if arguments is not None:
|
||||
payload["arguments"] = arguments
|
||||
stream.write(json.dumps(payload) + "\n")
|
||||
stream.flush()
|
||||
while True:
|
||||
response = json.loads(stream.readline())
|
||||
if "return" in response or "error" in response:
|
||||
if "error" in response:
|
||||
raise RuntimeError(response["error"])
|
||||
return
|
||||
|
||||
command("qmp_capabilities")
|
||||
|
||||
def press(name):
|
||||
path, prop, pressed, released = buttons[name]
|
||||
command("qom-set", {"path": path, "property": prop, "value": pressed})
|
||||
time.sleep(0.06)
|
||||
command("qom-set", {"path": path, "property": prop, "value": released})
|
||||
time.sleep(1 if name == "select" else 0.12)
|
||||
|
||||
row = col = 0
|
||||
|
||||
def move_to(target_row, target_col):
|
||||
global row, col
|
||||
while row:
|
||||
old_width = widths[row]
|
||||
press("up")
|
||||
row -= 1
|
||||
col = col * widths[row] // old_width
|
||||
while col:
|
||||
press("left")
|
||||
col -= 1
|
||||
while row < target_row:
|
||||
old_width = widths[row]
|
||||
press("down")
|
||||
row += 1
|
||||
col = col * widths[row] // old_width
|
||||
while col < target_col:
|
||||
press("right")
|
||||
col += 1
|
||||
|
||||
for char in text.lower():
|
||||
move_to(*keys[char])
|
||||
press("select")
|
||||
move_to(*ok)
|
||||
press("select")
|
||||
@@ -0,0 +1,186 @@
|
||||
"""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 memory(args):
|
||||
state = state_dir(args)
|
||||
emu.require_running(state)
|
||||
if args.dump:
|
||||
print(emu.dump_memory(state, args.region, args.dump))
|
||||
return
|
||||
for row in emu.memory_usage(state):
|
||||
percent = 100 * row["touched"] / row["size"]
|
||||
print(
|
||||
f"{row['region']:<7}{row['base']:#010x} {row['size'] // 1024:>4} KiB total"
|
||||
f" {row['touched'] // 1024:>4} KiB touched ({percent:.0f}%)"
|
||||
)
|
||||
|
||||
|
||||
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("memory", help="RAM high-water marks, or dump a region with --dump")
|
||||
add_state(command)
|
||||
command.add_argument("--region", choices=emu.MEMORY_REGIONS, default="dram")
|
||||
command.add_argument("--dump", help="write the raw region to this file")
|
||||
command.set_defaults(handler=memory)
|
||||
|
||||
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)
|
||||
Executable
+343
@@ -0,0 +1,343 @@
|
||||
"""Core emulator control shared by the CLI and the web UI."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
FLASH_SIZE = 16 * 1024 * 1024
|
||||
APP_OFFSET = 0x10000
|
||||
PARTITIONS_OFFSET = 0x8000
|
||||
RELEASED_ADC = 4095
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.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),
|
||||
"bottom-1": ("/machine/adc", "adci[1]", 3512, RELEASED_ADC),
|
||||
"bottom-2": ("/machine/adc", "adci[1]", 2694, RELEASED_ADC),
|
||||
"bottom-3": ("/machine/adc", "adci[1]", 1493, RELEASED_ADC),
|
||||
"bottom-4": ("/machine/adc", "adci[1]", 5, RELEASED_ADC),
|
||||
"power": ("/machine/gpio", "input-level[3]", False, True),
|
||||
}
|
||||
VARIANTS = ("x3", "x4")
|
||||
# ESP32-C3 RAM windows. dram and iram alias the same internal SRAM, so their numbers overlap.
|
||||
MEMORY_REGIONS = {
|
||||
"dram": (0x3FC80000, 0x60000),
|
||||
"iram": (0x4037C000, 0x60000 + 16 * 1024),
|
||||
"rtcram": (0x50000000, 0x2000),
|
||||
}
|
||||
|
||||
|
||||
def default_state_dir():
|
||||
return Path(tempfile.gettempdir()) / f"xteink-emu-{os.getuid()}"
|
||||
|
||||
|
||||
def require_env(name):
|
||||
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):
|
||||
try:
|
||||
return int((state / "pid").read_text().strip())
|
||||
except (FileNotFoundError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def process_alive(pid):
|
||||
if pid is None:
|
||||
return False
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
return True
|
||||
except ProcessLookupError:
|
||||
return False
|
||||
except PermissionError:
|
||||
return True
|
||||
|
||||
|
||||
def is_running(state):
|
||||
return process_alive(pid_from(state))
|
||||
|
||||
|
||||
def require_running(state):
|
||||
pid = pid_from(state)
|
||||
if not process_alive(pid):
|
||||
raise RuntimeError(f"xteink-emu is not running in {state}")
|
||||
return pid
|
||||
|
||||
|
||||
def qmp_command(state, execute, arguments=None, timeout=5):
|
||||
with socket.socket(socket.AF_UNIX) as sock:
|
||||
sock.settimeout(timeout)
|
||||
sock.connect(str(state / "qmp.sock"))
|
||||
stream = sock.makefile("rwb")
|
||||
json.loads(stream.readline())
|
||||
|
||||
def send(command):
|
||||
stream.write(json.dumps(command).encode() + b"\n")
|
||||
stream.flush()
|
||||
while True:
|
||||
response = json.loads(stream.readline())
|
||||
if "event" in response:
|
||||
continue
|
||||
if "error" in response:
|
||||
raise RuntimeError(response["error"]["desc"])
|
||||
return response.get("return")
|
||||
|
||||
send({"execute": "qmp_capabilities"})
|
||||
command = {"execute": execute}
|
||||
if arguments is not None:
|
||||
command["arguments"] = arguments
|
||||
return send(command)
|
||||
|
||||
|
||||
def build_flash(firmware, output):
|
||||
app = firmware.read_bytes()
|
||||
if len(app) == FLASH_SIZE:
|
||||
output.write_bytes(app)
|
||||
return
|
||||
if not app or app[0] != 0xE9:
|
||||
raise RuntimeError(f"{firmware} is not an ESP32 app image")
|
||||
if APP_OFFSET + len(app) > FLASH_SIZE:
|
||||
raise RuntimeError(f"{firmware} does not fit in a 16 MiB flash image")
|
||||
|
||||
flash = bytearray(b"\xff" * FLASH_SIZE)
|
||||
bootloader = require_env("XTEINK_BOOTLOADER").read_bytes()
|
||||
partitions = require_env("XTEINK_PARTITIONS").read_bytes()
|
||||
flash[: len(bootloader)] = bootloader
|
||||
flash[PARTITIONS_OFFSET : PARTITIONS_OFFSET + len(partitions)] = partitions
|
||||
flash[APP_OFFSET : APP_OFFSET + len(app)] = app
|
||||
output.write_bytes(flash)
|
||||
|
||||
|
||||
def hold_power(state, hold_ms):
|
||||
path, prop, pressed, released = BUTTONS["power"]
|
||||
qmp_command(state, "qom-set", {"path": path, "property": prop, "value": pressed})
|
||||
try:
|
||||
time.sleep(hold_ms / 1000)
|
||||
finally:
|
||||
qmp_command(state, "qom-set", {"path": path, "property": prop, "value": released})
|
||||
|
||||
|
||||
def launch(state, firmware, sdcard, variant="x3", power_hold_ms=2000, interactive=False):
|
||||
"""Boot QEMU in `state`, returning the process once its QMP socket answers."""
|
||||
old_pid = pid_from(state)
|
||||
if process_alive(old_pid):
|
||||
raise RuntimeError(f"xteink-emu is already running as PID {old_pid} in {state}")
|
||||
if variant not in VARIANTS:
|
||||
raise RuntimeError(f"unknown variant: {variant}")
|
||||
|
||||
firmware = Path(firmware).expanduser().resolve()
|
||||
sdcard = Path(sdcard).expanduser().resolve()
|
||||
if not firmware.is_file():
|
||||
raise RuntimeError(f"firmware not found: {firmware}")
|
||||
if not sdcard.is_file():
|
||||
raise RuntimeError(f"SD card image not found: {sdcard}")
|
||||
|
||||
state.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
for name in ("pid", "qmp.sock", "serial.log", "qemu.log", "wait.offset"):
|
||||
(state / name).unlink(missing_ok=True)
|
||||
(state / "serial.log").touch()
|
||||
(state / "wait.offset").write_text("0\n")
|
||||
flash = state / "flash.bin"
|
||||
build_flash(firmware, flash)
|
||||
efuse = bytearray(1024)
|
||||
efuse[0x18:0x1E] = bytes.fromhex("563412005452")
|
||||
efuse[0x26] = 0x0C
|
||||
(state / "efuse.bin").write_bytes(efuse)
|
||||
|
||||
command = [
|
||||
str(require_env("XTEINK_QEMU")),
|
||||
"-machine", f"xteink,variant={variant}",
|
||||
"-accel", "tcg",
|
||||
"-L", str(require_env("XTEINK_BIOS")),
|
||||
"-display", "gtk" if interactive else "none",
|
||||
"-serial", "stdio",
|
||||
"-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",
|
||||
"-drive", f"file={flash},if=mtd,format=raw",
|
||||
"-drive", f"file={state / 'efuse.bin'},if=none,format=raw,id=efuse",
|
||||
"-global", "driver=nvram.esp32c3.efuse,property=drive,value=efuse",
|
||||
"-drive", f"file={sdcard},if=sd,format=raw",
|
||||
]
|
||||
if interactive:
|
||||
process = subprocess.Popen(command)
|
||||
else:
|
||||
with (state / "serial.log").open("ab") as serial_log:
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=serial_log,
|
||||
stderr=subprocess.STDOUT,
|
||||
start_new_session=True,
|
||||
)
|
||||
(state / "pid").write_text(f"{process.pid}\n")
|
||||
|
||||
deadline = time.monotonic() + 10
|
||||
while time.monotonic() < deadline:
|
||||
if process.poll() is not None:
|
||||
message = (state / "serial.log").read_text(errors="replace")
|
||||
raise RuntimeError(f"QEMU exited during startup\n{message}")
|
||||
try:
|
||||
qmp_command(state, "query-status")
|
||||
if power_hold_ms:
|
||||
hold_power(state, power_hold_ms)
|
||||
return process
|
||||
except (FileNotFoundError, ConnectionRefusedError, socket.timeout):
|
||||
time.sleep(0.05)
|
||||
process.terminate()
|
||||
raise RuntimeError("QEMU did not open its QMP socket")
|
||||
|
||||
|
||||
def shutdown(state):
|
||||
pid = require_running(state)
|
||||
try:
|
||||
qmp_command(state, "quit")
|
||||
except (ConnectionError, FileNotFoundError):
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
|
||||
deadline = time.monotonic() + 5
|
||||
while process_alive(pid) and time.monotonic() < deadline:
|
||||
time.sleep(0.05)
|
||||
if process_alive(pid):
|
||||
os.kill(pid, signal.SIGKILL)
|
||||
(state / "pid").unlink(missing_ok=True)
|
||||
(state / "qmp.sock").unlink(missing_ok=True)
|
||||
return pid
|
||||
|
||||
|
||||
def wait_for_log(state, match, timeout):
|
||||
"""Scan the serial log from the persistent cursor and return the first matching line."""
|
||||
require_running(state)
|
||||
offset_path = state / "wait.offset"
|
||||
try:
|
||||
pattern = re.compile(match)
|
||||
except re.error as error:
|
||||
raise RuntimeError(f"invalid regex: {error}") from error
|
||||
try:
|
||||
offset = int(offset_path.read_text().strip())
|
||||
except (FileNotFoundError, ValueError):
|
||||
offset = 0
|
||||
|
||||
deadline = time.monotonic() + timeout
|
||||
with (state / "serial.log").open("r", errors="replace") as log:
|
||||
log.seek(offset)
|
||||
while True:
|
||||
line = log.readline()
|
||||
if line:
|
||||
if pattern.search(line):
|
||||
offset_path.write_text(f"{log.tell()}\n")
|
||||
return line
|
||||
continue
|
||||
if time.monotonic() >= deadline:
|
||||
raise RuntimeError(f"timed out after {timeout:g}s waiting for /{match}/")
|
||||
if not is_running(state):
|
||||
raise RuntimeError("QEMU exited while waiting for serial output")
|
||||
time.sleep(0.05)
|
||||
|
||||
|
||||
def frame_generation(state):
|
||||
"""Panel refresh counter; changes only when the e-ink display actually redraws."""
|
||||
return qmp_command(state, "qom-get", {"path": "/machine", "property": "frame-generation"})
|
||||
|
||||
|
||||
def await_frame(state, since, timeout):
|
||||
"""Block until the panel redraws, returning the new generation or None on timeout."""
|
||||
deadline = time.monotonic() + timeout
|
||||
while True:
|
||||
current = frame_generation(state)
|
||||
if current != since:
|
||||
return current
|
||||
if time.monotonic() >= deadline:
|
||||
return None
|
||||
time.sleep(0.05)
|
||||
|
||||
|
||||
def adc_samples(state, prop):
|
||||
"""Number of times the guest has read this ADC channel."""
|
||||
channel = prop[prop.index("[") + 1 : prop.index("]")]
|
||||
return qmp_command(state, "qom-get", {"path": "/machine/adc", "property": f"adcsamples[{channel}]"})
|
||||
|
||||
|
||||
def await_samples(state, prop, count, timeout):
|
||||
"""Block until the guest reads the channel again.
|
||||
|
||||
Holding a button for a fixed wall-clock time is unreliable: the firmware only polls the
|
||||
resistor ladder between e-ink refreshes, which can block for seconds.
|
||||
"""
|
||||
start = adc_samples(state, prop)
|
||||
deadline = time.monotonic() + timeout
|
||||
while adc_samples(state, prop) - start < count:
|
||||
if time.monotonic() >= deadline:
|
||||
raise RuntimeError(f"timed out after {timeout:g}s waiting for the guest to sample {prop}")
|
||||
time.sleep(0.01)
|
||||
|
||||
|
||||
def set_button(state, name, down):
|
||||
path, prop, pressed, released = BUTTONS[name]
|
||||
qmp_command(state, "qom-set", {"path": path, "property": prop, "value": pressed if down else released})
|
||||
|
||||
|
||||
def press(state, name, samples=4, timeout=90, hold_ms=500):
|
||||
path, prop, _, _ = BUTTONS[name]
|
||||
adc = not path.endswith("gpio")
|
||||
set_button(state, name, down=True)
|
||||
try:
|
||||
if adc:
|
||||
await_samples(state, prop, samples, timeout)
|
||||
else:
|
||||
time.sleep(hold_ms / 1000)
|
||||
finally:
|
||||
set_button(state, name, down=False)
|
||||
if adc:
|
||||
await_samples(state, prop, samples, timeout)
|
||||
|
||||
|
||||
def dump_memory(state, region, output):
|
||||
base, size = MEMORY_REGIONS[region]
|
||||
output = Path(output).expanduser().resolve()
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
qmp_command(state, "pmemsave", {"val": base, "size": size, "filename": str(output)}, timeout=30)
|
||||
return output
|
||||
|
||||
|
||||
def memory_usage(state):
|
||||
"""Bytes the guest has written per RAM region.
|
||||
|
||||
QEMU zeroes RAM at reset, so a non-zero byte is one the firmware touched. That makes this a
|
||||
high-water mark rather than live heap usage: freed blocks keep their old contents.
|
||||
"""
|
||||
scratch = state / "memory.bin"
|
||||
rows = []
|
||||
try:
|
||||
for region, (base, size) in MEMORY_REGIONS.items():
|
||||
data = dump_memory(state, region, scratch).read_bytes()
|
||||
rows.append({"region": region, "base": base, "size": size, "touched": len(data) - data.count(0)})
|
||||
finally:
|
||||
scratch.unlink(missing_ok=True)
|
||||
return rows
|
||||
|
||||
|
||||
def screendump(state, output, settle_ms=0):
|
||||
# The e-ink panel needs a full refresh cycle before a capture shows the latest frame.
|
||||
if settle_ms:
|
||||
time.sleep(settle_ms / 1000)
|
||||
output = Path(output).expanduser().resolve()
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
qmp_command(state, "screendump", {"filename": str(output), "format": "png"}, timeout=30)
|
||||
return output
|
||||
@@ -0,0 +1,229 @@
|
||||
"""Browser front end: boot firmware, drive the buttons, watch the screen and serial log."""
|
||||
|
||||
import http.server
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import urllib.parse
|
||||
from pathlib import Path
|
||||
|
||||
from .. import emu
|
||||
from . import sdcard
|
||||
|
||||
PAGE = Path(__file__).resolve().parent / "index.html"
|
||||
MAX_UPLOAD = 512 * 1024 * 1024
|
||||
LONG_POLL_SECONDS = 25
|
||||
UPLOADS = {"firmware": "firmware.bin", "sdcard": "sdcard.img"}
|
||||
|
||||
|
||||
def settings_path(state):
|
||||
return state / "web.json"
|
||||
|
||||
|
||||
def settings(state):
|
||||
try:
|
||||
return json.loads(settings_path(state).read_text())
|
||||
except (FileNotFoundError, ValueError):
|
||||
return {}
|
||||
|
||||
|
||||
def save_settings(state, values):
|
||||
settings_path(state).write_text(json.dumps(settings(state) | values))
|
||||
|
||||
|
||||
def blank_sdcard(path, size_mb):
|
||||
"""Create a FAT32 card so the UI works without the user supplying an image."""
|
||||
mkfs = shutil.which("mkfs.fat")
|
||||
if not mkfs:
|
||||
raise RuntimeError("mkfs.fat not found; upload an SD card image instead")
|
||||
with path.open("wb") as image:
|
||||
image.truncate(size_mb * 1024 * 1024)
|
||||
subprocess.run([mkfs, "-F", "32", str(path)], check=True, capture_output=True)
|
||||
|
||||
|
||||
def serve(state, host, port, log_bytes):
|
||||
state.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
capture = state / "web-screen.png"
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
protocol_version = "HTTP/1.1"
|
||||
|
||||
def reply(self, body, content_type="text/plain; charset=utf-8", status=200, headers=()):
|
||||
if isinstance(body, str):
|
||||
body = body.encode()
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", content_type)
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
for name, value in headers:
|
||||
self.send_header(name, value)
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def reply_json(self, value, status=200):
|
||||
self.reply(json.dumps(value), "application/json", status)
|
||||
|
||||
def query(self):
|
||||
return urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
|
||||
|
||||
def body(self):
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
if length > MAX_UPLOAD:
|
||||
raise RuntimeError(f"upload larger than {MAX_UPLOAD // 1024 // 1024} MiB")
|
||||
return self.rfile.read(length)
|
||||
|
||||
def status_payload(self):
|
||||
stored = settings(state)
|
||||
return {
|
||||
"running": emu.is_running(state),
|
||||
"variant": stored.get("variant", "x3"),
|
||||
"firmware": stored.get("firmware"),
|
||||
"sdcard": stored.get("sdcard"),
|
||||
"buttons": list(emu.BUTTONS),
|
||||
"variants": list(emu.VARIANTS),
|
||||
}
|
||||
|
||||
def do_GET(self):
|
||||
route = urllib.parse.urlparse(self.path).path
|
||||
query = self.query()
|
||||
try:
|
||||
if route == "/":
|
||||
return self.reply(PAGE.read_bytes(), "text/html; charset=utf-8")
|
||||
if route == "/status":
|
||||
return self.reply_json(self.status_payload())
|
||||
if route == "/screen.png":
|
||||
emu.require_running(state)
|
||||
# Long Poll - The panel only redraws on a real refresh, so block until the
|
||||
# guest bumps the frame counter instead of re-capturing on a timer.
|
||||
since = query.get("since", [None])[0]
|
||||
generation = emu.frame_generation(state)
|
||||
if since is not None and str(generation) == since:
|
||||
generation = emu.await_frame(state, generation, LONG_POLL_SECONDS)
|
||||
if generation is None:
|
||||
return self.reply(b"", status=304)
|
||||
emu.screendump(state, capture)
|
||||
return self.reply(
|
||||
capture.read_bytes(),
|
||||
"image/png",
|
||||
headers=[("X-Frame-Generation", str(generation))],
|
||||
)
|
||||
if route == "/files":
|
||||
path = query.get("path", ["/"])[0]
|
||||
return self.reply_json(sdcard.listing(state / UPLOADS["sdcard"], path))
|
||||
if route == "/files/read":
|
||||
path = query.get("path", [""])[0]
|
||||
data = sdcard.read(state / UPLOADS["sdcard"], path)
|
||||
try:
|
||||
text = data.decode()
|
||||
except UnicodeDecodeError:
|
||||
return self.reply_json({"binary": True, "size": len(data)})
|
||||
return self.reply_json({"binary": False, "text": text})
|
||||
if route == "/memory":
|
||||
emu.require_running(state)
|
||||
return self.reply_json(emu.memory_usage(state))
|
||||
if route == "/memory/dump":
|
||||
emu.require_running(state)
|
||||
region = query.get("region", ["dram"])[0]
|
||||
if region not in emu.MEMORY_REGIONS:
|
||||
return self.send_error(404)
|
||||
dump = emu.dump_memory(state, region, state / "web-memory.bin")
|
||||
return self.reply(
|
||||
dump.read_bytes(),
|
||||
"application/octet-stream",
|
||||
headers=[("Content-Disposition", f'attachment; filename="{region}.bin"')],
|
||||
)
|
||||
if route == "/log":
|
||||
# Tail only; a long-lived emulator log outgrows what the page can hold.
|
||||
return self.reply((state / "serial.log").read_bytes()[-log_bytes:])
|
||||
except (RuntimeError, OSError) as error:
|
||||
return self.reply(str(error), status=500)
|
||||
self.send_error(404)
|
||||
|
||||
def do_POST(self):
|
||||
route = urllib.parse.urlparse(self.path).path
|
||||
query = self.query()
|
||||
try:
|
||||
if route == "/upload":
|
||||
name = query.get("name", [""])[0]
|
||||
if name not in UPLOADS:
|
||||
return self.send_error(404)
|
||||
data = self.body()
|
||||
if not data:
|
||||
raise RuntimeError(f"empty {name} upload")
|
||||
(state / UPLOADS[name]).write_bytes(data)
|
||||
filename = query.get("filename", [name])[0]
|
||||
save_settings(state, {name: filename})
|
||||
return self.reply_json(self.status_payload())
|
||||
|
||||
if route == "/sdcard/blank":
|
||||
size_mb = max(8, min(int(query.get("size-mb", ["64"])[0]), 4096))
|
||||
blank_sdcard(state / UPLOADS["sdcard"], size_mb)
|
||||
save_settings(state, {"sdcard": f"blank {size_mb} MiB"})
|
||||
return self.reply_json(self.status_payload())
|
||||
|
||||
if route == "/boot":
|
||||
variant = query.get("variant", ["x3"])[0]
|
||||
hold = int(query.get("power-hold-ms", ["2000"])[0])
|
||||
firmware = state / UPLOADS["firmware"]
|
||||
card = state / UPLOADS["sdcard"]
|
||||
if not firmware.is_file():
|
||||
raise RuntimeError("upload a firmware image first")
|
||||
if not card.is_file():
|
||||
blank_sdcard(card, 64)
|
||||
save_settings(state, {"sdcard": "blank 64 MiB"})
|
||||
if emu.is_running(state):
|
||||
emu.shutdown(state)
|
||||
emu.launch(state, firmware, card, variant=variant, power_hold_ms=hold)
|
||||
save_settings(state, {"variant": variant})
|
||||
return self.reply_json(self.status_payload())
|
||||
|
||||
if route == "/stop":
|
||||
if emu.is_running(state):
|
||||
emu.shutdown(state)
|
||||
return self.reply_json(self.status_payload())
|
||||
|
||||
if route.startswith("/files/"):
|
||||
# Editing detaches the card from a live guest, mirroring "apply and reboot".
|
||||
if emu.is_running(state):
|
||||
emu.shutdown(state)
|
||||
image = state / UPLOADS["sdcard"]
|
||||
path = query.get("path", [""])[0]
|
||||
if route == "/files/write":
|
||||
sdcard.write(image, path, self.body())
|
||||
elif route == "/files/mkdir":
|
||||
sdcard.mkdir(image, path)
|
||||
elif route == "/files/delete":
|
||||
sdcard.delete(image, path, query.get("directory", [""])[0] == "true")
|
||||
elif route == "/files/clear":
|
||||
size_mb = max(8, image.stat().st_size // 1024 // 1024) if image.is_file() else 64
|
||||
blank_sdcard(image, size_mb)
|
||||
save_settings(state, {"sdcard": f"blank {size_mb} MiB"})
|
||||
else:
|
||||
return self.send_error(404)
|
||||
return self.reply_json(self.status_payload())
|
||||
|
||||
if route == "/button":
|
||||
name = query.get("name", [""])[0]
|
||||
if name not in emu.BUTTONS:
|
||||
return self.send_error(404)
|
||||
emu.require_running(state)
|
||||
edge = query.get("edge", [""])[0]
|
||||
if edge in ("down", "up"):
|
||||
emu.set_button(state, name, down=edge == "down")
|
||||
else:
|
||||
emu.press(state, name, hold_ms=int(query.get("hold-ms", ["500"])[0]))
|
||||
return self.reply("ok")
|
||||
except (RuntimeError, OSError, ValueError, subprocess.CalledProcessError) as error:
|
||||
return self.reply(str(error), status=500)
|
||||
self.send_error(404)
|
||||
|
||||
def log_message(self, *_):
|
||||
pass
|
||||
|
||||
server = http.server.ThreadingHTTPServer((host, port), Handler)
|
||||
print(f"http://{host}:{server.server_address[1]}")
|
||||
print(f"state: {state}")
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
@@ -0,0 +1,461 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>xteink emulator</title>
|
||||
<style>
|
||||
:root { color-scheme: light; font-family: Inter, ui-sans-serif, system-ui, sans-serif; color: #20211f; background: #e9e5dc; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; }
|
||||
main { width: min(100% - 2rem, 70rem); margin: 0 auto; padding: 2.5rem 0 4rem; }
|
||||
header { margin-bottom: 1.5rem; }
|
||||
header p { max-width: 42rem; }
|
||||
h1 { margin: 0; font-size: clamp(2rem, 6vw, 4rem); line-height: 1; }
|
||||
.eyebrow { margin-bottom: .5rem; font-size: .75rem; font-weight: 800; letter-spacing: .16em; text-transform: uppercase; }
|
||||
.boot-panel { display: flex; flex-wrap: wrap; gap: 1rem; align-items: end; padding: 1rem; border: 1px solid #c8c2b6; border-radius: .75rem; background: #f7f4ed; }
|
||||
label { display: grid; gap: .4rem; font-size: .85rem; font-weight: 700; }
|
||||
input, select, button { font: inherit; }
|
||||
input, select { min-height: 2.6rem; padding: .5rem; border: 1px solid #aaa398; border-radius: .4rem; background: white; }
|
||||
button { min-height: 2.75rem; padding: .55rem 1rem; border: 1px solid #55534e; border-radius: .5rem; color: #fff; background: #343532; font-weight: 750; cursor: pointer; touch-action: none; }
|
||||
button:hover:not(:disabled) { background: #11120f; }
|
||||
button:focus-visible { outline: 3px solid #d2752b; outline-offset: 2px; }
|
||||
button:disabled { cursor: not-allowed; opacity: .4; }
|
||||
button.active { transform: translateY(2px); background: #d2752b; }
|
||||
.danger { border-color: #b94a3b; color: #a52d20; background: transparent; }
|
||||
.danger:hover:not(:disabled) { color: #fff; background: #a52d20; }
|
||||
.status { min-height: 1.5rem; margin: 1rem 0; }
|
||||
.status.error { color: #a52d20; font-weight: 700; }
|
||||
.workspace { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(1.5rem, 4vw, 3rem); align-items: start; }
|
||||
.device { display: flex; justify-content: center; }
|
||||
.device-frame { display: grid; grid-template-columns: auto minmax(14rem, 30rem) auto; grid-template-rows: auto auto auto; gap: .6rem; }
|
||||
.screen-shell { grid-column: 2; grid-row: 2; padding: 1.1rem; border-radius: 1.2rem; background: #30312e; box-shadow: 0 1rem 2.5rem #5d584c40; }
|
||||
img { display: block; width: 100%; height: auto; aspect-ratio: 528 / 792; background: #fff; image-rendering: pixelated; }
|
||||
.device-frame button { min-height: 0; border-radius: .6rem; font-size: .75rem; }
|
||||
.btn-power { grid-column: 2; grid-row: 1; height: 1.8rem; }
|
||||
.btn-side { grid-row: 2; width: 2rem; writing-mode: vertical-rl; }
|
||||
.btn-left { grid-column: 1; }
|
||||
.btn-right { grid-column: 3; }
|
||||
.btn-bottom { grid-column: 2; grid-row: 3; display: grid; grid-template-columns: repeat(4, 1fr); gap: .6rem; }
|
||||
.btn-bottom button { height: 1.8rem; }
|
||||
.inspector { min-width: 0; overflow: hidden; border: 1px solid #c8c2b6; border-radius: .75rem; background: #f7f4ed; }
|
||||
.tabs { display: grid; grid-template-columns: repeat(2, 1fr); border-bottom: 1px solid #c8c2b6; }
|
||||
.tabs button { border: 0; border-right: 1px solid #c8c2b6; border-radius: 0; color: #56534d; background: transparent; }
|
||||
.tabs button:last-child { border-right: 0; }
|
||||
.tabs button[aria-selected="true"] { color: #fff; background: #343532; }
|
||||
.tab-panel { padding: 1rem; }
|
||||
.file-toolbar { display: flex; gap: .4rem; align-items: center; flex-wrap: wrap; }
|
||||
.file-actions { display: flex; justify-content: flex-end; gap: .4rem; margin-top: .6rem; }
|
||||
.file-button { display: inline-flex; min-height: 1.9rem; align-items: center; padding: .3rem .6rem; border: 1px solid #55534e; border-radius: .4rem; color: #fff; background: #343532; font-size: .8rem; font-weight: 650; cursor: pointer; }
|
||||
.file-button:hover:not(:disabled) { background: #11120f; }
|
||||
.file-clear { border-color: #b94a3b; color: #a52d20; background: transparent; }
|
||||
.file-clear:hover:not(:disabled) { color: #fff; background: #a52d20; }
|
||||
.file-reboot { margin-left: auto; border-color: #b66020; background: #d2752b; }
|
||||
.file-reboot:hover:not(:disabled) { background: #b66020; }
|
||||
.file-location { display: flex; align-items: center; gap: .5rem; margin-bottom: .5rem; color: #625e55; }
|
||||
.file-location button { min-height: 1.9rem; padding: .2rem .55rem; font-size: .8rem; }
|
||||
.file-location code { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.file-list { width: 100%; margin-bottom: .75rem; border: 1px solid #c8c2b6; border-collapse: collapse; border-radius: .6rem; background: #fff; font-size: .82rem; }
|
||||
.file-list td { padding: .28rem .5rem; border-bottom: 1px solid #eee9df; }
|
||||
.file-list tr:last-child td { border-bottom: 0; }
|
||||
.file-list tbody tr:hover { background: #f1ede4; }
|
||||
.file-list tr.entry { cursor: pointer; }
|
||||
.file-list td:first-child { width: 1.4rem; text-align: center; }
|
||||
.file-list tr.directory td:nth-child(2) { font-weight: 700; }
|
||||
.file-list td:nth-child(3) { text-align: right; color: #8a8479; white-space: nowrap; }
|
||||
.file-list td:last-child { width: 2rem; padding: 0; text-align: center; }
|
||||
.file-empty { padding: 1.25rem !important; color: #777269; text-align: center; }
|
||||
.file-del { min-height: 0; padding: .1rem .35rem; border: 0; border-radius: .3rem; color: #a52d20; background: transparent; font-size: .85rem; line-height: 1; }
|
||||
.file-del:hover { color: #fff; background: #a52d20; }
|
||||
.file-preview { width: 100%; min-height: 14rem; max-height: 28rem; overflow: auto; padding: .85rem 1rem; border: 1px solid #c8c2b6; border-radius: .6rem; color: #343532; background: #fff; font-family: ui-monospace, monospace; line-height: 1.45; resize: vertical; }
|
||||
.file-preview:focus { outline: 3px solid #d2752b; outline-offset: 1px; }
|
||||
.file-preview:read-only { color: #777269; background: #f1ede4; }
|
||||
.log-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: .5rem; font-size: .85rem; color: #625e55; }
|
||||
.log-header label { display: flex; gap: .3rem; align-items: center; font-weight: 400; }
|
||||
.log { margin: 0; height: 38rem; overflow: auto; padding: .75rem 1rem; border-radius: .6rem; background: #1c1d1a; color: #d6d0c2; font: .78rem/1.4 ui-monospace, monospace; white-space: pre-wrap; word-break: break-word; }
|
||||
@media (max-width: 48rem) { main { padding-top: 1.5rem; } .workspace { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<header>
|
||||
<p class="eyebrow">ESP32-C3 emulator</p>
|
||||
<h1>xteink X3 / X4</h1>
|
||||
<p>Upload a firmware <code>.bin</code> (release app image or a full 16 MB flash) and boot it in the emulator running on this host. The SD card image persists between boots.</p>
|
||||
</header>
|
||||
|
||||
<form id="boot-form" class="boot-panel">
|
||||
<label>Firmware (.bin)
|
||||
<input id="firmware" type="file" accept=".bin,application/octet-stream">
|
||||
</label>
|
||||
<label>SD card image (optional)
|
||||
<input id="sdcard" type="file" accept=".img,.bin,application/octet-stream">
|
||||
</label>
|
||||
<label>Device
|
||||
<select id="variant"><option value="x3">X3</option><option value="x4">X4 (display stub)</option></select>
|
||||
</label>
|
||||
<button id="boot" type="submit">Boot firmware</button>
|
||||
<button id="stop" type="button" class="danger">Stop</button>
|
||||
</form>
|
||||
|
||||
<p id="status" class="status" role="status">Loading.</p>
|
||||
|
||||
<div class="workspace">
|
||||
<section class="device" aria-label="Emulated xteink device">
|
||||
<div class="device-frame">
|
||||
<button type="button" class="btn-power" data-button="power" data-hold-ms="2000">power</button>
|
||||
<button type="button" class="btn-side btn-left" data-button="left">left</button>
|
||||
<div class="screen-shell"><img id="screen" alt="E-ink display"></div>
|
||||
<button type="button" class="btn-side btn-right" data-button="right">right</button>
|
||||
<div class="btn-bottom">
|
||||
<button type="button" data-button="bottom-1">1</button>
|
||||
<button type="button" data-button="bottom-2">2</button>
|
||||
<button type="button" data-button="bottom-3">3</button>
|
||||
<button type="button" data-button="bottom-4">4</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="inspector" aria-label="Emulator inspector">
|
||||
<div class="tabs" role="tablist">
|
||||
<button type="button" role="tab" aria-selected="true" data-tab="logs">Logs</button>
|
||||
<button type="button" role="tab" aria-selected="false" data-tab="files">Files</button>
|
||||
<button type="button" role="tab" aria-selected="false" data-tab="memory">Memory</button>
|
||||
</div>
|
||||
|
||||
<div id="panel-logs" class="tab-panel">
|
||||
<div class="log-header">
|
||||
<span>Serial log</span>
|
||||
<label><input id="follow" type="checkbox" checked> follow</label>
|
||||
</div>
|
||||
<pre id="log" class="log" role="log" aria-live="polite"></pre>
|
||||
</div>
|
||||
|
||||
<div id="panel-memory" class="tab-panel" hidden>
|
||||
<div class="log-header">
|
||||
<span>RAM written since boot (high-water mark, not live heap)</span>
|
||||
<button id="memory-refresh" type="button" class="file-button">Refresh</button>
|
||||
</div>
|
||||
<table class="file-list"><tbody id="memory-list"></tbody></table>
|
||||
</div>
|
||||
|
||||
<div id="panel-files" class="tab-panel" hidden>
|
||||
<div id="files-browse">
|
||||
<div class="file-location">
|
||||
<button id="file-back" type="button" aria-label="Go up one folder" disabled>←</button>
|
||||
<code id="file-path">/</code>
|
||||
</div>
|
||||
<table class="file-list"><tbody id="file-list"></tbody></table>
|
||||
<div class="file-toolbar">
|
||||
<label class="file-button">Upload<input id="file-add" type="file" multiple hidden></label>
|
||||
<button id="file-new" type="button" class="file-button">New File</button>
|
||||
<button id="file-new-folder" type="button" class="file-button">New Folder</button>
|
||||
<button id="file-reset" type="button" class="file-button file-clear">Clear</button>
|
||||
<button id="file-reboot" type="button" class="file-button file-reboot">Apply & Reboot</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="files-edit" hidden>
|
||||
<div class="file-location">
|
||||
<button id="file-close" type="button" aria-label="Back to file list">←</button>
|
||||
<code id="file-edit-name"></code>
|
||||
<button id="file-delete" type="button" class="file-button file-clear">Delete</button>
|
||||
</div>
|
||||
<textarea id="file-editor" class="file-preview" spellcheck="false"></textarea>
|
||||
<div class="file-actions"><button id="file-save" type="button" class="file-button">Save</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const status = $("status"), screen = $("screen"), log = $("log"), follow = $("follow");
|
||||
const deviceButtons = [...document.querySelectorAll("[data-button]")];
|
||||
let running = false;
|
||||
|
||||
function report(message, error) {
|
||||
status.textContent = message;
|
||||
status.classList.toggle("error", Boolean(error));
|
||||
}
|
||||
|
||||
async function call(path, options) {
|
||||
const response = await fetch(path, options);
|
||||
const text = await response.text();
|
||||
if (!response.ok) throw new Error(text || response.statusText);
|
||||
return text;
|
||||
}
|
||||
|
||||
function applyStatus(payload) {
|
||||
// A fresh QEMU process restarts its frame counter, so drop the cursor across boots.
|
||||
if (payload.running !== running) generation = null;
|
||||
running = payload.running;
|
||||
for (const button of deviceButtons) button.disabled = !running;
|
||||
$("stop").disabled = !running;
|
||||
$("variant").value = payload.variant;
|
||||
const parts = [running ? "Running." : "Stopped."];
|
||||
if (payload.firmware) parts.push(`firmware: ${payload.firmware}`);
|
||||
if (payload.sdcard) parts.push(`sd: ${payload.sdcard}`);
|
||||
report(parts.join(" "), false);
|
||||
if (running) watchScreen(); else screen.removeAttribute("src");
|
||||
}
|
||||
|
||||
// Long Poll - /screen.png blocks until the panel redraws, so one outstanding request keeps
|
||||
// the image as fresh as the e-ink hardware itself, with no polling interval to tune.
|
||||
let watching = false;
|
||||
let generation = null;
|
||||
|
||||
async function watchScreen() {
|
||||
if (watching) return;
|
||||
watching = true;
|
||||
try {
|
||||
while (running) {
|
||||
const url = "screen.png" + (generation === null ? "" : "?since=" + generation);
|
||||
const response = await fetch(url);
|
||||
if (response.status === 304) continue;
|
||||
if (!response.ok) throw new Error(await response.text());
|
||||
generation = response.headers.get("X-Frame-Generation");
|
||||
const blob = await response.blob();
|
||||
URL.revokeObjectURL(screen.src);
|
||||
screen.src = URL.createObjectURL(blob);
|
||||
}
|
||||
} catch (error) {
|
||||
report(String(error), true);
|
||||
} finally {
|
||||
watching = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshStatus() {
|
||||
try {
|
||||
applyStatus(JSON.parse(await call("status")));
|
||||
} catch (error) {
|
||||
report(String(error), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function upload(name, input) {
|
||||
if (!input.files.length) return;
|
||||
const file = input.files[0];
|
||||
report(`Uploading ${file.name}...`, false);
|
||||
await call(`upload?name=${name}&filename=${encodeURIComponent(file.name)}`, {
|
||||
method: "POST",
|
||||
body: await file.arrayBuffer(),
|
||||
});
|
||||
}
|
||||
|
||||
$("boot-form").onsubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
$("boot").disabled = true;
|
||||
try {
|
||||
await upload("firmware", $("firmware"));
|
||||
await upload("sdcard", $("sdcard"));
|
||||
report("Booting...", false);
|
||||
applyStatus(JSON.parse(await call(`boot?variant=${$("variant").value}`, { method: "POST" })));
|
||||
} catch (error) {
|
||||
report(String(error), true);
|
||||
} finally {
|
||||
$("boot").disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
$("stop").onclick = async () => {
|
||||
try {
|
||||
applyStatus(JSON.parse(await call("stop", { method: "POST" })));
|
||||
} catch (error) {
|
||||
report(String(error), true);
|
||||
}
|
||||
};
|
||||
|
||||
for (const button of deviceButtons) {
|
||||
button.onclick = async () => {
|
||||
button.classList.add("active");
|
||||
button.disabled = true;
|
||||
const hold = button.dataset.holdMs ? `&hold-ms=${button.dataset.holdMs}` : "";
|
||||
try {
|
||||
await call(`button?name=${button.dataset.button}${hold}`, { method: "POST" });
|
||||
} catch (error) {
|
||||
report(String(error), true);
|
||||
} finally {
|
||||
button.classList.remove("active");
|
||||
button.disabled = !running;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function refreshLog() {
|
||||
try {
|
||||
log.textContent = await call("log");
|
||||
if (follow.checked) log.scrollTop = log.scrollHeight;
|
||||
} catch (error) {
|
||||
report(String(error), true);
|
||||
}
|
||||
}
|
||||
|
||||
for (const tab of document.querySelectorAll("[data-tab]")) {
|
||||
tab.onclick = () => {
|
||||
for (const other of document.querySelectorAll("[data-tab]")) {
|
||||
other.setAttribute("aria-selected", String(other === tab));
|
||||
$("panel-" + other.dataset.tab).hidden = other !== tab;
|
||||
}
|
||||
if (tab.dataset.tab === "files") refreshFiles();
|
||||
if (tab.dataset.tab === "memory") refreshMemory();
|
||||
};
|
||||
}
|
||||
|
||||
async function refreshMemory() {
|
||||
const list = $("memory-list");
|
||||
list.innerHTML = "";
|
||||
let rows;
|
||||
try {
|
||||
rows = JSON.parse(await call("memory"));
|
||||
} catch (error) {
|
||||
report(String(error), true);
|
||||
return;
|
||||
}
|
||||
for (const entry of rows) {
|
||||
const row = list.insertRow();
|
||||
const percent = (100 * entry.touched / entry.size).toFixed(0);
|
||||
row.insertCell().textContent = entry.region;
|
||||
row.insertCell().textContent = "0x" + entry.base.toString(16);
|
||||
row.insertCell().textContent =
|
||||
`${Math.round(entry.touched / 1024)} / ${Math.round(entry.size / 1024)} KiB (${percent}%)`;
|
||||
const download = document.createElement("a");
|
||||
download.href = "memory/dump?region=" + entry.region;
|
||||
download.textContent = "dump";
|
||||
download.download = entry.region + ".bin";
|
||||
row.insertCell().append(download);
|
||||
}
|
||||
}
|
||||
|
||||
$("memory-refresh").onclick = refreshMemory;
|
||||
|
||||
let directory = "/";
|
||||
let editing = null;
|
||||
|
||||
function join(base, name) {
|
||||
return base === "/" ? "/" + name : base + "/" + name;
|
||||
}
|
||||
|
||||
function showBrowser(show) {
|
||||
$("files-browse").hidden = !show;
|
||||
$("files-edit").hidden = show;
|
||||
}
|
||||
|
||||
async function refreshFiles() {
|
||||
showBrowser(true);
|
||||
$("file-path").textContent = directory;
|
||||
$("file-back").disabled = directory === "/";
|
||||
const list = $("file-list");
|
||||
list.replaceChildren();
|
||||
let entries;
|
||||
try {
|
||||
entries = JSON.parse(await call("files?path=" + encodeURIComponent(directory)));
|
||||
} catch (error) {
|
||||
report(String(error), true);
|
||||
return;
|
||||
}
|
||||
if (!entries.length) {
|
||||
list.innerHTML = '<tr><td colspan="4" class="file-empty">Empty folder</td></tr>';
|
||||
return;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const row = list.insertRow();
|
||||
row.className = entry.directory ? "entry directory" : "entry";
|
||||
row.insertCell().textContent = entry.directory ? "\u{1F4C1}" : "\u{1F4C4}";
|
||||
row.insertCell().textContent = entry.name;
|
||||
row.insertCell().textContent = entry.directory ? "" : `${entry.size} B`;
|
||||
const remove = document.createElement("button");
|
||||
remove.className = "file-del";
|
||||
remove.textContent = "\u2715";
|
||||
remove.onclick = async (event) => {
|
||||
event.stopPropagation();
|
||||
await mutate(`files/delete?path=${encodeURIComponent(join(directory, entry.name))}&directory=${entry.directory}`);
|
||||
refreshFiles();
|
||||
};
|
||||
row.insertCell().append(remove);
|
||||
row.onclick = () => {
|
||||
if (entry.directory) {
|
||||
directory = join(directory, entry.name);
|
||||
refreshFiles();
|
||||
} else {
|
||||
openFile(join(directory, entry.name));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function mutate(path, body) {
|
||||
try {
|
||||
applyStatus(JSON.parse(await call(path, { method: "POST", body })));
|
||||
return true;
|
||||
} catch (error) {
|
||||
report(String(error), true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function openFile(path) {
|
||||
try {
|
||||
const file = JSON.parse(await call("files/read?path=" + encodeURIComponent(path)));
|
||||
editing = path;
|
||||
$("file-edit-name").textContent = path;
|
||||
$("file-editor").value = file.binary ? `(binary file, ${file.size} bytes)` : file.text;
|
||||
$("file-editor").readOnly = file.binary;
|
||||
$("file-save").hidden = file.binary;
|
||||
showBrowser(false);
|
||||
} catch (error) {
|
||||
report(String(error), true);
|
||||
}
|
||||
}
|
||||
|
||||
$("file-back").onclick = () => {
|
||||
directory = directory.replace(/\/[^/]+$/, "") || "/";
|
||||
refreshFiles();
|
||||
};
|
||||
$("file-close").onclick = refreshFiles;
|
||||
$("file-save").onclick = async () => {
|
||||
await mutate("files/write?path=" + encodeURIComponent(editing), $("file-editor").value);
|
||||
refreshFiles();
|
||||
};
|
||||
$("file-delete").onclick = async () => {
|
||||
await mutate(`files/delete?path=${encodeURIComponent(editing)}&directory=false`);
|
||||
refreshFiles();
|
||||
};
|
||||
$("file-add").onchange = async (event) => {
|
||||
for (const file of event.target.files) {
|
||||
if (!await mutate("files/write?path=" + encodeURIComponent(join(directory, file.name)), await file.arrayBuffer())) break;
|
||||
}
|
||||
event.target.value = "";
|
||||
refreshFiles();
|
||||
};
|
||||
$("file-new").onclick = async () => {
|
||||
const name = prompt("File name");
|
||||
if (!name) return;
|
||||
await mutate("files/write?path=" + encodeURIComponent(join(directory, name)), "");
|
||||
refreshFiles();
|
||||
};
|
||||
$("file-new-folder").onclick = async () => {
|
||||
const name = prompt("Folder name");
|
||||
if (!name) return;
|
||||
await mutate("files/mkdir?path=" + encodeURIComponent(join(directory, name)));
|
||||
refreshFiles();
|
||||
};
|
||||
$("file-reset").onclick = async () => {
|
||||
if (!confirm("Erase the SD card image?")) return;
|
||||
directory = "/";
|
||||
await mutate("files/clear");
|
||||
refreshFiles();
|
||||
};
|
||||
$("file-reboot").onclick = async () => {
|
||||
report("Booting...", false);
|
||||
await mutate(`boot?variant=${$("variant").value}`);
|
||||
};
|
||||
|
||||
setInterval(refreshLog, 1000);
|
||||
setInterval(refreshStatus, 5000);
|
||||
refreshStatus();
|
||||
refreshLog();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,79 @@
|
||||
"""FAT32 card contents, read and written through mtools.
|
||||
|
||||
Mutations require a stopped emulator: QEMU caches the image, so host writes to a live card
|
||||
would be lost or corrupt the filesystem.
|
||||
"""
|
||||
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
LISTING = re.compile(r"^(?P<short>.{12})\s+(?P<size><DIR>|[\d ]+)\s+\S+\s+\S+\s?(?P<long>.*)$")
|
||||
|
||||
|
||||
def mtool(name):
|
||||
path = shutil.which(name)
|
||||
if not path:
|
||||
raise RuntimeError(f"{name} not found; install mtools to manage the SD card")
|
||||
return path
|
||||
|
||||
|
||||
def run(name, image, *arguments, stdin=None):
|
||||
result = subprocess.run(
|
||||
[mtool(name), "-i", str(image), *arguments],
|
||||
input=stdin,
|
||||
capture_output=True,
|
||||
)
|
||||
if result.returncode:
|
||||
message = (result.stderr or result.stdout).decode(errors="replace").strip()
|
||||
raise RuntimeError(message or f"{name} failed")
|
||||
return result.stdout
|
||||
|
||||
|
||||
def clean(path):
|
||||
"""Normalise a card path, rejecting anything that could escape the image."""
|
||||
parts = [part for part in str(path).replace("\\", "/").split("/") if part not in ("", ".")]
|
||||
if any(part == ".." or ":" in part for part in parts):
|
||||
raise RuntimeError(f"invalid path: {path}")
|
||||
return "/" + "/".join(parts)
|
||||
|
||||
|
||||
def drive(path):
|
||||
return f"::{clean(path)}"
|
||||
|
||||
|
||||
def listing(image, path):
|
||||
entries = []
|
||||
for line in run("mdir", image, drive(path)).decode(errors="replace").splitlines():
|
||||
match = LISTING.match(line)
|
||||
if not match:
|
||||
continue
|
||||
short, size, long_name = match.group("short"), match.group("size"), match.group("long")
|
||||
name = long_name.strip() or short[:8].strip() + ("." + short[8:].strip() if short[8:].strip() else "")
|
||||
# The free-space summary line also matches the entry shape, but carries no name.
|
||||
if not name or name in (".", ".."):
|
||||
continue
|
||||
directory = size == "<DIR>"
|
||||
entries.append({
|
||||
"name": name,
|
||||
"directory": directory,
|
||||
"size": None if directory else int(size.replace(" ", "")),
|
||||
})
|
||||
entries.sort(key=lambda entry: (not entry["directory"], entry["name"].lower()))
|
||||
return entries
|
||||
|
||||
|
||||
def read(image, path):
|
||||
return run("mtype", image, drive(path))
|
||||
|
||||
|
||||
def write(image, path, data):
|
||||
run("mcopy", image, "-o", "-", drive(path), stdin=data)
|
||||
|
||||
|
||||
def mkdir(image, path):
|
||||
run("mmd", image, drive(path))
|
||||
|
||||
|
||||
def delete(image, path, directory):
|
||||
run("mdeltree" if directory else "mdel", image, drive(path))
|
||||
@@ -6675,7 +6675,6 @@ int tcg_gen_code(TCGContext *s, TranslationBlock *tb, uint64_t pc_start)
|
||||
}
|
||||
*(uint32_t *)wasm_blob_ptr_base = s->code_ptr - wasm_blob_ptr_base - 4;
|
||||
|
||||
// record importing helper functions
|
||||
size_base = (uint32_t*)s->code_ptr;
|
||||
s->code_ptr += 4;
|
||||
memcpy(s->code_ptr, target_helper_funcs, num_helper_funcs * 4);
|
||||
|
||||
+27
-3
@@ -22,7 +22,9 @@
|
||||
#if !defined(CONFIG_TCG_INTERPRETER) && defined(EMSCRIPTEN)
|
||||
|
||||
#include "qemu/osdep.h"
|
||||
#include "cpu.h"
|
||||
#include "exec/cpu_ldst.h"
|
||||
#include "exec/cpu-common.h"
|
||||
#include "tcg/tcg-op.h"
|
||||
#include "tcg/tcg-ldst.h"
|
||||
#include <string.h>
|
||||
@@ -32,6 +34,12 @@
|
||||
|
||||
__thread uintptr_t tci_tb_ptr;
|
||||
__thread bool wasm_tci_only_tb;
|
||||
static int instantiated_wasm_count;
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int wasm_instantiated_tb_count(void)
|
||||
{
|
||||
return qatomic_read(&instantiated_wasm_count);
|
||||
}
|
||||
|
||||
/* Disassemble TCI bytecode. */
|
||||
int print_insn_tci(bfd_vma addr, disassemble_info *info)
|
||||
@@ -68,6 +76,7 @@ EM_JS(int, instantiate_wasm, (), {
|
||||
var ptr = export_vec_begin + 4 * Module.__wasm32_tb.cur_core_num;
|
||||
const fidx = addFunction(inst.exports.start, 'ii');
|
||||
Module.__wasm32_tb.dynamic_functions.add(fidx);
|
||||
globalThis.__xteinkDynamicFunctions = Module.__wasm32_tb.dynamic_functions.size;
|
||||
memory_v.setUint32(ptr, fidx, true);
|
||||
|
||||
const remove_n = memory_v.getInt32(Module.__wasm32_tb.to_remove_instance_idx_ptr, true);
|
||||
@@ -79,6 +88,7 @@ EM_JS(int, instantiate_wasm, (), {
|
||||
}
|
||||
}
|
||||
memory_v.setInt32(Module.__wasm32_tb.to_remove_instance_idx_ptr, 0, true);
|
||||
globalThis.__xteinkDynamicFunctions = Module.__wasm32_tb.dynamic_functions.size;
|
||||
}
|
||||
|
||||
return 0;
|
||||
@@ -98,6 +108,7 @@ EM_JS(void, flush_tb_instances, (), {
|
||||
}
|
||||
}
|
||||
memory_v.setInt32(tb.to_remove_instance_idx_ptr, 0, true);
|
||||
globalThis.__xteinkDynamicFunctions = tb.dynamic_functions.size;
|
||||
});
|
||||
|
||||
__thread bool initdone = false;
|
||||
@@ -110,6 +121,13 @@ int cur_core_num_max = 0;
|
||||
__thread static int to_remove_instance[TO_REMOVE_INSTANCE_SIZE];
|
||||
__thread static int to_remove_instance_idx = 0;
|
||||
|
||||
int wasm_disable_tb_removal;
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE void wasm_set_disable_tb_removal(void)
|
||||
{
|
||||
wasm_disable_tb_removal = 1;
|
||||
}
|
||||
|
||||
void remove_tb(void *tb_ptr) {
|
||||
int32_t *slot = (int32_t*)((uint8_t*)tb_ptr + export_vec_off);
|
||||
int32_t f = *slot;
|
||||
@@ -117,6 +135,11 @@ void remove_tb(void *tb_ptr) {
|
||||
return;
|
||||
}
|
||||
*slot = 0;
|
||||
/* E18: leak the wasm function instead of recycling its table index, to test
|
||||
* whether removeFunction index reuse is the corruption source. */
|
||||
if (wasm_disable_tb_removal) {
|
||||
return;
|
||||
}
|
||||
if (to_remove_instance_idx == TO_REMOVE_INSTANCE_SIZE) {
|
||||
flush_tb_instances();
|
||||
}
|
||||
@@ -564,7 +587,7 @@ __thread tcg_target_ulong regs[TCG_TARGET_NB_REGS];
|
||||
|
||||
static inline uintptr_t tcg_qemu_tb_exec_tci(CPUArchState *env)
|
||||
{
|
||||
uint32_t *tb_ptr = (uint8_t*)ctx.tb_ptr + *(uint32_t*)ctx.tb_ptr;
|
||||
uint32_t *tb_ptr = (uint8_t *)ctx.tb_ptr + *(uint32_t *)ctx.tb_ptr;
|
||||
uint64_t *stack = ctx.stack;
|
||||
|
||||
regs[TCG_AREG0] = (tcg_target_ulong)env;
|
||||
@@ -1144,7 +1167,7 @@ static inline uintptr_t tcg_qemu_tb_exec_tci(CPUArchState *env)
|
||||
// enter to wasm TB
|
||||
return 0;
|
||||
}
|
||||
tb_ptr = (uint8_t*)tb_ptr + *(uint32_t*)tb_ptr;
|
||||
tb_ptr = (uint8_t *)tb_ptr + *(uint32_t *)tb_ptr;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1167,7 +1190,7 @@ static inline uintptr_t tcg_qemu_tb_exec_tci(CPUArchState *env)
|
||||
// enter to wasm TB
|
||||
return 0;
|
||||
}
|
||||
tb_ptr = (uint8_t*)tb_ptr + *(uint32_t*)tb_ptr;
|
||||
tb_ptr = (uint8_t *)tb_ptr + *(uint32_t *)tb_ptr;
|
||||
|
||||
break;
|
||||
|
||||
@@ -1250,6 +1273,7 @@ uintptr_t QEMU_DISABLE_CFI tcg_qemu_tb_exec(CPUArchState *env,
|
||||
res = tcg_qemu_tb_exec_tci(env);
|
||||
} else {
|
||||
instantiate_wasm();
|
||||
qatomic_inc(&instantiated_wasm_count);
|
||||
res = ((wasm_func_ptr)(*(uint32_t*)tb_entry_ptr))(&ctx);
|
||||
}
|
||||
if ((uint32_t)ctx.tb_ptr == 0) {
|
||||
|
||||
@@ -386,6 +386,8 @@ static void tcg_wasm_out_op_end(TCGContext *s)
|
||||
|
||||
static void tcg_wasm_out_op_i32_eqz(TCGContext *s){ tcg_wasm_out8(s, 0x45); }
|
||||
static void tcg_wasm_out_op_i32_eq(TCGContext *s){ tcg_wasm_out8(s, 0x46); }
|
||||
static void tcg_wasm_out_op_i32_lt_u(TCGContext *s){ tcg_wasm_out8(s, 0x49); }
|
||||
static void tcg_wasm_out_op_i32_ge_u(TCGContext *s){ tcg_wasm_out8(s, 0x4f); }
|
||||
static void tcg_wasm_out_op_i32_and(TCGContext *s){ tcg_wasm_out8(s, 0x71); }
|
||||
static void tcg_wasm_out_op_i32_or(TCGContext *s){ tcg_wasm_out8(s, 0x72); }
|
||||
//static void tcg_wasm_out_op_i32_xor(TCGContext *s){ tcg_wasm_out8(s, 0x73); }
|
||||
@@ -2232,7 +2234,7 @@ static uint8_t tcg_wasm_out_tlb_load(TCGContext *s, TCGReg addr, MemOpIdx oi, bo
|
||||
tcg_wasm_out_op_global_get_r(s, addr);
|
||||
tcg_wasm_out_op_i64_add(s);
|
||||
tcg_wasm_out_op_local_set(s, TMP64_0_IDX);
|
||||
|
||||
|
||||
tcg_wasm_out_op_end(s);
|
||||
|
||||
|
||||
@@ -2500,7 +2502,6 @@ static void tcg_wasm_out_qemu_st(TCGContext *s, const TCGArg *args, bool is_64)
|
||||
tcg_wasm_out_op_i64_eqz(s);
|
||||
tcg_wasm_out_op_if_noret(s);
|
||||
|
||||
// call helper
|
||||
tcg_wasm_out_op_global_get_r(s, TCG_AREG0);
|
||||
tcg_wasm_out_op_i32_wrap_i64(s);
|
||||
tcg_wasm_out_op_global_get_r(s, addr_reg);
|
||||
@@ -3366,19 +3367,16 @@ static uint8_t tcg_tci_out_qemu_ldst(TCGContext *s, TCGOpcode opc, const TCGArg
|
||||
static void tcg_out_qemu_ld(TCGContext *s, TCGOpcode opc, const TCGArg *args, bool is_64)
|
||||
{
|
||||
tcg_tci_out_qemu_ldst(s, opc, args);
|
||||
// Aliased qemu_ld - The wasm32 wrapper corrupts vCPU TLB state when the load destination also holds the address.
|
||||
if (args[0] == args[1]) {
|
||||
wasm_tci_only_tb = true;
|
||||
}
|
||||
tcg_wasm_out_qemu_ld(s, args, is_64);
|
||||
}
|
||||
static void tcg_out_qemu_st(TCGContext *s, TCGOpcode opc, const TCGArg *args, bool is_64)
|
||||
{
|
||||
tcg_tci_out_qemu_ldst(s, opc, args);
|
||||
tcg_wasm_out_qemu_st(s, args, is_64);
|
||||
/* ponytail: Store-Containing TBs Run on TCI - Upstream qemu-wasm sets this for every guest store.
|
||||
* PC-windowed bisection (docs/wasm-inflate-panic.md E3) showed the corruption is NOT a single
|
||||
* miscompiled TB: inflate, ROM+IRAM, flash-lower, and flash-upper each compile safely alone, but
|
||||
* the full set corrupts. It is an aggregate timing/rewind or TB-instance race in the compiled
|
||||
* store path, which is why upstream punts all store-TBs to TCI. Ceiling: store-TBs interpreted
|
||||
* (perf cost). Upgrade path: make the restartable-block store path rewind/instance safe. */
|
||||
wasm_tci_only_tb = true;
|
||||
}
|
||||
static void tcg_out_deposit_i32(TCGContext *s, TCGOpcode opc, TCGReg dest, TCGReg arg1, TCGReg arg2, int pos, int len)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user