Fetch esp-emulator browser build from GitHub releases; release packaging (0.10.1)

- tools/fetch_esp_emu.py: download esp-emu-<ver>-wasm.tar.gz from the
  espressif/esp-emulator release into vendor/esp-emu-browser/www (proxy-aware
  via https_proxy, sha256-verified against SHA256SUMS, cached in
  vendor/downloads/, idempotent, --version to switch baseline)
- npm run build/test trigger the fetch automatically; serve.py, sync_www.py
  and the m0/m1 tests default to the vendored copy (ESP_EMU_WWW/--www
  override) — no machine-local absolute paths left in the dev flow
- install.sh/install.bat + tools/package_release.py (npm run package):
  release archives ship the pre-compiled UI + the esp-emulator fetch script
- docs updated (README/AGENT/UPGRADE/ROADMAP/CHANGELOG, EN+zh)
This commit is contained in:
darceye committed 2026-11-02 09:38:03 +08:00
1 parent 9cd5863d36
commit 81915288f5
22 files changed
+532 -34

No files matched your search

+7
View File
@@ -152,3 +152,10 @@ tests/firmware/peris_probe/build/
# build output (compiled from src/ by tsc) # build output (compiled from src/ by tsc)
/build/ /build/
# esp-emulator browser (wasm) build fetched from GitHub releases by
# tools/fetch_esp_emu.py (npm run build does this automatically)
/vendor/
# release packaging output (npm run package)
/dist/
+11 -2
View File
@@ -10,10 +10,12 @@ verification method learned so far. **Read it fully before touching anything.**
```bash ```bash
npm install # typescript via npmmirror (see §1) npm install # typescript via npmmirror (see §1)
npm run build # tsc: src/ → build/ (mandatory after src changes; build/ not in git) npm run build # prebuild fetches the esp-emulator browser build into
# vendor/esp-emu-browser/ on first run, then tsc: src/ → build/
# (mandatory after src changes; build/ not in git)
npm test # unit tests + integration smoke npm test # unit tests + integration smoke
npm run serve # http://127.0.0.1:8080/esp-emu-peris.html npm run serve # http://127.0.0.1:8080/esp-emu-peris.html
npm run sync # publish into _tools/esp-emu-browser/www (build first; --clean removes stale output) npm run sync # publish into vendor/esp-emu-browser/www (build first; --clean removes stale output)
``` ```
Architecture & data flow: `docs/ARCHITECTURE.md`; peripheral development: Architecture & data flow: `docs/ARCHITECTURE.md`; peripheral development:
@@ -28,6 +30,13 @@ and `docs/FEASIBILITY.md` (M0 measurements).
`https://registry.npmmirror.com/<pkg>/-/<name>-<ver>.tgz` and unpack `https://registry.npmmirror.com/<pkg>/-/<name>-<ver>.tgz` and unpack
(that is how xterm was vendored into `peris/vendor/`; the package ships the (that is how xterm was vendored into `peris/vendor/`; the package ships the
**unminified** build, no .min). **unminified** build, no .min).
- **GitHub downloads need the local proxy**:
`https_proxy=http://127.0.0.1:10809`. The esp-emulator browser (wasm)
build is fetched from the espressif/esp-emulator GitHub release by
`tools/fetch_esp_emu.py` (runs automatically on `npm run build`/`npm test`;
honours `https_proxy`; sha256-verified; cached in `vendor/downloads/`;
unpacked into `vendor/esp-emu-browser/www`). Override the location with
`ESP_EMU_WWW=/path/to/www`.
- **Playwright browser binaries** not installed (network); to install: - **Playwright browser binaries** not installed (network); to install:
`PLAYWRIGHT_DOWNLOAD_HOST=https://cdn.npmmirror.com/binaries/playwright`. `PLAYWRIGHT_DOWNLOAD_HOST=https://cdn.npmmirror.com/binaries/playwright`.
- **ESP-IDF**: master checkout at `C:\esp\master\esp-idf`; enable the - **ESP-IDF**: master checkout at `C:\esp\master\esp-idf`; enable the
+10 -2
View File
@@ -9,10 +9,12 @@
```bash ```bash
npm install # typescript 走 npmmirror(见 §1) npm install # typescript 走 npmmirror(见 §1)
npm run build # tsc:src/ → build/(改 src 后必须;build/ 不入 git) npm run build # prebuild 先把 esp-emulator 浏览器构建下载到
# vendor/esp-emu-browser/(首次),然后 tsc:src/ → build/
#(改 src 后必须;build/ 不入 git)
npm test # 单元测试 + 集成冒烟 npm test # 单元测试 + 集成冒烟
npm run serve # http://127.0.0.1:8080/esp-emu-peris.html npm run serve # http://127.0.0.1:8080/esp-emu-peris.html
npm run sync # 发布进 _tools/esp-emu-browser/www(先 build;--clean 清残留) npm run sync # 发布进 vendor/esp-emu-browser/www(先 build;--clean 清残留)
``` ```
架构与数据流见 `docs/ARCHITECTURE.zh-cn.md`;外设开发见 `docs/DEVICE-API.zh-cn.md`; 架构与数据流见 `docs/ARCHITECTURE.zh-cn.md`;外设开发见 `docs/DEVICE-API.zh-cn.md`;
@@ -24,6 +26,12 @@ esp-emulator wasm 能力边界见 `docs/EMU-API.zh-cn.md` 与 `docs/FEASIBILITY.
`npm --registry=https://registry.npmmirror.com install ...`;静态资源从 `npm --registry=https://registry.npmmirror.com install ...`;静态资源从
`https://registry.npmmirror.com/<pkg>/-/<name>-<ver>.tgz` 下 tarball 解包 `https://registry.npmmirror.com/<pkg>/-/<name>-<ver>.tgz` 下 tarball 解包
(xterm 就是这样 vendor 进 `peris/vendor/` 的;包内是**非压缩**版,无 .min)。 (xterm 就是这样 vendor 进 `peris/vendor/` 的;包内是**非压缩**版,无 .min)。
- **GitHub 下载要走本机代理**:`https_proxy=http://127.0.0.1:10809`。
esp-emulator 浏览器(wasm)构建由 `tools/fetch_esp_emu.py` 从
espressif/esp-emulator 的 GitHub release 下载(`npm run build`/`npm test`
自动触发;识别 `https_proxy`;sha256 校验;tarball 缓存在
`vendor/downloads/`;解压到 `vendor/esp-emu-browser/www`)。用
`ESP_EMU_WWW=/path/to/www` 可改指向。
- **Playwright 浏览器二进制**未装(网络);要装: - **Playwright 浏览器二进制**未装(网络);要装:
`PLAYWRIGHT_DOWNLOAD_HOST=https://cdn.npmmirror.com/binaries/playwright`。 `PLAYWRIGHT_DOWNLOAD_HOST=https://cdn.npmmirror.com/binaries/playwright`。
- **ESP-IDF**:master 检出在 `C:\esp\master\esp-idf`,环境启用用 - **ESP-IDF**:master 检出在 `C:\esp\master\esp-idf`,环境启用用
+23 -2
View File
@@ -47,7 +47,10 @@ places; devices carry their own versions).
npm install npm install
# dev loop: edit src/ → build → test → merged dev server # dev loop: edit src/ → build → test → merged dev server
npm run build npm run build # first run also downloads the esp-emulator browser
# (wasm) build from the espressif/esp-emulator
# GitHub release into vendor/esp-emu-browser/ —
# set https_proxy first if you need a proxy
npm test npm test
npm run serve # http://127.0.0.1:8080/esp-emu-peris.html npm run serve # http://127.0.0.1:8080/esp-emu-peris.html
@@ -64,12 +67,30 @@ npm run serve # http://127.0.0.1:8080/esp-emu-peris.html
``` ```
**Publish** the peripherals panel into the official www directory (add-only, **Publish** the peripherals panel into the official www directory (add-only,
never modifies; build first): never modifies; build first) — by default the fetched `vendor/esp-emu-browser/www`,
or any checkout via `--www` / `ESP_EMU_WWW`:
```bash ```bash
npm run sync # copies the entry html + peris/ (incl. build output) + examples/ npm run sync # copies the entry html + peris/ (incl. build output) + examples/
``` ```
## From a release package
Download `esp-emu-peris-<version>.zip` (or `.tar.gz`) from the
[releases page](https://github.com/darceye/esp-emu-peris/releases), unpack
and run the installer — it downloads the esp-emulator browser (wasm) build
from GitHub releases into `vendor/` (needs Python 3; behind a firewall set
`https_proxy` first, e.g. `https_proxy=http://127.0.0.1:10809`):
```bash
./install.sh # Windows: install.bat
python serve.py # http://127.0.0.1:8080/esp-emu-peris.html
```
No npm/node needed — the package ships the UI pre-compiled. For a different
esp-emulator version: `./install.sh --version 0.52.0` (or
`python tools/fetch_esp_emu.py --version 0.52.0`).
## Capabilities & limits (measured; details in docs/FEASIBILITY.md) ## Capabilities & limits (measured; details in docs/FEASIBILITY.md)
| Peripheral | Status | Approach | | Peripheral | Status | Approach |
+21 -2
View File
@@ -40,7 +40,10 @@ AGENT.md 环境/架构/坑(贡献前必读)
npm install npm install
# 开发循环:改 src/ → 构建 → 测试 → 起 merged 开发服务器 # 开发循环:改 src/ → 构建 → 测试 → 起 merged 开发服务器
npm run build npm run build # 首次运行会自动从 espressif/esp-emulator 的
# GitHub release 下载浏览器(wasm)构建到
# vendor/esp-emu-browser/——需要代理的话先设
# https_proxy
npm test npm test
npm run serve # http://127.0.0.1:8080/esp-emu-peris.html npm run serve # http://127.0.0.1:8080/esp-emu-peris.html
@@ -54,12 +57,28 @@ npm run serve # http://127.0.0.1:8080/esp-emu-peris.html
# 选中) # 选中)
``` ```
把外设面板**发布**进官方 www 目录(只增不改;先 build): 把外设面板**发布**进官方 www 目录(只增不改;先 build)——默认目标是自动下载
的 `vendor/esp-emu-browser/www`,也可用 `--www` / `ESP_EMU_WWW` 指向任意目录:
```bash ```bash
npm run sync # 复制入口 html + peris/(含编译产物)+ examples/ npm run sync # 复制入口 html + peris/(含编译产物)+ examples/
``` ```
## 从 release 包使用
从 [releases 页面](https://github.com/darceye/esp-emu-peris/releases)下载
`esp-emu-peris-<版本>.zip`(或 `.tar.gz`)解压后运行安装脚本——它会从
GitHub release 下载 esp-emulator 浏览器(wasm)构建到 `vendor/`(需要
Python 3;有防火墙先设 `https_proxy`,如 `https_proxy=http://127.0.0.1:10809`):
```bash
./install.sh # Windows 下:install.bat
python serve.py # http://127.0.0.1:8080/esp-emu-peris.html
```
无需 npm/node——包内已带编译好的 UI。要换 esp-emulator 版本:
`./install.sh --version 0.52.0`(或 `python tools/fetch_esp_emu.py --version 0.52.0`)。
## 能力与限制(实测结论,详见 docs/FEASIBILITY.zh-cn.md) ## 能力与限制(实测结论,详见 docs/FEASIBILITY.zh-cn.md)
| 外设 | 状态 | 途径 | | 外设 | 状态 | 途径 |
+17
View File
@@ -8,6 +8,23 @@ CHANGELOG.zh-cn.md).
Rules: major = breaking protocol/architecture change; minor = new capability; Rules: major = breaking protocol/architecture change; minor = new capability;
patch = fixes and small changes. patch = fixes and small changes.
## peris 0.10.1
- **No more machine-local absolute paths**: the esp-emulator browser (wasm)
build is fetched on demand from the `espressif/esp-emulator` GitHub
release into `vendor/esp-emu-browser/` by the new
`tools/fetch_esp_emu.py` — `npm run build` / `npm test` trigger it
automatically on first run. Proxy-aware (`https_proxy`), sha256-verified
against the release `SHA256SUMS`, cached in `vendor/downloads/`,
idempotent; `--version <ver>` switches the baseline (see UPGRADE.md).
`serve.py` / `sync_www.py` / the tests default to the vendored copy
(override with `--www` / `ESP_EMU_WWW`).
- **Release packaging**: `npm run package` (`tools/package_release.py`)
builds `esp-emu-peris-<ver>.zip` / `.tar.gz` for the GitHub release page;
the archives ship the UI pre-compiled plus `install.sh` / `install.bat`,
which fetch the esp-emulator browser build — a release user needs only
Python 3, no npm/node.
## peris 0.10.0 ## peris 0.10.0
- **Logic analyzer** (`logic-analyzer.js` v1.0.0, logic in `la-lib.js`): - **Logic analyzer** (`logic-analyzer.js` v1.0.0, logic in `la-lib.js`):
+14
View File
@@ -6,6 +6,20 @@
与英文版 CHANGELOG.md)。 与英文版 CHANGELOG.md)。
规则:主版本=协议/架构破坏性变更;次版本=新能力;修订=修复与小改。 规则:主版本=协议/架构破坏性变更;次版本=新能力;修订=修复与小改。
## peris 0.10.1
- **不再依赖本机绝对路径**:esp-emulator 浏览器(wasm)构建改由新增的
`tools/fetch_esp_emu.py` 按需从 `espressif/esp-emulator` 的 GitHub
release 下载到 `vendor/esp-emu-browser/`——`npm run build` / `npm test`
首次运行自动触发。支持代理(`https_proxy`)、按 release `SHA256SUMS`
做 sha256 校验、tarball 缓存在 `vendor/downloads/`、幂等;`--version
<版本>` 可切换基线(见 UPGRADE.zh-cn.md)。`serve.py` / `sync_www.py` /
测试默认指向该目录(`--www` / `ESP_EMU_WWW` 可覆盖)。
- **发布打包**:`npm run package`(`tools/package_release.py`)生成面向
GitHub release 页面的 `esp-emu-peris-<版本>.zip` / `.tar.gz`;包内自带
编译好的 UI 与 `install.sh` / `install.bat` 安装脚本(负责下载
esp-emulator 浏览器构建)——release 用户只需 Python 3,无需 npm/node。
## peris 0.10.0 ## peris 0.10.0
- **逻辑分析仪**(`logic-analyzer.js` v1.0.0,逻辑在 `la-lib.js`):面向固件 - **逻辑分析仪**(`logic-analyzer.js` v1.0.0,逻辑在 `la-lib.js`):面向固件
+3 -2
View File
@@ -126,7 +126,8 @@ esp-emulator releases (upgrade procedure in UPGRADE.md):
## Maintenance items ## Maintenance items
- `update_esp_emu_browser.bat` (in `_tools`, currently empty): script the - ~~`update_esp_emu_browser.bat`~~ — solved differently: `tools/fetch_esp_emu.py`
UPGRADE.md procedure. downloads/unpacks any esp-emulator release (`--version <ver>`), see
UPGRADE.md.
- `src/core/mmio/`: add the C5/C6/H2/P4/S3 address tables (extracted from each - `src/core/mmio/`: add the C5/C6/H2/P4/S3 address tables (extracted from each
chip's IDF soc headers, same method as M0). chip's IDF soc headers, same method as M0).
+2 -1
View File
@@ -102,5 +102,6 @@
## 维护项 ## 维护项
- `update_esp_emu_browser.bat`(在 `_tools`,当前为空):按 UPGRADE.zh-cn.md 流程脚本化。 - ~~`update_esp_emu_browser.bat`~~——已用更好的方式解决:`tools/fetch_esp_emu.py`
可下载/解压任意 esp-emulator release(`--version <版本>`),见 UPGRADE.zh-cn.md。
- `src/core/mmio/`:补 C5/C6/H2/P4/S3 地址表(从各自 IDF soc 头文件提取,方法同 M0)。 - `src/core/mmio/`:补 C5/C6/H2/P4/S3 地址表(从各自 IDF soc 头文件提取,方法同 M0)。
+9 -6
View File
@@ -4,15 +4,18 @@
## Pinned baseline ## Pinned baseline
Current baseline: **esp-emulator v0.51.0** (`_tools/esp-emu-browser/www/pkg`, Current baseline: **esp-emulator v0.51.0** (`vendor/esp-emu-browser/www/pkg`,
wasm-bindgen `--target web`). fetched from the GitHub release by `tools/fetch_esp_emu.py`, wasm-bindgen
`--target web`).
## Upgrade steps ## Upgrade steps
1. Download the new wasm package (e.g. `esp-emu-<ver>-wasm.tar.gz`) and 1. Fetch the new wasm package:
unpack the new `www/` (`_tools` already has `python tools/fetch_esp_emu.py --version <ver>` — downloads
`update_esp_emu_browser.bat`, currently empty — fill it following this `esp-emu-<ver>-wasm.tar.gz` from the espressif/esp-emulator GitHub
procedure). release and unpacks its `www/` into `vendor/esp-emu-browser/www`
(honours `https_proxy`; bump `DEFAULT_VERSION` in the script afterwards).
Alternatively point `ESP_EMU_WWW=/path/to/new/www` at any unpacked copy.
2. **Run the M0 probe first**: `node tests/m0_probe.mjs <firmware path 2. **Run the M0 probe first**: `node tests/m0_probe.mjs <firmware path
unchanged; run straight against the pkg>`. The probe imports the new unchanged; run straight against the pkg>`. The probe imports the new
`pkg/esp_emu.js` directly; if any of the six conclusions `pkg/esp_emu.js` directly; if any of the six conclusions
+7 -3
View File
@@ -4,12 +4,16 @@
## 锁定版本 ## 锁定版本
当前基线:**esp-emulator v0.51.0**(`_tools/esp-emu-browser/www/pkg`,wasm-bindgen `--target web`)。 当前基线:**esp-emulator v0.51.0**(`vendor/esp-emu-browser/www/pkg`,由
`tools/fetch_esp_emu.py` 从 GitHub release 下载,wasm-bindgen `--target web`)。
## 升级步骤 ## 升级步骤
1. 下载新版 wasm 包(如 `esp-emu-<ver>-wasm.tar.gz`)解压出新的 `www/` 1. 拉取新版 wasm 包:`python tools/fetch_esp_emu.py --version <ver>`——从
(`_tools` 下已有 `update_esp_emu_browser.bat`,目前为空,可按此逻辑补全)。 espressif/esp-emulator 的 GitHub release 下载 `esp-emu-<ver>-wasm.tar.gz`
并把其中的 `www/` 解压到 `vendor/esp-emu-browser/www`(识别 `https_proxy`;
事后把脚本里的 `DEFAULT_VERSION` 一并升级)。也可以用
`ESP_EMU_WWW=/path/to/new/www` 直接指向任意解压副本。
2. **先跑 M0 探针**:`node tests/m0_probe.mjs <新固件路径不动,直接对 pkg>`。 2. **先跑 M0 探针**:`node tests/m0_probe.mjs <新固件路径不动,直接对 pkg>`。
探针直接 import 新 `pkg/esp_emu.js`,六项结论(Q1a/Q1b/Q2a/Q2b/Q3a/Q3b)若有变化, 探针直接 import 新 `pkg/esp_emu.js`,六项结论(Q1a/Q1b/Q2a/Q2b/Q3a/Q3b)若有变化,
先更新 `docs/FEASIBILITY.zh-cn.md` 与 `peris/mmio/` 地址表,再动其它代码。 先更新 `docs/FEASIBILITY.zh-cn.md` 与 `peris/mmio/` 地址表,再动其它代码。
+20
View File
@@ -0,0 +1,20 @@
@echo off
rem esp-emu-peris installer: fetches the esp-emulator browser (wasm) build
rem from https://github.com/espressif/esp-emulator/releases into vendor\.
rem Requires Python 3. Behind a firewall set the proxy env first, e.g.
rem set https_proxy=http://127.0.0.1:10809
rem install.bat
setlocal
cd /d "%~dp0"
where python >nul 2>nul
if %errorlevel%==0 (
python tools\fetch_esp_emu.py %*
exit /b %errorlevel%
)
where python3 >nul 2>nul
if %errorlevel%==0 (
python3 tools\fetch_esp_emu.py %*
exit /b %errorlevel%
)
echo error: Python 3 is required (python or python3 on PATH) 1>&2
exit /b 1
+9
View File
@@ -0,0 +1,9 @@
#!/bin/sh
# esp-emu-peris installer: fetches the esp-emulator browser (wasm) build
# from https://github.com/espressif/esp-emulator/releases into vendor/.
# Requires Python 3. Behind a firewall set the usual proxy env first, e.g.
# https_proxy=http://127.0.0.1:10809 ./install.sh
set -e
cd "$(dirname "$0")"
if command -v python3 >/dev/null 2>&1; then PY=python3; else PY=python; fi
exec "$PY" tools/fetch_esp_emu.py "$@"
+9 -2
View File
@@ -1,18 +1,25 @@
{ {
"name": "esp-emu-peris", "name": "esp-emu-peris",
"version": "0.10.0", "version": "0.10.1",
"private": true, "private": true,
"type": "module", "type": "module",
"description": "Simulated peripherals for the esp-emulator browser (wasm) build", "description": "Simulated peripherals for the esp-emulator browser (wasm) build",
"scripts": { "scripts": {
"fetch": "python tools/fetch_esp_emu.py",
"prebuild": "python tools/fetch_esp_emu.py",
"build": "tsc -p tsconfig.json", "build": "tsc -p tsconfig.json",
"check": "tsc -p tsconfig.json --noEmit", "check": "tsc -p tsconfig.json --noEmit",
"serve": "python serve.py", "serve": "python serve.py",
"sync": "python sync_www.py", "sync": "python sync_www.py",
"presync": "python tools/fetch_esp_emu.py",
"test": "node --test tests/unit/core.test.mjs tests/unit/devices.test.mjs tests/unit/protocol.test.mjs && node tests/m1_smoke.mjs", "test": "node --test tests/unit/core.test.mjs tests/unit/devices.test.mjs tests/unit/protocol.test.mjs && node tests/m1_smoke.mjs",
"pretest": "python tools/fetch_esp_emu.py",
"test:unit": "node --test tests/unit/core.test.mjs tests/unit/devices.test.mjs tests/unit/protocol.test.mjs", "test:unit": "node --test tests/unit/core.test.mjs tests/unit/devices.test.mjs tests/unit/protocol.test.mjs",
"test:smoke": "node tests/m1_smoke.mjs", "test:smoke": "node tests/m1_smoke.mjs",
"test:m0": "node tests/m0_probe.mjs" "pretest:smoke": "python tools/fetch_esp_emu.py",
"test:m0": "node tests/m0_probe.mjs",
"pretest:m0": "python tools/fetch_esp_emu.py",
"package": "python tools/package_release.py"
}, },
"devDependencies": { "devDependencies": {
"typescript": "5.9" "typescript": "5.9"
+9 -3
View File
@@ -11,7 +11,9 @@ Routes:
everything else -> this repository everything else -> this repository
Usage: python serve.py [port] [--www PATH] Usage: python serve.py [port] [--www PATH]
default port 8080, default www ../_tools/esp-emu-browser/www default port 8080, default www vendor/esp-emu-browser/www
(fetched from the espressif/esp-emulator release by
tools/fetch_esp_emu.py — npm run build does that automatically)
""" """
import argparse import argparse
import os import os
@@ -58,14 +60,18 @@ class Handler(SimpleHTTPRequestHandler):
def main(): def main():
default_www = os.path.normpath(os.path.join(REPO, '..', '_tools', 'esp-emu-browser', 'www')) default_www = os.environ.get(
'ESP_EMU_WWW',
os.path.normpath(os.path.join(REPO, 'vendor', 'esp-emu-browser', 'www')))
ap = argparse.ArgumentParser(description=__doc__) ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument('port', nargs='?', type=int, default=8080) ap.add_argument('port', nargs='?', type=int, default=8080)
ap.add_argument('--www', default=default_www, help='esp-emu-browser www directory') ap.add_argument('--www', default=default_www, help='esp-emu-browser www directory')
args = ap.parse_args() args = ap.parse_args()
if not os.path.isdir(os.path.join(args.www, 'pkg')): if not os.path.isdir(os.path.join(args.www, 'pkg')):
print(f'error: {args.www}/pkg not found — pass --www /path/to/esp-emu-browser/www', file=sys.stderr) print(f'error: {args.www}/pkg not found — run "python tools/fetch_esp_emu.py" '
f'(or "npm run build") first, or pass --www /path/to/esp-emu-browser/www',
file=sys.stderr)
sys.exit(1) sys.exit(1)
Handler.www_dir = args.www Handler.www_dir = args.www
+1 -1
View File
@@ -2,4 +2,4 @@
* Project version (keep in sync with package.json and docs/CHANGELOG.md). * Project version (keep in sync with package.json and docs/CHANGELOG.md).
* Device plugins carry their own per-device version in Device.version. * Device plugins carry their own per-device version in Device.version.
*/ */
export const PERIS_VERSION = '0.10.0'; export const PERIS_VERSION = '0.10.1';
+5 -2
View File
@@ -5,7 +5,8 @@ entry HTML, the peris/ tree and examples/. Existing stock files
(index.html, app.js, worker.js, pkg/) are never touched. (index.html, app.js, worker.js, pkg/) are never touched.
Usage: python sync_www.py [--www PATH] [--clean] Usage: python sync_www.py [--www PATH] [--clean]
default www ../_tools/esp-emu-browser/www default www vendor/esp-emu-browser/www (fetched from the
espressif/esp-emulator release by tools/fetch_esp_emu.py)
--clean removes a previously synced peris/ from www first --clean removes a previously synced peris/ from www first
""" """
import argparse import argparse
@@ -39,7 +40,9 @@ def sync(www, clean=False):
def main(): def main():
default_www = os.path.normpath(os.path.join(REPO, '..', '_tools', 'esp-emu-browser', 'www')) default_www = os.environ.get(
'ESP_EMU_WWW',
os.path.normpath(os.path.join(REPO, 'vendor', 'esp-emu-browser', 'www')))
ap = argparse.ArgumentParser(description=__doc__) ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument('--www', default=default_www, help='esp-emu-browser www directory') ap.add_argument('--www', default=default_www, help='esp-emu-browser www directory')
ap.add_argument('--clean', action='store_true', help='remove old peris/ in www before copying') ap.add_argument('--clean', action='store_true', help='remove old peris/ in www before copying')
+6 -2
View File
@@ -4,12 +4,16 @@
* RMT channel-0 RAM symbols during P3, plus cycles() behaviour across a * RMT channel-0 RAM symbols during P3, plus cycles() behaviour across a
* small-batch window. Run: node tests/m0_debug.mjs * small-batch window. Run: node tests/m0_debug.mjs
*/ */
import { readFileSync } from 'node:fs'; import { readFileSync, existsSync } from 'node:fs';
import { pathToFileURL, fileURLToPath } from 'node:url'; import { pathToFileURL, fileURLToPath } from 'node:url';
import path from 'node:path'; import path from 'node:path';
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const WWW = 'D:/d3/_git/esp_proj/_tools/esp-emu-browser/www'; const WWW = process.env.ESP_EMU_WWW || path.join(REPO, 'vendor', 'esp-emu-browser', 'www');
if (!existsSync(path.join(WWW, 'pkg', 'esp_emu.js'))) {
console.error(`error: ${path.join(WWW, 'pkg', 'esp_emu.js')} not found — run "npm run build" (or "python tools/fetch_esp_emu.py") first, or set ESP_EMU_WWW`);
process.exit(1);
}
const FW = path.join(REPO, 'tests/firmware/peris_probe/build/merged-binary.bin'); const FW = path.join(REPO, 'tests/firmware/peris_probe/build/merged-binary.bin');
const { initSync, WasmEmulator } = await import( const { initSync, WasmEmulator } = await import(
+6 -2
View File
@@ -17,12 +17,16 @@
* *
* Usage: node tests/m0_probe.mjs [firmware.bin] [--json] * Usage: node tests/m0_probe.mjs [firmware.bin] [--json]
*/ */
import { readFileSync } from 'node:fs'; import { readFileSync, existsSync } from 'node:fs';
import { pathToFileURL, fileURLToPath } from 'node:url'; import { pathToFileURL, fileURLToPath } from 'node:url';
import path from 'node:path'; import path from 'node:path';
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const WWW = 'D:/d3/_git/esp_proj/_tools/esp-emu-browser/www'; const WWW = process.env.ESP_EMU_WWW || path.join(REPO, 'vendor', 'esp-emu-browser', 'www');
if (!existsSync(path.join(WWW, 'pkg', 'esp_emu.js'))) {
console.error(`error: ${path.join(WWW, 'pkg', 'esp_emu.js')} not found — run "npm run build" (or "python tools/fetch_esp_emu.py") first, or set ESP_EMU_WWW`);
process.exit(1);
}
const FW = process.argv[2] ?? path.join(REPO, 'tests/firmware/peris_probe/build/merged-binary.bin'); const FW = process.argv[2] ?? path.join(REPO, 'tests/firmware/peris_probe/build/merged-binary.bin');
// --- ESP32-C3 MMIO map (from IDF soc headers, see docs/FEASIBILITY.md) --- // --- ESP32-C3 MMIO map (from IDF soc headers, see docs/FEASIBILITY.md) ---
+7 -2
View File
@@ -50,7 +50,11 @@ import { pathToFileURL, fileURLToPath } from 'node:url';
import path from 'node:path'; import path from 'node:path';
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const WWW = 'D:/d3/_git/esp_proj/_tools/esp-emu-browser/www'; const WWW = process.env.ESP_EMU_WWW || path.join(REPO, 'vendor', 'esp-emu-browser', 'www');
if (!existsSync(path.join(WWW, 'pkg', 'esp_emu.js'))) {
console.error(`error: ${path.join(WWW, 'pkg', 'esp_emu.js')} not found — run "npm run build" (or "python tools/fetch_esp_emu.py") first, or set ESP_EMU_WWW`);
process.exit(1);
}
const FW = process.argv[2] ?? path.join(REPO, 'tests/firmware/peris_probe/build/merged-binary.bin'); const FW = process.argv[2] ?? path.join(REPO, 'tests/firmware/peris_probe/build/merged-binary.bin');
const { initSync, WasmEmulator } = await import( const { initSync, WasmEmulator } = await import(
@@ -597,7 +601,8 @@ check('S6 unknown type rejected', ready3?.ok === false && ready3.errors.some((e)
// decode 0xFF (Code B), scan-limit 7, intensity 4, eight blank digits, // decode 0xFF (Code B), scan-limit 7, intensity 4, eight blank digits,
// shutdown 1 — 14 register words, then the control task pushes values // shutdown 1 — 14 register words, then the control task pushes values
// once WiFi is up (display_show → display_tick → non-zero digits). // once WiFi is up (display_show → display_tick → non-zero digits).
const GUN_FW = 'D:/d3/_git/esp_proj/gun_flag_monster/gun/release/0.1.0/gun-0.1.0-merged.bin'; // optional machine-local firmware: set GUN_FW to run this section elsewhere
const GUN_FW = process.env.GUN_FW || 'D:/d3/_git/esp_proj/gun_flag_monster/gun/release/0.1.0/gun-0.1.0-merged.bin';
if (existsSync(GUN_FW)) { if (existsSync(GUN_FW)) {
const emu2 = new WasmEmulator('esp32s3'); const emu2 = new WasmEmulator('esp32s3');
if (emu2.has_default_rom()) emu2.load_default_rom(); if (emu2.has_default_rom()) emu2.load_default_rom();
+229
View File
@@ -0,0 +1,229 @@
#!/usr/bin/env python3
"""
Fetch the esp-emulator browser (wasm) build from the espressif/esp-emulator
GitHub releases and unpack it into vendor/esp-emu-browser/www.
- Idempotent: skips the download when the requested version is already in
place (vendor/esp-emu-browser/VERSION + www/pkg/esp_emu_bg.wasm).
- Verifies the tarball against the release SHA256SUMS asset.
- Honours the standard proxy environment variables
(https_proxy / HTTPS_PROXY / all_proxy ...), e.g.
https_proxy=http://127.0.0.1:10809 python tools/fetch_esp_emu.py
Usage: python tools/fetch_esp_emu.py [--version 0.51.0] [--force] [--no-verify]
version default: 0.51.0 (or env ESP_EMU_VERSION)
downloaded tarballs are cached in vendor/downloads/ (never re-downloaded)
Afterwards: python serve.py -> http://127.0.0.1:8080/esp-emu-peris.html
"""
import argparse
import hashlib
import os
import shutil
import sys
import tarfile
import tempfile
import time
import urllib.error
import urllib.request
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DEFAULT_VERSION = '0.51.0'
VENDOR_DIR = os.path.join(REPO, 'vendor', 'esp-emu-browser')
CACHE_DIR = os.path.join(REPO, 'vendor', 'downloads')
RETRIES = 3
TIMEOUT = 60
def log(msg):
print(f'[fetch_esp_emu] {msg}', flush=True)
def build_opener():
"""urllib opener that honours proxy env vars (https_proxy etc.)."""
proxies = {}
# explicit env first (works everywhere, incl. Windows without registry proxies)
for scheme in ('http', 'https', 'all'):
for var in (f'{scheme}_proxy'.lower(), f'{scheme}_proxy'.upper()):
val = os.environ.get(var)
if val:
proxies.setdefault('https' if scheme == 'all' else scheme, val)
if not proxies:
proxies = urllib.request.getproxies()
return urllib.request.build_opener(urllib.request.ProxyHandler(proxies))
def download(opener, url, dest, retries=RETRIES):
for attempt in range(1, retries + 1):
try:
log(f'downloading {url}')
with opener.open(url, timeout=TIMEOUT) as resp, open(dest, 'wb') as out:
total = int(resp.headers.get('Content-Length') or 0)
done = 0
while True:
chunk = resp.read(1 << 16)
if not chunk:
break
out.write(chunk)
done += len(chunk)
if total:
pct = done * 100 // total
print(f'\r {done // 1024} / {total // 1024} KiB ({pct}%)', end='', flush=True)
print()
return True
except (urllib.error.URLError, OSError) as e:
log(f'download attempt {attempt}/{retries} failed: {e}')
if os.path.exists(dest):
os.remove(dest)
if attempt < retries:
time.sleep(2 * attempt)
return False
def sha256_of(path):
h = hashlib.sha256()
with open(path, 'rb') as f:
for chunk in iter(lambda: f.read(1 << 20), b''):
h.update(chunk)
return h.hexdigest()
def verify(opener, tarball, version, asset):
"""Check the tarball against the release SHA256SUMS asset (best effort
by default: a failed SUMS *download* only warns, a hash mismatch fails)."""
sums_url = (f'https://github.com/espressif/esp-emulator/releases/download/'
f'v{version}/SHA256SUMS')
try:
with opener.open(sums_url, timeout=TIMEOUT) as resp:
text = resp.read().decode('utf-8', 'replace')
except (urllib.error.URLError, OSError) as e:
log(f'warning: could not fetch SHA256SUMS ({e}); skipping verification')
return True
want = None
for line in text.splitlines():
parts = line.split()
if len(parts) == 2 and parts[1] == asset:
want = parts[0]
break
if want is None:
log(f'warning: {asset} not listed in SHA256SUMS; skipping verification')
return True
got = sha256_of(tarball)
if got != want:
log(f'error: sha256 mismatch for {asset}\n want {want}\n got {got}')
return False
log(f'sha256 ok ({want[:16]}…)')
return True
def safe_extract(tar, dest):
try:
tar.extractall(dest, filter='data') # Python 3.12+
return
except TypeError:
pass
# older Pythons: reject absolute paths and .. escapes manually
base = os.path.abspath(dest)
for m in tar.getmembers():
target = os.path.abspath(os.path.join(dest, m.name))
if not target.startswith(base + os.sep) and target != base:
raise RuntimeError(f'unsafe path in archive: {m.name}')
tar.extractall(dest)
def move_tree(src, dst):
"""Directory rename with retries: Windows Defender/indexer transiently
locks freshly extracted files (WinError 5); copy+delete as a fallback."""
for attempt in range(4):
try:
os.rename(src, dst)
return
except OSError:
time.sleep(0.5 * (attempt + 1))
shutil.copytree(src, dst, dirs_exist_ok=True)
shutil.rmtree(src, ignore_errors=True)
def install(tarball, version):
"""Unpack <root>/esp-emu-<ver>-wasm/www into vendor/esp-emu-browser/www."""
vendor_parent = os.path.join(REPO, 'vendor')
staging = os.path.join(vendor_parent, f'.staging-{int(time.time())}')
old = os.path.join(VENDOR_DIR, 'www.old')
target = os.path.join(VENDOR_DIR, 'www')
os.makedirs(staging, exist_ok=True)
with tarfile.open(tarball, 'r:gz') as tar:
safe_extract(tar, staging)
inner = os.path.join(staging, f'esp-emu-{version}-wasm', 'www')
if not os.path.isfile(os.path.join(inner, 'pkg', 'esp_emu_bg.wasm')):
shutil.rmtree(staging, ignore_errors=True)
raise RuntimeError(f'unexpected archive layout: {inner}/pkg missing')
os.makedirs(VENDOR_DIR, exist_ok=True)
if os.path.isdir(old):
shutil.rmtree(old, ignore_errors=True)
if os.path.isdir(target):
move_tree(target, old)
try:
move_tree(inner, target)
except OSError:
if os.path.isdir(old) and not os.path.isdir(target): # best-effort restore
move_tree(old, target)
raise
finally:
shutil.rmtree(staging, ignore_errors=True)
shutil.rmtree(old, ignore_errors=True)
with open(os.path.join(VENDOR_DIR, 'VERSION'), 'w', encoding='utf-8') as f:
f.write(f'{version}\n')
log(f'installed esp-emulator browser {version} -> '
f'{os.path.relpath(target, REPO).replace(os.sep, "/")}')
def already_installed(version):
return (os.path.isfile(os.path.join(VENDOR_DIR, 'VERSION'))
and open(os.path.join(VENDOR_DIR, 'VERSION'), encoding='utf-8').read().strip() == version
and os.path.isfile(os.path.join(VENDOR_DIR, 'www', 'pkg', 'esp_emu_bg.wasm')))
def main():
ap = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('--version', default=os.environ.get('ESP_EMU_VERSION', DEFAULT_VERSION),
help=f'esp-emulator release tag without "v" (default {DEFAULT_VERSION})')
ap.add_argument('--force', action='store_true', help='re-download/re-extract even if present')
ap.add_argument('--no-verify', action='store_true', help='skip sha256 verification')
args = ap.parse_args()
version = args.version.strip().lstrip('vV')
asset = f'esp-emu-{version}-wasm.tar.gz'
url = (f'https://github.com/espressif/esp-emulator/releases/download/'
f'v{version}/{asset}')
if already_installed(version) and not args.force:
log(f'esp-emulator browser {version} already present '
f'(vendor/esp-emu-browser) — nothing to do')
return 0
os.makedirs(CACHE_DIR, exist_ok=True)
tarball = os.path.join(CACHE_DIR, asset)
opener = build_opener()
if os.path.isfile(tarball) and not args.force:
log(f'using cached {os.path.relpath(tarball, REPO)}')
else:
if not download(opener, url, tarball):
log(f'error: download failed after {RETRIES} attempts '
f'(behind a firewall? set https_proxy=… and retry)')
return 1
if not args.no_verify and not verify(opener, tarball, version, asset):
os.remove(tarball)
return 1
try:
install(tarball, version)
except (RuntimeError, tarfile.TarError, OSError) as e:
log(f'error: extraction failed: {e}')
return 1
log('next: python serve.py -> http://127.0.0.1:8080/esp-emu-peris.html')
return 0
if __name__ == '__main__':
sys.exit(main())
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""
Build the esp-emu-peris release archives for the GitHub release page.
Stages the repository (git-tracked files + the compiled build/ tree) into
dist/package/esp-emu-peris-<version>/ and writes
dist/release/esp-emu-peris-<version>.zip and .tar.gz.
The archives include install.sh / install.bat, which fetch the
esp-emulator browser (wasm) build from espressif/esp-emulator releases —
so a release user needs no local esp-emulator checkout.
Usage: python tools/package_release.py (run "npm run build" first)
"""
import json
import os
import shutil
import subprocess
import sys
import tarfile
import zipfile
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DIST = os.path.join(REPO, 'dist')
EXTRA_DIRS = ['build'] # not in git, but shipped pre-compiled
def log(msg):
print(f'[package_release] {msg}', flush=True)
def tracked_files():
out = subprocess.run(['git', 'ls-files'], cwd=REPO, capture_output=True,
text=True, check=True).stdout
return [line for line in out.splitlines() if line.strip()]
def stage(version):
stage_root = os.path.join(DIST, 'package')
stage_dir = os.path.join(stage_root, f'esp-emu-peris-{version}')
if os.path.isdir(stage_root):
shutil.rmtree(stage_root)
os.makedirs(stage_dir)
files = tracked_files()
skip = {'.gitignore', '.gitattributes'} # not meaningful outside a repo
skip_prefixes = ('.vscode/',) # IDE prefs carry machine-local paths
copied = 0
for rel in files:
if rel in skip or rel.startswith(skip_prefixes):
continue
src = os.path.join(REPO, rel)
if not os.path.isfile(src):
continue
dst = os.path.join(stage_dir, rel)
os.makedirs(os.path.dirname(dst), exist_ok=True)
shutil.copy2(src, dst)
copied += 1
for d in EXTRA_DIRS:
src = os.path.join(REPO, d)
if not os.path.isdir(src):
log(f'warning: {d}/ missing — run "npm run build" first')
continue
for root, _dirs, names in os.walk(src):
for name in names:
s = os.path.join(root, name)
rel = os.path.relpath(s, REPO)
dst = os.path.join(stage_dir, rel)
os.makedirs(os.path.dirname(dst), exist_ok=True)
shutil.copy2(s, dst)
copied += 1
log(f'staged {copied} files -> {os.path.relpath(stage_dir, REPO)}')
return stage_dir
def make_zip(stage_dir, dest):
with zipfile.ZipFile(dest, 'w', zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
for root, _dirs, names in os.walk(stage_dir):
for name in sorted(names):
s = os.path.join(root, name)
zf.write(s, os.path.relpath(s, os.path.dirname(stage_dir)))
log(f'wrote {os.path.relpath(dest, REPO)} ({os.path.getsize(dest) // 1024} KiB)')
def make_targz(stage_dir, dest):
with tarfile.open(dest, 'w:gz') as tf:
tf.add(stage_dir, arcname=os.path.basename(stage_dir))
log(f'wrote {os.path.relpath(dest, REPO)} ({os.path.getsize(dest) // 1024} KiB)')
def main():
with open(os.path.join(REPO, 'package.json'), encoding='utf-8') as f:
version = json.load(f)['version']
release_dir = os.path.join(DIST, 'release')
os.makedirs(release_dir, exist_ok=True)
stage_dir = stage(version)
make_zip(stage_dir, os.path.join(release_dir, f'esp-emu-peris-{version}.zip'))
make_targz(stage_dir, os.path.join(release_dir, f'esp-emu-peris-{version}.tar.gz'))
log('release archives ready — upload with:')
log(f' gh release create v{version} '
f'dist/release/esp-emu-peris-{version}.zip '
f'dist/release/esp-emu-peris-{version}.tar.gz')
return 0
if __name__ == '__main__':
sys.exit(main())