Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
896b21fe44
|
||
|
|
cc6b82d7ec
|
||
|
|
4794bed2cf | ||
|
|
e6070f91ee | ||
|
|
26731958ea | ||
|
|
de7263569c | ||
|
|
c9ce7b64ef | ||
|
|
f3b09ca9a6 | ||
|
|
1cccc1aa00 | ||
|
|
f0538fa233 | ||
|
|
47ef7c63dd | ||
|
|
b74c6654e5 | ||
|
|
ac3d639d6b | ||
|
|
2ed16ff88d | ||
|
|
6f83ebbe3c | ||
|
|
4667d94c45 | ||
|
|
76b96cc374 | ||
|
|
03f7279b53 |
+4
-11
@@ -12,14 +12,7 @@ build-std = ["std", "panic_abort"]
|
|||||||
[env]
|
[env]
|
||||||
MCU = "esp32s3"
|
MCU = "esp32s3"
|
||||||
|
|
||||||
# Use the ESP-IDF installation and toolchains that are already on the machine
|
# esp-idf-sys downloads this version unless IDF_PATH points to an existing SDK.
|
||||||
# (eim's install under ~/.espressif) instead of downloading another copy into
|
# Keep machine-specific paths in the environment instead of this shared file.
|
||||||
# ./.embuild. Without this the first build re-downloads several hundred MB of
|
ESP_IDF_VERSION = { value = "v5.5.5", force = false }
|
||||||
# Xtensa GCC/LLVM.
|
ESP_IDF_TOOLS_INSTALL_DIR = { value = "global", force = false }
|
||||||
#
|
|
||||||
# `global` means ~/.espressif, which is where eim installs.
|
|
||||||
#
|
|
||||||
# Drop these two lines to let esp-idf-sys clone and fetch the ESP-IDF version it
|
|
||||||
# pins itself (v5.5.5 by default), into ~/.espressif.
|
|
||||||
IDF_PATH = { value = "/Users/peng/.espressif/esp-idf/v5.5.5", force = true }
|
|
||||||
ESP_IDF_TOOLS_INSTALL_DIR = { value = "global", force = true }
|
|
||||||
|
|||||||
@@ -20,6 +20,54 @@ WIFI_PASS=your-password
|
|||||||
#CAM_RES=vga
|
#CAM_RES=vga
|
||||||
# JPEG quality 0-63, lower is better quality
|
# JPEG quality 0-63, lower is better quality
|
||||||
#CAM_QUALITY=12
|
#CAM_QUALITY=12
|
||||||
|
# Optional adaptive compression ceiling, 0 = fixed (default), else QUALITY..63.
|
||||||
|
# Shed load on sender congestion, restore detail after 15 stable seconds.
|
||||||
|
#CAM_QUALITY_MAX=0
|
||||||
|
# Stream cap, 0 = uncapped (default), 1-60 = maximum fps.
|
||||||
|
# Paces frame retrieval and UDP delivery; sensor clocks remain unchanged.
|
||||||
|
#CAM_MAX_FPS=0
|
||||||
|
# OV5640 PLL profile: default (driver timing) | vga30 (VGA ~30.09 fps).
|
||||||
|
# vga30 preserves HTS/VTS: XCLK 20, VCO 610, SYSCLK 61, PCLK 15.25 MHz.
|
||||||
|
# Retune exposure-line limits when switching profiles; keep HTS at default.
|
||||||
|
#CAM_CLOCK_PROFILE=default
|
||||||
|
# Capture frame buffers in PSRAM: 3 (default) or 2 (lower capture queue depth).
|
||||||
|
# Prefer 3 for burst tolerance; 2 showed worse long-run peak frame loss here.
|
||||||
|
#CAM_FB_COUNT=3
|
||||||
|
# Sensor grayscale effect while retaining hardware JPEG, 0 = colour (default).
|
||||||
|
#CAM_GRAYSCALE=0
|
||||||
|
# OV5640 AE target bias: 0 (default), -1 (~-1.1 EV), -2 (~-1.9 EV).
|
||||||
|
# This target alone does not guarantee a shorter shutter.
|
||||||
|
#CAM_EXPOSURE_EV=0
|
||||||
|
# Optional OV5640 driver AE level -5..5; requires CAM_EXPOSURE_EV=0.
|
||||||
|
# Unset uses the EV setting. -1 lowers the target 55 -> 45 (~-0.29 EV),
|
||||||
|
# a finer brightness adjustment than CAM_EXPOSURE_EV=-1. AEC/AGC stay on.
|
||||||
|
#CAM_AE_LEVEL=-1
|
||||||
|
# OV5640 fixed shutter in sensor lines, 0 = automatic (default), AGC stays on.
|
||||||
|
# 220 lines is roughly 10 ms with default VGA HTS 2060; verify timing/logs.
|
||||||
|
#CAM_SHUTTER_LINES=0
|
||||||
|
# Optional automatic exposure cap in sensor lines, 0 = driver default.
|
||||||
|
# AEC/AGC stay automatic, night frame extension is disabled. Mutually exclusive
|
||||||
|
# with a nonzero fixed shutter. Default VGA: 440 lines is about 20 ms.
|
||||||
|
# With vga30 use 592 lines for about 20 ms, including two 50 Hz AEC bands.
|
||||||
|
#CAM_EXPOSURE_MAX_LINES=0
|
||||||
|
# OV5640 automatic gain ceiling, 0 = driver default, else 1..63 times.
|
||||||
|
# More gain can brighten short exposures but also amplifies noise.
|
||||||
|
#CAM_GAIN_CEILING=0
|
||||||
|
# OV5640 horizontal total, 0 = driver default. Even values only; hardware test
|
||||||
|
# required. Changing HTS also changes the time represented by one shutter line.
|
||||||
|
# HTS 1500 / 1800 caused green/magenta flicker on this module; keep 0.
|
||||||
|
#CAM_HTS=0
|
||||||
|
# Minimum gap between successful video datagrams, 0 = burst (default).
|
||||||
|
# Allowed 0..5000 us in FreeRTOS tick increments (1000 us in this build).
|
||||||
|
#UDP_PACKET_GAP_US=0
|
||||||
|
# Total yielding-retry allowance per JPEG, shared by all fragments/peers.
|
||||||
|
# 0 = fail on backpressure; default 8000 us, 0..25000 in tick increments.
|
||||||
|
# Frame deadlines remain 25 ms sending / 100 ms since capture start.
|
||||||
|
#UDP_RETRY_BUDGET_US=8000
|
||||||
|
# Internal ESP chip temperature load limit, Celsius; 0 disables, default 75.
|
||||||
|
# At the limit halve fps (rounded up); restore after cooling by 5 C.
|
||||||
|
# This is a proactive load threshold, not a specified silicon safety limit.
|
||||||
|
#CAM_THERMAL_LIMIT_C=75
|
||||||
# Set to 0 if your board inverts VSYNC in hardware.
|
# Set to 0 if your board inverts VSYNC in hardware.
|
||||||
#CAM_VSYNC_INVERT=1
|
#CAM_VSYNC_INVERT=1
|
||||||
|
|
||||||
@@ -28,3 +76,13 @@ WIFI_PASS=your-password
|
|||||||
# the AP (a hidden SSID, for instance) - otherwise the scan reports the real
|
# the AP (a hidden SSID, for instance) - otherwise the scan reports the real
|
||||||
# mode and this only makes it fail faster.
|
# mode and this only makes it fail faster.
|
||||||
#WIFI_AUTH=wpa2
|
#WIFI_AUTH=wpa2
|
||||||
|
# Optional maximum Wi-Fi TX power, integer dBm (2..20). Unset keeps SDK default.
|
||||||
|
# Test on your own board/network; higher power does not always improve video.
|
||||||
|
# Current delivery requests 15 dBm; this ESP32-S3 reports a 15.00 dBm cap.
|
||||||
|
# Runtime load control does not lower it; reconnects reapply the same cap.
|
||||||
|
# WIFI_TX_POWER_DBM=15
|
||||||
|
|
||||||
|
# Diagnostic builds only: intentionally disconnect once, after N seconds of
|
||||||
|
# the streaming loop, hold offline 5 seconds, then exercise runtime recovery.
|
||||||
|
# Keep 0 (default) for normal firmware; does not change router/TUN settings.
|
||||||
|
#WIFI_TEST_DROP_AFTER_S=0
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# actionlint 配置。
|
||||||
|
#
|
||||||
|
# 这个仓库的 CI 跑在 Gitea 上,但 actionlint 用的是 GitHub 的规则,所以有一条
|
||||||
|
# 政策性的误报要忽略掉:
|
||||||
|
#
|
||||||
|
# the runner of "actions/upload-artifact@v3" action is too old to run on GitHub Actions
|
||||||
|
#
|
||||||
|
# 它对 GitHub 是成立的(v3 声明的 node16 运行时 GitHub 已经不再执行),但我们是
|
||||||
|
# Gitea:Gitea 的 runner 用自己的 node 跑 JS action,v3 能正常把产物传上去;反过来
|
||||||
|
# v4 在 Gitea 上**根本跑不了** —— v4 里的 @actions/artifact 会拒绝非 github.com
|
||||||
|
# 的服务器(GHESNotSupportedError,原文见 .github/workflows/build-client.yml 文件头
|
||||||
|
# 第 3 条,那是 2026-10-15 第一次真实运行的日志里抄下来的)。
|
||||||
|
paths:
|
||||||
|
.github/workflows/*.yml:
|
||||||
|
ignore:
|
||||||
|
- 'the runner of "actions/upload-artifact@v3" action is too old to run on GitHub Actions'
|
||||||
+201
-112
@@ -1,42 +1,95 @@
|
|||||||
# 构建桌面客户端(desktop/,Tauri v2 + React)。
|
# 构建桌面客户端(desktop/,Tauri v2 + React)。
|
||||||
#
|
#
|
||||||
# 产物都挂在这次运行下面(Actions 页面直接下载),保留 14 天:
|
# 产物都挂在这次运行下面(Actions 页面直接下载),保留 14 天:
|
||||||
# Linux x64 deb + AppImage cam-desktop_<版本>_amd64.deb / .AppImage ← 默认每次编
|
# Linux x64 deb + AppImage RMIC-2026_<版本>_amd64.deb / .AppImage ← 默认每次编
|
||||||
# Windows x64 NSIS 安装包 cam-desktop_<版本>_x64-setup.exe ← 有 runner 才编
|
# Windows x64 NSIS 安装包 RMIC-2026_<版本>_x64-setup.exe ← 默认每次编
|
||||||
# macOS 通用 .app + .dmg arm64 与 x86_64 合成一个包 ← 有 runner 才编
|
# macOS 通用 .app + .dmg arm64 与 x86_64 合成一个包 ← 还没有机器,手动勾选才编
|
||||||
#
|
#
|
||||||
# ── 触发 ─────────────────────────────────────────────────────────────────────
|
# ── 触发 ─────────────────────────────────────────────────────────────────────
|
||||||
# * push 到 main/master —— 只认 desktop/、protocol/ 与本文件的改动,改固件不会
|
# * push 到 main/master —— 只认 desktop/、protocol/ 与本文件的改动,改固件不会
|
||||||
# 白占 runner
|
# 白占 runner
|
||||||
# * 打 v* tag、开 PR
|
# * 打 v* tag、开 PR
|
||||||
# * 手动 Run workflow:逐个平台勾选,不进 paths 过滤("就想编一次"走它)
|
# * 手动 Run workflow:逐个平台勾选,不进 paths 过滤("就想编一次"走它)
|
||||||
# Windows 与 macOS 两个 job 目前默认不跑,原因与打开方式见下面第 1 条。
|
# 平台开关一律看勾选框(理由见下面第 4 条)。
|
||||||
#
|
#
|
||||||
# ── 跑在 Gitea 上要先对上的两件事 ────────────────────────────────────────────
|
# ── 跑在 Gitea 上要先对上的两件事 ────────────────────────────────────────────
|
||||||
# 1. runner 标签是注册 runner 时自己起的名字,猜不到,所以三个 job 都写成
|
# 1. runner 标签是注册 runner 时自己起的名字,猜不到,所以三个 job 都写成
|
||||||
# "仓库变量优先、常见标签兜底":
|
# "仓库变量优先、常见标签兜底":
|
||||||
# runs-on: ${{ vars.RUNNER_LINUX || 'ubuntu-latest' }}
|
# runs-on: ${{ vars.RUNNER_LINUX || 'ubuntu-latest' }}
|
||||||
# 标签对不上时 job 是**排队**而不是报错 —— 排队会一直占着这次运行不结束,
|
# 标签对不上时 job 是**排队**而不是报错 —— 排队会一直占着这次运行不结束,
|
||||||
# 所以第一次跑之前先看一眼 runner 页面上那几行标签。
|
# 所以上线新平台前先看一眼 runner 页面上那几行标签。
|
||||||
#
|
#
|
||||||
# 本仓库目前只有 Linux 的两个 runner 标签(ubuntu-latest / ubuntu-24.04 /
|
# 2026-10-15 实测在线的(括号里是它们自己声明的标签):
|
||||||
# ubuntu-22.04),所以**默认只跑 Linux**:Windows 与 macOS 两个 job 要等
|
# * simba-runner-01 (ubuntu-latest / ubuntu-24.04 / ubuntu-22.04)
|
||||||
# 仓库变量 ENABLE_WINDOWS / ENABLE_MACOS 设成 true 才会跑(手动 Run workflow
|
# * windows-runner-01 (windows-latest / windows-2025 / windows-2022)
|
||||||
# 时另看勾选框),机器的标签则用 RUNNER_WINDOWS / RUNNER_MACOS 指过去。
|
# * macOS:还没有
|
||||||
# 等那两台机器上的 runner 注册好再打开,否则每次 push 都会多出两个永远排队的
|
# 所以 Windows 默认就跑;macOS 的 job 写成"手动勾选才跑"。
|
||||||
# job。
|
|
||||||
#
|
#
|
||||||
# 为什么不能拿 Linux runner 交叉编出来:
|
# 为什么不能拿 Linux runner 交叉编出来:
|
||||||
# * Windows:Rust 侧用 mingw 交叉编还能出个裸 exe,但 tauri 的 NSIS 打包器
|
# * Windows:Rust 侧用 mingw 交叉编还能出个裸 exe,但 tauri 的 NSIS 打包器
|
||||||
# 本身只能在 Windows 上跑,出不了安装包;
|
# 本身只能在 Windows 上跑,出不了安装包;
|
||||||
# * macOS:要 Apple SDK 与 codesign/lipo,只能在 macOS 上编。
|
# * macOS:要 Apple SDK 与 codesign/lipo,只能在 macOS 上编。
|
||||||
# 所以这两个平台的 job 留着是为了"机器到位那天直接开",不是摆设。
|
# Windows runner 上要预装好 MSVC Build Tools(link.exe)、WebView2 运行时与
|
||||||
# 那两台机器上要预装好:Windows 的 MSVC Build Tools(link.exe)、macOS 的
|
# rustup(dtolnay/rust-toolchain 靠它装工具链);macOS 上要 Xcode Command Line
|
||||||
# Xcode Command Line Tools,以及 rustup(dtolnay/rust-toolchain 靠它装工具链)。
|
# Tools + rustup。Linux 的 job 通常跑在容器里,没装 rustup 也没关系 —— 见兜底那步。
|
||||||
# Linux 的 job 通常跑在容器里,所以没装 rustup 也没关系 —— 见下面那步兜底。
|
|
||||||
# 2. 这个文件放在 .github/workflows/ 下就够:Gitea 的 [actions] WORKFLOW_DIRS
|
# 2. 这个文件放在 .github/workflows/ 下就够:Gitea 的 [actions] WORKFLOW_DIRS
|
||||||
# 默认值就是 [".gitea/workflows", ".github/workflows"](源码里的默认值,且
|
# 默认值就是 [".gitea/workflows", ".github/workflows"](源码里的默认值,且
|
||||||
# .yml 与 .yaml 都认),所以两个目录都是原生支持的,不用挪。
|
# .yml 与 .yaml 都认),所以两个目录都是原生支持的,不用挪。
|
||||||
|
# 3. 两处"GitHub 能跑、Gitea 上不行"的地方,都是 2026-10-15 第一次真实运行的
|
||||||
|
# 日志里钉出来的:
|
||||||
|
# * 传产物用 upload-artifact@v3,**不能用 v4**。v4 里的 @actions/artifact 会
|
||||||
|
# 拒绝非 github.com 的服务器,日志原文:
|
||||||
|
# GHESNotSupportedError: @actions/artifact v2.0.0+, upload-artifact@v4+
|
||||||
|
# and download-artifact@v4+ are not currently supported on GHES
|
||||||
|
# Gitea 实现的是 v3 那套接口(ACTIONS_RUNTIME_URL),所以 v3 能传上去。
|
||||||
|
# * 一个缓存都没开。这台实例的缓存服务在 job 容器里连不上,日志原文:
|
||||||
|
# Failed to restore: getCacheEntry failed: connect ETIMEDOUT 172.18.0.2:37315
|
||||||
|
# setup-node 的 cache 每次要干等 4 分 39 秒才放弃(两个 job 各一次)。
|
||||||
|
# 等缓存能从容器访问到了再加回来,写法就是:
|
||||||
|
# setup-node 的 with 里加:cache: pnpm / cache-dependency-path: desktop/pnpm-lock.yaml
|
||||||
|
# 各 job 里加:
|
||||||
|
# - uses: actions/cache@v4
|
||||||
|
# with:
|
||||||
|
# path: |
|
||||||
|
# ~/.cargo/registry/index
|
||||||
|
# ~/.cargo/registry/cache
|
||||||
|
# ~/.cargo/git/db
|
||||||
|
# key: ${{ runner.os }}-cargo-registry-${{ hashFiles('protocol/Cargo.lock', 'desktop/core/Cargo.lock', 'desktop/src-tauri/Cargo.lock') }}
|
||||||
|
# restore-keys: ${{ runner.os }}-cargo-registry-
|
||||||
|
# 4. 平台开关只用 github.event.inputs,**不用仓库变量**。2026-10-15 那次 Windows# runner 明明在线、job 也确实派给了它(它自己的日志里写着
|
||||||
|
# "Runner labels: windows-latest, windows-2025, windows-2022"),产物却一直是
|
||||||
|
# skipped —— 卡在 job 级 if 的 `vars.ENABLE_WINDOWS == 'true'` 上:Gitea 的
|
||||||
|
# runner 在 job 级 if 里拿不到(或拿不对)vars 上下文,而这个条件写反的代价是
|
||||||
|
# "静悄悄不编",最难发现。同一份文件里 `github.event.inputs.linux != 'false'`
|
||||||
|
# 这个形式在 runner 日志里能看到正常求值(evaluating expression '...' evaluated
|
||||||
|
# to 'true'),所以平台开关统一成这种:
|
||||||
|
# * Linux / Windows:`github.event.inputs.<平台> != 'false'` → 默认跑,
|
||||||
|
# 手动触发时取消勾选就不跑
|
||||||
|
# * macOS:`github.event.inputs.macos == 'true'` → 默认不跑,等有机器了改成
|
||||||
|
# 上一种形式即可
|
||||||
|
# runs-on 里的 vars(RUNNER_*)不受影响,标签不匹配时用那个覆盖。
|
||||||
|
# 5. Windows runner 的 job 跑在**低完整性**上(实测 Mandatory Label\Low Mandatory
|
||||||
|
# Level, S-1-16-4096),而 Windows 不允许低完整性往中完整性写:用户目录
|
||||||
|
# (~/.rustup、~/.cargo、%TEMP%、%LOCALAPPDATA%)统统 Access denied (os error 5)
|
||||||
|
# —— ACL 是干净的(SYSTEM / Administrators / 本人 全 F,没有任何 Deny),拦它的是
|
||||||
|
# 完整性级别,所以"放宽权限"一点用都没有。表现是一串看着毫不相干的错误:
|
||||||
|
# * Git Bash 的 mktemp 写 /tmp 被拒(它的 /tmp 跟着 TEMP 走;TEMP 不可写时退回
|
||||||
|
# <Git 安装目录>\tmp,那个目录普通权限进不去);
|
||||||
|
# * rustup: could not create temp file C:\Users\admin\.rustup\tmp\xxx_file
|
||||||
|
# 处置:Windows job 把能重定向的都指到 runner 自己的工作目录
|
||||||
|
# (C:\Users\admin\runner\windows\cache,job 内写探测实测可写)——
|
||||||
|
# RUSTUP_HOME / CARGO_HOME / npm_config_cache / npm_config_store_dir /
|
||||||
|
# RUNNER_TOOL_CACHE / LOCALAPPDATA / APPDATA,加上 TEMP/TMP/TMPDIR、
|
||||||
|
# pnpm/action-setup 的 dest,以及 **USERPROFILE**。
|
||||||
|
# 最后这个是被 pnpm 逼出来的:它的 store 操作锁不在 store-dir 下面,也不看
|
||||||
|
# LOCALAPPDATA,而是按 <USERPROFILE>\AppData\Local\pnpm-store-operation-locks
|
||||||
|
# 算 —— 只把 LOCALAPPDATA 指走它照样去开原来的路径(报错原文见 run 8 的日志),
|
||||||
|
# 把 USERPROFILE 指走它才跟着走(本机实测:探针 profile 下立刻出现
|
||||||
|
# all-stores.lock)。
|
||||||
|
# 想从根上解决:让 gitea-runner 以**中完整性**启动(在普通终端里跑
|
||||||
|
# run-runner.ps1,而不是从被沙箱化的低完整性 shell 里拉起来),那样这些重定向
|
||||||
|
# 就都可以撤掉。这台机器上 runner 由计划任务 GiteaRunner-Windows(admin /
|
||||||
|
# Limited)拉起,但进程链上的完整性已经落到了 Low。
|
||||||
#
|
#
|
||||||
# ── 三个平台都显式写 --target,这不是啰嗦 ────────────────────────────────────
|
# ── 三个平台都显式写 --target,这不是啰嗦 ────────────────────────────────────
|
||||||
# desktop/.cargo/config.toml 里钉着 `target = "aarch64-apple-darwin"`(本机开发用;
|
# desktop/.cargo/config.toml 里钉着 `target = "aarch64-apple-darwin"`(本机开发用;
|
||||||
@@ -80,18 +133,18 @@ on:
|
|||||||
- ".github/workflows/build-client.yml"
|
- ".github/workflows/build-client.yml"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
# Linux 的 runner 有,默认勾上;另两个平台目前没有 runner,默认不勾 ——
|
# Linux 与 Windows 的 runner 都在线,默认勾上;macOS 还没有机器,默认不勾
|
||||||
# 勾了而没有对应 runner,那个 job 会一直排队。
|
# (勾了会一直排队)。开关为什么不用仓库变量见文件头第 4 条。
|
||||||
linux:
|
linux:
|
||||||
description: "构建 Linux(deb + AppImage)"
|
description: "构建 Linux(deb + AppImage)"
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: true
|
||||||
windows:
|
windows:
|
||||||
description: "构建 Windows(NSIS 安装包)——需要 Windows runner"
|
description: "构建 Windows(NSIS 安装包)"
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: true
|
||||||
macos:
|
macos:
|
||||||
description: "构建 macOS(通用 dmg)——需要 macOS runner"
|
description: "构建 macOS(通用 dmg)——还没有 macOS runner,勾了会排队"
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
|
||||||
@@ -148,7 +201,8 @@ jobs:
|
|||||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# pnpm 必须在 setup-node 之前:setup-node 的 cache: pnpm 要用它算 store 路径。
|
# pnpm 装在 setup-node 之前:这一步只靠 runner 镜像里自带的 node(实测
|
||||||
|
# /opt/hostedtoolcache 里有 24.x),而下面的 pnpm install 要用它。
|
||||||
- name: 装 pnpm
|
- name: 装 pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
@@ -158,13 +212,17 @@ jobs:
|
|||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: pnpm
|
|
||||||
cache-dependency-path: desktop/pnpm-lock.yaml
|
|
||||||
|
|
||||||
- name: 前端依赖(严格按 lock 装)
|
- name: 前端依赖(严格按 lock 装)
|
||||||
working-directory: desktop
|
working-directory: desktop
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
# 版本号与改动日志是从 git 历史算出来的(scripts/version.mjs)。这一步确认
|
||||||
|
# 仓库里的 version.json 没有被落下:有新提交却没重新生成时,装包里的版本会
|
||||||
|
# 悄悄停在旧值上,而那种错误要到发布之后才会被发现。
|
||||||
|
- name: 版本一致性(version.json 是否跟上了 git 历史)
|
||||||
|
run: node scripts/version.mjs --check
|
||||||
|
|
||||||
- name: 类型检查(tsc --noEmit)
|
- name: 类型检查(tsc --noEmit)
|
||||||
working-directory: desktop
|
working-directory: desktop
|
||||||
run: pnpm run check
|
run: pnpm run check
|
||||||
@@ -178,21 +236,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
targets: x86_64-unknown-linux-gnu
|
targets: x86_64-unknown-linux-gnu
|
||||||
|
|
||||||
# 只缓存 registry(crate 源码),不缓存 target/:自建 runner 的容器每次都是
|
|
||||||
# 干净的,几 GB 的 target 缓存拉下来的时间常常比重新编译还长,磁盘也扛不住。
|
|
||||||
# 缓存服务在不同 Gitea 版本上差异较大,所以失败不让它拖垮构建。
|
|
||||||
- name: cargo 缓存
|
|
||||||
uses: actions/cache@v4
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry/index
|
|
||||||
~/.cargo/registry/cache
|
|
||||||
~/.cargo/git/db
|
|
||||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('protocol/Cargo.lock', 'desktop/core/Cargo.lock', 'desktop/src-tauri/Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-cargo-registry-
|
|
||||||
|
|
||||||
# protocol 没有自己的 rust-toolchain.toml,会被根目录那个 `esp` 接住,
|
# protocol 没有自己的 rust-toolchain.toml,会被根目录那个 `esp` 接住,
|
||||||
# 所以这里必须 +stable 显式指定。
|
# 所以这里必须 +stable 显式指定。
|
||||||
- name: protocol 单元测试(15)
|
- name: protocol 单元测试(15)
|
||||||
@@ -272,25 +315,11 @@ jobs:
|
|||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: pnpm
|
|
||||||
cache-dependency-path: desktop/pnpm-lock.yaml
|
|
||||||
|
|
||||||
- name: 前端依赖(严格按 lock 装)
|
- name: 前端依赖(严格按 lock 装)
|
||||||
working-directory: desktop
|
working-directory: desktop
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: cargo 缓存
|
|
||||||
uses: actions/cache@v4
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry/index
|
|
||||||
~/.cargo/registry/cache
|
|
||||||
~/.cargo/git/db
|
|
||||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('protocol/Cargo.lock', 'desktop/core/Cargo.lock', 'desktop/src-tauri/Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-cargo-registry-
|
|
||||||
|
|
||||||
# 上面 apt 装了 ffmpeg,所以 recording.rs 里那几条"真的起 ffmpeg 并读回帧数"
|
# 上面 apt 装了 ffmpeg,所以 recording.rs 里那几条"真的起 ffmpeg 并读回帧数"
|
||||||
# 的测试不会走 has_ffmpeg() 的跳过分支,是真的在验。
|
# 的测试不会走 has_ffmpeg() 的跳过分支,是真的在验。
|
||||||
# 如果这台 runner 上它们长期不稳定,删掉这一步即可 —— 它们不在守门 job 里,
|
# 如果这台 runner 上它们长期不稳定,删掉这一步即可 —— 它们不在守门 job 里,
|
||||||
@@ -305,9 +334,9 @@ jobs:
|
|||||||
run: pnpm tauri build --target ${{ env.TARGET }} --bundles deb,appimage
|
run: pnpm tauri build --target ${{ env.TARGET }} --bundles deb,appimage
|
||||||
|
|
||||||
- name: 上传产物
|
- name: 上传产物
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: cam-desktop-linux-x64
|
name: rmic-2026-linux-x64
|
||||||
# 一个都没找到就报错:产物路径写错却"构建成功"是最难发现的一种失败。
|
# 一个都没找到就报错:产物路径写错却"构建成功"是最难发现的一种失败。
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
@@ -321,67 +350,145 @@ jobs:
|
|||||||
windows:
|
windows:
|
||||||
name: Windows x64(NSIS 安装包)
|
name: Windows x64(NSIS 安装包)
|
||||||
needs: checks
|
needs: checks
|
||||||
# 默认不跑:本仓库目前没有 Windows runner,跑了只会一直排队。有了机器之后
|
# 默认就跑:windows-runner-01 在线(自报标签 windows-latest)。
|
||||||
# 在仓库变量里加 ENABLE_WINDOWS=true 就会跟着每次 push 自动跑;手动触发时
|
# 开关用 inputs 而不是仓库变量的原因见文件头第 4 条 —— 这个坑正是它踩出来的。
|
||||||
# 勾 "构建 Windows" 也能单独跑一次。标签用 RUNNER_WINDOWS 指。
|
if: ${{ github.event.inputs.windows != 'false' }}
|
||||||
if: >-
|
# 这台机器上要预装:MSVC Build Tools(tauri 的 Rust 侧要 link.exe)、WebView2
|
||||||
${{
|
# 运行时(装了包的应用要它才能起窗口;打包本身不需要)与 rustup。
|
||||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.windows == 'true')
|
|
||||||
|| (github.event_name != 'workflow_dispatch' && vars.ENABLE_WINDOWS == 'true')
|
|
||||||
}}
|
|
||||||
# 自建 Windows runner 上要有 MSVC Build Tools(tauri 的 Rust 侧要 link.exe)、
|
|
||||||
# WebView2 运行时(装了包的应用要它才能起窗口;打包本身不需要)与 rustup。
|
|
||||||
runs-on: ${{ vars.RUNNER_WINDOWS || 'windows-latest' }}
|
runs-on: ${{ vars.RUNNER_WINDOWS || 'windows-latest' }}
|
||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
env:
|
env:
|
||||||
TARGET: x86_64-pc-windows-msvc
|
TARGET: x86_64-pc-windows-msvc
|
||||||
CARGO_BUILD_TARGET: x86_64-pc-windows-msvc
|
CARGO_BUILD_TARGET: x86_64-pc-windows-msvc
|
||||||
|
# 见文件头第 5 条:这台 runner 的 job 跑在**低完整性**(Low Mandatory Level,
|
||||||
|
# S-1-16-4096)上,而 Windows 不允许低完整性往中完整性写 —— 用户目录全中招:
|
||||||
|
# ~/.rustup、~/.cargo、%TEMP%、%LOCALAPPDATA% 下新建任何东西都是
|
||||||
|
# "Access denied (os error 5)",ACL 再宽松也没用(没有 Deny ACE,是完整性
|
||||||
|
# 级别拦的)。所以 CI 用到的每个"家目录"都挪到 runner 自己的工作目录下,
|
||||||
|
# 那几处在 job 内的写探测里实测可写。换一台 Windows runner 时改这几行。
|
||||||
|
RUSTUP_HOME: C:\Users\admin\runner\windows\cache\ci-rustup
|
||||||
|
CARGO_HOME: C:\Users\admin\runner\windows\cache\ci-cargo
|
||||||
|
# LOCALAPPDATA/APPDATA 也得改:pnpm 12 的 store 锁与 cache/state 都挂在
|
||||||
|
# %LOCALAPPDATA% 下(报错原文:pnpm-store-operation-locks\all-stores.lock:
|
||||||
|
# 拒绝访问),tauri 打包时下载的 NSIS 也放这儿。本机实测:把 LOCALAPPDATA
|
||||||
|
# 指到可写目录后,pnpm 会在那里建 pnpm-cache / pnpm-state,install 正常。
|
||||||
|
LOCALAPPDATA: C:\Users\admin\runner\windows\cache\ci-localappdata
|
||||||
|
APPDATA: C:\Users\admin\runner\windows\cache\ci-appdata
|
||||||
|
npm_config_cache: C:\Users\admin\runner\windows\cache\ci-npm
|
||||||
|
RUNNER_TOOL_CACHE: C:\Users\admin\runner\windows\cache\ci-toolcache
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# 宿主模式的 Windows runner 上,PATH 里的 `bash` 是 C:\Windows\System32\bash.exe
|
||||||
|
# —— WSL 的启动器,而 WSL 在 runner 账户下起不来,报 E_ACCESSDENIED(日志里是
|
||||||
|
# 一串 GBK 被当 UTF-8 解出来的乱码 + https://github.com/microsoft/WSL/issues)。
|
||||||
|
# 于是任何 `shell: bash` 的步骤、以及用 bash 写的 composite action
|
||||||
|
# (dtolnay/rust-toolchain 就是),都会在 0 秒内失败。
|
||||||
|
# Git for Windows 自带 bash,把它的 bin 前置到 PATH —— GITHUB_PATH 的语义就是
|
||||||
|
# **前置**插入,所以它会盖过 System32 那个 —— 后面这些步骤才跑得起来。
|
||||||
|
#
|
||||||
|
# 换掉 bash 之后还有第二个坑:这个 job 的环境里 TEMP/TMP 是空的,而 Git Bash 的
|
||||||
|
# /tmp 是从 TEMP 推出来的;没有 TEMP 它会退回 <Git 安装目录>\tmp
|
||||||
|
# (C:\Program Files\Git\tmp),那个目录普通权限写不进去,于是 dtolnay 里那句
|
||||||
|
# log=$(mktemp) 直接挂掉:
|
||||||
|
# mktemp: failed to create file via template '/tmp/tmp.XXXXXXXXXX': Permission denied
|
||||||
|
# 所以顺手给 job 指定一个自己建的可写目录(workspace 的同级 ci-tmp)。
|
||||||
|
# 两个坑都是 2026-10-15 真跑 Windows job 时踩出来的,本地按这样改完后
|
||||||
|
# mktemp、rustup toolchain install、rustup target add 全部正常。
|
||||||
|
- name: 让 bash 指向 Git for Windows,并给 job 一个可写的临时目录
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
if (-not $env:GITHUB_PATH) { throw '这个 runner 没提供 GITHUB_PATH,没法给后续步骤改 PATH' }
|
||||||
|
if (-not $env:GITHUB_ENV) { throw '这个 runner 没提供 GITHUB_ENV,没法给后续步骤加变量' }
|
||||||
|
if (-not $env:GITHUB_WORKSPACE) { throw '这个 runner 没提供 GITHUB_WORKSPACE' }
|
||||||
|
|
||||||
|
$gitBash = 'C:\Program Files\Git\bin'
|
||||||
|
if (-not (Test-Path (Join-Path $gitBash 'bash.exe'))) {
|
||||||
|
throw "找不到 $gitBash\bash.exe:这台机器上没装 Git for Windows,bash 会落到 WSL 上"
|
||||||
|
}
|
||||||
|
Add-Content -Path $env:GITHUB_PATH -Value $gitBash -Encoding ascii
|
||||||
|
|
||||||
|
$tmpRoot = Join-Path (Split-Path -Parent $env:GITHUB_WORKSPACE) 'ci-tmp'
|
||||||
|
New-Item -ItemType Directory -Force -Path $tmpRoot | Out-Null
|
||||||
|
Add-Content -Path $env:GITHUB_ENV -Value "TEMP=$tmpRoot" -Encoding ascii
|
||||||
|
Add-Content -Path $env:GITHUB_ENV -Value "TMP=$tmpRoot" -Encoding ascii
|
||||||
|
Add-Content -Path $env:GITHUB_ENV -Value "TMPDIR=$($tmpRoot -replace '\\', '/')" -Encoding ascii
|
||||||
|
|
||||||
|
# 第三个坑:pnpm 的 store 操作锁是按 <USERPROFILE>\AppData\Local\
|
||||||
|
# pnpm-store-operation-locks 算的 —— 实测只把 LOCALAPPDATA 指走它不跟,
|
||||||
|
# 只把 USERPROFILE 指走它就跟着走(换完 USERPROFILE 后探针目录里立刻出现
|
||||||
|
# all-stores.lock)。所以 USERPROFILE 也得指到能写的地方,并且先把
|
||||||
|
# AppData\Local、AppData\Roaming 建好。
|
||||||
|
$profileRoot = Join-Path (Split-Path -Parent $env:GITHUB_WORKSPACE) 'ci-profile'
|
||||||
|
New-Item -ItemType Directory -Force -Path (Join-Path $profileRoot 'AppData\Local'), (Join-Path $profileRoot 'AppData\Roaming') | Out-Null
|
||||||
|
Add-Content -Path $env:GITHUB_ENV -Value "USERPROFILE=$profileRoot" -Encoding ascii
|
||||||
|
|
||||||
|
"bash = $(Join-Path $gitBash 'bash.exe')"
|
||||||
|
"TEMP/TMP/TMPDIR = $tmpRoot"
|
||||||
|
"USERPROFILE = $profileRoot"
|
||||||
|
|
||||||
- name: 装 Rust(stable)
|
- name: 装 Rust(stable)
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
targets: x86_64-pc-windows-msvc
|
targets: x86_64-pc-windows-msvc
|
||||||
|
|
||||||
- name: 装 pnpm
|
|
||||||
uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
version: ${{ env.PNPM_VERSION }}
|
|
||||||
|
|
||||||
- name: 装 Node
|
- name: 装 Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: pnpm
|
|
||||||
cache-dependency-path: desktop/pnpm-lock.yaml
|
# 这里**不用** pnpm/action-setup:它装的是 standalone 的 pnpm 二进制,而那个
|
||||||
|
# 二进制在这台 runner 的低完整性沙箱里走到"把包装进 virtual store"时必崩:
|
||||||
|
# thread '<unknown>' (6716) has overflowed its stack
|
||||||
|
# corepack 拉下来的是同一个二进制(也崩),所以改成用 npm 装 JS 版 pnpm ——
|
||||||
|
# 它跑在 node 上,是另一条代码路径。装到 node 自己的目录里(RUNNER_TOOL_CACHE
|
||||||
|
# 已经指到可写位置,见 job env)。
|
||||||
|
- name: 装 pnpm(npm 装 JS 版,绕开 standalone 二进制)
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
# 前面几次尝试在 node 目录里留下过 pnpm/pnpx 的影子文件,npm 会以
|
||||||
|
# EEXIST 拒绝覆盖,先清掉(node 目录在 RUNNER_TOOL_CACHE 下,可写)。
|
||||||
|
$nodeDir = Split-Path (Get-Command node).Source
|
||||||
|
foreach ($n in 'pnpm', 'pnpx', 'pnpm.cmd', 'pnpx.cmd', 'pnpm.ps1', 'pnpx.ps1') {
|
||||||
|
Remove-Item (Join-Path $nodeDir $n) -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
npm install -g pnpm@12.8.1
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "npm 装 pnpm 失败(exit $LASTEXITCODE)" }
|
||||||
|
"pnpm = $((Get-Command pnpm).Source)"
|
||||||
|
pnpm --version
|
||||||
|
|
||||||
- name: 前端依赖(严格按 lock 装)
|
- name: 前端依赖(严格按 lock 装)
|
||||||
working-directory: desktop
|
working-directory: desktop
|
||||||
run: pnpm install --frozen-lockfile
|
shell: pwsh
|
||||||
|
# 这台 runner 的 job 跑在低完整性沙箱里(见文件头第 5 条),standalone 的
|
||||||
|
# pnpm 到"把包装进 virtual store"那一步会直接栈溢出:
|
||||||
|
# thread '<unknown>' (6716) has overflowed its stack
|
||||||
|
# (store 已经在可写目录、也改成 copy 导入了,仍然复现;同样的命令在普通
|
||||||
|
# Medium 会话里跑得好好的。)所以先试 standalone,崩了就退回 corepack:
|
||||||
|
# 让 node 去跑 pnpm 的 JS 实现,绕开那个二进制。
|
||||||
|
run: |
|
||||||
|
$pnpmArgs = @('install', '--frozen-lockfile',
|
||||||
|
'--store-dir', 'C:\Users\admin\runner\windows\cache\ps',
|
||||||
|
'--package-import-method=copy', '--reporter=append-only')
|
||||||
|
pnpm @pnpmArgs
|
||||||
|
if ($LASTEXITCODE -eq 0) { exit 0 }
|
||||||
|
"::warning::standalone pnpm 失败(exit $LASTEXITCODE),改用 corepack + node 跑 pnpm"
|
||||||
|
corepack enable
|
||||||
|
corepack prepare pnpm@12.8.1 --activate
|
||||||
|
pnpm @pnpmArgs
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
|
||||||
- name: cargo 缓存
|
# nsis 是纯安装包,不需要 WiX;tauri 会自己把 NSIS 下到用户缓存里(这个缓存
|
||||||
uses: actions/cache@v4
|
# 也跟着 LOCALAPPDATA 走,见上面 job env),所以这台 runner 要能访问 GitHub
|
||||||
continue-on-error: true
|
# (离线环境改成 --bundles msi 也一样要下 WiX)。
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry/index
|
|
||||||
~/.cargo/registry/cache
|
|
||||||
~/.cargo/git/db
|
|
||||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('protocol/Cargo.lock', 'desktop/core/Cargo.lock', 'desktop/src-tauri/Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-cargo-registry-
|
|
||||||
|
|
||||||
# nsis 是纯安装包,不需要 WiX;tauri 会自己把 NSIS 下到用户缓存里,所以
|
|
||||||
# 这台 runner 要能访问 GitHub(离线环境改成 --bundles msi 也一样要下 WiX)。
|
|
||||||
- name: 打包(tauri build)
|
- name: 打包(tauri build)
|
||||||
working-directory: desktop
|
working-directory: desktop
|
||||||
run: pnpm tauri build --target ${{ env.TARGET }} --bundles nsis
|
run: pnpm tauri build --target ${{ env.TARGET }} --bundles nsis
|
||||||
|
|
||||||
- name: 上传产物
|
- name: 上传产物
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: cam-desktop-windows-x64
|
name: rmic-2026-windows-x64
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
path: |
|
path: |
|
||||||
@@ -393,14 +500,10 @@ jobs:
|
|||||||
macos:
|
macos:
|
||||||
name: macOS 通用(.app + .dmg)
|
name: macOS 通用(.app + .dmg)
|
||||||
needs: checks
|
needs: checks
|
||||||
# 同 Windows:默认不跑,有机器之后设仓库变量 ENABLE_MACOS=true,标签用
|
# 还没有 macOS runner,所以只有"手动触发 + 勾上"才跑;等机器注册好,把这一行
|
||||||
# RUNNER_MACOS 指。
|
# 换成 `github.event.inputs.macos != 'false'` 就变成默认跑(同 Windows)。
|
||||||
if: >-
|
if: ${{ github.event.inputs.macos == 'true' }}
|
||||||
${{
|
# 那台机器上要有 Xcode Command Line Tools(链接要它)与 rustup。
|
||||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.macos == 'true')
|
|
||||||
|| (github.event_name != 'workflow_dispatch' && vars.ENABLE_MACOS == 'true')
|
|
||||||
}}
|
|
||||||
# 自建 macOS runner 上要有 Xcode Command Line Tools(链接要它)与 rustup。
|
|
||||||
runs-on: ${{ vars.RUNNER_MACOS || 'macos-latest' }}
|
runs-on: ${{ vars.RUNNER_MACOS || 'macos-latest' }}
|
||||||
timeout-minutes: 150
|
timeout-minutes: 150
|
||||||
env:
|
env:
|
||||||
@@ -426,25 +529,11 @@ jobs:
|
|||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: pnpm
|
|
||||||
cache-dependency-path: desktop/pnpm-lock.yaml
|
|
||||||
|
|
||||||
- name: 前端依赖(严格按 lock 装)
|
- name: 前端依赖(严格按 lock 装)
|
||||||
working-directory: desktop
|
working-directory: desktop
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: cargo 缓存
|
|
||||||
uses: actions/cache@v4
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry/index
|
|
||||||
~/.cargo/registry/cache
|
|
||||||
~/.cargo/git/db
|
|
||||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('protocol/Cargo.lock', 'desktop/core/Cargo.lock', 'desktop/src-tauri/Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-cargo-registry-
|
|
||||||
|
|
||||||
# 只想要当前架构、想省一半编译时间的话,把 --target 换成 aarch64-apple-darwin
|
# 只想要当前架构、想省一半编译时间的话,把 --target 换成 aarch64-apple-darwin
|
||||||
# (Apple Silicon 的 runner)并去掉上面 targets 里的 x86_64 即可。
|
# (Apple Silicon 的 runner)并去掉上面 targets 里的 x86_64 即可。
|
||||||
- name: 打包(tauri build)
|
- name: 打包(tauri build)
|
||||||
@@ -454,9 +543,9 @@ jobs:
|
|||||||
# 第二行是兜底:不同 tauri-cli 版本给通用包放的目录略有差异,宁可多写一行,
|
# 第二行是兜底:不同 tauri-cli 版本给通用包放的目录略有差异,宁可多写一行,
|
||||||
# 也不要上传时静默少东西。(注意 path 是块标量,里面写 # 是路径不是注释。)
|
# 也不要上传时静默少东西。(注意 path 是块标量,里面写 # 是路径不是注释。)
|
||||||
- name: 上传产物
|
- name: 上传产物
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: cam-desktop-macos-universal
|
name: rmic-2026-macos-universal
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
+10
@@ -12,6 +12,13 @@
|
|||||||
/target/
|
/target/
|
||||||
/protocol/target/
|
/protocol/target/
|
||||||
/armor/target/
|
/armor/target/
|
||||||
|
/.embuild/
|
||||||
|
/managed_components/
|
||||||
|
/sdkconfig
|
||||||
|
/sdkconfig.old
|
||||||
|
/sdkconfig.*
|
||||||
|
!/sdkconfig.defaults
|
||||||
|
!/sdkconfig.defaults.*
|
||||||
|
|
||||||
# 本机专属的 cargo 注册表缓存。
|
# 本机专属的 cargo 注册表缓存。
|
||||||
#
|
#
|
||||||
@@ -29,6 +36,9 @@
|
|||||||
# macOS 的目录元数据。不锚定,子目录里的也一起忽略。
|
# macOS 的目录元数据。不锚定,子目录里的也一起忽略。
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Python diagnostic helpers.
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
# `docs/simba_docs`:工作区里另起的一个 Astro 文档站,与本仓库的目录结构无关,
|
# `docs/simba_docs`:工作区里另起的一个 Astro 文档站,与本仓库的目录结构无关,
|
||||||
# 也不属于本项目的产物。留着不跟踪,免得以后 `git add -A` 顺手把它带进来。
|
# 也不属于本项目的产物。留着不跟踪,免得以后 `git add -A` 顺手把它带进来。
|
||||||
/docs/
|
/docs/
|
||||||
|
|||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"major": 0,
|
||||||
|
"minor": 2,
|
||||||
|
"patch": 0,
|
||||||
|
"build": 34,
|
||||||
|
"protocol": 1,
|
||||||
|
"date": "2026-10-19",
|
||||||
|
"baseline": "v0.1.0",
|
||||||
|
"commits": 1,
|
||||||
|
"groups": {
|
||||||
|
"feat": [
|
||||||
|
{
|
||||||
|
"scope": "client",
|
||||||
|
"text": "RMIC::2026 界面重做、可计算的版本机制与固件版本联动",
|
||||||
|
"breaking": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "client",
|
||||||
|
"text": "界面换成墨黑蓝底 + 单支红的矩形语言,切角全部去掉,仅装甲条保留平行四边形",
|
||||||
|
"breaking": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "client",
|
||||||
|
"text": "自托管 HarmonyOS Sans SC 与 JetBrains Mono,中文与拉丁数字逐字回退",
|
||||||
|
"breaking": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "client",
|
||||||
|
"text": "新增启动序列、首启门、用户端 HUD、裁判端控制台、设置抽屉与固件维护页",
|
||||||
|
"breaking": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "client",
|
||||||
|
"text": "裁判端中缝与下方控制台可拖,鼠标落在哪一路下方就看哪一路",
|
||||||
|
"breaking": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "client",
|
||||||
|
"text": "设置抽屉常驻实时预览,与画面共用同一份准星绘制代码",
|
||||||
|
"breaking": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "client",
|
||||||
|
"text": "动效改用 Material 3 的缓动曲线与时长,逐条替换掉自造的贝塞尔",
|
||||||
|
"breaking": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "version",
|
||||||
|
"text": "版本号与 CHANGELOG 由 scripts/version.mjs 从 git 历史算出,不再手写",
|
||||||
|
"breaking": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "firmware",
|
||||||
|
"text": "Hello 报文携带语义化版本、构建号与自报协议号,负载向后兼容",
|
||||||
|
"breaking": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "client",
|
||||||
|
"text": "固件版本显示在绑定列表与裁判端每一格;协议不匹配时给出具体原因",
|
||||||
|
"breaking": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "firmware",
|
||||||
|
"text": "固件包版本与客户端同源,都从 version.json 读",
|
||||||
|
"breaking": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fix": [
|
||||||
|
{
|
||||||
|
"scope": "client",
|
||||||
|
"text": "getCurrentWindow() 在没有 Tauri 注入时同步抛异常,浏览器预览整棵树被卸载",
|
||||||
|
"breaking": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "client",
|
||||||
|
"text": "启动序列的淡出 effect 清掉了自己刚设的 setTimeout,界面永远停在启动页",
|
||||||
|
"breaking": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "client",
|
||||||
|
"text": "设置抽屉左侧导航的锚点没挂到 section 上,且用错了坐标系",
|
||||||
|
"breaking": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "protocol",
|
||||||
|
"text": "Hello 改用宽松帧头解析,让协议不匹配的板子能被解释而不是静默消失",
|
||||||
|
"breaking": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "build",
|
||||||
|
"text": "Windows 上 bindgen 加载不了 libclang 的同目录依赖,构建脚本补上搜索路径",
|
||||||
|
"breaking": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "build",
|
||||||
|
"text": "esp-idf-sys 在长路径下拒绝构建,固件构建改用短的目标目录",
|
||||||
|
"breaking": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": [
|
||||||
|
{
|
||||||
|
"scope": "client",
|
||||||
|
"text": "队标图标,含光学尺寸分级与手工拼的多尺寸 ICO",
|
||||||
|
"breaking": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "ci",
|
||||||
|
"text": "守门 job 增加版本一致性检查,产物名改为 rmic-2026-*",
|
||||||
|
"breaking": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"docs": [
|
||||||
|
{
|
||||||
|
"scope": null,
|
||||||
|
"text": "补充界面语言、版本机制与协议通告布局的说明",
|
||||||
|
"breaking": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# 改动记录
|
||||||
|
|
||||||
|
> 本文件由 `scripts/version.mjs` 依据 git 历史生成,**请勿手改**。
|
||||||
|
> 提交信息遵循 Conventional Commits,一条一个条目;要求见该脚本的文件头。
|
||||||
|
> 建立这套机制之前的人工整理记录保存在 [`docs/CHANGELOG-legacy.md`](docs/CHANGELOG-legacy.md)。
|
||||||
|
|
||||||
|
## v0.2.0 — 2026-10-19
|
||||||
|
|
||||||
|
构建号 **34** · 线路协议 **1** · 1 个提交(v0.1.0 → v0.2.0)
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
|
||||||
|
- **client** · RMIC::2026 界面重做、可计算的版本机制与固件版本联动
|
||||||
|
- **client** · 界面换成墨黑蓝底 + 单支红的矩形语言,切角全部去掉,仅装甲条保留平行四边形
|
||||||
|
- **client** · 自托管 HarmonyOS Sans SC 与 JetBrains Mono,中文与拉丁数字逐字回退
|
||||||
|
- **client** · 新增启动序列、首启门、用户端 HUD、裁判端控制台、设置抽屉与固件维护页
|
||||||
|
- **client** · 裁判端中缝与下方控制台可拖,鼠标落在哪一路下方就看哪一路
|
||||||
|
- **client** · 设置抽屉常驻实时预览,与画面共用同一份准星绘制代码
|
||||||
|
- **client** · 动效改用 Material 3 的缓动曲线与时长,逐条替换掉自造的贝塞尔
|
||||||
|
- **version** · 版本号与 CHANGELOG 由 scripts/version.mjs 从 git 历史算出,不再手写
|
||||||
|
- **firmware** · Hello 报文携带语义化版本、构建号与自报协议号,负载向后兼容
|
||||||
|
- **client** · 固件版本显示在绑定列表与裁判端每一格;协议不匹配时给出具体原因
|
||||||
|
- **firmware** · 固件包版本与客户端同源,都从 version.json 读
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
- **client** · getCurrentWindow() 在没有 Tauri 注入时同步抛异常,浏览器预览整棵树被卸载
|
||||||
|
- **client** · 启动序列的淡出 effect 清掉了自己刚设的 setTimeout,界面永远停在启动页
|
||||||
|
- **client** · 设置抽屉左侧导航的锚点没挂到 section 上,且用错了坐标系
|
||||||
|
- **protocol** · Hello 改用宽松帧头解析,让协议不匹配的板子能被解释而不是静默消失
|
||||||
|
- **build** · Windows 上 bindgen 加载不了 libclang 的同目录依赖,构建脚本补上搜索路径
|
||||||
|
- **build** · esp-idf-sys 在长路径下拒绝构建,固件构建改用短的目标目录
|
||||||
|
|
||||||
|
### 构建
|
||||||
|
|
||||||
|
- **client** · 队标图标,含光学尺寸分级与手工拼的多尺寸 ICO
|
||||||
|
- **ci** · 守门 job 增加版本一致性检查,产物名改为 rmic-2026-*
|
||||||
|
|
||||||
|
### 文档
|
||||||
|
|
||||||
|
- 补充界面语言、版本机制与协议通告布局的说明
|
||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
# 开发与验证
|
||||||
|
|
||||||
|
编译入口见 [README.md](README.md),硬件限制见 [HARDWARE.md](HARDWARE.md),历史变化见 [CHANGELOG.md](CHANGELOG.md)。
|
||||||
|
|
||||||
|
## 镜像导出
|
||||||
|
|
||||||
|
以下使用 espflash 4.4.0 与固定的 `esp-1.97.0.0` 工具链。完成构建后,使用 `esp-idf-sys` 复制到 `release/` 的 Bootloader。Windows PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$targetDir = if ($env:CARGO_TARGET_DIR) { $env:CARGO_TARGET_DIR } else { 'target' }
|
||||||
|
New-Item -ItemType Directory -Force target/firmware | Out-Null
|
||||||
|
espflash save-image --chip esp32s3 --flash-size 16mb --flash-mode dio --flash-freq 40mhz --partition-table partitions.csv --bootloader "$targetDir/xtensa-esp32s3-espidf/release/bootloader.bin" --merge --skip-padding "$targetDir/xtensa-esp32s3-espidf/release/esp32" target/firmware/esp32s3-merged.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
Linux / macOS(Bash):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
target_dir="${CARGO_TARGET_DIR:-target}"
|
||||||
|
mkdir -p target/firmware
|
||||||
|
espflash save-image --chip esp32s3 --flash-size 16mb --flash-mode dio --flash-freq 40mhz --partition-table partitions.csv --bootloader "$target_dir/xtensa-esp32s3-espidf/release/bootloader.bin" --merge --skip-padding "$target_dir/xtensa-esp32s3-espidf/release/esp32" target/firmware/esp32s3-merged.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
合并镜像包含 Bootloader、分区表、App 和必要地址填充,`--skip-padding` 避免将文件填满 16 MB。镜像从 `0x0` 写入:
|
||||||
|
|
||||||
|
```text
|
||||||
|
espflash write-bin --chip esp32s3 --port COM5 0x0 target/firmware/esp32s3-merged.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
去掉导出命令的 `--merge --skip-padding` 可生成独立 App,它需写入 `partitions.csv` 声明的 Factory 偏移,并与板上 Bootloader 和分区表匹配。坏板重编可能改变该偏移;新板优先使用 ELF 或合并镜像。
|
||||||
|
|
||||||
|
## 主机测试
|
||||||
|
|
||||||
|
协议层不依赖 ESP-IDF。进入 `protocol/`,以 Windows MSVC 为例:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cargo +stable test --locked --target x86_64-pc-windows-msvc --config 'unstable.build-std=[]'
|
||||||
|
```
|
||||||
|
|
||||||
|
Linux 常见 host 为 `x86_64-unknown-linux-gnu`,Apple Silicon 为 `aarch64-apple-darwin`,可用 `rustc +stable -vV` 查看。`--target` 覆盖仓库里的其他平台配置,空 `build-std` 避免主机测试继承嵌入式标准库重编译。
|
||||||
|
|
||||||
|
装甲规则从 `armor/` 执行 `cargo +stable test -p armor-protocol`,桌面核心从 `desktop/core/` 执行 `cargo +stable test`,同样按需指定宿主 `--target` 和空 `build-std`。
|
||||||
|
|
||||||
|
限帧、温度回差和 JPEG 压缩回压策略可以独立在主机运行,无需连接板卡。Windows 在根目录执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
New-Item -ItemType Directory -Force target | Out-Null
|
||||||
|
rustc +stable --edition 2021 --test src/stream_control.rs -o target/stream-control-tests.exe
|
||||||
|
.\target\stream-control-tests.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
实际持续运行的串口与客户端通道记录方法见 [STABILITY.md](STABILITY.md)。主机策略测试不等于已经触发板上过温减载。
|
||||||
|
|
||||||
|
## 清理构建缓存
|
||||||
|
|
||||||
|
改动 C 组件或 `sdkconfig.defaults` 后,增量构建若没有反映变化,可从根目录执行:
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo clean --release --target xtensa-esp32s3-espidf -p esp-idf-sys
|
||||||
|
```
|
||||||
|
|
||||||
|
随后用构建脚本重编,确认 CMake / Ninja 重新编译相应组件。仍有缓存问题时使用 `cargo clean` 完整清理。不要删除 `components/esp32-camera/target/`。
|
||||||
|
|
||||||
|
构建脚本设置 `CARGO_WORKSPACE_DIR`,并通过 `ESP_IDF_GLOB_BASE` / `ESP_IDF_GLOB_PARTITIONS` 将分区表复制到生成的 CMake 工程,因此支持外部 `CARGO_TARGET_DIR` 和不同 Cargo 输出层级。Windows 使用短目录,如 `C:\s3vt`;设置 `ESP_IDF_PATH_ISSUES=ignore` 或 `subst` 不能消除底层路径限制。
|
||||||
|
|
||||||
|
## 发现与协议
|
||||||
|
|
||||||
|
- 图传板每 2 秒广播 Hello;客户端每 800 ms 广播 Subscribe,并向已发现的在线板卡 IP 单播续订,固定绑定 UDP 5600。
|
||||||
|
- 全局广播可能被 VPN / 虚拟网卡路由到其它接口。收到 Hello 后按设备 IP 定向续订,可以使用通往实际局域网的路由;队伍端仅向绑定的板卡增加单播续订。
|
||||||
|
- 固件使用订阅包的源地址作为发送目标,6 秒不续订即移除,最多保留 4 个对端。
|
||||||
|
- 新客户端为每个接收器生成随机会话 ID;同一源 IP 下,同一会话换端口会更新推流目标。旧版公共固定 ID `0x0C110001` 保持按地址区分,避免合并同机旧客户端。
|
||||||
|
- MAC 派生的 `sender_id` 不随 DHCP 地址变化,适合客户端持久绑定。
|
||||||
|
- Hello 和视频都是设备在线证据;客户端列表清理与固件订阅使用不同超时。
|
||||||
|
- 数据报为 27 字节头 + 最多 1400 字节载荷,采用小端编码;字段及拒收条件见 [protocol/README.md](protocol/README.md)。
|
||||||
|
|
||||||
|
缺失分片不重传,不完整帧整体丢弃。JPEG 由传感器编码,桌面后端转交原始 JPEG,由 WebView 解码,避免接收线程进行耗时编解码。
|
||||||
|
|
||||||
|
## 性能测量
|
||||||
|
|
||||||
|
本次在保留 TUN 环境中的对照测试、最终参数与验证边界见 [PERFORMANCE.md](PERFORMANCE.md)。
|
||||||
|
|
||||||
|
下表为原 README 保存的**历史单次测试数据**,不是本次构建实测或性能保证。配置为 OV5640、VGA;原始日志、时长和完整网络条件未随这些数字保存。
|
||||||
|
|
||||||
|
| 指标 | 早期 HTTP MJPEG / TCP | 后续 UDP |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 传感器产出 | 约 22 fps | 约 22 fps |
|
||||||
|
| 客户端收到 | 约 1.9 fps | 约 22 fps |
|
||||||
|
| 吞吐 | 约 0.28 Mbit/s | 约 1.6 Mbit/s |
|
||||||
|
| 丢帧 | 未记录 | 当次报告 0% |
|
||||||
|
|
||||||
|
这些数据不足以单独证明性能差异的根因或无线链路容量。复测时记录板型、固件版本、分辨率、画质、信号、订阅者数量、时长和丢帧数。
|
||||||
|
|
||||||
|
FPS 应按**同一时间窗口的帧数增量 / 窗口秒数**计算。累计帧数与窗口时长混用会产生假数值,UI 轮询间隔也不代表相机出帧间隔。探测脚本的重组状态需按设备及帧隔离,避免测量错误。
|
||||||
|
|
||||||
|
可重复测量:`python scripts/perf_probe.py <板卡IP> --seconds 30 --json perf.json`。默认使用独立的临时端口和 1 MiB 接收缓冲,每 800 ms 定向续订。脚本只依赖 Python 标准库;`--save-frame frame.jpg` 可保存首张完整 JPEG。公平比较应保持订阅者数量、分辨率、画质与网络条件一致。
|
||||||
|
|
||||||
|
`frame_loss_percent` 包含不完整、超时及帧号缺口,不包含开始订阅前或测量结束后的帧。`assembly_p95_ms` 是首末分片到达的时间差,`inter_frame_p95_ms` 是完整帧到达间隔,均使用高精度计时;没有发送时间戳及两端时钟同步,不能称为端到端延迟。
|
||||||
|
|
||||||
|
固件日志将采集尝试与完整提交分开计数。`submitted` 只表示全部分片交给了网络,不代表无线确认或客户端显示;`send failures` 可能包含随后重试成功的调用,应结合 `peer frames dropped` 和接收端指标判断。
|
||||||
|
|
||||||
|
画面链路测量使用裁判端 `--latency-report <新文件>`,让相机拍摄校准页左侧标记,固定相机和窗口位置。标记经正常采集、网络与绘制路径回到客户端,源与回显共用主机单调时钟;不需要 ESP 时钟同步。测试期间只保留一个原生客户端,额外的本地 WebSocket 观察器不增加 UDP 订阅。方法、刷新估计边界及最新结果见 [LATENCY.md](LATENCY.md)。
|
||||||
|
|
||||||
|
按记录文件的半开行号区间复算延迟:
|
||||||
|
|
||||||
|
```text
|
||||||
|
python scripts/analyze_latency.py --report latency-run.ndjson --range fb3-range.json --range fb2-range.json --out latency-result.json
|
||||||
|
```
|
||||||
|
|
||||||
|
区间文件至少包含 `start_row`、`end_row` 和 `fb_count`,行号从 0 开始;报告关闭后再分析,避免读取写入中的半行。分析器拒绝重复源标记、无效时间边界与空样本,输出 P50/P95/P99、rAF 区间宽度和解码完成到绘制的耗时。JPEG 接收间隔仍是独立指标。
|
||||||
|
|
||||||
|
恢复策略是独立纯 Rust 模块,可在主机运行:
|
||||||
|
|
||||||
|
```text
|
||||||
|
rustc +stable --test src/link_control.rs -o link-control-tests
|
||||||
|
./link-control-tests
|
||||||
|
```
|
||||||
|
|
||||||
|
Windows 为输出文件加 `.exe`。实机故障注入仅在诊断构建设置 `WIFI_TEST_DROP_AFTER_S`:主循环运行相应秒数后断开一次并保持 5 秒,正式构建保持 `0`。重连后需观察重新取址、socket 重建和订阅恢复,不能仅凭 connect 调用成功判定恢复视频。
|
||||||
|
|
||||||
|
## 板上验证
|
||||||
|
|
||||||
|
阻塞与缓冲交接测试:`python scripts/test-latest-frame.py` 使用主机 clang/gcc 编译真实 `cam_take_latest` 函数及队列替身,验证持续生产下有限取帧、旧缓冲仅归还一次、无新帧保留有效旧帧,以及首次超时不泄漏。`rustc +stable --test src/stream_control.rs` 验证发送期限、共享重试预算和零重试模式;`desktop/` 的 `pnpm test:stream` 验证 WebView 积压、解码阻塞及过期帧清理。实机验证范围另见 [REALTIME.md](REALTIME.md)。
|
||||||
|
|
||||||
|
1. 相机识别、PSRAM 初始化及稳定采集。
|
||||||
|
2. Wi-Fi 完成关联和 DHCP,得到有效 IP。
|
||||||
|
3. 客户端启动后订阅者增加并持续显示画面。
|
||||||
|
4. `python scripts/udp_probe.py 10` 独立核对协议及帧统计。
|
||||||
|
5. 分别验证多端订阅、断网重连、不同分辨率与持续运行。
|
||||||
|
|
||||||
|
编译、镜像生成与主机测试通过均不等于烧录、板上运行或硬件稳定性已经验证。
|
||||||
@@ -0,0 +1,464 @@
|
|||||||
|
# 烧录 / 维护客户端架构设计(ESP32-S3 图传板)
|
||||||
|
|
||||||
|
> 状态:**提议中(Proposed)**,等待第 9 节的问题清单答复后冻结。
|
||||||
|
> 输入约束:现场存在 SPI Flash 坏块;需要兼顾 OTA 与现场排障;**不改固件源码、不重新编译**;
|
||||||
|
> 优先“模板固件 + 动态 patch”。
|
||||||
|
> 本文所有结论都在仓库里逐条对过代码/脚本/分区表/文档,引用位置见各节链接。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 结论摘要(先看这五条)
|
||||||
|
|
||||||
|
1. **“不改固件源码”可以做到;但“真 OTA”做不到。** 当前固件没有 `ota_0/ota_1/otadata` 分区,
|
||||||
|
没有 `esp_ota` 调用,UDP 协议里也没有任何命令通道(只有 Hello/Subscribe/视频/血量)。
|
||||||
|
设备自己没有能力接收固件。所谓“兼顾 OTA”,在现约束下只能是**现场 USB 串口更新**,
|
||||||
|
或者先落一个 OTA-就绪的分区表(只改二进制,不改源码)作为未来选项。**这是必须先决的问题**。
|
||||||
|
2. **坏块规避其实根本不需要重编译。** `scripts/flash.sh` 之所以要重编,是因为它把新偏移写回
|
||||||
|
`partitions.csv`(构建输入)再触发重建;而 **App 镜像不包含自己的分区偏移**,偏移只存在于
|
||||||
|
`0x8000` 的 `partition-table.bin`(0xC00 字节 + 末尾 16 字节 MD5)。
|
||||||
|
→ 对同一份已构建镜像,**搜索偏移 + 重写分区表 + 原样写 App**,全程零编译。
|
||||||
|
这是本方案最大的收益点,也是“模板 + patch”的第一块拼图。
|
||||||
|
3. **SSID/密码不是 NVS,是 `.rodata` 里的字符串字面量**(`env!` 编译期注入)。
|
||||||
|
二进制里能直接 grep 到明文。所以“动态改 SSID”= 字节级 patch **App 镜像**,
|
||||||
|
且必须连带处理**字符串长度常量**与**镜像 checksum/SHA256**。
|
||||||
|
长度常量能否可靠定位是这个方案真正的技术风险(见 7.2)。
|
||||||
|
4. **坏块信息不能“读”出来,只能“写进去再读回来”差分出来。** `flash_id` 只给芯片型号与容量,
|
||||||
|
SFDP 只给参数,启动日志只给症状。仓库里 `0x10863` 的 bit6 挂在 1,就是整段回读比对发现的
|
||||||
|
(HARDWARE.md)。因此客户端必须自带「写入图案 → 回读 → 差分」与「写后整段校验差分」两条链路。
|
||||||
|
5. **推荐形态:纯逻辑核心库 `flash-core` + 优先 CLI + 桌面端加一页**。
|
||||||
|
把最危险的逻辑(镜像格式、分区表、偏移搜索、补丁)做成不碰 I/O 的纯函数,
|
||||||
|
在主机上用**真实 1.1 MB 镜像做往返测试**——这与仓库已有的 `protocol/`、`stream_control`、
|
||||||
|
`link_control`「能在主机跑测试的策略代码」风格一致。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 现状梳理(对着代码说)
|
||||||
|
|
||||||
|
### 1.1 工程与构建/烧录链
|
||||||
|
|
||||||
|
| 路径 | 角色 | 与本次需求的关系 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| [src/](src/) | S3 图传固件(根 Cargo 包,xtensa) | 烧录对象 |
|
||||||
|
| [partitions.csv](partitions.csv) | 自定义分区表源文件 | 坏字节规避的**构建输入**(现状),本方案改为产物 |
|
||||||
|
| [scripts/build.ps1](scripts/build.ps1) / [scripts/build.sh](scripts/build.sh) | 读 `.env` → `cargo build` | 现场不想跑 |
|
||||||
|
| [scripts/flash.sh](scripts/flash.sh) | build → 搜偏移 → 改 csv → **重建** → 校验 → esptool 写 | 只支持 macOS,必须重编 |
|
||||||
|
| [desktop/](desktop/) | Tauri v2 + React 客户端(视频/血量/录制) | **完全没有串口/烧录能力**(全仓库 `ssid|serial|espflash` 只命中 serde) |
|
||||||
|
| [target/tools/espflash/espflash.exe](target/tools/espflash/espflash.exe) | 已落地的 espflash 4.4.0 | 现成的协议实现,可直接当库或 sidecar |
|
||||||
|
| [target/firmware/](target/firmware/) | 已导出的 bootloader / partition-table / app / merged | 真实样本,用于离线验收测试 |
|
||||||
|
|
||||||
|
产物三元组(README「编译固件」表)是 **bootloader + partition-table + app**,
|
||||||
|
今天现场要么用 ELF(`espflash flash ... --partition-table partitions.csv`),
|
||||||
|
要么用 merged(从 `0x0` 全片写)。这两条路都**没有**坏字节规避能力。
|
||||||
|
|
||||||
|
### 1.2 固件侧:配置是编译期的,运行期没有任何入口
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/main.rs:36-37
|
||||||
|
const SSID: &str = env!("WIFI_SSID");
|
||||||
|
const PASSWORD: &str = env!("WIFI_PASS");
|
||||||
|
```
|
||||||
|
|
||||||
|
- 每次启动都会用编译期值覆盖驱动配置:`wifi.set_configuration(&client_config(auth)?)`
|
||||||
|
([src/main.rs:502](src/main.rs#L502))。所以 **NVS 里即使存着旧配置也不会被采用**。
|
||||||
|
- `EspDefaultNvsPartition` 只是被交给 `EspWifi::new`([src/main.rs:178](src/main.rs#L178)),
|
||||||
|
用于驱动自身的存储,不参与凭据解析。
|
||||||
|
- 对比:[armor/armor-firmware/src/settings.rs](armor/armor-firmware/src/settings.rs) 的 C3 装甲板
|
||||||
|
**有** NVS 设置层(队伍/颜色)——说明“运行期配置”在本仓库是被认可的,只是 S3 固件没做。
|
||||||
|
- 构建脚本强制要求凭据([scripts/build.ps1:40-43](scripts/build.ps1#L40-L43)),
|
||||||
|
`.env` 里是 `WIFI_SSID='SR-602-2G4'`(10 字节)、`WIFI_PASS=12345678`(8 字节)。
|
||||||
|
- 已确认:该字面量就在构建产物里(对 [target/firmware/esp32s3-app.bin](target/firmware/esp32s3-app.bin)
|
||||||
|
检索 `SR-602-2G4` 有命中)。
|
||||||
|
|
||||||
|
### 1.3 OTA:三条路全断
|
||||||
|
|
||||||
|
| OTA 必要条件 | 现状 | 结论 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `ota_0/ota_1/otadata` 分区 | 只有 `factory`([partitions.csv:16-18](partitions.csv#L16-L18)) | 可用**重写分区表**补上(不改源码) |
|
||||||
|
| 固件内有 `esp_ota_*` 接收端 | 无(全仓库无 `esp_ota`) | **必须改源码**,与本约束冲突 |
|
||||||
|
| 设备可达的控制/数据传输通道 | UDP 只有 Hello/Subscribe/视频/血量([protocol/README.md](protocol/README.md) 无线格式里无命令字) | **必须改源码** |
|
||||||
|
|
||||||
|
→ 结论:**“禁止重新编译” ⇒ 真 OTA 不可行**。这不是实现难度问题,是固件里没有任何代码能把
|
||||||
|
新镜像写进 flash。
|
||||||
|
|
||||||
|
### 1.4 坏 Flash 的真实现状(两个模组,两种缺陷)
|
||||||
|
|
||||||
|
| 模组 | 证据 | 缺陷 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 健康板(MAC `90:70:69:f8:f2:14`) | [budget40-verify-all.log](target/camera-diagnostics/budget40-verify-all.log):App @ `0x10000`,`verify OK` | 无 |
|
||||||
|
| 故障板(开发板) | [HARDWARE.md:59-67](HARDWARE.md#L59-L67) | `0xFADE7` bit5–7 不可写(经验判据:`data[addr] & 0xE0 == 0`);`0x10863` bit6 读回为 1(期望 `0x2e` 实读 `0x6e`) |
|
||||||
|
|
||||||
|
关键推论(对架构有决定性影响):
|
||||||
|
|
||||||
|
- `0x10863` 位于**未分配空洞** `0x10000–0x70000`(`phy_init` 结束到 `factory` 开始之间,
|
||||||
|
380 KB 全是空闲 flash)。→ 这块空洞就是天然的**坏块探测试验场**,写坏也不要紧。
|
||||||
|
- `0xFADE7` 落在 App 镜像的**代码段**内(镜像布局见 1.5),所以“把那个字节改成别的安全值”
|
||||||
|
会改坏指令;**唯一安全的动作是挪分区**。
|
||||||
|
- 现行偏移已经漂过至少三次:`0x10000` → `0x50000`([LATENCY.md:88](LATENCY.md#L88))→ `0x70000`
|
||||||
|
(当前 [partitions.csv](partitions.csv))。每次重编都要重新核对,这正是要消灭的脆弱点。
|
||||||
|
- 缺陷有两种**方向相反**的形态:卡在 0(写不进 1)与卡在 1(写不进 0)。后者在“擦除后读回 0xFF”
|
||||||
|
的检查里是**隐形**的,必须写 0 才暴露。探测逻辑必须两种都打图案。
|
||||||
|
|
||||||
|
### 1.5 已实测出来的镜像布局(`fps-final-raw-serial.log` 逐段对齐算出)
|
||||||
|
|
||||||
|
```
|
||||||
|
0x000000 esp_image_header_t (24B) magic E9, segment_count, entry_addr, hash_appended
|
||||||
|
0x000018 segment 0 header (8B) → load 0x3c0d0020 ← DROM / .rodata(SSID 字面量在这里)
|
||||||
|
0x000020 segment 0 data (0x2a2a8) 开头 256B 是 esp_app_desc_t(version/project_name/idf_ver…)
|
||||||
|
0x02a2d0 segment 1 (0x05d48) → 0x3fc9e500 DRAM
|
||||||
|
0x030020 segment 2 (0xc60c0) → 0x42000020 IROM 代码(0xFADE7 落在这里)
|
||||||
|
0x0f60e8 segment 3 (0x021c0)
|
||||||
|
0x0f82b0 segment 4 (0x0163f8)
|
||||||
|
0x10e6b0 segment 5 (0x00020)
|
||||||
|
→ 段尾 16B 对齐填充 + 1B checksum(XOR, 种子 0xEF) + 16B 对齐 + 32B SHA256
|
||||||
|
0x10e700 = 1,107,712 B(与 budget40-verify-all.log 的 0x10e700 一致)
|
||||||
|
```
|
||||||
|
|
||||||
|
几个直接可用的副产品:
|
||||||
|
|
||||||
|
- `esp_app_desc_t` 在 **App 分区 + 0x20**,内含 `version` / `project_name` / `idf_ver` / 编译时间。
|
||||||
|
**不需要让板子跑起来**,读 256 字节就能回答“这块板上到底是哪版固件”。
|
||||||
|
- 分区表就是 0x8000 起的 0xC00 字节(`verify 0xc00 @ 0x8000` 佐证),末尾 16 字节 MD5。
|
||||||
|
改写偏移只要重算这 16 字节。
|
||||||
|
|
||||||
|
### 1.6 需求 vs 现状 gap
|
||||||
|
|
||||||
|
| 需求 | 现状 | Gap |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 插入即自动识别 | 无 | 串口枚举 + ROM 探测 + 读镜像描述符 |
|
||||||
|
| 选文件烧录 | 只能用脚本 + 手敲端口 | 文件分类(bundle / merged / app / ELF)+ 计划预览 |
|
||||||
|
| 自动跳过坏块 | 半自动:改 csv + 重编 + 手工核对 | 偏移搜索 + 分区表重写 + 写后差分 |
|
||||||
|
| 动态改 SSID | 改 `.env` + 重编 + 重烧 | 镜像内字面量 patch(含长度常量与哈希) |
|
||||||
|
| OTA | 无 | **不可行**(见 1.3) |
|
||||||
|
| 现场排障 | 串口日志靠人看 | 结构化阈值断言 + 回读差分 + 设备档案/回滚 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 三个方案对比
|
||||||
|
|
||||||
|
### 方案 A:桌面端内嵌烧录模块(Tauri 单进程)
|
||||||
|
|
||||||
|
在 [desktop/](desktop/) 里加 `flash` 页面,Rust 侧用 `espflash` 库直接开串口。
|
||||||
|
|
||||||
|
- ✅ 现场只有一个程序;已有 NSIS 打包、CI、配置与密码闸门可复用。
|
||||||
|
- ✅ 视频端本来就要连板子,烧完顺手进监控很自然。
|
||||||
|
- ❌ WebView 里跑长任务 + 进度 + 日志尾随,需要额外的事件/取消/背压设计。
|
||||||
|
- ❌ 烧录是**系统级、不可中断、会变砖风险**的操作,和“看画面”共享一个进程的失败域。
|
||||||
|
- ❌ 无法在 CI / 无人值守 / 远程 SSH 场景复用。
|
||||||
|
|
||||||
|
### 方案 B(推荐):纯逻辑核心库 + 双前端(CLI 优先,GUI 复用)
|
||||||
|
|
||||||
|
`flash/core`(无 I/O,纯函数,主机可测)+ `flashctl` CLI + 桌面端后续加一页调用 Tauri 命令。
|
||||||
|
|
||||||
|
- ✅ 最容易错的部分(镜像格式、checksum/SHA256、分区表 MD5、偏移搜索、补丁)**全部可在主机测**,
|
||||||
|
且可以用仓库里真实的 1.1 MB 镜像做往返验收。
|
||||||
|
- ✅ CLI 先天可脚本化:CI 能跑、现场能远程跑、能出确定退出码与机器可读报告。
|
||||||
|
- ✅ GUI 后置,做完 CLI 后界面只是薄壳;失败域分开(GUI 崩了不影响烧录工具)。
|
||||||
|
- ❌ 多一个二进制要分发(可由桌面端作为 sidecar 一起打包解决)。
|
||||||
|
- ❌ 现场非技术用户需要 GUI(用 P2 的 Tauri 页面补上)。
|
||||||
|
|
||||||
|
### 方案 C:外部进程编排(客户端只做壳,调 `espflash.exe` / `esptool.py`)
|
||||||
|
|
||||||
|
- ✅ 落地最快,协议风险最低;`target/tools/espflash/espflash.exe` 已经在了。
|
||||||
|
- ✅ 与现有 `scripts/*.sh` 心智一致。
|
||||||
|
- ❌ **拿不到回读差分**(espflash 内部校验只给 pass/fail,不给字节列表)——而差分是本方案
|
||||||
|
坏块检测的核心,只能再调一次 `read-flash` 自己比。
|
||||||
|
- ❌ 解析人类可读输出、处理 `esptool.py` 的 Python 依赖,现场极易碎(现有 flash.sh 就是被
|
||||||
|
“macOS 串口名 + termios + 本地 IDF venv”绑死的)。
|
||||||
|
- ❌ 补丁/分区表重写仍要自己写,等于 B 的核心工作一样不少,只省了协议层。
|
||||||
|
|
||||||
|
### 决策矩阵(权重按当前项目处境给)
|
||||||
|
|
||||||
|
| 维度 | 权重 | A 内嵌 | **B 核心库+双前端** | C 外部编排 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 可测试性(本方案最需要) | 30% | 6 | **9** | 4 |
|
||||||
|
| 坏块检测能力(要回读差分) | 25% | 8 | **9** | 5 |
|
||||||
|
| 现场可用性 | 20% | 9 | 7 | 6 |
|
||||||
|
| 线上/CI 可自动化 | 15% | 3 | **9** | 6 |
|
||||||
|
| 交付成本 | 10% | 6 | 6 | **9** |
|
||||||
|
| 加权 | | 6.45 | **8.10** | 5.65 |
|
||||||
|
|
||||||
|
**推荐 B**,并明确:GUI 不是不做,而是**放到 P2**——先用 CLI 把危险逻辑做对、做实测。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 推荐架构
|
||||||
|
|
||||||
|
### 3.1 分层(依赖方向单向,内层不认识外层的存在)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────── 前端(可替换) ───────────────────────────────┐
|
||||||
|
│ flashctl CLI(P0/P1) desktop Tauri 命令 + 「维护」页(P2) │
|
||||||
|
└───────────────┬────────────────────────────────────┬────────────────────────┘
|
||||||
|
│ │
|
||||||
|
┌───────────────▼────────────────────────────────────▼────────────────────────┐
|
||||||
|
│ flash-device —— I/O 适配层(唯一碰硬件的地方) │
|
||||||
|
│ · 串口枚举/VID-PID 过滤 · espflash Connection/Flasher 包装 · 回读 │
|
||||||
|
│ · 监视器(日志尾随 + 断言)· 自动复位/进下载模式策略 │
|
||||||
|
└───────────────────────────────┬──────────────────────────────────────────────┘
|
||||||
|
│ 只传「字节 + 计划 + 观察值」
|
||||||
|
┌───────────────────────────────▼──────────────────────────────────────────────┐
|
||||||
|
│ flash-core —— 纯逻辑(无 I/O、无 serde-io、可在主机完整测试) │
|
||||||
|
│ image │ parttable │ placement │ badmap │ manifest │ patch │ verify │ session │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
建议目录(沿用本仓库“每个目标一套 workspace”的既有做法):
|
||||||
|
|
||||||
|
```
|
||||||
|
flash/
|
||||||
|
core/ crate flash-core lib,纯函数 + 单测(含对真实镜像的往返测试)
|
||||||
|
cli/ crate flashctl bin,含 flash-device 适配(串口/espflash)
|
||||||
|
templates/ 模板生产脚本:从 ELF 生成 manifest(开发期一次性,不进现场)
|
||||||
|
```
|
||||||
|
|
||||||
|
`desktop/src-tauri` 之后以 path 依赖引 `flash-core` + `flash/cli` 的适配层。
|
||||||
|
|
||||||
|
### 3.2 模块职责
|
||||||
|
|
||||||
|
| 模块 | 职责 | 关键不变量 / 失败模式 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `image` | 解析 ESP-IDF App 镜像:header、段表、`esp_app_desc_t`、checksum 位置、SHA256 位置;重算哈希;打字节补丁 | 往返测试:对未修改镜像重算出的 checksum/SHA256 必须与文件里已有的**逐字节相同** |
|
||||||
|
| `parttable` | 解析/序列化 0xC00 分区表;改某条的 offset;重算 MD5 | 输出必须能被 `esp-idf` 的 `esp_partition` 规则接受(4 KiB 对齐、不重叠、不越界) |
|
||||||
|
| `placement` | 给定(镜像字节, 坏点表, 约束)枚举合法 App 偏移,**并给出每个候选被拒的原因** | 纯函数;同一输入必须给同一结果;找不到时必须说清是哪条约束卡住 |
|
||||||
|
| `badmap` | 缺陷模型:`{addr, stuck0_mask, stuck1_mask, sector, confidence}`;提供 `byte_storable(addr, value)` | 两向谓词:`(v & stuck0)==0 && (v & stuck1)==stuck1` |
|
||||||
|
| `manifest` | 模板清单:镜像哈希绑定、补丁点(偏移/容量/长度字位置)、能力标志 | 清单与镜像哈希不匹配 → **拒绝**,不做“尽力而为” |
|
||||||
|
| `patch` | 应用凭据补丁(字符串 + 长度字)+ 哈希重算,输出变更日志 | 每次补丁都产出「改了什么、改在哪、前后值」,写进报告 |
|
||||||
|
| `verify` | 期望字节 vs 回读字节 → 缺陷列表(地址、期望、实读、推导出的 stuck mask) | 差分必须能表达“读出比期望多 1 位/少 1 位” |
|
||||||
|
| `session` | 状态机:`Enumerate → Identify → LoadBundle → Plan → Patch → Write → Verify → BootCheck → Report` | 每步可 dry-run;任一步失败可回滚到写前备份 |
|
||||||
|
| `report` | `FlashReport`(本次)与 `DeviceProfile`(按 MAC 累积的坏点表/上次好镜像/历史) | 档案只作**提示**,最终决策永远以本次回读为准 |
|
||||||
|
|
||||||
|
### 3.3 数据模型(三份 JSON,人可读、可 diff、可回滚)
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
// manifest.json —— 与模板镜像一起发布,模板生产线生成
|
||||||
|
{
|
||||||
|
"schema": 1,
|
||||||
|
"app_sha256": "…", // 本清单只对这一份 app.bin 有效
|
||||||
|
"chip": "esp32s3",
|
||||||
|
"image": { "size": 1107712, "segments": 6, "hash_appended": true },
|
||||||
|
"patch_points": [
|
||||||
|
{ "id": "ssid", "kind": "utf8", "str_offset": 123456, "capacity": 10,
|
||||||
|
"len_words": [{"offset": 210000, "width": 4}], "max_len": 32 },
|
||||||
|
{ "id": "password", "kind": "utf8", "str_offset": 123466, "capacity": 8,
|
||||||
|
"len_words": [], "max_len": 64 }
|
||||||
|
],
|
||||||
|
"capabilities": { "equal_length_only": ["password"] }
|
||||||
|
}
|
||||||
|
|
||||||
|
// device-<mac>.json —— 主机侧设备档案(累积)
|
||||||
|
{ "mac": "…", "chip": "esp32s3", "flash_jedec": "0x…", "flash_size": 16777216,
|
||||||
|
"defects": [ {"addr": 1027047, "stuck0": 224, "stuck1": 0, "confidence": "verified"} ],
|
||||||
|
"last_good": { "bundle": "…", "app_offset": "0x50000", "at": "…" },
|
||||||
|
"history": [ { "at": "…", "app_offset": "0x70000", "result": "ok" } ] }
|
||||||
|
|
||||||
|
// report-<timestamp>.json —— 每次烧录一份,可交给现场/复核
|
||||||
|
{ "device": "…", "bundle": "…", "plan": { "app_offset": "0x70000", "patches": ["ssid"], "erase": [...] },
|
||||||
|
"write": { "ok": true }, "verify": { "mismatches": [] },
|
||||||
|
"boot": { "signature": "Loaded app from partition at offset 0x70000", "ip": "…" } }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 状态机(每步都能停、都能讲清为什么)
|
||||||
|
|
||||||
|
```
|
||||||
|
Enumerate ─▶ Identify ─▶ LoadBundle ─▶ Plan ──(人工确认)──▶ Patch ─▶ Write ─▶ Verify ─▶ BootCheck ─▶ Report
|
||||||
|
│ │ │ │ │ │ │ │
|
||||||
|
│ │ │ └─ 打印全部计划:偏移/补丁/擦除范围/预计耗时
|
||||||
|
│ │ └─ 分类:bundle | merged | app | ELF;类型必须**显示并由人确认**
|
||||||
|
│ └─ 芯片型号、MAC、Flash JEDEC/容量、0x8000 分区表、App 描述符(离线读)
|
||||||
|
└─ 端口枚举 + VID/PID 过滤;只有一个候选才自动选中
|
||||||
|
```
|
||||||
|
|
||||||
|
`Plan` 必须可 `--dry-run` 输出到 stdout(JSON),这是 CI 与现场复核的接缝。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 坏块屏蔽的落点(四层,各层归属明确)
|
||||||
|
|
||||||
|
### 4.1 检测(`flash-device::probe` + `flash-core::verify`)
|
||||||
|
|
||||||
|
| 手段 | 能发现什么 | 成本 | 用在哪 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `flash_id` / JEDEC + SFDP | 厂商、型号、容量、时序 —— **不含缺陷** | 毫秒 | 识别芯片、判断是否可疑批次 |
|
||||||
|
| **图案探测**:向空洞 `0x10000–0x70000` 写 `00 / 55 / AA / E0 / 1F` 等图案并回读差 | 卡 0、卡 1、整扇区失效 | 380 KB × N 图案,约十几秒 | 首次接管一块板 / 用户点「深度体检」 |
|
||||||
|
| **写后整段回读差** | 本次实际没写进去的字节(**权威**) | 1.1 MB 回读,秒级 | **每次烧录都做**,不可选 |
|
||||||
|
| 串口日志诊断 | 症状:`invalid header` / checksum 失败 / rst 原因 | 秒级 | 板子已经在现场、只能看现象时 |
|
||||||
|
|
||||||
|
要点:
|
||||||
|
- 图案探测必须**双向**:写 0 找卡 1,写 1 找卡 0。擦除态 `0xFF` 会让“卡 1”完全隐形——
|
||||||
|
`0x10863` 就是整段回读才发现就是这个原因。
|
||||||
|
- 探测区选空洞而不是 NVS:**不要为了体检去毁 NVS**。若怀疑 NVS 区有坏点,单独提供
|
||||||
|
“备份 NVS → 探测 → 还原”的危险模式,默认关闭。
|
||||||
|
- probe 结束后必须 `erase` 干净,并**复核空洞回到 0xFF**,避免留下疑似数据。
|
||||||
|
|
||||||
|
### 4.2 决策(`flash-core::placement`,纯函数)
|
||||||
|
|
||||||
|
```
|
||||||
|
输入:image(bytes), badmap, {flash_size, align(min 4KiB, 首选 64KiB), min_offset=0x20000,
|
||||||
|
app_size=0x120000, 保留区表}
|
||||||
|
对每个候选 off(升序,先 64 KiB 网格,再 4 KiB 网格兜底):
|
||||||
|
① off 4 KiB 对齐,off ≥ min_offset,off + image.len() ≤ flash_size
|
||||||
|
② off ≥ 0x20000 且分区不与 0x10863 所在扇区重叠(等价于“不覆盖已坏空洞”)
|
||||||
|
③ 对每个“已证坏点 b”:若 off ≤ b < off+image.len(),要求 byte_storable(b, image[b-off])
|
||||||
|
④ 分区 [off, off+0x120000) 不与任何 bad sector 相交(注意:**镜像尾部之外的 slack 不算**)
|
||||||
|
输出:第一个满足全部条件的 off;若都没有,逐个候选打印“被 ③ 卡住,b=0xFADE7 需要 image 字节
|
||||||
|
的 bit5..7 为 0,实际 0x6E”——这正是本仓库一贯的“失败要能解释”的风格。
|
||||||
|
```
|
||||||
|
|
||||||
|
三点必须写进设计:
|
||||||
|
|
||||||
|
- **对齐放宽到 4 KiB**:App 分区只要求 4 KiB 对齐,`flash.sh` 用 64 KiB 是自我限制。
|
||||||
|
候选从 ~230 个变成 ~3700 个,命中概率高一个数量级(单坏点 1/8,双坏点也能稳过)。
|
||||||
|
- **分区大小 ≠ 镜像大小**:`0x120000` 的分区里镜像只占 `0x10E700`,尾部 71 KiB 是空的。
|
||||||
|
坏扇区落在尾部 slack 里**无害**,别把它算成阻塞。
|
||||||
|
- **bootloader 与分区表固定偏移**:`0x0`(0x5160) 与 `0x8000`(0xC00) 挪不了。
|
||||||
|
这里只能做**字节级吸收**:例如坏点落在分区表某条 `label` 字段里,就换一个 label 让那一位合法
|
||||||
|
(label 是自由文本);落在 `magic/offset/size/MD5` 上则该板报废。这条路径要显式实现并明确其边界。
|
||||||
|
|
||||||
|
### 4.3 执行(`flash-core::parttable` + `image`)
|
||||||
|
|
||||||
|
- 改写 `partition-table.bin` 中 `factory` 的 offset(和必要时 size)→ 重算末尾 16 字节 MD5。
|
||||||
|
- **App 镜像原样写入新偏移**——镜像里没有任何“我在哪个偏移”的信息,改位置不改变它。
|
||||||
|
- 只有在“分区表/bootloader 必须字节吸收”这一层才动镜像,并且要重算 checksum/SHA256。
|
||||||
|
|
||||||
|
### 4.4 验证与回滚(`session` + `report`)
|
||||||
|
|
||||||
|
- 写前:把 `0x0–0x9000` + 分区表 + App 分区整段回读到 `rescue-<mac>-<ts>.bin`(约 1.2 MB)。
|
||||||
|
这既是回滚素材,也是一份“坏点快照”。
|
||||||
|
- 写后:回读 → 差分 → 若有 mismatch,**先更新 badmap,再重新规划**,而不是简单重试。
|
||||||
|
- 引导:抓串口,断言 `Loaded app from partition at offset 0x<off>` 与
|
||||||
|
`ESP32 camera is live on <ip>`;失败则按 rst 原因给出分类结论(镜像校验失败 / 找不到分区 /
|
||||||
|
Wi-Fi 没连上 / 相机没起来)。**写入成功 ≠ 能跑**,这是仓库文档反复强调的点。
|
||||||
|
|
||||||
|
**为什么这套不会变砖**:ROM 下载模式永远在,只要不是 eFuse 层面的破坏(secure boot / flash
|
||||||
|
encryption / 烧写保护),任何 App/分区表错误都只是“不启动”,可以用同一根 USB 线救回来。
|
||||||
|
前提是**确认这些 eFuse 特性没开**(见问题 Q6)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 动态 patch 的落点
|
||||||
|
|
||||||
|
### 5.1 必须重算的两样东西(否则一定不开机)
|
||||||
|
|
||||||
|
App 镜像的完整性由 bootloader 校验:
|
||||||
|
|
||||||
|
```
|
||||||
|
checksum = 0xEF XOR (所有段的所有数据字节) // 1 字节,位于段尾 16B 对齐之后
|
||||||
|
sha256 = SHA256(镜像中它之前的全部字节) // 32 字节,仅当 header.hash_appended=1
|
||||||
|
```
|
||||||
|
|
||||||
|
任何字节补丁都会同时破坏这两者。`flash-core::image` 必须实现并在**真实镜像上做往返测试**
|
||||||
|
(重算结果与文件里已有的字节必须完全相同)——这是整个方案最便宜、最有效的验收手段,
|
||||||
|
在碰硬件之前就能证明实现是对的。
|
||||||
|
|
||||||
|
另外:`esp_app_desc_t` 里的 `app_elf_sha256` 是 ELF 的哈希,补丁后必然与内容不符。
|
||||||
|
它只用于 core dump 关联,**没有校验作用**,可以接受;但要在报告里写一句,避免以后有人拿它
|
||||||
|
当“镜像指纹”。真正的指纹用整镜像 SHA256。
|
||||||
|
|
||||||
|
### 5.2 SSID / 密码补丁的两种模式(这是本方案的真实技术风险)
|
||||||
|
|
||||||
|
事实基础:
|
||||||
|
|
||||||
|
- `const SSID: &str = env!("WIFI_SSID")` 是编译期常量,长度也是编译期常量。
|
||||||
|
- 字面量本身在 `.rodata`(segment 0,`0x3c0d0020` 附近),二进制里可 grep。
|
||||||
|
- 但**长度**在机器码里可能以三种形式存在:`.rodata` 里的 `(ptr,len)` 静态对
|
||||||
|
(`format_args!` 走 trait object 时几乎必然产生一份)、`movi` 立即数、或两者混用。
|
||||||
|
|
||||||
|
| 模式 | 做法 | 前提 | 风险 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| **M1 等长补丁** | 只改写字符串字节,长度不动 | 新值**字节长度 == 模板槽位长度** | 低。仅当现场 SSID 长度固定时可用 |
|
||||||
|
| **M2 长度字补丁** | 改字节 + 改所有 `(ptr,len)` 长度字 | 模板生产期能**定位全部**长度字并验证 | 中高。混用立即数时会出现“日志显示新 SSID 但连接用的旧长度” |
|
||||||
|
|
||||||
|
工程对策(三条,缺一不可):
|
||||||
|
|
||||||
|
1. **模板生产线生成 manifest**(`flash/templates/`):构建出 ELF 后,用符号表 + 字面量地址
|
||||||
|
反查所有引用,产出 `str_offset / capacity / len_words[]`。这是一次性、离线、可审计的活动,
|
||||||
|
**不发生在现场**。
|
||||||
|
2. **清单里写清能力**:`capabilities.equal_length_only: ["password"]` 之类。M2 只在
|
||||||
|
“长度字全部定位成功”时启用;否则清单退化为 M1,客户端在长度不匹配时**拒绝并给出可执行的
|
||||||
|
建议**(“本模板 SSID 槽位 10 字节,请把路由器 SSID 设为 10 字节,或使用 N 字节模板”)。
|
||||||
|
3. **硬件验收一次,之后才允许现场用**:模板发布前跑一遍「patch 成另一个等长 SSID → 烧 → 看日志
|
||||||
|
是否连上」。连不上就把该模板降级为 M1。这一步可以脚本化,属于发布门禁。
|
||||||
|
|
||||||
|
> 注意 `format_args!` 会**把补丁后的 SSID 原样打进日志**(`"{SSID}" was not in the scan results`),
|
||||||
|
> 所以现场日志本身就能佐证补丁是否生效——这是免费的验收信号,也是排障的第一手材料。
|
||||||
|
|
||||||
|
### 5.3 模板固件的构建约定(一次性,发布期)
|
||||||
|
|
||||||
|
```text
|
||||||
|
WIFI_SSID = 32 字节占位(留满槽位,便于 M2);WIFI_PASS = 64 字节占位
|
||||||
|
其余 .env 参数按交付基线冻结
|
||||||
|
产物:bootloader.bin + partition-table.template + app.bin + manifest.json → 一个 bundle
|
||||||
|
```
|
||||||
|
|
||||||
|
这样**现场只选 SSID/密码,不再触碰 `.env`、不再跑 cargo**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 关键风险
|
||||||
|
|
||||||
|
| # | 风险 | 影响 | 对策 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| R1 | 长度字定位不全,M2 补丁“半生效” | 板子连不上 Wi-Fi,看起来像“密码错了” | 清单声明能力 + 发布门禁实测 + 不匹配就拒绝(不猜) |
|
||||||
|
| R2 | 补丁后未重算 checksum/SHA256 | 不启动(可救,但现场中断) | 真实镜像往返测试;写入前本地自校验 |
|
||||||
|
| R3 | 偏移漂移:新镜像可能**没有**合法偏移 | 该板无法升级 | 4 KiB 对齐搜索 + 尾部 slack 检查 + 找不到时明确报“这块板的 flash 太坏” |
|
||||||
|
| R4 | 坏点表串板/过期(缺陷随温度、擦写次数变化) | 写进去才发现 | 档案只作提示;**每次写后必做回读差分** |
|
||||||
|
| R5 | 坏点在 bootloader/分区表固定区 | 无法规避 | 仅 label 等自由字段可吸收;其余明确报废,别赌 |
|
||||||
|
| R6 | 自动复位/进下载模式不可靠 | “插入即烧”的第一步就失败 | 先实测;失败时降级为“按 BOOT 提示 + 重试”,并把步骤画在界面上 |
|
||||||
|
| R7 | 多程序抢串口 / 抢 UDP 5600 | 现场打不开或烧不了 | 串口独占提示;烧录模式与视频模式互斥提示(沿用现有 `startup-error.txt` 风格) |
|
||||||
|
| R8 | secure boot / flash encryption / eFuse 保护开启 | **整套方案失效**(镜像是加密/签名的) | 上线前确认关闭;客户端探测到加密镜像直接拒绝并说明 |
|
||||||
|
| R9 | 模板与清单版本错配 | 打到错误偏移 | 清单绑定 `app_sha256`,不匹配即拒绝 |
|
||||||
|
| R10 | OTA 需求最终被判定为硬需求 | 架构返工 | **第 9 节 Q2 必须先答**;先落 OTA-ready 分区表作为廉价期权 |
|
||||||
|
| R11 | `esp_app_desc_t.app_elf_sha256` 补丁后不符 | 误导后人 | 报告里注明;指纹改用整镜像 SHA256 |
|
||||||
|
| R12 | 破坏 NVS(改分区布局或误擦) | 丢绑定/校准,重连变慢 | 默认不擦 NVS;提供备份/还原;改布局前显式确认 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 必须向用户确认的问题(决定架构是否成立)
|
||||||
|
|
||||||
|
| # | 问题 | 为什么现在必须答 | 我的默认建议 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| **Q1** | 客户端宿主平台与技术栈?Windows-only 还是三平台?扩展 [desktop/](desktop/) 还是独立工具? | 决定打包、串口栈、UI 形态 | Rust 核心库 + CLI(P1)→ Tauri 页面(P2),Windows 优先 |
|
||||||
|
| **Q2** | **“OTA”到底指什么**:(a) USB 现场更新;(b) 设备自己从网上拉;(c) 局域网推 | (b)(c) 与“禁止改固件”**直接互斥**,架构会完全不同 | 先按 (a) 做;同时只改分区表落一个 OTA-ready 布局作期权 |
|
||||||
|
| **Q3** | SSID 修改是否接受**等长约束**?现场 SSID 长度是否固定?是否出现过 ≥16 字节或含中文/空格? | 决定 M1 够不够,还是必须做 M2 并承担风险 | 双模:M1 保底,M2 需模板验收通过才启用 |
|
||||||
|
| **Q4** | 是否允许**写入图案做坏块探测**(十几秒、会擦除未分配空洞)?坏板一共几块?坏点是否稳定复现? | 决定检测深度与是否值得做 probe | 首次接管一块板自动探测一次;之后只做写后差分 |
|
||||||
|
| **Q5** | 现有板子的**自动复位/进下载模式**可靠吗(是否必须手按 BOOT/EN)? | 决定“插入即烧录”能不能成立 | 实测;不可靠则做“半自动 + 引导” |
|
||||||
|
| **Q6** | 是否启用过 secure boot / flash encryption / eFuse 写保护? | 开了则本方案**整体失效** | 假设关闭,需你确认 |
|
||||||
|
| **Q7** | 现场更新时可否重写**分区表与 NVS**?丢了客户端绑定/设置能接受吗? | 有些修复必须动布局 | 默认不碰 NVS;改布局前备份并确认 |
|
||||||
|
| **Q8** | 是否需要同一个客户端也支持 **ESP32-C3 装甲板**([armor/](armor/))? | 影响芯片抽象与模板管理 | 预留 chip 抽象,P2 再接 C3 |
|
||||||
|
| **Q9** | 现场是否允许**任意固件文件**(用户挑一个 `.bin/.elf`),还是只允许“官方 bundle”? | 决定 manifest 从哪来:bundle 自带 vs 客户端现场解析 ELF | 两者都支持:任意文件走“无 patch 模式”(坏块规避仍然有效),bundle 才有 SSID patch |
|
||||||
|
| **Q10** | 坏块规避失败(该板无合法偏移)时的产品行为:拒绝升级 / 允许“尽力而为” / 提示换模组? | 决定失败语义 | **拒绝并给证据**,不做尽力而为 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 分期建议
|
||||||
|
|
||||||
|
| 阶段 | 内容 | 验收标准 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **P0**(1 周) | `flash-core`:`image` + `parttable` + 真实镜像往返测试;CLI 能“读懂一份 bundle + 打印计划(dry-run)” | 未修改镜像的 checksum/SHA256 重算与原文件逐字节一致 |
|
||||||
|
| **P1**(2 周) | `placement` + `verify` + 串口写入 + 回读差分 + 引导断言;**在健康板与故障板上各跑通一次** | 故障板零重编译完成升级;`0xFADE7` 落点字节被证明可写;日志断言通过 |
|
||||||
|
| **P2**(2 周) | manifest 生产线 + SSID/patch(M1 先上,M2 门禁通过再开)+ 设备档案 + 回滚 | 同一 bundle 换 SSID 后板子连上新 AP,无需任何编译 |
|
||||||
|
| **P3** | desktop Tauri「维护」页(复用 P1/P2 的一切) | 现场非技术用户可完成“插入 → 识别 → 选包 → 烧 → 看到连上” |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附录 A:两条 ADR
|
||||||
|
|
||||||
|
### ADR-001:坏字节规避从“重编译”改为“分区表重写”
|
||||||
|
|
||||||
|
- **状态**:提议中
|
||||||
|
- **背景**:`0xFADE7` 的 bit5–7 不可写;`scripts/flash.sh` 通过改 `partitions.csv` 并重建来避开它,
|
||||||
|
但重建会移动镜像内容,于是需要“第二次验证”,且只支持 macOS、必须带完整 IDF 工具链。
|
||||||
|
- **决策**:把 App 偏移从**构建输入**改成**烧录期的计划值**:搜索偏移 → 改写
|
||||||
|
`partition-table.bin`(重算 MD5)→ App 镜像原样写入。
|
||||||
|
- **备选**:① 继续重编译(现状);② 为每块板生成专用 merged 镜像;③ 用 `otadata` 之类间接重定向。
|
||||||
|
- **影响**:现场彻底摆脱工具链;同一份镜像可服务健康板与故障板;代价是客户端必须正确实现镜像与
|
||||||
|
分区表格式(用真实镜像的往返测试兜底)。
|
||||||
|
|
||||||
|
### ADR-002:核心逻辑与 I/O 分离,CLI 先于 GUI
|
||||||
|
|
||||||
|
- **状态**:提议中
|
||||||
|
- **背景**:本方案里最容易错、错了最难在现场发现的,是镜像格式、哈希重算、偏移搜索与差分;
|
||||||
|
而这些恰好都能在主机上测。仓库已有 `protocol/`、`stream_control`、`link_control` 的先例。
|
||||||
|
- **决策**:`flash-core` 纯函数、无 I/O;`flash-device` 单独承担串口与协议;GUI 最后接。
|
||||||
|
- **备选**:直接写进 Tauri 后端(失败域大、难测);用 Python 脚本 (现有风格) 但无法复用 Rust 类型。
|
||||||
|
- **影响**:多一个 crate 与一条编译链;换来可在 CI 上守护的不变量,以及将来 GUI/CLI/远程三处复用。
|
||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
# 图传板硬件说明
|
||||||
|
|
||||||
|
对应 Freenove ESP32-S3-WROOM CAM、ESP32-S3-WROOM-1-N16R8 与 OV5640。构建和烧录入口见 [README.md](README.md)。
|
||||||
|
|
||||||
|
## 摄像头引脚
|
||||||
|
|
||||||
|
配置以 `src/camera.rs` 的 `pins` 模块为准。DATA 按 D0–D7(传感器 Y2–Y9)排列。
|
||||||
|
|
||||||
|
| 信号 | GPIO |
|
||||||
|
| --- | --- |
|
||||||
|
| RESET | 0 |
|
||||||
|
| XCLK | 15 |
|
||||||
|
| PCLK | 13 |
|
||||||
|
| VSYNC | 6 |
|
||||||
|
| HREF | 7 |
|
||||||
|
| SIOD / SDA | 4 |
|
||||||
|
| SIOC / SCL | 5 |
|
||||||
|
| D0–D7 | 11, 9, 8, 10, 12, 18, 17, 16 |
|
||||||
|
| PWDN | 未连接(`-1`) |
|
||||||
|
|
||||||
|
当前 XCLK 为 20 MHz。不要直接套用 AI-Thinker、XIAO 等其他板卡的相机引脚表。
|
||||||
|
|
||||||
|
本模组需避开的资源包括 Flash / PSRAM 总线(GPIO 26–37)、USB(19 / 20)、串口(43 / 44)及模组内部引脚,以原理图为最终依据。占用 Flash / PSRAM 引脚可能造成传输期间 CPU 挂死、串口静默。
|
||||||
|
|
||||||
|
## 内存与任务配置
|
||||||
|
|
||||||
|
参数位于根目录 `sdkconfig.defaults`:
|
||||||
|
|
||||||
|
| 参数 | 当前值 | 用途 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `CONFIG_SPIRAM_MODE_OCT` | `y` | 8 MB Octal PSRAM |
|
||||||
|
| `CONFIG_SPIRAM_SPEED_80M` | `y` | PSRAM 时钟 |
|
||||||
|
| `CONFIG_CAMERA_PSRAM_DMA` | `n` | DMA 经内部 RAM 中转后复制到 PSRAM |
|
||||||
|
| `CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP` | `n` | Wi-Fi / lwIP 缓冲保留在内部 RAM |
|
||||||
|
| `CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240` | `y` | CPU 240 MHz |
|
||||||
|
| `CONFIG_CAMERA_CORE1` | `y` | 显式选择 CPU1,与 Wi-Fi 分开(只禁用 CORE0 不足以改变 choice 默认值) |
|
||||||
|
| `CONFIG_FREERTOS_HZ` | `1000` | 发送拥塞时以 1 ms tick 让出调度 |
|
||||||
|
| `CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM` | `64` | 发送缓冲数量,使用内部 RAM |
|
||||||
|
| `CONFIG_ESP_WIFI_TX_BA_WIN` | `32` | 保留 AMPDU 发送聚合 |
|
||||||
|
|
||||||
|
相机使用 3 个 PSRAM 帧缓冲及 `GRAB_LATEST`,优先取得较新的画面。JPEG 编码由传感器完成,S3 主要负责采集和发送。
|
||||||
|
|
||||||
|
原测试板启用 PSRAM DMA 后曾出现 `NO-EOI`:JPEG 结束标记位于驱动末尾 DMA 搜索窗口之外,随后停止交付帧。因此默认关闭该配置;更换驱动后应单独验证持续采集。
|
||||||
|
|
||||||
|
## Flash 与分区
|
||||||
|
|
||||||
|
默认 **16 MB、DIO、40 MHz**,分区来自 `partitions.csv`:
|
||||||
|
|
||||||
|
| 分区 | 偏移 | 大小 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| NVS | `0x9000` | `0x6000` |
|
||||||
|
| PHY init | `0xF000` | `0x1000` |
|
||||||
|
| Factory app | 以 `partitions.csv` 为准,坏板重编时可能调整 | `0x120000`(1152 KiB) |
|
||||||
|
|
||||||
|
分区表位于 `0x8000`,S3 Bootloader 从 `0x0` 开始。App 写入分区表声明的偏移,不能直接套用 `0x10000`。修改分区或 Flash 模式后需重新构建,成套更新 Bootloader、分区表和 App。
|
||||||
|
|
||||||
|
健康硬件可以评估恢复 `0x10000` App 偏移或更快 Flash 配置,但需重新验证。
|
||||||
|
|
||||||
|
## 损坏 Flash 模组的兼容处理
|
||||||
|
|
||||||
|
早期测试模组在绝对地址 **`0xFADE7`** 存在写入异常,bit 5–7 无法正确写入。这是特定模组的故障。当前 App 偏移来自该板的开发过程,不能保证新镜像仍避开坏字节。
|
||||||
|
|
||||||
|
2026-10-18 的整段回读还发现 **`0x10863`** 的 bit 6 异常读回为 1:期望 `0x2e`,读回 `0x6e`,该轮 App 只有这一处字节不同,尚未单独擦写复核是否为永久故障。该模组的 App 偏移需至少为 `0x20000`,并继续检查 `0xFADE7`;烧录工具的校验失败必须先排除,不能当作网络或相机故障。
|
||||||
|
|
||||||
|
`scripts/flash.sh` 保留的流程为:编译、导出 App、搜索 64 KiB 对齐的可写偏移、修改分区、重新构建和验证,再写入三段镜像。重新构建可能移动镜像内容,因此第二次验证不可省略。
|
||||||
|
|
||||||
|
脚本依赖 macOS 串口名、Unix `termios`、本地 ESP-IDF Python 目录及旧版工具参数,不适合作为通用烧录入口。坏板需核对工具版本及坏地址字节;普通板卡使用 README 流程。更换故障模组比长期依赖镜像位置更可靠。
|
||||||
|
|
||||||
|
## 本地相机组件
|
||||||
|
|
||||||
|
`components/esp32-camera/` 包含上游 2.1.7 及本地兼容补丁,`components/cam_wrapper/` 提供 Rust FFI 封装。
|
||||||
|
|
||||||
|
本地增加 `esp_camera_fb_get_latest` / `cam_take_latest`:等待第一张有效帧后,以有限次数零等待取出更新帧,并归还替换的缓冲。`cam_take(0)` 表示单次非阻塞尝试,不在空队列、坏 JPEG 或 DMA 复位哨兵上继续等待。升级驱动时需保留或移植这些补丁,并运行缓冲所有权测试;不能用“先检查有帧、再调用可能阻塞 4 秒的 get”替代。
|
||||||
|
|
||||||
|
S3 的 `ll_cam.c` 中有 `CAM_IO_MUX_REG()` 本地补丁,用于 SCCB / I2C 引脚连接。升级驱动后需检查补丁并验证传感器探测和采集。
|
||||||
|
|
||||||
|
`components/esp32-camera/target/` 是按芯片划分的**源代码**,清理缓存时不能删除,也不能忽略所有层级的 `target/`。
|
||||||
+88
@@ -0,0 +1,88 @@
|
|||||||
|
# 约 30 fps、Wi-Fi 重连与画面链路延迟验证(2026-10-18)
|
||||||
|
|
||||||
|
当前交付已增加阻塞恢复与新鲜度处理,见 [REALTIME.md](REALTIME.md)。本页保留此前减光确认和画面延迟对照;本页 SHA 与测量结果均属于当时构建,不是当前固件的重测结果。
|
||||||
|
|
||||||
|
## 先前三缓冲与小幅减光记录
|
||||||
|
|
||||||
|
根据用户长程运行反馈,双缓冲在峰值负载下丢帧明显,当前交付恢复 **`CAM_FB_COUNT=3`**,保留突发发送期间的采集余量。约 30 fps 完整 PLL 配置、VGA 彩色、正常白平衡、Wi-Fi 自动重连和自适应 JPEG 均保留。双缓冲保留为试验选项,下面的短时延迟对照属于历史记录,不能证明长程峰值稳定性。
|
||||||
|
|
||||||
|
针对现场轻微过亮,增加 `CAM_AE_LEVEL=-1`,将 OV5640 AE 目标从 55 微调到 45,目标比值约 −0.29 EV;`CAM_EXPOSURE_EV=0`,自动曝光和增益仍联动,曝光上限保持 592 行(约 20 ms)。这会小幅降低亮度目标,不保证实际快门必然缩短。当前 50 Hz 抗频闪步长约 10 ms,直接小减曝光上限可能跨越一整档,因此使用 AE 目标细调。该目标比值不等于经过照度标定的实际 EV。
|
||||||
|
|
||||||
|
本次已通过 COM8 烧录与 Bootloader、分区表、App 的独立校验。重新启动后串口观察 45.15 秒,复位和采集错误均为 0,订阅已恢复;末个窗口提交 29.7 fps。观察中仍有 UDP 发送回压,三缓冲不代表完全消除路由器拥塞。本次未重新测量原生完整接收损失率或画布延迟,也未重新进行长程峰值测试。
|
||||||
|
|
||||||
|
用户已确认当前三缓冲、小幅减光版本亮度合适、颜色稳定。
|
||||||
|
|
||||||
|
该阶段 App SHA-256:`05aff1f53519fbb52b89ca526c3be11cc78f37a3356f588986336cf7870924eb`。完整选择理由、当前参数与短测记录见制品 `BUFFER-DECISION.json`;下面的 90 秒/五分钟对照、CDF 和旧接收曲线保留为历史数据,不能套用到本次亮度微调后的镜像。
|
||||||
|
|
||||||
|
## 此前短时对照(历史)
|
||||||
|
|
||||||
|
此前试验使用 VGA 彩色、完整 PLL 配置和双缓冲。保留 Mihomo TUN,只有一个 UDP 订阅者,使用 Windows 生产客户端的真实视频绘制路径,非演示模式。用户已确认新时钟配置亮度够用、颜色正常,未再出现短 HTS 方案的绿紫闪烁。
|
||||||
|
|
||||||
|
| 指标 | 三缓冲基线 | 双缓冲试验配置 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 原生接收窗口 | 90.008 s | 300.003 s |
|
||||||
|
| 完整接收帧 | 2659 | 8909 |
|
||||||
|
| 帧损失 | 32 / 1.189% | 61 / 0.680% |
|
||||||
|
| 完整接收帧率 | 29.54 fps | 29.70 fps |
|
||||||
|
| 本地 JPEG 通道 | 27.23 fps | 27.90 fps |
|
||||||
|
| 标记回显有效样本 | 767 | 2641 |
|
||||||
|
| 标记回显采样跨度 | 109.68 s | 295.08 s |
|
||||||
|
| 标记 → 首次可解码画布回显 P50 | 128.4 ms | 121.5 ms |
|
||||||
|
| 标记 → 首次可解码画布回显 P95 | 230.7 ms | 193.4 ms |
|
||||||
|
| 回显估计 P99 / 最大值 | 285.3 / 443.2 ms | 248.9 / 461.5 ms |
|
||||||
|
|
||||||
|
这次窗口的 P50 降低 6.8 ms、P95 降低 37.3 ms。双缓冲减少潜在采集排队,同时保住接近 30 fps 的实际交付。两个窗口的网络回压、JPEG 自适应档位和时长不同,属于连续实机对照,不能把全部改善归因于缓冲数,也不是所有路由器或场景的保证。此前约 22 fps 的测试保留在 [STABILITY.md](STABILITY.md)。
|
||||||
|
|
||||||
|
双缓冲试验五分钟全程在线,复位 0 次、采集错误 0 次,通道断开 0 次、计数重置 0 次。抽取 61 张 JPEG 均完整解码为 640×480 RGB;没有用灰度或降低色深换帧率。最小内部空闲堆 118771 B;实际 JPEG 档位 18–28,温度 58–60°C,未触发减载。
|
||||||
|
|
||||||
|
## 该测试阶段的配置与时钟
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
CAM_RES=vga
|
||||||
|
CAM_CLOCK_PROFILE=vga30
|
||||||
|
CAM_FB_COUNT=3
|
||||||
|
CAM_QUALITY=18
|
||||||
|
CAM_QUALITY_MAX=30
|
||||||
|
CAM_MAX_FPS=0
|
||||||
|
CAM_GRAYSCALE=0
|
||||||
|
CAM_EXPOSURE_EV=0
|
||||||
|
CAM_AE_LEVEL=-1
|
||||||
|
CAM_SHUTTER_LINES=0
|
||||||
|
CAM_EXPOSURE_MAX_LINES=592
|
||||||
|
CAM_GAIN_CEILING=32
|
||||||
|
CAM_HTS=0
|
||||||
|
CAM_THERMAL_LIMIT_C=75
|
||||||
|
WIFI_TX_POWER_DBM=14
|
||||||
|
UDP_PACKET_GAP_US=0
|
||||||
|
WIFI_TEST_DROP_AFTER_S=0
|
||||||
|
```
|
||||||
|
|
||||||
|
XCLK 保持 20 MHz,使用完整 PLL 分频组合:VCO 610 MHz、SYSCLK 61 MHz、DVP PCLK 15.25 MHz;保持默认 HTS 2060 / VTS 984 和读出窗口,名义节拍约 30.09 fps。此前缩短 HTS 至 1500 / 1800 的方案会闪色,未采用。驱动回调的参数顺序与私有 PLL 辅助函数不同,必须按 `sensor_t.set_pll` 的签名调用。
|
||||||
|
|
||||||
|
下列实际曝光、增益范围属于此前双缓冲试验。当前 AE 目标改为 45/55,自动增益保留,曝光上限 592 行约 19.99 ms;实际曝光 590–590 行,估算 19.92–19.92 ms,增益 8.00–13.50×。最大 50 Hz band 数保留为 2,避免提升 SYSCLK 后向下取整只剩一个 band 导致画面过暗。未强制负 EV 或固定低快门。行数与毫秒换算依赖当前时钟,不能直接套用其他配置。
|
||||||
|
|
||||||
|
参考:[OV5640 数据手册](https://files.waveshare.com/upload/d/da/OV5640_DataSheet.pdf)、[Espressif OV5640 驱动](https://github.com/espressif/esp32-camera/blob/master/sensors/ov5640.c)。新组合的 VCO 和 PCLK 在该手册列出的范围内;这不代替板上信号完整性和画面验证。
|
||||||
|
|
||||||
|
## Wi-Fi 运行时恢复
|
||||||
|
|
||||||
|
断开、失去有效 IP 或地址变化后停止 UDP 发送并清理订阅目标。关联和 DHCP 分别使用 15 秒期限;首次立即尝试,失败按 1、2、4、8、16、30 秒退避。事件回调只更新状态,主循环负责调用驱动;认证进行中不会反复 connect。取址后重建 socket、恢复无线参数、丢弃旧采集帧并重新广播,保留摄像头、曝光、增益、设备 ID 和帧序号。
|
||||||
|
|
||||||
|
实机诊断构建主动断开并保持 5 秒:检测断线后 9021 ms 恢复网络,其中重试发起到恢复日志为 4029 ms(含取址和恢复处理);无需重启,客户端随后自动续订恢复视频。该测试使用同一恢复逻辑、60 MHz SYSCLK 的预检构建;最终交付为 61 MHz,故障注入参数为 0。恢复到同一个 IP 已验证,路由器长时间关机、密码改变和 DHCP 换址尚未分别做硬件测试。启动阶段第一次联网仍使用原有扫描和认证模式探测逻辑。
|
||||||
|
|
||||||
|
处理方式遵循 [ESP-IDF 断线事件说明](https://docs.espressif.com/projects/esp-idf/en/v5.5.5/esp32s3/api-guides/wifi.html#wifi-event-sta-disconnected):有效 IP 恢复后再重建上层 socket。4 项恢复策略测试覆盖认证不被重启、关联/DHCP 超时、指数退避和新断线重置状态。
|
||||||
|
|
||||||
|
## 延迟测量方法与边界
|
||||||
|
|
||||||
|
Windows 客户端使用裁判配置和 `--latency-report <新的NDJSON路径>` 启动。相机固定对准左侧四角标记,右侧沿正常 `VideoSurface` 路径绘制真实回显。源标记每 50 ms 更新,记录每个编号首次可解码的画布回显;四角透视校正、Gray 码和互补行拒绝无法可靠识别的曝光混合帧。最初逐刷新更新在 180 Hz 屏幕上会让一次曝光跨越多个编号,已在正式对照前修正。
|
||||||
|
|
||||||
|
源标记和回显均使用同一客户端 `performance.now()`,无需 ESP 时钟同步。绘制时刻以相邻 rAF 夹定,表中为上下界中点;此前双缓冲样本的区间宽度中位数为 11.4 ms。该范围包括相机取样等待、曝光、采集、无线传输、重组、本地 WebSocket、解码和画布绘制。它是**屏幕标记事件到首次可解码画布回显的估计**,没有测量源/输出面板扫描或像素响应;严格的物理光学端到端延迟需用高速相机复核。未识别帧不成为样本,首次可解码回显可能晚于较早的模糊回显,不能把有效样本分布当作每一帧的延迟。
|
||||||
|
|
||||||
|
此前双缓冲试验中,解码完成到画布绘制平均 2.72 ms;标记分析平均 2.02 ms。该分析只在校准模式运行,正常客户端不做画布回读。字段 `decodeToDrawMs` 从解码完成开始,不代表 JPEG 解码耗时。接收间隔、分片组装耗时及发送函数耗时各有独立口径,不能替代本测量。
|
||||||
|
|
||||||
|
`scripts/analyze_latency.py` 可按半开区间行号复算 NDJSON。完整结果、参数、行号范围和边界见制品 `LATENCY-RESULT.json`,分布图见 `LATENCY.png`。原始校准记录保存在本机 `target/latency/`,不把画面、私有配置或令牌日志打进制品。
|
||||||
|
|
||||||
|
## 交付与硬件限制
|
||||||
|
|
||||||
|
该阶段 App SHA-256:`05aff1f53519fbb52b89ca526c3be11cc78f37a3356f588986336cf7870924eb`。Bootloader、分区表和 App 已经 COM8 烧录并独立校验,ZIP 内含校验清单。
|
||||||
|
|
||||||
|
本模组除原有 `0xFADE7` 坏点,还发现 `0x10863` 的 bit 6 异常读回为 1,整段回读只差这一字节,是否永久故障尚未单独擦写复核;已把应用分区移至 `0x50000` 并验证 `0xFADE7` 对应字节可写。该兼容布局针对故障模组,每次重编仍需重新核对,详见 [HARDWARE.md](HARDWARE.md)。
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# 图传性能排查与验证(2026-10-18)
|
||||||
|
|
||||||
|
本文保留早期短时对照;当前交付已回退到 15 dBm,功率、距离与发送拥塞分析见 [POWER.md](POWER.md),阻塞处理见 [REALTIME.md](REALTIME.md)。此前约 30 fps 与回显延迟数据见 [LATENCY.md](LATENCY.md),更早验证见 [STABILITY.md](STABILITY.md)。下列数字对应当时的传感器时序和网络状态。
|
||||||
|
|
||||||
|
## 环境与结论
|
||||||
|
|
||||||
|
测试使用当前 ESP32-S3-N16R8 / OV5640,VGA 640×480、8 MB PSRAM、CPU 240 MHz、ESP-IDF v5.5.5。板端连接 2.4 GHz Wi-Fi,实际协商 HT20 / CCMP,主要有效窗口的信号约 −18~−32 dBm;Windows 通过同一路由器通信,Mihomo TUN 始终保持启用。单客户端对照使用 1 MiB 接收缓冲,每 800 ms 单播续订。
|
||||||
|
|
||||||
|
传感器在 CPU0 和 CPU1 均可输出约 22.2 fps。低帧率期间持续出现 `sendto` 的 `ENOMEM`(errno 12),完整帧提交率低于采集率,客户端收到不完整分片。普通系统内存仍有约 100 KiB 以上可用,证据指向无线发送拥塞与驱动缓冲回压。核绑定和单独扩大 Windows 缓冲均未解决主要瓶颈。
|
||||||
|
|
||||||
|
降低最大发射功率、使用视频队列,并降低单帧大小后有所改善。功率切换对照中,20 dBm 的窗口明显恶化,10 dBm 的两次窗口约 12 fps;14 dBm 的结果仍有波动。仅凭这些数据无法确定供电、天线、射频或 AP 的具体物理原因。
|
||||||
|
|
||||||
|
## 接收端对照
|
||||||
|
|
||||||
|
| 配置 | 时长 | 完整帧 | 窗口 fps | 帧损失 | 完整帧间隔 P95 | 分片组装 P95 |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| 问题复测:20 dBm / 普通队列 / quality 12 | 30 s | 165 | 5.49 | 74.69% | 672.0 ms | 109.0 ms |
|
||||||
|
| 14 dBm / 普通队列 / quality 12 | 30 s | 477 | 15.88 | 27.84% | 242.2 ms | 145.4 ms |
|
||||||
|
| 14 dBm / 视频队列 / quality 12 | 60 s | 1091 | 18.18 | 17.41% | 194.4 ms | 103.2 ms |
|
||||||
|
| 该阶段选择:14 dBm / 视频队列 / quality 18 | 60 s | 1177 | 19.62 | 11.30% | 192.8 ms | 94.2 ms |
|
||||||
|
|
||||||
|
表中 fps = 完整帧数 / 整个测量时长。帧损失包括被替换的不完整帧、超时及帧号缺口;末尾仍在接收的帧不计损失。丢帧率与旧客户端的口径不同,旧版会低估实际丢帧。
|
||||||
|
|
||||||
|
无线链路有明显时变,早期其它窗口也出现过约 5~15 fps,表格不是长期保证或所有板卡的通用最优参数。前两行使用 CPU0,后两行使用 CPU1;核绑定对照未消除发送拥塞,最终保留显式 CPU1 配置。关闭 AMPDU 的试验几乎无法得到完整帧,已撤回;忙等重试也已撤回。另一次 40 ms 重试预算试验期间,有新客户端加入导致两个订阅者,且信号和画面复杂度变化;该轮约 12.6 fps,不能用于证明重试预算的单项影响,未作为最终配置依据。
|
||||||
|
|
||||||
|
## 最终参数与改动
|
||||||
|
|
||||||
|
- 该测试阶段本机 `.env` 选择 `CAM_QUALITY=18`、`WIFI_TX_POWER_DBM=14`。分辨率保持 VGA;quality 数字增大表示压缩更强,细节会有所损失。仓库默认 quality 仍为 12,未设置发射功率时保留 SDK 默认上限;当前功率选择见 [POWER.md](POWER.md)。
|
||||||
|
- 视频 socket 设置 `IP_TOS=128`,使用 AC_VI,并保留 AMPDU;64 个动态 TX 缓冲、TX BA window 32、FreeRTOS 1000 Hz。JPEG 在 PSRAM 中原地读取,仅复用一个数据报缓冲;临时发送失败让出调度重试,等待预算为每帧 25 ms。
|
||||||
|
- 客户端按发现的设备 IP 定向续订,接收缓冲请求 1 MiB。随机会话 ID 让同一会话的端口变化更新目标地址;旧固定 ID 保留按地址区分。实测迁移后旧端口无新视频,新端口继续接收;两个旧版订阅仍各自收到数据。
|
||||||
|
- 接收器拒绝迟到旧帧、重复完成帧和矛盾分片,处理帧号回绕、设备重启及超时。视频 WebSocket 只保留各路最新帧;前端每路至多一次解码与一张最新待解码帧,避免持续积压。
|
||||||
|
|
||||||
|
## Windows 实际客户端验证
|
||||||
|
|
||||||
|
使用已构建的 release EXE 和独立裁判端测试配置,非演示模式。原用户配置、密码和绑定保留。
|
||||||
|
|
||||||
|
约 30 秒窗口中,后端完整帧增量 624、丢帧增量 38,窗口帧损失约 5.74%。界面连接存在,独立观察连接收到 619 张完整 JPEG,通道帧率 20.62 fps,帧间隔 P95 为 103.0 ms,无损坏 JPEG。
|
||||||
|
|
||||||
|
这项验证覆盖真实板卡、UDP 接收与重组、GUI 的 WebSocket 连接及二进制视频通道。另有 20 项协议测试、83 项客户端核心测试、9 项后端视频通道测试、5 项前端解码队列测试,以及 Windows 类型检查和生产打包通过。未将整个后端测试集或录制功能记为已验证。
|
||||||
|
|
||||||
|
**时间边界:**组装耗时是首末分片到达的差值,帧间隔是相邻完整帧到达的差值,均不是摄像头到屏幕的端到端延迟。未做两端时钟同步或实际屏幕的光学延迟测量,也未自动验证 WebView 最终绘制像素。
|
||||||
|
|
||||||
|
## 复测与原始记录
|
||||||
|
|
||||||
|
先退出其它图传接收进程,避免增加订阅负载:
|
||||||
|
|
||||||
|
```text
|
||||||
|
python scripts/perf_probe.py <板卡IP> --seconds 60 --json perf.json --save-frame frame.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
本机原始 JSON、抽帧和串口日志在 `target/camera-diagnostics/`,随构建缓存忽略;主要文件为 `fps-core0-raw.json`、`fps-tx14-raw.json`、`fps-quality12-raw.json`、`fps-final-raw.json`、`fps-final-client-result.json`、`session-migration.json`。固件与 Windows 制品的 `BUILD-INFO.json` 记录最终测量及验证范围,ZIP 不包含用户运行配置。
|
||||||
|
|
||||||
|
设计依据:[ESP-IDF 5.5.5 的 UDP 发送回压说明](https://docs.espressif.com/projects/esp-idf/en/v5.5.5/esp32s3/api-guides/lwip.html#limitations)、[Wi-Fi QoS 的 IP precedence / AC_VI 映射](https://docs.espressif.com/projects/esp-idf/en/v5.5.5/esp32s3/api-guides/wifi.html#wi-fi-qos)。这些文档说明机制,性能数字来自上述本机测量。
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# 发射功率、断连距离与拥塞分析(2026-10-18)
|
||||||
|
|
||||||
|
当前交付请求 `WIFI_TX_POWER_DBM=15`,实板驱动回读 **15.00 dBm**。根据用户的距离与拥塞反馈,从 17 dBm 回退;启动和 Wi-Fi 重连后应用同一设置,压缩与温控控制器不自动降低发射功率。三缓冲、VGA 彩色、AE 目标 45、约 20 ms 曝光上限和 PLL 时钟保持不变。
|
||||||
|
|
||||||
|
这里的数值是驱动配置的发射功率上限。API 回读不能代替功率计,不能认为所有 PHY 速率下、每个数据包的天线端输出都恰好相同。
|
||||||
|
|
||||||
|
## 先前 17 dBm 记录说明了什么
|
||||||
|
|
||||||
|
| 记录 | 时间 | 原生接收帧率 | 原生帧损失率 | 板端发送失败 / 应用重试 / 放弃目标帧 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 17 dBm 第一轮 | 视频 30 s / 串口 75 s | 29.78 fps | 0.667% | 90 / 80 / 10(整段串口) |
|
||||||
|
| 17 dBm 第二轮,增加周期回读 | 视频 30 s / 串口 75 s | 27.92 fps | 6.889% | 498 / 446 / 52(整段串口) |
|
||||||
|
|
||||||
|
两轮的发射上限相同,曝光与缓冲参数相同,但无线状态、场景内容和时间并未锁定,且第二轮多了周期功率读数。因此这些记录不构成严格 A/B 对照,不能单凭它们断定“增加 3 dB 必然导致某个百分比的丢帧”。用户反馈是回退依据,日志用于定位失败发生在哪一层。
|
||||||
|
|
||||||
|
第二轮内部空闲堆最少 120039 字节、最大连续块最少 43008 字节,温度 57–58°C,无温控限帧、复位或采集错误,只有一个 UDP 订阅者。出现发送失败时伴有 `errno 12 / ENOMEM`,优先指向网络发送资源的瞬时耗尽。堆仍有余量不能排除受配额限制的 Wi-Fi TX 缓冲已满,也不能把这个错误直接当成内存泄漏。
|
||||||
|
|
||||||
|
ESP-IDF 明确说明,UDP 连续 `send/sendto` 可以因底层驱动 TX 缓冲全部占用而返回 `ENOMEM`。目前日志中的“446 次重试”是**应用调用 sendto 的重试**,不是已测得的 802.11 空口重传次数。来源:[ESP-IDF v5.5.5 lwIP 文档](https://docs.espressif.com/projects/esp-idf/en/v5.5.5/esp32s3/api-guides/lwip.html#limitations)。
|
||||||
|
|
||||||
|
## 为什么距离改善不明显
|
||||||
|
|
||||||
|
1. **功率差只有 3 dB。** 14→17 dBm 的配置上限约为两倍功率,不是两倍距离。按简化路径损耗模型 `距离倍率 = 10^(3/(10n))`,自由空间 `n=2` 时约 1.41 倍,假设室内 `n=3–4` 时约 1.19–1.26 倍。这只是模型估算,不是本次测得的距离。
|
||||||
|
2. **收发链路必须同时成立。** 增强板卡发射不能增强板卡接收;AP 信标、应答、订阅和数据回程仍依赖另一方向。ESP-IDF 的断连说明包含持续丢失信标等原因。当前记录没有最远点的断连 reason、AP 侧接收强度或重传统计,不能确定距离边界由哪一方向限制。来源:[ESP-IDF 断连事件说明](https://docs.espressif.com/projects/esp-idf/en/v5.5.5/esp32s3/api-guides/wifi.html#wifi-event-sta-disconnected)。
|
||||||
|
3. **回读是上限,RSSI 有方向。** 板端日志里的 RSSI 是板卡接收到 AP 的信号,不是 AP 接收到板卡的信号。17 dBm 第二轮 RSSI 为 −25 至 −14 dBm,属于近距离记录,不能拿来证明远距离的上行链路已经改善。
|
||||||
|
|
||||||
|
## 为什么拥塞仍会加重
|
||||||
|
|
||||||
|
已确认的应用行为是:每约 33 ms 取得一张 JPEG,再把它拆成多个数据报连续提交,分片间隔为 0;成功返回只表示本地网络栈接受,不能证明 AP 或电脑已经收到。每帧 8 ms 重试预算和 25 ms 持有期限限制应用等待,但不能撤回已经进入 SDK 的旧分片。
|
||||||
|
|
||||||
|
提高发射上限不会增加信道带宽或路由器的排队能力。若空口竞争、速率回退或重传使驱动排空变慢,突发提交仍会填满 TX 缓冲。这是与日志一致的机制推断;本次没有 CCA 忙占比、MAC 重传、实际发送 MCS 或 AP 队列数据,尚不能把它们中的某一项确认为功率变化造成的根因。来源:[ESP-IDF TX 数据路径与缓冲说明](https://docs.espressif.com/projects/esp-idf/en/v5.5.5/esp32s3/api-guides/wifi.html#how-to-improve-wi-fi-performance)。
|
||||||
|
|
||||||
|
更高功率改变干扰覆盖、射频线性度或供电瞬态,也可能间接影响链路,但这里没有相应测量,不将“接收端过载”“功放饱和”或“电压跌落”写成已确认原因。无 Brownout 记录也不能证明射频供电完全没有瞬态变化。
|
||||||
|
|
||||||
|
电脑当前通过 **5 GHz / 信道 161 / 802.11ac** 连接,摄像头使用 2.4 GHz。因此不能直接声称视频在同一个 2.4 GHz 信道被路由器重复发送了两次;摄像头上行、AP 转发、电脑接收与 TUN 处理仍可能分别排队,TUN 保持原样。
|
||||||
|
|
||||||
|
## 还需区分本地跳帧
|
||||||
|
|
||||||
|
17 dBm 第二轮视频区间中,原生接收了 838 张完整 JPEG,而辅助本地 WebSocket 观察器收到 582 张。两者不是同一个指标:后端采用最新帧覆盖和帧龄期限,慢消费者可能主动跳过已完整收到的帧;少收到的本地消息不能全部算成无线丢帧,更不能直接等同于真实屏幕绘制帧率。
|
||||||
|
|
||||||
|
## 当前回退验证与后续定位
|
||||||
|
|
||||||
|
测试期间用户补充观察:用手握持开发板时丢包明显增多。测试没有逐秒标记握持、位置和朝向,五分钟记录应视作现场连续性观察,不能当作固定摆位的功率 A/B 测试。人体靠近 PCB 天线导致的吸收、失谐或遮挡成为更值得优先排查的线索。
|
||||||
|
|
||||||
|
本轮只改变功率上限到 15 dBm,保持其他图传参数与单 UDP 订阅,做五分钟收流观察;结果由交付包 `POWER-RESULT.json` 记录。之前的 17 dBm 记录保留为 `POWER-HISTORY-tx17.json`,14 dBm 的阻塞处理记录保留在 [REALTIME.md](REALTIME.md)。本轮不重新测量最远断连距离或物理屏幕端到端延迟。
|
||||||
|
|
||||||
|
下一项值得单独对照的是**分片突发整形**:固定 15 dBm、曝光和 JPEG 策略,按发送回压逐步比较小批次提交与短暂让出调度,并同时看原生完整帧率、网络丢帧、主动跳帧和帧龄。不能仅通过增大缓冲或延长重试,把丢帧转化为持续排队延迟。若要确认距离瓶颈,需要同步记录远点断连原因、AP 侧板卡 RSSI、MAC 重传和信道忙占比。
|
||||||
|
|
||||||
|
## 蛇形 PCB 天线的握持改善
|
||||||
|
|
||||||
|
手接近天线会改变其周围的电磁环境,人体和外壳可使 PCB 天线失谐;人体也会吸收、遮挡部分射频能量。这是对用户观察的机制解释,本板尚未测量 S11、效率或握持前后的辐射图。TI 的 2.4 GHz PCB 天线实测例子显示,加入外壳并手持后匹配状态明显变化,需要在实际使用条件下匹配。来源:[TI 天线阻抗测量与匹配,章节 6](https://www.ti.com/lit/an/swra726/swra726.pdf)。
|
||||||
|
|
||||||
|
建议按以下顺序改善:
|
||||||
|
|
||||||
|
1. **先改善握持和摆位。** 用塑料支架或绝缘延长握柄固定,让蛇形天线端伸出、远离手掌;握住远离天线的一端。可先尝试让手与天线相距约 3–5 cm,再按实测增加距离,这不是保证性能的固定阈值。
|
||||||
|
2. **留出天线净空。** Espressif 建议成品外壳中天线各方向至少 15 mm 净空;这不是“距离手 15 mm 就一定够”的保证。天线附近避开电池、金属支架、屏蔽片和紧贴的 USB 线或排线,外壳在天线区域使用非金属材料。来源:[ESP32-S3 PCB 天线布局指南](https://docs.espressif.com/projects/esp-hardware-design-guidelines/en/latest/esp32s3/pcb-layout-design.html#general-principles-of-pcb-layout-for-modules-positioning-a-module-on-a-base-board)。
|
||||||
|
3. **固定朝向再比较。** 在同一位置和画面下,保持开发板朝向固定,比较“不接触 → 握远端 → 手靠近天线 → 移开手”,每段 30–60 秒,同时记录完整帧损失和板端 RSSI。单看 RSSI 不能覆盖上行质量,但可帮助区分朝向、遮挡与队列负载变化。
|
||||||
|
4. **需要长期手持时,优先考虑外置天线版本。** `ESP32-S3-WROOM-1` 为 PCB 天线,`WROOM-1U` 为外置天线接口版本。选兼容摄像头接线、Flash/PSRAM 配置的板卡,将合适的 2.4 GHz 天线安放在握持区域之外;具体封装、接口和射频连接须按实际板卡核对。外置天线也应远离手和金属。来源:[Espressif 模组数据手册](https://documentation.espressif.com/esp32-s3-wroom-1_wroom-1u_datasheet_en.pdf)。
|
||||||
|
5. **必须贴近人体使用时,按最终结构调谐。** 外壳、支架、馈线和手持姿态都参与最终匹配,需要 VNA/射频测量后调整匹配网络。直接延长蛇形走线、随意焊线或并接两根天线不能代替调谐;在现板上先做可逆的机械隔离更容易验证。
|
||||||
|
|
||||||
|
## 15 dBm 本轮结果
|
||||||
|
|
||||||
|
当前 15 dBm 交付 App SHA-256:`a6092e4b0cf42819c04424e066651f285b8961312a73822d667532f800e46767`,应用分区 `0x70000`。COM8 烧录与三段独立校验通过;355.1 秒串口的 71 次功率回读均为 15.00 dBm。五分钟观察:原生完整接收 **29.68 fps**,丢帧 **1.045%**,本地 JPEG 通道 **28.23 fps**,31 张抽帧全部完整解码为 640×480 RGB,无无效 JPEG、设备离线、通道断开、复位或采集错误;仍有最长 1.646 秒到帧间隔,不能将此轮称为无卡顿。距离与拥塞分析、测试边界见 [POWER.md](POWER.md) 和制品 `POWER-RESULT.json`。
|
||||||
|
|
||||||
|
按时间近似对齐的板端五分钟区间有 731 次发送失败、653 次应用重试、78 次目标帧放弃;温度 56–58°C,RSSI -35 至 -11 dBm。板端区间由五秒采样的结束时间选择,边界并非逐包精确对齐。与此前短测时长、现场无线状态和 JPEG 内容不同,不能将差值归因于功率这一项。
|
||||||
@@ -1,395 +1,316 @@
|
|||||||
# ESP32-S3 摄像头图传(Rust / ESP-IDF / UDP)
|
# ESP32-S3 摄像头图传系统
|
||||||
|
|
||||||
两块 ESP32-S3-WROOM-1-N16R8 图传板(DVP 接口接 OV5640),向队伍客户端与裁判终端推
|
基于 **Rust + ESP-IDF** 的局域网实时图传系统。ESP32-S3 采集摄像头输出的 JPEG,通过 UDP 向桌面客户端推流;桌面端提供队伍端与裁判端界面,并接收 ESP32-C3 装甲板的血量广播。
|
||||||
UDP 图传。
|
|
||||||
|
|
||||||
固件是跑在 ESP-IDF 上的 Rust,用官方 esp32-camera 组件采集;桌面端是 **Tauri v2 +
|
## 功能与架构
|
||||||
React**,用户端与裁判端两套页面(见 [`desktop/README.md`](desktop/README.md))。同一套
|
|
||||||
系统里还有一队一块的**装甲板**(ESP32-C3,一主三从),负责四路击打检测、灯带与整车血量,
|
- 摄像头自动探测、JPEG 采集、UDP 分片发送与完整帧重组。
|
||||||
并把血量广播给局域网里的两端。
|
- 局域网广播发现与订阅,单块图传板最多向 4 个客户端发送画面。
|
||||||
|
- 运行中 Wi-Fi 断开后自动重连,重新获得 IP 后重建 UDP socket 并恢复订阅。
|
||||||
|
- 使用 MAC 派生的稳定设备 ID;两块图传板可烧录同一固件,在客户端绑定队伍。
|
||||||
|
- Tauri v2 + React 桌面端:视频显示、双方血量、设备绑定与录制。
|
||||||
|
- 独立 ESP32-C3 装甲板固件:四路受击检测、WS2812 灯带及血量上报。
|
||||||
|
|
||||||
```text
|
```text
|
||||||
protocol/ 线路格式 + 帧重组 三端共用,无 ESP-IDF 依赖,14 个单元测试
|
ESP32-S3 图传板 A ─┐ ┌─ 队伍客户端
|
||||||
. 图传固件 -> ESP32-S3 交叉编译,Xtensa
|
├─ 同一局域网 / UDP ─┼─ 裁判客户端
|
||||||
armor/ 装甲板固件 -> ESP32-C3 独立 workspace,RISC-V;规则层 22 个单元测试
|
ESP32-S3 图传板 B ─┤ └─ 队伍客户端
|
||||||
desktop/ 桌面端 Tauri v2 + React;用户端(双方血条 + 瞄准器)/ 裁判端(可改队名)
|
ESP32-C3 装甲主板 ─┘
|
||||||
core/ 不含界面与 Tauri 的网络层、配置、密码(可单测)
|
|
||||||
src-tauri/ Tauri 后端:推流、命令、录制
|
|
||||||
src/ React 前端:两套页面 + HUD
|
|
||||||
```
|
```
|
||||||
|
|
||||||
五份产物各自独立构建,各有自己的 `.cargo/config.toml`。
|
图传使用 UDP **5600**,装甲板血量使用 UDP **5601**。板卡与客户端需在可互相通信、允许广播的网络中;关闭 AP 客户端隔离,并允许客户端程序通过防火墙。
|
||||||
|
|
||||||
## 拓扑
|
## 项目结构
|
||||||
|
|
||||||
|
| 路径 | 用途 |
|
||||||
|
| --- | --- |
|
||||||
|
| `src/` | ESP32-S3 图传固件,根目录 Cargo 项目 |
|
||||||
|
| `components/` | 本地相机驱动及 C/Rust 接口封装 |
|
||||||
|
| `protocol/` | 共享 UDP 协议与帧重组,支持主机测试 |
|
||||||
|
| `armor/` | 独立 ESP32-C3 装甲板 workspace |
|
||||||
|
| `desktop/` | Tauri + React 客户端及独立网络核心 `core/` |
|
||||||
|
| `scripts/` | 构建、烧录与探测脚本 |
|
||||||
|
| `reference/armor/` | 早期 C 版装甲板工程 |
|
||||||
|
|
||||||
|
各子项目的构建目标不同。以下编译、烧录流程针对**根目录的 ESP32-S3 图传固件**。
|
||||||
|
|
||||||
|
## 硬件要求
|
||||||
|
|
||||||
|
| 项目 | 当前配置 |
|
||||||
|
| --- | --- |
|
||||||
|
| 开发板 | Freenove ESP32-S3-WROOM CAM |
|
||||||
|
| 模组 | ESP32-S3-WROOM-1-N16R8,16 MB Flash + 8 MB Octal PSRAM |
|
||||||
|
| 摄像头 | OV5640;驱动包含 OV2640 / OV3660 支持,接线需核对 |
|
||||||
|
| 网络 | 2.4 GHz Wi-Fi |
|
||||||
|
| 连接 | 支持数据传输的 USB 线、可识别的串口及稳定供电 |
|
||||||
|
|
||||||
|
默认采集 VGA(640 × 480)、彩色 JPEG quality 12,按传感器节拍发送。相机引脚、Flash 分区和 PSRAM 配置见 [HARDWARE.md](HARDWARE.md)。换板型前必须核对接线,避免占用 Flash / PSRAM 引脚。
|
||||||
|
|
||||||
|
## ESP Rust 工具链入门
|
||||||
|
|
||||||
|
本项目使用 Rust `std` 和 ESP-IDF。ESP32-S3 属于 **Xtensa** 架构,需要通过 `espup` 安装 Espressif Rust 工具链;普通 `rustup target add` 不能替代这一步。根目录固定选择名为 `esp-1.97.0.0` 的工具链,避免浮动版本影响构建。
|
||||||
|
|
||||||
|
### 1. 安装基础依赖与 Rust 工具
|
||||||
|
|
||||||
|
先安装 [Rust / rustup](https://rustup.rs/)、[Git](https://git-scm.com/downloads)、[Python 3](https://www.python.org/downloads/)(建议 3.11)、[CMake](https://cmake.org/download/) 与 [Ninja](https://ninja-build.org/),确保命令在 `PATH` 中。
|
||||||
|
|
||||||
|
- **Windows**:使用 PowerShell 7.5 或更新版本,安装 Visual Studio Build Tools 的“使用 C++ 的桌面开发”和 Windows SDK,使用 MSVC Rust 主机工具链。仓库和工具链尽量使用较短、无空格和中文的路径。
|
||||||
|
- **Linux(Debian / Ubuntu)**:安装 `build-essential pkg-config libudev-dev libssl-dev python3 python3-venv cmake ninja-build git curl`。
|
||||||
|
- **macOS**:安装 Xcode Command Line Tools(`xcode-select --install`),以及 Python、CMake 和 Ninja。
|
||||||
|
|
||||||
|
以下命令显式使用 `+stable`,即使在仓库内执行,也不会要求事先已有 `esp`:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
┌──────────────────────────────┐
|
rustup toolchain install stable
|
||||||
│ 裁判终端(PC,仅显示) │
|
cargo +stable install espup --locked
|
||||||
│ 队伍一 + 队伍二 + 双方血量 │
|
cargo +stable install ldproxy --locked
|
||||||
└───────────────┬──────────────┘
|
cargo +stable install espflash --version 4.4.0 --locked
|
||||||
│ 同一个局域网,广播可用
|
espup install --name esp-1.97.0.0 --toolchain-version 1.97.0.0 --targets esp32s3 --std
|
||||||
┌────────────────────┼────────────────────┐
|
|
||||||
│ │ │
|
|
||||||
┌───────┴───────┐ ┌───────┴───────┐ ┌───────┴───────┐
|
|
||||||
│ 图传板 A │ │ 图传板 B │ │ 装甲板主板 │
|
|
||||||
│ (队伍一) │ │ (队伍二) │ │ ×2(每队一块)│
|
|
||||||
└───────────────┘ └───────────────┘ └───────────────┘
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**图传板不区分队伍。** 它们只知道自己是谁(MAC 派生的 `sender_id`),"哪一边"由
|
也可以从各工具的官方 releases 下载预编译程序。`espup --std` 安装本项目所需的 Rust / LLVM 工具,ESP-IDF 的 C 工具链由后续构建准备。
|
||||||
客户端决定。烧同一份固件即可,配置全在客户端一侧。
|
|
||||||
|
|
||||||
**装甲板反过来:队伍是编译期定的**(`ARMOR_TEAM=0/1`,同时决定灯色红/蓝),因为血量
|
Linux / macOS 每次新开终端后执行:
|
||||||
上报里必须带 `team` 字段,而且灯色现场不该靠按键切错。见 `armor/README.md`。
|
|
||||||
|
|
||||||
裁判终端同时收两块图传板,也收两块装甲板主板在 5601 上广播的血量。队伍客户端也收
|
```bash
|
||||||
全部板子的视频,然后在**界面层**过滤到绑定的那一块;**血量相反,两边都显示**——用户端的
|
. "$HOME/export-esp.sh"
|
||||||
画面左上角是自己、右上角是对手。血量本来就是明文广播的(裁判端与用户端收的是同一份),而
|
```
|
||||||
"自己的血与对手的血一起看"是这项运动里最基本的对照,藏起来只会让操作手看不见对手的血。
|
|
||||||
|
|
||||||
## 实测性能
|
Windows 安装后重新打开 PowerShell,让环境变量生效;若当前终端仍找不到 LLVM,可执行 `. "$HOME/export-esp.ps1"`。检查结果:
|
||||||
|
|
||||||
VGA 640x480,传感器产出上限 22 fps(OV5640 在此配置下的实测值)。
|
```text
|
||||||
|
rustup toolchain list
|
||||||
|
rustc +esp-1.97.0.0 --version
|
||||||
|
cargo +esp-1.97.0.0 --version
|
||||||
|
espflash --version
|
||||||
|
```
|
||||||
|
|
||||||
| | TCP(HTTP MJPEG) | UDP(现在) |
|
官方入门资料:[The Rust on ESP Book](https://docs.espressif.com/projects/rust/book/getting-started/toolchain.html)、[espup](https://github.com/esp-rs/espup)、[espflash](https://github.com/esp-rs/espflash/tree/v4.4.0/espflash)。
|
||||||
|
|
||||||
|
### 2. 准备 ESP-IDF
|
||||||
|
|
||||||
|
默认使用 **ESP-IDF v5.5.5**。首次构建时,`esp-idf-sys` 下载 SDK、对应 C 工具链及 Python 依赖,默认缓存到用户目录的 `.espressif/`;需要网络、磁盘空间及缓存目录写入权限。
|
||||||
|
|
||||||
|
已有匹配版本 SDK 时可在终端设置 `IDF_PATH`,不要提交个人绝对路径:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Windows:替换为实际 SDK 源码目录。
|
||||||
|
$env:IDF_PATH = 'C:\Espressif\frameworks\esp-idf-v5.5.5'
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux / macOS:替换为实际 SDK 源码目录。
|
||||||
|
export IDF_PATH="$HOME/esp/esp-idf-v5.5.5"
|
||||||
|
```
|
||||||
|
|
||||||
|
使用自动下载时,清除指向其他 SDK 的旧 `IDF_PATH`。缓存位置等高级设置见 [esp-idf-sys 构建选项](https://github.com/esp-rs/esp-idf-sys/blob/master/BUILD-OPTIONS.md)。
|
||||||
|
|
||||||
|
## 配置 Wi-Fi 与摄像头
|
||||||
|
|
||||||
|
在根目录复制 `.env.example` 为 `.env`,再用编辑器填写;已有 `.env` 时不要覆盖。
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Windows PowerShell
|
||||||
|
Copy-Item .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux / macOS
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
WIFI_SSID='你的2.4G网络名称'
|
||||||
|
WIFI_PASS='你的Wi-Fi密码'
|
||||||
|
|
||||||
|
# 可选,省略时使用固件默认值。
|
||||||
|
#CAM_RES=vga
|
||||||
|
#CAM_QUALITY=12
|
||||||
|
#CAM_QUALITY_MAX=0
|
||||||
|
#CAM_MAX_FPS=0
|
||||||
|
#CAM_CLOCK_PROFILE=default
|
||||||
|
#CAM_FB_COUNT=3
|
||||||
|
#CAM_GRAYSCALE=0
|
||||||
|
#CAM_EXPOSURE_EV=0
|
||||||
|
#CAM_AE_LEVEL=-1
|
||||||
|
#CAM_SHUTTER_LINES=0
|
||||||
|
#CAM_EXPOSURE_MAX_LINES=0
|
||||||
|
#CAM_GAIN_CEILING=0
|
||||||
|
#CAM_HTS=0
|
||||||
|
#UDP_PACKET_GAP_US=0
|
||||||
|
#UDP_RETRY_BUDGET_US=8000
|
||||||
|
#CAM_THERMAL_LIMIT_C=75
|
||||||
|
#CAM_VSYNC_INVERT=1
|
||||||
|
#WIFI_AUTH=wpa2
|
||||||
|
```
|
||||||
|
|
||||||
|
| 变量 | 默认值 | 说明 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 传感器实际产出 | 22 fps | 22 fps |
|
| `WIFI_SSID` | 无,必填 | 2.4 GHz 网络名称,最多 32 字节 |
|
||||||
| 客户端实际收到 | **1.9 fps** | **22 fps** |
|
| `WIFI_PASS` | 空字符串 | Wi-Fi 密码;开放网络可留空 |
|
||||||
| 实际吞吐 | 0.28 Mbit/s | 1.6 Mbit/s |
|
| `CAM_RES` | `vga` | `qqvga` / `qvga` / `cif` / `hvga` / `vga` / `svga` / `xga` / `sxga` / `uxga` |
|
||||||
| 丢帧 | — | 0% |
|
| `CAM_QUALITY` | `12` | 0–63,越小通常画质越好、JPEG 越大 |
|
||||||
|
| `CAM_QUALITY_MAX` | `0` | `0` 固定画质;设为 `CAM_QUALITY`–63 时按发送回压在两者之间调节压缩,保留彩色和分辨率。拥塞时每 5 秒增大最多 3 档,连续稳定 15 秒才恢复 1 档 |
|
||||||
|
| `CAM_MAX_FPS` | `0` | 取帧与发送的最高节拍,1–60;`0` 不限帧。仅限制消费节拍,不修改传感器时钟 |
|
||||||
|
| `CAM_CLOCK_PROFILE` | `default` | `default` 保留驱动时钟;`vga30` 使用 OV5640 VGA 约 30.09 fps PLL 配置,保持 HTS/VTS 和读出窗口,SYSCLK 61 MHz / PCLK 15.25 MHz。切换后需重新核对曝光行数 |
|
||||||
|
| `CAM_FB_COUNT` | `3` | PSRAM 帧缓冲数,允许 2 或 3。当前交付选三缓冲,保留发送突发时的采集余量;双缓冲曾在短测降低部分回显延迟,但用户长程运行反馈峰值丢帧明显,保留为试验选项 |
|
||||||
|
| `CAM_GRAYSCALE` | `0` | `1` 使用传感器灰度效果,保留硬件 JPEG;`0` 彩色 |
|
||||||
|
| `CAM_EXPOSURE_EV` | `0` | OV5640 自动曝光目标偏置:`-1` / `-2` 约对应 −1.1 / −1.9 EV;不是标定后的绝对 EV,也不保证快门缩短 |
|
||||||
|
| `CAM_AE_LEVEL` | 未设置 | 可选 OV5640 驱动 AE 档位,−5–5;仅与 `CAM_EXPOSURE_EV=0` 同用。`-1` 将目标从 55 小幅降至 45,目标比值约 −0.29 EV,保留自动曝光/增益;未设置时按 EV 参数处理。实际快门和亮度变化取决于场景 |
|
||||||
|
| `CAM_SHUTTER_LINES` | `0` | OV5640 固定快门,单位为传感器曝光行;`0` 自动曝光,非零保持自动增益。默认 VGA / HTS 2060 下 220 行估算约 10 ms;数值不能超过当前 VTS |
|
||||||
|
| `CAM_EXPOSURE_MAX_LINES` | `0` | OV5640 自动曝光上限,`0` 保留驱动默认。非零保持 AEC/AGC 联动并禁用夜景延帧,不能与固定快门同时使用。默认 VGA 下 440 行约 20 ms;`vga30` 下 592 行约 20 ms。实际曝光由场景和抗频闪步长决定 |
|
||||||
|
| `CAM_GAIN_CEILING` | `0` | OV5640 自动增益上限,`0` 保留驱动默认;1–63,单位为倍,实际上限略低于整数档。短曝光遇到室内弱光时可提高,但暗部噪点和 JPEG 负载可能增加 |
|
||||||
|
| `CAM_HTS` | `0` | OV5640 水平总长度试验参数,`0` 使用驱动默认。缩短消隐会影响彩色稳定性,本机 1500 / 1800 出现绿紫闪烁,已撤回;几何检查通过不代表可用 |
|
||||||
|
| `UDP_PACKET_GAP_US` | `0` | 成功提交视频数据报之间的最小间隔;0–5000 μs,须为调度 tick 的整数倍(当前为 1000 μs)。配合有界帧等待,避免集中突发写入队列 |
|
||||||
|
| `UDP_RETRY_BUDGET_US` | `8000` | 每帧、所有分片及订阅者共用的让出调度重试预算,0–25000 μs,须为 tick 整数倍;`0` 为非阻塞失败即弃。总发送检查期限为 25 ms,采集帧龄期限为 100 ms;分片间隔也计入期限,不会额外延长 |
|
||||||
|
| `CAM_THERMAL_LIMIT_C` | `75` | 芯片内部温度达到该值时减半帧率(向上取整),下降 5°C 后恢复;40–95,`0` 禁用。原先不限帧时降至 8 fps |
|
||||||
|
| `CAM_VSYNC_INVERT` | `1` | 驱动反相 VSYNC;硬件已反相时填 `0` |
|
||||||
|
| `WIFI_AUTH` | 自动选择 | `none` / `wep` / `wpa` / `wpa2` / `wpawpa2` / `wpa3` / `wpa2wpa3` / `wapi` |
|
||||||
|
| `WIFI_TX_POWER_DBM` | SDK 默认上限 | 可选整数 2–20 dBm;当前交付请求 15 dBm,本板驱动回读 15.00 dBm;启动及重连时应用,拥塞和温控逻辑不降低此上限 |
|
||||||
|
|
||||||
TCP 那一栏只用了链路能力的 1% 左右(ESP32-S3 在良好 2.4GHz 链路上 TCP 实测上限约
|
`.env` 使用简单的 `KEY=value`;值含空格、`#` 或 `$` 时用单引号包围,不使用变量展开或命令替换。脚本会加载 `.env`,直接 `cargo build` 不会自动读取它。上述参数在编译时写入固件,修改后需要重新编译和烧录。`.env` 已被 Git 忽略;固件也包含凭据,分享前应重新配置。
|
||||||
20–30 Mbit/s)。**链路是空的**,帧却堵在队头阻塞后面等一个实时画面等不起的重传计时
|
|
||||||
器。
|
|
||||||
|
|
||||||
UDP 没有重传与确认:缺了分片的帧整个丢弃,下一帧顶上。丢帧率实测 0%,因为带宽从来
|
运行时断线恢复不重置摄像头、曝光或增益。首次尝试立即发起,失败后按 1、2、4、8、16、30 秒退避;关联与 DHCP 分别有 15 秒期限。恢复 IP 后丢弃旧采集帧、重建 socket 并重新广播,由客户端自动续订。启动时首次联网仍使用扫描与认证模式探测流程。实现依据 [ESP-IDF 断线事件处理说明](https://docs.espressif.com/projects/esp-idf/en/v5.5.5/esp32s3/api-guides/wifi.html#wifi-event-sta-disconnected)。
|
||||||
不是瓶颈——即便如此也只用掉 6–9%。
|
|
||||||
|
|
||||||
其余数字:平均帧 22 kB,约 350 包/秒,每帧约 17 个分片,客户端观测的帧间隔 7 ms。
|
## 编译固件
|
||||||
|
|
||||||
## 为什么视频走 UDP 而不是 TCP
|
在仓库根目录执行:
|
||||||
|
|
||||||
**延迟优先。** 丢失 5% 的画面远好于等待 500 ms 的重传。
|
```powershell
|
||||||
|
# Windows PowerShell
|
||||||
**没有队头阻塞。** 一帧的任一分片卡住,后面所有帧都跟着卡——对实时画面这正好是错
|
$env:CARGO_TARGET_DIR = 'C:\s3vt'
|
||||||
的方向。
|
.\scripts\build.ps1
|
||||||
|
|
||||||
**带宽不是约束。** 见上表,TCP 那栏证明了链路是空的,所以 TCP 的优势在这里换不来
|
|
||||||
任何东西。
|
|
||||||
|
|
||||||
代价是丢包丢整帧。这被接受了:**撕裂的画面比过期的画面更糟**,所以接收端不做补洞,
|
|
||||||
只等下一张完整帧。
|
|
||||||
|
|
||||||
## 帧率必须按计数除以墙钟
|
|
||||||
|
|
||||||
"22 fps" 这个数字不是对帧间隔取平均得来的,是每个窗口的**帧数增量除以窗口时长**。
|
|
||||||
|
|
||||||
一次 UI 轮询(33ms)之间可能完成多帧,平均间隔量的是**显示**的刷新快慢而不是**相机**
|
|
||||||
的出帧快慢——那会把上限 22fps 的传感器报成 36fps。**一个笃定的错误数字比没有数字更
|
|
||||||
糟**,现场你会以为还有性能余量,然后花时间去找一个不存在的问题。
|
|
||||||
|
|
||||||
这个错误在本项目里出现过两次(客户端的 `View::update_fps`、探测脚本的分窗打印),两
|
|
||||||
次都是同一个形状:分子用累计值,分母用窗口值,于是报出随时间单调上升的假数字。
|
|
||||||
|
|
||||||
## 快速开始
|
|
||||||
|
|
||||||
### 1. 固件
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cp .env.example .env # 填 WIFI_SSID / WIFI_PASS
|
|
||||||
./scripts/flash.sh --monitor 20
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`flash.sh` 会编译、挑选避开坏字节的 app 偏移、烧录三段(bootloader / 分区表 /
|
```bash
|
||||||
app)、打印串口日志。ESP-IDF 位置从 `.cargo/config.toml` 读,不用先 `activate`。
|
# Linux / macOS
|
||||||
|
./scripts/build.sh
|
||||||
`.env`:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
WIFI_SSID=你的2.4G网络
|
|
||||||
WIFI_PASS=密码
|
|
||||||
|
|
||||||
# 装甲板(armor/)用这两个里的第一个;队伍号决定灯色与上报里的 team 字段:
|
|
||||||
#ARMOR_TEAM=0 # 0 = 队伍一(红),1 = 队伍二(蓝)
|
|
||||||
|
|
||||||
# 以下都有默认值:
|
|
||||||
#CAM_RES=vga # qqvga | qvga | cif | hvga | vga | svga | xga | sxga | uxga
|
|
||||||
#CAM_QUALITY=12 # 0-63,数字越小画质越好、字节越少
|
|
||||||
#CAM_VSYNC_INVERT=1 # 若板子在硬件上反相了 VSYNC,填 0
|
|
||||||
#WIFI_AUTH=wpa2 # 固定认证方式,仅当扫描看不到 AP 时需要(如隐藏 SSID)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
凭据由 `env!` 在**编译期**读取,所以没有运行时解析路径,也不会因为忘了读 `.env` 而
|
脚本执行 `cargo build --release --locked`,目标为 `xtensa-esp32s3-espidf`。首次编译包含 Rust 标准库、ESP-IDF 和相机组件,耗时较长。
|
||||||
在板子上带着别人的密码。`ARMOR_TEAM` 同理:换队伍重刷装甲板固件即可,不用擦 NVS。
|
|
||||||
|
|
||||||
`WIFI_AUTH` 一般不用设:固件先扫描 AP,用它报告的真实认证方式去连,只有扫不到时才
|
Windows 的 ESP-IDF 构建输出路径需不超过 88 字符,因此示例使用短目录 `C:\s3vt`(需可写,且不与其他项目共用)。`subst` 不能绕过该检查。下表的 `<TARGET_DIR>` 在 Windows 示例中为 `C:/s3vt`,Linux / macOS 默认是仓库的 `target`。每次新开 Windows 终端都要设置相同的 `CARGO_TARGET_DIR`。
|
||||||
需要人工指定。指定之后**只**试这一种方式——不成功就直接报错,而不是挨个试 10 秒。
|
|
||||||
挨个试等于在猜一个本该已知的值。
|
|
||||||
|
|
||||||
### 2. 装甲板固件(ESP32-C3)
|
| 产物 | 位置 |
|
||||||
|
| --- | --- |
|
||||||
|
| 固件 ELF | `<TARGET_DIR>/xtensa-esp32s3-espidf/release/esp32`(没有 `.exe` 后缀) |
|
||||||
|
| Bootloader | `<TARGET_DIR>/xtensa-esp32s3-espidf/release/bootloader.bin` |
|
||||||
|
| 分区表 BIN | `<TARGET_DIR>/xtensa-esp32s3-espidf/release/partition-table.bin` |
|
||||||
|
| 分区表源文件 | 根目录 `partitions.csv` |
|
||||||
|
|
||||||
```sh
|
ELF 供 `espflash` 烧录与符号解析,不能当作 BIN 直接写入 Flash。Bootloader、分区表与 App 应来自同一次构建。
|
||||||
scripts/flash_armor.sh # 编译 + 烧录 + 20 秒串口日志
|
|
||||||
python3 scripts/armor_probe.py 15 # 不开界面,单看板子在广播什么血量
|
## 烧录与串口验证
|
||||||
|
|
||||||
|
### 1. 查找串口
|
||||||
|
|
||||||
|
```text
|
||||||
|
espflash list-ports
|
||||||
```
|
```
|
||||||
|
|
||||||
接线表、血量规则、C3 的 RMT/ADC 踩坑都在 [`armor/README.md`](armor/README.md)。
|
Windows 常见 `COM5`;Linux 为 `/dev/ttyACM0` 或 `/dev/ttyUSB0`;macOS 为 `/dev/cu.usbmodem...`。关闭其他串口监视器,Linux 需具备串口访问权限。
|
||||||
|
|
||||||
### 3. 桌面端
|
### 2. 烧录 ELF
|
||||||
|
|
||||||
```sh
|
本项目使用自定义分区表,显式传入 `partitions.csv` 和本次构建的 Bootloader。将示例端口替换为实际端口:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$targetDir = if ($env:CARGO_TARGET_DIR) { $env:CARGO_TARGET_DIR } else { 'target' }
|
||||||
|
espflash flash --chip esp32s3 --port COM5 --flash-size 16mb --flash-mode dio --flash-freq 40mhz --partition-table partitions.csv --bootloader "$targetDir/xtensa-esp32s3-espidf/release/bootloader.bin" --monitor "$targetDir/xtensa-esp32s3-espidf/release/esp32"
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
target_dir="${CARGO_TARGET_DIR:-target}"
|
||||||
|
espflash flash --chip esp32s3 --port /dev/ttyACM0 --flash-size 16mb --flash-mode dio --flash-freq 40mhz --partition-table partitions.csv --bootloader "$target_dir/xtensa-esp32s3-espidf/release/bootloader.bin" --monitor "$target_dir/xtensa-esp32s3-espidf/release/esp32"
|
||||||
|
```
|
||||||
|
|
||||||
|
连接失败时按住 **BOOT**,点按 **RESET / EN**,松开 BOOT 后重试。正常启动应看到相机识别、Wi-Fi 获取 IP 和 `ESP32 camera is live` 日志。没有客户端时,`0 clients subscribed` 属于正常状态。
|
||||||
|
|
||||||
|
仅查看日志:
|
||||||
|
|
||||||
|
```text
|
||||||
|
espflash monitor --port COM5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 导出 BIN
|
||||||
|
|
||||||
|
`espflash save-image --merge` 可导出从 `0x0` 写入的合并镜像。完整命令、独立镜像偏移与测试方法见 [DEVELOPMENT.md](DEVELOPMENT.md)。
|
||||||
|
|
||||||
|
保留的 `scripts/flash.sh` 针对**特定坏 Flash 模组**,依赖 macOS 环境,会修改分区并重编译。普通板卡使用上述标准流程;坏板限制见 [HARDWARE.md](HARDWARE.md#损坏-flash-模组的兼容处理)。
|
||||||
|
|
||||||
|
## 客户端与装甲板
|
||||||
|
|
||||||
|
桌面端入口如下。安装依赖、主机 target 设置与功能说明见 [desktop/README.md](desktop/README.md):
|
||||||
|
|
||||||
|
```text
|
||||||
cd desktop
|
cd desktop
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm app # = tauri dev
|
pnpm app
|
||||||
```
|
```
|
||||||
|
|
||||||
没有板子也能把整条链路跑起来:`CAM_DEMO=1 pnpm app` 会合成**协议数据报**喂进真实的接收
|
装甲板是独立的 **ESP32-C3 / RISC-V** 工程,不能使用 S3 的 ELF。`ARMOR_TEAM=0/1` 在编译期决定红 / 蓝队伍。接线、规则与构建烧录见 [armor/README.md](armor/README.md)。
|
||||||
路径(分片、重组、发现、掉血派生、两道闸门全在跑真代码),界面上标着"演示模式"。
|
|
||||||
|
|
||||||
首次启动会问这台机器是哪一端:**用户端**要绑一块图传板与一块装甲板(改动需密码),
|
## 常见问题
|
||||||
**裁判端**不需要任何配置就能看两路。
|
|
||||||
|
|
||||||
**`desktop/.cargo/config.toml` 里的 `target = "aarch64-apple-darwin"` 换机器必须改。**
|
| 现象 | 检查方法 |
|
||||||
desktop/ 嵌在固件的目录树里,会继承父目录那个 `xtensa-esp32s3-espidf` 的交叉编译配置,
|
|
||||||
然后在依赖深处以 `libc` 找不到 `MAP_FAILED` 的形式失败。
|
|
||||||
|
|
||||||
界面、视频通道、密码模型、录制与排查都在 [`desktop/README.md`](desktop/README.md)。
|
|
||||||
|
|
||||||
## 发现是双向的
|
|
||||||
|
|
||||||
单靠一个方向建立不了"谁要看我":
|
|
||||||
|
|
||||||
- 板子每 **2 秒**广播一次 Hello —— "我在线,我是谁"
|
|
||||||
- 客户端每 **800 ms**广播一次 Subscribe —— "我是这个角色,请给我画面"
|
|
||||||
|
|
||||||
板子从订阅的**源地址**学到推流目标(所以客户端的 socket 必须固定绑 5600 端口,不能
|
|
||||||
用临时端口)。
|
|
||||||
|
|
||||||
对端靠**源地址**记忆,不发探测包。续订本来就每 800 ms 一次,所以"还活着吗"这个问题
|
|
||||||
由续订本身回答了——超时即代表它离开了,多一份心跳状态就多一个能出错的地方。
|
|
||||||
|
|
||||||
两端各有自己的超时淘汰,阈值不同:
|
|
||||||
|
|
||||||
| | 超时 | 为什么是这个量级 |
|
|
||||||
|---|---|---|
|
|
||||||
| 固件丢弃订阅者 | 6 s | 续订间隔 800 ms,7 倍余量,偶发丢包不会把人踢掉 |
|
|
||||||
| 客户端移除板子 | 60 s | 只影响界面列表,不影响推流 |
|
|
||||||
|
|
||||||
客户端的 60 s 刻意很长:界面列表里留着一块刚重启还没广播的板子,比它短暂消失更可接
|
|
||||||
受。而它每 5 秒才扫一次,不值得为这个上一份状态。
|
|
||||||
|
|
||||||
固件同时最多向 **4 个**订阅者推流(`MAX_PEERS`)。每个对端每帧要一次 `sendto`,这是帧
|
|
||||||
CPU 成本的倍数;三台客户端(两队 + 裁判)够用,且不让一个陈旧的地址条目拖慢每一帧。
|
|
||||||
|
|
||||||
## 身份:为什么用 MAC 派生
|
|
||||||
|
|
||||||
`protocol::id_from_mac()` 用 FNV-1a 从 MAC 派生 `sender_id`,避开 0(裁判终端的保留
|
|
||||||
值)。
|
|
||||||
|
|
||||||
**用 MAC 而不是 IP**,因为客户端要把绑定存进配置文件并在重启后继续有效——DHCP 换地
|
|
||||||
址是常态,MAC 不会变。用 MAC 也意味着烧同一份固件即可,不需要任何"我是谁"的配置。
|
|
||||||
|
|
||||||
## 线路格式
|
|
||||||
|
|
||||||
27 字节头 + 至多 1400 字节载荷,全部小端。细节在 `protocol/README.md`,那里有完整
|
|
||||||
的字节图和每个拒收条件。
|
|
||||||
|
|
||||||
三处关键决定:
|
|
||||||
|
|
||||||
- **27 字节不凑整。** 头只占每个 1427 字节数据报的 1.9%,而为对齐在两端各写一段
|
|
||||||
手写偏移常量,是引入静默错位的更划算的代价。
|
|
||||||
- **每个数据报 1427 字节,低于 1500 MTU。** 超 MTU 的数据报要么被 AP 丢、要么被 IP
|
|
||||||
分片,两种情况的丢包都记在应用账上而不是网络账上——后者本来更容易排查。
|
|
||||||
- **`version` 不匹配就拒收**,而不是按错误布局继续解析。后者表现为随机崩溃或画面错
|
|
||||||
乱,比拒绝连接难查得多。
|
|
||||||
|
|
||||||
## 硬件
|
|
||||||
|
|
||||||
Freenove ESP32-S3-WROOM CAM,ESP32-S3-WROOM-1-N16R8(16 MB flash + 8 MB octal
|
|
||||||
PSRAM),OV5640。
|
|
||||||
|
|
||||||
### 引脚
|
|
||||||
|
|
||||||
`src/camera.rs` 的 `pins` 模块:
|
|
||||||
|
|
||||||
| | GPIO |
|
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| RESET | 0 |
|
| 提示缺少 `esp-1.97.0.0` | 按上述命令安装指定名称的工具链,重新打开终端后检查版本 |
|
||||||
| XCLK | 15 |
|
| `espup` 查询 GitHub 版本返回 403 | 对上述固定版本安装命令追加 `--skip-version-parse`,或稍后重试 |
|
||||||
| PCLK | 13 |
|
| 找不到 `ldproxy` / `libclang` | 安装链接工具;Unix 加载 `export-esp.sh`,Windows 重开终端 |
|
||||||
| VSYNC | 6 |
|
| ESP-IDF 路径或版本错误 | 清除旧 `IDF_PATH` 或指向 v5.5.5 SDK |
|
||||||
| HREF | 7 |
|
| Windows 路径过长 | 缩短仓库和工具链路径,见开发说明 |
|
||||||
| SIOD (SDA) | 4 |
|
| Wi-Fi 连不上 | 核对 2.4 GHz、凭据和认证方式,再重新编译烧录 |
|
||||||
| SIOC (SCL) | 5 |
|
| 相机无输出 / 初始化失败 | 核对板型接线、供电与 Octal PSRAM |
|
||||||
| DATA[0..8] | 11, 9, 8, 10, 12, 18, 17, 16 |
|
| `NO-EOI`、几帧后停止 | 确认 `CONFIG_CAMERA_PSRAM_DMA=n` |
|
||||||
|
| 在线但没有画面 | 检查客户端订阅、UDP 5600、防火墙及 AP 隔离 |
|
||||||
|
| 开启 VPN / 虚拟网卡后设备在线但无画面 | 使用支持按已发现设备 IP 定向续订的客户端;仅发送 `255.255.255.255` 广播可能走错网卡 |
|
||||||
|
| 改 C 组件后无变化 | 清理 `esp-idf-sys` 构建缓存后重编,见开发说明 |
|
||||||
|
| 主机项目被编为 Xtensa | 显式指定宿主 target,见各子项目说明 |
|
||||||
|
|
||||||
### 千万不要套用别的板子的引脚表
|
用 `python scripts/udp_probe.py 10` 可独立检查视频协议;装甲板用 `python scripts/armor_probe.py 15`。这些脚本需要板卡在线,构建通过不代表已完成硬件验证。
|
||||||
|
|
||||||
这块板子的 GPIO 分配是死的:
|
测量帧率和丢帧可运行以下命令,将 IP 换成板卡实际地址。对照测试时先退出桌面客户端,避免额外订阅者增加无线负载:
|
||||||
|
|
||||||
| 范围 | 占用 |
|
```text
|
||||||
|---|---|
|
python scripts/perf_probe.py 192.168.1.139 --seconds 30 --json perf.json
|
||||||
| 26–32 | SPI flash |
|
```
|
||||||
| 33–37 | octal PSRAM |
|
|
||||||
| 19 / 20 | USB |
|
|
||||||
| 43 / 44 | console |
|
|
||||||
| 45 / 46 | 模组内部 |
|
|
||||||
|
|
||||||
直接套用 AI-Thinker ESP32-CAM 的引脚表会有 **7 根冲突**。冲突的表现不是"某根线不
|
脚本定向续订,支持保留 VPN / TUN 的环境。输出包含完整帧率、帧损失率、接收间隔和组装耗时;这些时间不等于摄像头到屏幕的端到端延迟。
|
||||||
通",而是 `PIN_FUNC_SELECT` 把某个外设接到了 flash 或 PSRAM 上,**CPU 在传输中途挂
|
|
||||||
死**——没有报错、没有回溯、串口直接静默。
|
|
||||||
|
|
||||||
改引脚前先对照上面这张表。
|
运动图传保持 `CAM_GRAYSCALE=0`,优先使用自动曝光上限和自动增益协同控制拖影。室内强制 −1 EV / 固定短快门在本机过暗,已撤回;提高增益上限也会增加噪点。需要限流时可设置 `CAM_MAX_FPS=15`,追求帧率时使用 `0`;可启用有界 JPEG 压缩调节应对发送回压,分片间隔按网络实测选择。灰度效果仍保留硬件 JPEG,不是改为 RGB565 或降低 JPEG 采样位数。75°C 是主动减载阈值,温度来自 ESP 芯片内部,不能代表摄像头或环境温度,也不是芯片损坏温度规格。五分钟彩色对照与此前中止的灰度长测见 [STABILITY.md](STABILITY.md)。
|
||||||
|
|
||||||
### 那块坏掉的 flash 字节
|
## 版本号与改动日志
|
||||||
|
|
||||||
测试用的这块模组在 `0xFADE7` 有个字节的 bit 5–7 **写不进去**。镜像放在哪里会决定哪
|
版本号和 `CHANGELOG.md` 不手写,从 git 历史算:
|
||||||
个镜像字节落到这个地址,落上去就校验失败、bootloader 拒绝启动。
|
|
||||||
|
|
||||||
镜像本身不因位置而变,所以移动 app 分区就够了。`scripts/flash.sh` 会挑最低的 64 KB
|
|
||||||
对齐、且该地址处字节 flash 真的能存的偏移,写回 `partitions.csv`,然后重新编译
|
|
||||||
(重编译会让镜像位移,所以编译后再验证一次)。
|
|
||||||
|
|
||||||
**换到健康硬件**:把 `factory` 分区放回 `0x10000`,删掉 `flash.sh` 里的
|
|
||||||
`BAD_FLASH_ADDR`,别的什么都不用改。
|
|
||||||
|
|
||||||
Flash 跑 DIO @ 40 MHz 而不是这颗片子的标称 QIO/80 MHz:测试模组的坏字节在 quad 模式
|
|
||||||
下会被推到 boot loop。QIO 对**代码执行**速度的提升是明显的(程序从 flash 跑,cache
|
|
||||||
miss 直接打在这条总线上),健康硬件上值得换回去试。
|
|
||||||
|
|
||||||
### `PSRAM DMA` 是故意关掉的
|
|
||||||
|
|
||||||
开着的话,esp32-camera 让 GDMA 直接把帧缓冲写进 PSRAM,然后**只在最后一个 DMA 节点
|
|
||||||
(1 KB)里**找 JPEG 结束标记。标记落在窗口外就丢帧并报 `NO-EOI`——在这块板子上是大部
|
|
||||||
分的帧,几帧之后驱动干脆不再交出任何帧,而且不恢复。
|
|
||||||
|
|
||||||
关掉之后驱动 DMA 到内部 RAM 的中转缓冲,再 memcpy 进 PSRAM。每帧多一次约 23 KB 的
|
|
||||||
memcpy,但搜索整个缓冲找标记。对一条能用的图传链路,这是对的取舍。
|
|
||||||
|
|
||||||
### CPU 频率
|
|
||||||
|
|
||||||
**`CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y`** 是 `sdkconfig.defaults` 里最值钱的一行,
|
|
||||||
而且免费。
|
|
||||||
|
|
||||||
ESP-IDF v5.5 的 Kconfig 把 ESP32-S3 默认成 **160** MHz(见
|
|
||||||
`components/esp_system/port/soc/esp32s3/Kconfig.cpu`),所以默认编译悄悄浪费了一半
|
|
||||||
CPU。相机采集、esp32-camera 里的 JPEG 搜索、lwIP、发送路径全是 CPU 密集的,这一行一
|
|
||||||
次性把它们全部搬上去。
|
|
||||||
|
|
||||||
### WiFi 缓冲留在内部 RAM
|
|
||||||
|
|
||||||
`CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=n`。
|
|
||||||
|
|
||||||
这个值曾经按"PSRAM 很空闲"的直觉写成 `y`,方向恰好相反:WiFi 描述符和 TCP 段**每个
|
|
||||||
包都要碰**,内部 SRAM 比隔着内存总线的 PSRAM 快一个数量级。把它们放进 PSRAM 的表现是
|
|
||||||
高延迟和差吞吐,无论 PSRAM 还剩多少。
|
|
||||||
|
|
||||||
帧缓冲相反——它大、相对少碰,留在 PSRAM。
|
|
||||||
|
|
||||||
### 相机任务钉在 CPU1
|
|
||||||
|
|
||||||
`CONFIG_CAMERA_CORE0=n`(即跑在 CPU1)。WiFi 任务钉在 CPU0。esp32-camera 的采集任务
|
|
||||||
时间是短促且延迟敏感的(DMA 完成、JPEG 标记搜索、拷进帧缓冲),在共享核上会被抢占,
|
|
||||||
每一帧都被拉长。双核芯片上没有理由共享。
|
|
||||||
|
|
||||||
## vendored esp32-camera
|
|
||||||
|
|
||||||
`components/esp32-camera/` 是上游 2.1.7 的副本,因为组件注册表的版本求解器找不到它。
|
|
||||||
|
|
||||||
它打了一处补丁:`target/esp32s3/ll_cam.c` 里的 `CAM_IO_MUX_REG()`。这个宏负责把引脚
|
|
||||||
接到 I2C(OV5640 的 SCCB 走 I2C),不接的话传感器根本不会被发现。
|
|
||||||
|
|
||||||
**注意这个目录叫 `target/`,但它不是构建目录。** 它是上游组件的按芯片分目录
|
|
||||||
(`esp32/`、`esp32s2/`、`esp32s3/`),里面是 `ll_cam.c` 等源码,包含上述补丁。所以
|
|
||||||
`.gitignore` 里三条 `target/` 规则全部锚定在根目录——这是有意的。
|
|
||||||
|
|
||||||
## 改过 C 文件之后
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
rm -rf target/xtensa-esp32s3-espidf/release/build/esp-idf-sys-*
|
node scripts/version.mjs --write # 计算并写回 version.json / CHANGELOG / 客户端版本
|
||||||
|
node scripts/version.mjs --check # 只校验(CI 与发布前用)
|
||||||
```
|
```
|
||||||
|
|
||||||
不删不会重新编译,你的补丁看起来像完全没生效。
|
- 版本号由提交类型决定:`BREAKING CHANGE:` → major,`feat` → minor,`fix`/`perf` → patch。
|
||||||
|
- 构建号 = `git rev-list --count HEAD`,单调、不撞车。
|
||||||
|
- 同一次计算还产出 `version.json`(固件 `build.rs` 读它)、`CHANGELOG.json`、
|
||||||
|
`desktop/src/lib/version.ts`。
|
||||||
|
|
||||||
## 摄像头端不编码,客户端不编码
|
**固件与客户端的版本是联动过的**:板子每 2 秒广播的 `Hello` 里带着语义化版本、构建号与
|
||||||
|
它编译进去的线路协议号;客户端读出来显示在设置与裁判端每一格下面,协议对不上时直接说明
|
||||||
|
原因("板子是协议 2,客户端是协议 1,画面会被丢弃,请重新烧录"),而不是只表现为没有画面。
|
||||||
|
|
||||||
OV5640 的 JPEG 编码器**在传感器内部**。DVP 接口出来的一帧要么是 JPEG,要么是 YUV 裸
|
细节见 [`desktop/README.md`](desktop/README.md#版本与改动日志算出来的不是写出来的) 与
|
||||||
数据。
|
[`protocol/README.md`](protocol/README.md#通告里的版本信息)。
|
||||||
|
## 文档索引
|
||||||
|
|
||||||
不编码的话,单帧从约 21 KB 涨到 **450 KB**(21 倍),而 ESP32-S3 **没有硬件 JPEG
|
- [CHANGELOG.md](CHANGELOG.md):改动记录与历史修复。
|
||||||
编码器**(整个 ESP32 系列只有 P4 有,`esp_driver_jpeg` 里
|
- [HARDWARE.md](HARDWARE.md):接线、内存、Flash 与驱动限制。
|
||||||
`CONFIG_IDF_TARGET_ESP32P4` 是写死的)。
|
- [DEVELOPMENT.md](DEVELOPMENT.md):镜像导出、测试、协议与性能测量。
|
||||||
|
- [PERFORMANCE.md](PERFORMANCE.md):保留 TUN 的图传排查、参数取舍与实际测试结果。
|
||||||
同理,客户端也不编码:板子出的就是传感器里的 JPEG,客户端只解码。ESP32-S3 也没有硬
|
- [STABILITY.md](STABILITY.md):限帧、灰度与温度减载,长时间运行测试。
|
||||||
件 JPEG 解码器。
|
- [REALTIME.md](REALTIME.md):阻塞时取最新帧、超期丢弃和客户端积压处理。
|
||||||
|
- [POWER.md](POWER.md):15 dBm 回退、功率与距离的关系,以及发送拥塞证据。
|
||||||
## 为什么收包和解码要分开线程
|
- [LATENCY.md](LATENCY.md):当前约 30 fps 配置、Wi-Fi 重连及实际画面链路延迟对照。
|
||||||
|
- [protocol/README.md](protocol/README.md):UDP 线路格式及帧重组。
|
||||||
一张 VGA 的 JPEG 解码要几毫秒。放在收包线程里会挤进数据报之间,造成**链路上本不存在
|
- [armor/README.md](armor/README.md):装甲板固件与规则。
|
||||||
的丢帧**——不是真的丢了网络包,是应用来不及取走 socket 缓冲。
|
- [desktop/README.md](desktop/README.md):桌面端配置、录制与排查。
|
||||||
|
- [docs/firmware-provisioning-evaluation.md](docs/firmware-provisioning-evaluation.md):自动烧录、坏区规避与 SSID 配置 patch 的工具评估、MVE 与测试矩阵。
|
||||||
现在桌面端把 JPEG **原样**交给 WebView,由浏览器的原生解码器解——后端连解都不解,
|
- [docs/firmware-maintenance.md](docs/firmware-maintenance.md):已实现的固件维护页、官方 bundle 格式、坏块规避流程与实机验证记录。
|
||||||
接收线程只做一次 16 字节头的拼接与一次广播。这一条比上面那条更彻底:省掉的不是几毫秒,
|
|
||||||
而是一整个线程与一次内存拷贝。细节在 [`desktop/README.md`](desktop/README.md)。
|
|
||||||
|
|
||||||
## 排查
|
|
||||||
|
|
||||||
| 现象 | 原因 |
|
|
||||||
|---|---|
|
|
||||||
| 板子无输出、串口静默 | GPIO 冲突。CPU 在传输中途挂了,见上面那张占用表 |
|
|
||||||
| 几帧后停止出图,报 `NO-EOI` | `PSRAM DMA` 被打开了 |
|
|
||||||
| 板子在线但 `0 clients subscribed` | 订阅包格式不对。`Subscribe` 的 `role` 字节紧跟 27 字节头,在偏移 27 |
|
|
||||||
| 一直接到但画面偶尔解不出来 | 帧重组的问题。**先怀疑你的测量工具**:本项目的探测脚本就因为重组状态没按 `frame_id` 隔离,误报过 34 张坏帧 |
|
|
||||||
| `libc` 找不到 `MAP_FAILED` | `desktop/.cargo/config.toml` 的 target 没改成宿主三元组 |
|
|
||||||
| 串口看不到任何输出 | 非 TTY 下 `espflash monitor` 不可用,用 `/tmp/rd2.py` 裸读 |
|
|
||||||
| WiFi 连不上 | SSID 必须在 2.4GHz。5G 频段看不见 |
|
|
||||||
| 裁判端两块画面宽窄不一 | 两格必须是 `flex: 1 1 0` 且 `min-width: 0`:按内容分宽度时,占位文字长的那一格会多占十几个像素 |
|
|
||||||
| 裁判端两格显示同一块板子 | 旧版槽位分配的真 bug。现在候选里要排除另一个槽位占着的 id |
|
|
||||||
| 有画面但状态栏说"离线" | 视频帧也必须算存活证据(`Discovery::touch`),不能只看 Hello |
|
|
||||||
| 改了绑定却"没生效" | 改动需要先解锁;后端每个改动命令都会校验,界面上的入口只是入口 |
|
|
||||||
|
|
||||||
独立核对线路格式:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
python3 scripts/udp_probe.py 10
|
|
||||||
```
|
|
||||||
|
|
||||||
它手写了每一个协议常量,**不依赖 `protocol` crate**——正因为如此,如果 crate 的编码
|
|
||||||
有错,脚本会立刻报出不一致。
|
|
||||||
|
|
||||||
## 四份文档
|
|
||||||
|
|
||||||
| | |
|
|
||||||
|---|---|
|
|
||||||
| 根 `README.md` | 你在这里。拓扑、性能、构建烧录、硬件坑 |
|
|
||||||
| `protocol/README.md` | 线路格式、帧重组的取舍、每个拒收条件 |
|
|
||||||
| `armor/README.md` | 装甲板接线表、血量规则、上报、C3 的 RMT/ADC 踩坑 |
|
|
||||||
| `desktop/README.md` | 两套页面、视频通道路径、两道闸门、密码模型、录制与排查 |
|
|
||||||
|
|
||||||
## 装甲板(已实现)
|
|
||||||
|
|
||||||
`armor/` 是一套独立的 Rust workspace(目标 **ESP32-C3**,RISC-V):
|
|
||||||
|
|
||||||
- `armor-protocol` —— 规则层:血量(100 起、每击 −3)、每板独立的 50 ms 受击冷却、
|
|
||||||
受击后**只有那一路**灯带全黑 50 ms、按键 FSM、上报编码。纯计算,主机上 22 个测试。
|
|
||||||
- `armor-firmware` —— 固件:四路压电片 40 kHz ADC 连续采样、四路 WS2812(C3 只有一个
|
|
||||||
RMT 通道可用,靠 `rmt_tx_switch_gpio` 轮流驱动)、NVS、BOOT 键、WiFi + UDP。
|
|
||||||
|
|
||||||
硬件是"一主三从":三块从板里没有 MCU,各自只有一路灯带和一路压电片,直接接线到主板。
|
|
||||||
接线表与实现细节见 [`armor/README.md`](armor/README.md)。
|
|
||||||
|
|
||||||
```sh
|
|
||||||
scripts/build_armor.sh # 编译(WIFI_SSID/WIFI_PASS/ARMOR_TEAM 取自 .env)
|
|
||||||
scripts/flash_armor.sh # 编译 + 烧录 + 串口日志
|
|
||||||
python3 scripts/armor_probe.py 15 # 不开界面,单看板子在广播什么血量
|
|
||||||
```
|
|
||||||
|
|
||||||
桌面端侧:`desktop/core/src/armor.rs` 的 `ArmorHealthStore` 监听 5601,按 `(team, index)`
|
|
||||||
存最后一次读数,四条都在 1.5 s 内收到才显示——**没有数据时显示占位符而不是 0**,
|
|
||||||
0 看起来像真值,现场看到会以为对方已经没血了。界面上的血量画在画面角上(一条分 5 格的
|
|
||||||
平行四边形,20 点血一格),归零不写"已击毁/损毁",而是由图传中央的「胜利 / 击败」横幅表达
|
|
||||||
(见 [`desktop/README.md`](desktop/README.md));数字变化也会打进客户端日志。
|
|
||||||
|
|
||||||
同一块板子还会被单独记下来(帧头里的 MAC 派生 id),所以两端都能列出"场上有哪几块装甲板"
|
|
||||||
并选一块绑定——命中反馈也是从这个 id 上归位的。**这不需要改固件**:主板一直在用
|
|
||||||
`encode_from(sender_id)` 把自己的 id 打进帧头。
|
|
||||||
|
|||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
# 阻塞恢复与最新帧处理(2026-10-18)
|
||||||
|
|
||||||
|
## 当前交付
|
||||||
|
|
||||||
|
当前 15 dBm 交付 App SHA-256:`a6092e4b0cf42819c04424e066651f285b8961312a73822d667532f800e46767`,应用分区 `0x70000`。COM8 烧录与三段独立校验通过;355.1 秒串口的 71 次功率回读均为 15.00 dBm。五分钟观察:原生完整接收 **29.68 fps**,丢帧 **1.045%**,本地 JPEG 通道 **28.23 fps**,31 张抽帧全部完整解码为 640×480 RGB,无无效 JPEG、设备离线、通道断开、复位或采集错误;仍有最长 1.646 秒到帧间隔,不能将此轮称为无卡顿。距离与拥塞分析、测试边界见 [POWER.md](POWER.md) 和制品 `POWER-RESULT.json`。
|
||||||
|
|
||||||
|
保留已确认可用的三块 PSRAM 采集缓冲、OV5640 VGA 彩色、约 30 fps PLL 时钟、AE 目标 45/55、约 20 ms 曝光上限及自动增益;Wi-Fi 自动重连和 TUN 兼容订阅继续保留。没有增加待发送 JPEG FIFO,也没有为了消除队列改成单块 DMA 缓冲。
|
||||||
|
|
||||||
|
发送程序只持有当前一张完整 JPEG;阻塞恢复后通过有限次数的零等待取帧选择最新有效帧,旧帧立即归还驱动。三块采集内存用于捕获余量,与必须按序播放的三帧队列不同。已有相机队列仍然有限,应用交接会跳过其旧帧。
|
||||||
|
|
||||||
|
完整 JPEG 取得后按 1400 字节载荷分片立即提交,复用一个数据报缓冲,直接读取 PSRAM。当前传感器驱动在 JPEG 结束后交出完整帧,本次没有修改为 DMA 尚未完成就读取其内容;不完整 JPEG 由接收端整帧丢弃,不进入绘制。
|
||||||
|
|
||||||
|
## 阻塞处理
|
||||||
|
|
||||||
|
| 阶段 | 处理方式 |
|
||||||
|
| --- | --- |
|
||||||
|
| 采集交接 | 首帧最多等待 100 ms,其后零等待、有限次数取更新的有效帧;归还被替代的旧缓冲 |
|
||||||
|
| 发送帧龄 | 用板内同一单调时钟检查从首个 DMA 缓冲开始的年龄,期限 100 ms |
|
||||||
|
| 当前帧发送 | 绝对检查期限 25 ms,成功发送分片不会续期,所有订阅者共享 |
|
||||||
|
| 拥塞重试 | 默认每帧最多 8 ms 让出调度重试;预算用尽或过期后放弃该目标剩余分片,转向新帧 |
|
||||||
|
| 原生本地通道 | 每路保留最新帧;不重放超过 100 ms 的缓存,写入超过 100 ms 时断开慢端以便重连 |
|
||||||
|
| WebView / 解码 | 原生端在 JPEG 前发送 `frameTiming`;JS 在解码前后拒绝本机滞留超过 100 ms 的帧并释放旧结果 |
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
CAM_FB_COUNT=3
|
||||||
|
CAM_AE_LEVEL=-1
|
||||||
|
CAM_EXPOSURE_EV=0
|
||||||
|
CAM_EXPOSURE_MAX_LINES=592
|
||||||
|
UDP_PACKET_GAP_US=0
|
||||||
|
UDP_RETRY_BUDGET_US=8000
|
||||||
|
WIFI_TX_POWER_DBM=15
|
||||||
|
```
|
||||||
|
|
||||||
|
当前交付根据用户反馈回退到 15 dBm,实板驱动回读 15.00 dBm。启动和重连应用同一功率上限,拥塞和温控逻辑不自动降功率;此前 17 dBm 的数据作为历史记录保留,见 [POWER.md](POWER.md)。
|
||||||
|
|
||||||
|
若要在发送回压时立即放弃本帧,可将 `UDP_RETRY_BUDGET_US=0` 后重编烧录。该模式仍保留三块采集内存和完整 JPEG 校验;含义是零让出重试,不是删除驱动和路由器中的所有缓冲。零重试策略已经主机测试,本次五分钟实机使用默认 8000 μs。
|
||||||
|
|
||||||
|
分片间隔也计入 25 ms 检查期限,不再额外延长帧的发送寿命;设置过大的间隔可能使整帧无法在期限内提交。分片在 SDK socket 中已经接受后无法由本策略撤回,底层调度和网络排队也无法由应用层强制抢占。以上为应用层新鲜度策略,不能保证每帧、无线链路或物理屏幕具有硬实时上限。
|
||||||
|
|
||||||
|
本地 JPEG 二进制头继续为 16 字节,旧消费者可忽略新增文本消息。新前端连接旧后端时仍可用,但没有原生发布时间,只能按 JS 收到时刻判断本地队列年龄。两端本机发布时间使用同机系统时间;这不是 ESP 与电脑的时钟同步,也不能据此计算无线单向延迟。JS 内部排队同时使用单调时钟检查。
|
||||||
|
|
||||||
|
## 此前 14 dBm 版本的实机五分钟检查
|
||||||
|
|
||||||
|
该次测试使用 14 dBm,App SHA-256:`9a7decd59572c3f280e7fd4c0a291645011b779025e56e99ea50087e92b5fa77`,应用分区 `0x60000`。以下五分钟数据属于该历史镜像,不是提高功率后的重测结果。该镜像曾经 COM8 烧录并独立三段校验,三段镜像必须成套使用。运行保留 TUN、只有一个 UDP 订阅者;串口和本地视频观察器不会产生额外 UDP 推流。
|
||||||
|
|
||||||
|
| 指标 | 本次结果 |
|
||||||
|
| --- | --- |
|
||||||
|
| 本地 JPEG 通道观察时长 | 300.005 s |
|
||||||
|
| 原生接收计数有效区间 | 180.937 s |
|
||||||
|
| 完整接收 / 丢帧 | 5390 / 26(仅有效区间) |
|
||||||
|
| 完整接收帧率 / 帧损失率 | 29.79 fps / 0.480%(仅有效区间) |
|
||||||
|
| 本地 JPEG 通道 | 29.69 fps |
|
||||||
|
| 本地 JPEG 通道到达间隔 P95 / 最大值 | 47.648 / 323.146 ms |
|
||||||
|
| 主动跳过旧采集帧 | 0 |
|
||||||
|
| 发送前过期帧 / 发送期限目标丢弃 | 0 / 0 |
|
||||||
|
| 提交时采集帧龄最大观测值 | 74.13 ms |
|
||||||
|
| 发送持有时间最大观测值 | 11.23 ms |
|
||||||
|
| 复位 / 采集错误 | 0 / 0 |
|
||||||
|
|
||||||
|
观察途中界面切到队伍端,按设计隐藏诊断计数,所以原生完整帧率和损失率只报告隐藏前的有效区间,不提供完整五分钟的原生丢帧率;本地 JPEG 通道则完整观察五分钟。保留用户当前界面选择,没有强制切回裁判端。两个最大值是板端自启动起的观测值,包含预热;帧龄从驱动的首个 DMA 缓冲时间戳开始,不是光子进入传感器到屏幕的时间。主动跳过的采集帧尚未分配网络帧号,不计入接收端丢帧率;两种计数不能混为一谈。另抽取 61 张 JPEG,均完整解码为 640×480 RGB。
|
||||||
|
|
||||||
|
部署前的 30 秒基线仅用于检查连通性,画面内容和无线状态有变化,不能据此证明本策略使丢帧率或光学延迟降低。本次未重新测量物理端到端延迟,也未证明所有长程拥塞峰值均已解决。较早的标记回显测量保留在 [LATENCY.md](LATENCY.md)。
|
||||||
|
|
||||||
|
## 阻塞恢复验证
|
||||||
|
|
||||||
|
真实原生视频通道上的辅助接收端主动阻塞事件循环 350 ms,生产 `StreamClient` 恢复后拒绝 8 张过期帧,帧号从 1886 前进至 1918。该测试没有新增 UDP 订阅者;JPEG 解码在此辅助测试中使用替身,验证的是本地积压年龄和恢复逻辑,实际 JPEG 完整解码另行检查。
|
||||||
|
|
||||||
|
- 12 项 Rust 调度策略测试:绝对帧期限、旧采集帧寿命、共享重试预算、零重试及原有调度策略。
|
||||||
|
- 4 项 C 驱动交接情景:有限次数追赶、旧缓冲仅归还一次、无有效新帧时保留已有帧、首次超时及单缓冲边界。
|
||||||
|
- 8 项前端测试:慢解码保留最新待处理帧、500 ms 本机积压拒绝、阻塞解码结果释放、待解码帧超期和原有关闭/重启处理。
|
||||||
|
- 10 项原生视频通道测试:时间标记先于对应 JPEG、重连不重放过期缓存及原有协议与通道行为。
|
||||||
|
|
||||||
|
使用 `python scripts/test-latest-frame.py` 运行驱动交接测试;`desktop/` 中运行 `pnpm test:stream`。实时通道故障测试为 `node desktop/scripts/test-live-freshness.mjs <本机私有调试日志> <结果JSON>`,日志内的通道令牌仅在程序内部读取,不应发布。完整测量与边界见制品 `REALTIME-RESULT.json`,不包含私有日志、用户配置或相机抽帧。
|
||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
# 彩色图传调度与持续运行验证(2026-10-18)
|
||||||
|
|
||||||
|
本文保留此前约 22 fps 配置及更早长测的历史记录。后续约 30 fps PLL 配置、缓冲选择、运行中断线恢复与画布回显延迟测量见 [LATENCY.md](LATENCY.md);以下参数与 SHA 属于当时的测试阶段,当前交付以新报告和制品 `BUILD-INFO.json` 为准。
|
||||||
|
|
||||||
|
## 测试目标与配置
|
||||||
|
|
||||||
|
使用 ESP32-S3-N16R8 / OV5640、VGA、8 MB PSRAM、CPU 240 MHz、Windows 原生客户端,保留 Mihomo TUN。每轮只运行一个 UDP 订阅者,串口记录器与本地 WebSocket 观察器不会新增 UDP 推流目标。Windows 到板卡的实际路由走 WLAN;电脑连接 5 GHz、板卡连接 2.4 GHz。
|
||||||
|
|
||||||
|
优化目标为彩色、内容可辨、较短曝光和尽可能高的完整接收帧率,每轮有效测量为 5 分钟。下文保留了亮度与时序调整过程;不同轮次的场景、曝光、压缩和网络状态有变化,接收数字是描述性对照,不能作为严格单因素实验。
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
CAM_RES=vga
|
||||||
|
CAM_QUALITY=18
|
||||||
|
CAM_QUALITY_MAX=30
|
||||||
|
CAM_MAX_FPS=0
|
||||||
|
CAM_GRAYSCALE=0
|
||||||
|
CAM_EXPOSURE_EV=0
|
||||||
|
CAM_SHUTTER_LINES=0
|
||||||
|
CAM_EXPOSURE_MAX_LINES=440
|
||||||
|
CAM_GAIN_CEILING=32
|
||||||
|
CAM_HTS=0
|
||||||
|
CAM_THERMAL_LIMIT_C=75
|
||||||
|
WIFI_TX_POWER_DBM=14
|
||||||
|
UDP_PACKET_GAP_US=0
|
||||||
|
```
|
||||||
|
|
||||||
|
这些参数在编译时生效。该阶段配置的板上无网络采集探针为 22.20 fps,保留驱动默认 HTS 2060 / VTS 984 / XCLK 20 MHz;未采用颜色异常的短 HTS,也未采用此前导致采集下降至 8.1 fps 的 VTS 调整。
|
||||||
|
|
||||||
|
## 曝光与彩色
|
||||||
|
|
||||||
|
OV5640 驱动的 AE 档位不是 EV 数值:名义零档目标为 55,档位 −3 的目标为 25,目标比值约为 −1.14 EV;档位 −4 的目标为 15,约为 −1.87 EV。固件的 `CAM_EXPOSURE_EV=-1/-2` 使用这两个近似档位;这不是经过照度标定的绝对 EV。
|
||||||
|
|
||||||
|
早期仅降低 AE 目标后,实际曝光仍为 885 行;固定 220 行约为 10 ms,但用户反馈太暗,固定约 20 ms 也仍然太暗,均已撤回。其他时钟或分辨率下,行数对应的时间不同,不能直接套用行数。
|
||||||
|
|
||||||
|
该阶段采用正常亮度目标、自动 AEC / AGC、440 行曝光上限,关闭夜景延帧,并按现有 50/60 Hz band step 同步约束最大 band 数。自动增益上限由原来的 15.5× 扩至 31.94×,给短曝光补偿空间;增益会放大噪点。预检实际曝光为 295 行,按 SYSCLK 45 MHz / HTS 2060 估算约 13.5 ms,约为早期 885 行曝光的三分之一。440 行上限约 20.1 ms,实际值由场景与频闪步长决定。
|
||||||
|
|
||||||
|
用户已确认新配置**亮度够用、颜色稳定**。它仍使用传感器硬件彩色 JPEG,不通过降低色深来提帧。
|
||||||
|
|
||||||
|
彩色 JPEG 抽帧已解码为 640×480 RGB,仍使用传感器硬件 JPEG 编码。没有用灰度换取本轮帧率。曝光行数日志用于验证快门配置,未进行运动靶拖影或照度标定测量。驱动依据:[Espressif OV5640 源码](https://github.com/espressif/esp32-camera/blob/master/sensors/ov5640.c)。
|
||||||
|
|
||||||
|
## 最终配置的五分钟验证
|
||||||
|
|
||||||
|
本轮测量从 `2026-10-18T12:24:10.381357+00:00` 到 `2026-10-18T12:29:10.384000+00:00`,持续 300.004 秒。下表对照前一轮固定约 20 ms、quality 18 的完整五分钟记录;前一轮用户确认过暗,本轮用户确认亮度够用、颜色稳定。
|
||||||
|
|
||||||
|
| 指标 | 固定快门对照 | 自动曝光上限 + 压缩调节 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 完整接收帧 | 5220 | 6520 |
|
||||||
|
| 丢帧 | 1430(21.50%) | 34(0.52%) |
|
||||||
|
| 完整接收帧率 | 17.40 fps | 21.73 fps |
|
||||||
|
| 本地 JPEG 通道帧率 | 13.45 fps | 20.99 fps |
|
||||||
|
| 通道帧间隔 P95 | 277.473 ms | 102.841 ms |
|
||||||
|
| 通道帧间隔最大值 | 1163.792 ms | 419.825 ms |
|
||||||
|
| 平均 JPEG 大小 | 12099 字节 | 16346 字节 |
|
||||||
|
|
||||||
|
本轮全程在线,无复位、采集错误、本地通道断开、计数重置或诊断计数缺失。视频包长度与首尾标记错误为 0;另抽取 148 张 JPEG 完整解码,均为 640×480 RGB。
|
||||||
|
|
||||||
|
测量窗口内曝光为 295–295 行;JPEG 档位为 18–22,实际增益 11.00–18.00×。内部堆最小 129455 字节,平均提交耗时 2.60 ms/帧,温度 54–59°C,未触发温度减载。
|
||||||
|
|
||||||
|
这次完整接收帧率接近当前可靠彩色时序的约 22.2 fps 上限。亮度、画面内容、JPEG 大小和网络负载在各轮之间变化,不能把全部差异归因于单一控制参数,也不能将五分钟结果作为长期或所有路由器的保证。帧间隔不是摄像头到屏幕的光学延迟。
|
||||||
|
|
||||||
|
本轮固件 SHA-256 为 `e297069e190dcccefbae6d95591703c6848f4e523f910e2c0cb44af91f698936`,当时与交付及板上镜像一致;最新交付见 LATENCY.md。配套 Bootloader、分区表及 App 均已独立读回校验。曲线和完整数据位于制品包内的 `STABILITY.png` / `SCHEDULING-RESULT.json`,不包含相机抽帧或令牌日志。
|
||||||
|
|
||||||
|
## 较早的五分钟彩色基线
|
||||||
|
|
||||||
|
以下为较早的完整 300.002 秒测量,HTS 保持驱动默认 2060,快门 220 行,UDP 分片间隔为 0。真实 Windows 客户端接收,非演示模式,单 UDP 订阅者。该场景 JPEG 约 9 KB,用户随后确认这一曝光方案过暗,因此不再作为最终交付配置。
|
||||||
|
|
||||||
|
| 指标 | 结果 |
|
||||||
|
| --- | --- |
|
||||||
|
| 完整接收帧 | 6647 |
|
||||||
|
| 丢帧 | 12,0.1802% |
|
||||||
|
| 完整接收帧率 | 22.1565 fps |
|
||||||
|
| 本地 JPEG 通道帧率 | 20.8365 fps,6251 帧 |
|
||||||
|
| 接收通道帧间隔 P95 / 最大值 | 102.459 / 408.082 ms |
|
||||||
|
| 平均 JPEG 大小 | 9113 字节 |
|
||||||
|
| 实际曝光 | 全程 220 行 |
|
||||||
|
| 采集错误 / 复位 / 本地连接断开 | 0 / 0 / 0 |
|
||||||
|
|
||||||
|
图传完整帧率已接近该采集配置的约 22.2 fps 上限;本地通道采用 latest 模式,突发到达的多帧可能被合并,不能用其帧数代表屏幕已绘制帧数。不同场景下 JPEG 大小和无线负载会变化。
|
||||||
|
|
||||||
|
## 间隔发送的中止轮次
|
||||||
|
|
||||||
|
1000 μs 最小分片间隔轮次在设备运行约 235 秒时出现串口 I/O 错误,随后系统不再枚举开发板串口,摄像头同时离线。观察器提前停止,未完成有效的五分钟对照,断连尾段不能归因于调度。
|
||||||
|
|
||||||
|
断连前 3 个完整在线窗口合计 181.192 秒,接收 21.58 fps、帧损失 1.96%,本地通道 17.25 fps,没有显示出逐包间隔的收益。它可能影响 AMPDU 聚合效率,但未抓取空口包,不能确认唯一原因。保留突发发送作为已验证基线,不采用这轮的完整汇总作性能结论。
|
||||||
|
|
||||||
|
## 已撤回的行时序提帧试验
|
||||||
|
|
||||||
|
新增可选 `CAM_HTS`,仅改变 OV5640 水平总时长,保留有效读出窗口及消隐余量,PLL、XCLK、VTS 不变。候选参数为 HTS 1500、快门 300 行,估算保持约 10 ms 曝光,采集节拍目标约 30.5 fps。
|
||||||
|
|
||||||
|
USB 恢复后,HTS 1500 的板上采集探针达到 30.49 fps,但白光环境出现明显绿紫闪烁,用户确认不可用。HTS 1800 的探针为 25.41 fps,仍出现明显偏紫,也已中止;两者均不作为有效五分钟彩色结果或交付配置。恢复默认 HTS 2060 后,用户确认颜色稳定。
|
||||||
|
|
||||||
|
1500 轮次开始时队伍 release 模式隐藏接收计数,因此该轮汇总中的接收 fps / 丢帧为无效值,不能当作零丢帧;观察脚本现已要求裁判诊断计数并记录诊断可用性。`0x380C/0x380D` 的水平总长定义见 [OV5640 数据手册](https://files.waveshare.com/upload/d/da/OV5640_DataSheet.pdf)。几何检查和 JPEG 成功解码都不足以证明彩色稳定。
|
||||||
|
|
||||||
|
## 室内亮度回归
|
||||||
|
|
||||||
|
用户确认 −1 EV / 固定 10 ms,以及恢复名义 0 EV / 固定约 20 ms 后仍然太暗。因此最终方案不再固定短快门,而是让 AEC / AGC 自动联动,并以曝光上限控制拖影。手册说明 AGC 通常在 AEC 到达上限后继续增加;仅保留 AGC 开关不能当作亮度已经补足的证据。需要结合实际曝光、增益和画面验证。
|
||||||
|
|
||||||
|
固定 20 ms 的完整五分钟轮次接收 5220 帧、丢帧 1430(21.50%),接收 17.40 fps,本地 JPEG 通道 13.45 fps,接收间隔 P95 277.473 ms、最大 1163.792 ms;全程在线、无采集错误。它保留为拥塞对照,未被选为最终配置。
|
||||||
|
|
||||||
|
## 发送调度
|
||||||
|
|
||||||
|
当前启用有界 JPEG 压缩调节,`CAM_QUALITY=18` 为画质恢复目标,`CAM_QUALITY_MAX=30` 为压缩上限。每 5 秒检查本机发送窗口:对端帧放弃比例达到 2%,或平均提交耗时达到 8 ms 时,压缩数字增加 3 档;连续 3 个无放弃且提交小于 4 ms 的窗口后,才降低 1 档恢复细节。无订阅或流量不足时不会当作健康窗口。
|
||||||
|
|
||||||
|
该反馈来自板端提交回压,不包含接收端 ACK;不能据此声称消除了路由器后续丢包。调节不改变彩色、分辨率或采集时钟,运行日志记录实际 JPEG 档位、曝光行数和增益。
|
||||||
|
|
||||||
|
视频数据报使用 WMM AC_VI、保留 AMPDU。`UDP_PACKET_GAP_US` 控制成功提交数据报的最小间隔,调度 tick 可能使实际等待更长。等待期间让出 CPU,避免在帧开始时集中向 Wi-Fi 队列灌入全部分片。
|
||||||
|
|
||||||
|
每帧预算为 25 ms 加计划分片间隔,总等待上限 40 ms;没有调度间隔时仍为 25 ms。超过预算则放弃该对端的剩余旧帧,不追赶补发。分片数量较多时需要重新选择间隔,不能把某个 VGA 配置的结果直接套到更大图像。
|
||||||
|
|
||||||
|
`submitted` 只表示分片全部交给了网络,不代表无线确认或屏幕显示。帧损失包含接收端未完成帧与帧号缺口;设置帧率上限后,latest 模式有意覆盖的旧采集帧不进入 UDP 帧编号,不计入该丢帧率。
|
||||||
|
|
||||||
|
## 此前中止的灰度长测
|
||||||
|
|
||||||
|
原计划 30 分钟的灰度 / 15 fps 长测按用户要求提前停止,保留了 **18 个完整分钟窗口**,合计 1080.15 秒:完整帧 15307、丢帧 806,平均 14.17 fps,帧损失 5.00%。板端记录温度约 48–58°C,无复位和采集错误。
|
||||||
|
|
||||||
|
早期丢帧上升时温度没有同步上升,温度下降期间也出现过明显发送失败。内存紧张后能够恢复;发送失败与队列回压更符合当前证据。这一轮不计为完成 30 分钟验证,且配置不同,不能与彩色不限帧试验直接作单因素对照。
|
||||||
|
|
||||||
|
温度控制仍可选:达到 75°C 时将已设置的帧率减半(向上取整),原来不限帧时降至 8 fps;降到 70°C 后恢复。阈值可设 40–95°C,`0` 禁用。75°C 是主动减载策略,不是芯片损坏温度规格。本轮未触发该策略。ESP 内部温度不能代表 OV5640 或环境温度:[ESP-IDF v5.5.5 温度传感器说明](https://docs.espressif.com/projects/esp-idf/en/v5.5.5/esp32s3/api-reference/peripherals/temp_sensor.html)。
|
||||||
|
|
||||||
|
## 复测与记录
|
||||||
|
|
||||||
|
退出其他图传客户端,使用一个非演示模式的 Windows 裁判端。release 队伍模式隐藏技术计数,不能用于本脚本的接收 fps / 丢帧测量;用户在本机切换身份后可继续使用原进程,本次没有改写用户配置。`RUST_LOG=cam_desktop=debug,cam_core=info` 会记录本地 WebSocket 地址及令牌,原始日志仅供本机诊断,不应公开。
|
||||||
|
|
||||||
|
从仓库根目录分别运行,串口及设备 ID 换成实际值:
|
||||||
|
|
||||||
|
```text
|
||||||
|
python scripts/serial_soak.py --port COM7 --seconds 330 --out target/soak/device --stop-file target/soak/stop
|
||||||
|
node desktop/scripts/soak-stream.mjs --log target/soak/referee-stderr.log --out target/soak/client --seconds 300 --warmup 10 --stop-file target/soak/stop --sample-every 5
|
||||||
|
```
|
||||||
|
|
||||||
|
串口脚本依赖 `pyserial`;观察脚本使用 Node.js 22 内置 WebSocket。默认设备 ID 为 `0x3D4D8A9A`,换板用 `--camera 0x设备ID`。创建 stop 文件可提前结束并保存部分窗口;新一轮使用不存在的 stop 文件路径。
|
||||||
|
|
||||||
|
串口每 5 秒保存温度、曝光行数、增益、JPEG 档位、内部堆、采集错误、发送重试、订阅数与帧率。客户端每分钟保存完整帧率、帧损失、JPEG 通道帧率、间隔 P95 / 最大值与连接状态,并标记离线、诊断不可用与末尾无视频时长。`--sample-every` 可选,单位为秒,抽帧仅留本机。JSON 标记实际时长与是否提前停止,不能将部分窗口称为完成 5 分钟。原始记录位于本机 `target/soak/`,随构建缓存忽略。
|
||||||
|
|
||||||
|
JPEG 长度与首尾标记校验不等于每张图的完整解码验证;本地 WebSocket 通道不代表 WebView 最终绘制。接收间隔与分片组装耗时均不是摄像头到屏幕的光学延迟。
|
||||||
@@ -1,3 +1,79 @@
|
|||||||
|
// ESP-IDF 的环境导出 + 固件版本信息注入。
|
||||||
|
//
|
||||||
|
// # 版本号为什么在这里注入
|
||||||
|
//
|
||||||
|
// 固件的版本必须和客户端、和 `version.json` 是**同一个数**。手工在 `src/` 里写一个
|
||||||
|
// 常量,第一次忘了改就分叉了,而且没有任何东西会报错——现场只会表现为"界面说固件 0.2.0,
|
||||||
|
// 实际烧的是别的"。
|
||||||
|
//
|
||||||
|
// 所以这里在编译期把 `version.json` 读进来,导出成 `cargo:rustc-env`,源码里用
|
||||||
|
// `env!()` 取。`version.json` 由 `scripts/version.mjs` 从 git 历史算出来,于是
|
||||||
|
// 一条链是通的:git → version.json → 固件二进制 → Hello 报文 → 客户端界面。
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
embuild::espidf::sysenv::output();
|
embuild::espidf::sysenv::output();
|
||||||
|
emit_version_env();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 把 `version.json` 里的版本拆成几个编译期常量。
|
||||||
|
fn emit_version_env() {
|
||||||
|
let manifest = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
|
||||||
|
let path = Path::new(&manifest).join("version.json");
|
||||||
|
|
||||||
|
// 这个文件改了要重新编译,否则版本号会停在旧值上而二进制看起来是新的。
|
||||||
|
println!("cargo:rerun-if-changed={}", path.display());
|
||||||
|
|
||||||
|
// 没有 version.json 也要能编:直接 cargo build 的人可能还没跑过生成脚本。
|
||||||
|
// 这时退回 0.0.0+0 并留一条编译器警告——比编不过友好,比编出一个假的版本诚实。
|
||||||
|
let (major, minor, patch, build) = match fs::read_to_string(&path) {
|
||||||
|
Ok(text) => match parse(&text) {
|
||||||
|
Some(parsed) => parsed,
|
||||||
|
None => {
|
||||||
|
println!("cargo:warning=version.json 解析失败,固件版本回退为 0.0.0+0");
|
||||||
|
(0, 0, 0, 0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => {
|
||||||
|
println!(
|
||||||
|
"cargo:warning=找不到 version.json,固件版本回退为 0.0.0+0(先跑 node scripts/version.mjs --write)"
|
||||||
|
);
|
||||||
|
(0, 0, 0, 0)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let packed = ((major as u32) << 16) | (((minor as u32) & 0xff) << 8) | ((patch as u32) & 0xff);
|
||||||
|
|
||||||
|
println!("cargo:rustc-env=RMIC_VERSION={major}.{minor}.{patch}");
|
||||||
|
println!("cargo:rustc-env=RMIC_VERSION_PACKED={packed}");
|
||||||
|
println!("cargo:rustc-env=RMIC_BUILD={build}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从 `version.json` 里抠出四段数字。
|
||||||
|
///
|
||||||
|
/// 刻意不引入 `serde_json`:解析四个字段不值得多一个编译期依赖,而固件的构建时间本来
|
||||||
|
/// 就是这个项目里最贵的东西。
|
||||||
|
fn parse(text: &str) -> Option<(u64, u64, u64, u64)> {
|
||||||
|
let field = |name: &str| -> Option<u64> {
|
||||||
|
let key = format!("\"{name}\"");
|
||||||
|
let at = text.find(&key)?;
|
||||||
|
let rest = &text[at + key.len()..];
|
||||||
|
let colon = rest.find(':')?;
|
||||||
|
let rest = &rest[colon + 1..];
|
||||||
|
let start = rest.find(|c: char| c.is_ascii_digit())?;
|
||||||
|
let digits: String = rest[start..]
|
||||||
|
.chars()
|
||||||
|
.take_while(|c| c.is_ascii_digit())
|
||||||
|
.collect();
|
||||||
|
digits.parse().ok()
|
||||||
|
};
|
||||||
|
|
||||||
|
Some((
|
||||||
|
field("major")?,
|
||||||
|
field("minor")?,
|
||||||
|
field("patch")?,
|
||||||
|
field("build")?,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
@@ -2,4 +2,4 @@ idf_component_register(SRCS "cam_wrapper.c"
|
|||||||
INCLUDE_DIRS "include"
|
INCLUDE_DIRS "include"
|
||||||
# esp_timer for the frame-rate probe; the camera component
|
# esp_timer for the frame-rate probe; the camera component
|
||||||
# pulls in the LCD_CAM driver and the FreeRTOS task API.
|
# pulls in the LCD_CAM driver and the FreeRTOS task API.
|
||||||
REQUIRES esp32-camera esp_timer)
|
REQUIRES esp32-camera esp_timer esp_driver_tsens)
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
/* Implementation of the cam_wrapper shim around esp32-camera. See cam_wrapper.h. */
|
/* Implementation of the cam_wrapper shim around esp32-camera. See cam_wrapper.h. */
|
||||||
|
|
||||||
#include "cam_wrapper.h"
|
#include "cam_wrapper.h"
|
||||||
|
#include "driver/temperature_sensor.h"
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_timer.h"
|
#include "esp_timer.h"
|
||||||
|
|
||||||
static void cam_wrapper_measure_fps(cam_wrapper_sensor_t sensor);
|
static float cam_wrapper_measure_fps(cam_wrapper_sensor_t sensor);
|
||||||
|
|
||||||
static const char *TAG = "cam_wrapper";
|
static const char *TAG = "cam_wrapper";
|
||||||
|
|
||||||
static sensor_t *s_sensor;
|
static sensor_t *s_sensor;
|
||||||
static char s_sensor_name[16] = "unknown";
|
static char s_sensor_name[16] = "unknown";
|
||||||
|
static temperature_sensor_handle_t s_temperature;
|
||||||
|
|
||||||
static void fill_config(const cam_wrapper_config_t *in, camera_config_t *out)
|
static void fill_config(const cam_wrapper_config_t *in, camera_config_t *out)
|
||||||
{
|
{
|
||||||
@@ -134,7 +136,7 @@ esp_err_t cam_wrapper_init(const cam_wrapper_config_t *config, cam_wrapper_senso
|
|||||||
* two is dropping frames. */
|
* two is dropping frames. */
|
||||||
#define PROBE_FRAMES 20
|
#define PROBE_FRAMES 20
|
||||||
|
|
||||||
static void cam_wrapper_measure_fps(cam_wrapper_sensor_t sensor)
|
static float cam_wrapper_measure_fps(cam_wrapper_sensor_t sensor)
|
||||||
{
|
{
|
||||||
uint32_t total = 0;
|
uint32_t total = 0;
|
||||||
int64_t start = esp_timer_get_time();
|
int64_t start = esp_timer_get_time();
|
||||||
@@ -158,7 +160,7 @@ static void cam_wrapper_measure_fps(cam_wrapper_sensor_t sensor)
|
|||||||
|
|
||||||
if (taken == 0 || elapsed_us <= 0) {
|
if (taken == 0 || elapsed_us <= 0) {
|
||||||
ESP_LOGW(TAG, "fps probe: got no frames");
|
ESP_LOGW(TAG, "fps probe: got no frames");
|
||||||
return;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
double fps = (double)taken * 1000000.0 / (double)elapsed_us;
|
double fps = (double)taken * 1000000.0 / (double)elapsed_us;
|
||||||
@@ -167,6 +169,159 @@ static void cam_wrapper_measure_fps(cam_wrapper_sensor_t sensor)
|
|||||||
"(sensor ceiling, no network involved)",
|
"(sensor ceiling, no network involved)",
|
||||||
taken, (long long)(elapsed_us / 1000), fps,
|
taken, (long long)(elapsed_us / 1000), fps,
|
||||||
(unsigned)(total / (uint32_t)taken));
|
(unsigned)(total / (uint32_t)taken));
|
||||||
|
return (float)fps;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t cam_wrapper_set_grayscale(int enabled)
|
||||||
|
{
|
||||||
|
if (!s_sensor || !s_sensor->set_special_effect) return ESP_ERR_NOT_SUPPORTED;
|
||||||
|
int rc = s_sensor->set_special_effect(s_sensor, enabled ? 2 : 0);
|
||||||
|
if (rc != 0) return ESP_FAIL;
|
||||||
|
ESP_LOGI(TAG, "sensor JPEG colour: %s", enabled ? "grayscale" : "colour");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t cam_wrapper_set_quality(unsigned quality)
|
||||||
|
{
|
||||||
|
if (quality > 63) return ESP_ERR_INVALID_ARG;
|
||||||
|
if (!s_sensor || !s_sensor->set_quality) return ESP_ERR_NOT_SUPPORTED;
|
||||||
|
return s_sensor->set_quality(s_sensor, (int)quality) == 0 ? ESP_OK : ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t cam_wrapper_set_exposure_bias(int ev)
|
||||||
|
{
|
||||||
|
if (ev < -2 || ev > 0) return ESP_ERR_INVALID_ARG;
|
||||||
|
if (!s_sensor || s_sensor->id.PID != OV5640_PID)
|
||||||
|
return ev == 0 ? ESP_OK : ESP_ERR_NOT_SUPPORTED;
|
||||||
|
/* Driver target = (level+5)*10+5, default 55. Targets 25 and 15
|
||||||
|
* are ~-1.14 and ~-1.87 EV, not literal driver levels -1 and -2. */
|
||||||
|
int level = ev == -1 ? -3 : ev == -2 ? -4 : 0;
|
||||||
|
if (s_sensor->set_exposure_ctrl(s_sensor, 1) != 0
|
||||||
|
|| s_sensor->set_gain_ctrl(s_sensor, 1) != 0
|
||||||
|
|| s_sensor->set_ae_level(s_sensor, level) != 0) return ESP_FAIL;
|
||||||
|
ESP_LOGI(TAG, "OV5640 auto exposure bias: requested %d EV, driver level %d, target %d / 55",
|
||||||
|
ev, level, (level + 5) * 10 + 5);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cam_wrapper_exposure_lines(void)
|
||||||
|
{
|
||||||
|
if (!s_sensor || s_sensor->id.PID != OV5640_PID) return -1;
|
||||||
|
int raw = s_sensor->get_reg(s_sensor, 0x3500, 0xfffff);
|
||||||
|
return raw < 0 ? -1 : raw >> 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cam_wrapper_gain_raw(void)
|
||||||
|
{
|
||||||
|
if (!s_sensor || s_sensor->id.PID != OV5640_PID) return -1;
|
||||||
|
return s_sensor->get_reg(s_sensor, 0x350a, 0x03ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t cam_wrapper_set_shutter_lines(unsigned lines)
|
||||||
|
{
|
||||||
|
if (!s_sensor || s_sensor->id.PID != OV5640_PID)
|
||||||
|
return lines == 0 ? ESP_OK : ESP_ERR_NOT_SUPPORTED;
|
||||||
|
int vts = s_sensor->get_reg(s_sensor, 0x380e, 0xffff);
|
||||||
|
if (vts <= 0 || lines > (unsigned)vts) return ESP_ERR_INVALID_ARG;
|
||||||
|
int previous = cam_wrapper_exposure_lines();
|
||||||
|
if (s_sensor->set_exposure_ctrl(s_sensor, lines == 0) != 0
|
||||||
|
|| s_sensor->set_gain_ctrl(s_sensor, 1) != 0) return ESP_FAIL;
|
||||||
|
if (lines && (s_sensor->set_aec_value(s_sensor, (int)lines) != 0
|
||||||
|
|| cam_wrapper_exposure_lines() != (int)lines)) return ESP_FAIL;
|
||||||
|
ESP_LOGI(TAG, "OV5640 shutter: previous %d lines, requested %u lines (0=auto), AGC enabled",
|
||||||
|
previous, lines);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t cam_wrapper_set_horizontal_timing(unsigned hts)
|
||||||
|
{
|
||||||
|
if (!hts) return ESP_OK;
|
||||||
|
if (!s_sensor || s_sensor->id.PID != OV5640_PID) return ESP_ERR_NOT_SUPPORTED;
|
||||||
|
int start = s_sensor->get_reg(s_sensor, 0x3800, 0x0fff);
|
||||||
|
int end = s_sensor->get_reg(s_sensor, 0x3804, 0x0fff);
|
||||||
|
int previous = s_sensor->get_reg(s_sensor, 0x380c, 0x0fff);
|
||||||
|
if (start < 0 || end < start || previous <= 0) return ESP_FAIL;
|
||||||
|
unsigned active = (unsigned)(end - start + 1) / (s_sensor->status.binning ? 2 : 1);
|
||||||
|
/* Preserve the active readout window and a conservative blanking margin.
|
||||||
|
* This is a guard against invalid geometry, not a stability guarantee. */
|
||||||
|
if ((hts & 1) || hts < active + 64 || hts > 4095) return ESP_ERR_INVALID_ARG;
|
||||||
|
/* Use the byte order that cannot transiently shorten the line below
|
||||||
|
* either valid endpoint while updating this live sensor. */
|
||||||
|
int rc;
|
||||||
|
if (hts < (unsigned)previous) {
|
||||||
|
rc = s_sensor->set_reg(s_sensor, 0x380d, 0xff, hts & 0xff)
|
||||||
|
|| s_sensor->set_reg(s_sensor, 0x380c, 0x0f, hts >> 8);
|
||||||
|
} else {
|
||||||
|
rc = s_sensor->set_reg(s_sensor, 0x380c, 0x0f, hts >> 8)
|
||||||
|
|| s_sensor->set_reg(s_sensor, 0x380d, 0xff, hts & 0xff);
|
||||||
|
}
|
||||||
|
if (rc || s_sensor->get_reg(s_sensor, 0x380c, 0x0fff) != (int)hts) return ESP_FAIL;
|
||||||
|
ESP_LOGI(TAG, "OV5640 horizontal timing: HTS %d -> %u, active %u, VTS %d (PLL/XCLK unchanged)",
|
||||||
|
previous, hts, active, s_sensor->get_reg(s_sensor, 0x380e, 0xffff));
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t cam_wrapper_set_exposure_limit(unsigned lines)
|
||||||
|
{
|
||||||
|
if (!lines) return ESP_OK;
|
||||||
|
if (!s_sensor || s_sensor->id.PID != OV5640_PID) return ESP_ERR_NOT_SUPPORTED;
|
||||||
|
int vts = s_sensor->get_reg(s_sensor, 0x380e, 0xffff);
|
||||||
|
int band50 = s_sensor->get_reg(s_sensor, 0x3a08, 0x03ff);
|
||||||
|
int band60 = s_sensor->get_reg(s_sensor, 0x3a0a, 0x03ff);
|
||||||
|
if (vts <= 4 || lines > (unsigned)(vts - 4) || band50 <= 0 || band60 <= 0)
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
unsigned max50 = lines / (unsigned)band50;
|
||||||
|
unsigned max60 = lines / (unsigned)band60;
|
||||||
|
if (!max50) max50 = 1;
|
||||||
|
if (!max60) max60 = 1;
|
||||||
|
if (max50 > 63 || max60 > 63) return ESP_ERR_INVALID_ARG;
|
||||||
|
int rc = s_sensor->set_exposure_ctrl(s_sensor, 0)
|
||||||
|
|| s_sensor->set_aec2(s_sensor, 0)
|
||||||
|
|| s_sensor->set_reg(s_sensor, 0x3a02, 0xff, lines >> 8)
|
||||||
|
|| s_sensor->set_reg(s_sensor, 0x3a03, 0xff, lines & 0xff)
|
||||||
|
|| s_sensor->set_reg(s_sensor, 0x3a14, 0x0f, lines >> 8)
|
||||||
|
|| s_sensor->set_reg(s_sensor, 0x3a15, 0xff, lines & 0xff)
|
||||||
|
|| s_sensor->set_reg(s_sensor, 0x3a0d, 0x3f, max60)
|
||||||
|
|| s_sensor->set_reg(s_sensor, 0x3a0e, 0x3f, max50)
|
||||||
|
|| s_sensor->set_gain_ctrl(s_sensor, 1)
|
||||||
|
|| s_sensor->set_exposure_ctrl(s_sensor, 1);
|
||||||
|
if (rc || s_sensor->get_reg(s_sensor, 0x3a02, 0xffff) != (int)lines
|
||||||
|
|| s_sensor->get_reg(s_sensor, 0x3a14, 0x0fff) != (int)lines)
|
||||||
|
return ESP_FAIL;
|
||||||
|
ESP_LOGI(TAG, "OV5640 auto exposure cap: %u lines, band maxima 50Hz=%u / 60Hz=%u, AEC/AGC auto, night mode off",
|
||||||
|
lines, max50, max60);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
float cam_wrapper_probe_frame_rate(void)
|
||||||
|
{
|
||||||
|
/* Drain the old three-buffer pipeline after changing the colour effect. */
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
cam_wrapper_fb_t fb;
|
||||||
|
if (cam_wrapper_fb_get(&fb, CAM_WRAPPER_SENSOR_AUTO) == ESP_OK)
|
||||||
|
cam_wrapper_fb_release(&fb);
|
||||||
|
}
|
||||||
|
return cam_wrapper_measure_fps(CAM_WRAPPER_SENSOR_AUTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t cam_wrapper_temperature_init(void)
|
||||||
|
{
|
||||||
|
if (s_temperature) return ESP_OK;
|
||||||
|
temperature_sensor_config_t config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(20, 100);
|
||||||
|
esp_err_t rc = temperature_sensor_install(&config, &s_temperature);
|
||||||
|
if (rc != ESP_OK) return rc;
|
||||||
|
rc = temperature_sensor_enable(s_temperature);
|
||||||
|
if (rc != ESP_OK) {
|
||||||
|
temperature_sensor_uninstall(s_temperature);
|
||||||
|
s_temperature = NULL;
|
||||||
|
}
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t cam_wrapper_temperature_read(float *celsius)
|
||||||
|
{
|
||||||
|
if (!s_temperature) return ESP_ERR_INVALID_STATE;
|
||||||
|
return temperature_sensor_get_celsius(s_temperature, celsius);
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t cam_wrapper_fb_get(cam_wrapper_fb_t *out, cam_wrapper_sensor_t sensor)
|
esp_err_t cam_wrapper_fb_get(cam_wrapper_fb_t *out, cam_wrapper_sensor_t sensor)
|
||||||
@@ -190,10 +345,29 @@ esp_err_t cam_wrapper_fb_get(cam_wrapper_fb_t *out, cam_wrapper_sensor_t sensor)
|
|||||||
out->height = fb->height;
|
out->height = fb->height;
|
||||||
out->format = (int)fb->format;
|
out->format = (int)fb->format;
|
||||||
out->handle = fb;
|
out->handle = fb;
|
||||||
|
out->capture_started_us = (int64_t)fb->timestamp.tv_sec * 1000000 + fb->timestamp.tv_usec;
|
||||||
|
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
esp_err_t cam_wrapper_fb_get_latest(cam_wrapper_fb_t *out, uint32_t timeout_ms)
|
||||||
|
{
|
||||||
|
if (!out) return ESP_ERR_INVALID_ARG;
|
||||||
|
memset(out, 0, sizeof(*out));
|
||||||
|
uint32_t skipped = 0;
|
||||||
|
camera_fb_t *fb = esp_camera_fb_get_latest(timeout_ms, &skipped);
|
||||||
|
if (!fb) return ESP_FAIL;
|
||||||
|
out->data = fb->buf;
|
||||||
|
out->len = fb->len;
|
||||||
|
out->width = fb->width;
|
||||||
|
out->height = fb->height;
|
||||||
|
out->format = (int)fb->format;
|
||||||
|
out->handle = fb;
|
||||||
|
out->capture_started_us = (int64_t)fb->timestamp.tv_sec * 1000000 + fb->timestamp.tv_usec;
|
||||||
|
out->skipped_before = skipped;
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
void cam_wrapper_fb_release(cam_wrapper_fb_t *fb)
|
void cam_wrapper_fb_release(cam_wrapper_fb_t *fb)
|
||||||
{
|
{
|
||||||
if (fb == NULL || fb->handle == NULL) {
|
if (fb == NULL || fb->handle == NULL) {
|
||||||
|
|||||||
@@ -112,6 +112,8 @@ typedef struct {
|
|||||||
size_t height;
|
size_t height;
|
||||||
int format;
|
int format;
|
||||||
void *handle; /* internal driver frame; do not touch */
|
void *handle; /* internal driver frame; do not touch */
|
||||||
|
int64_t capture_started_us; /* same monotonic clock as esp_timer_get_time */
|
||||||
|
uint32_t skipped_before; /* older completed frames released by latest get */
|
||||||
} cam_wrapper_fb_t;
|
} cam_wrapper_fb_t;
|
||||||
|
|
||||||
esp_err_t cam_wrapper_init(const cam_wrapper_config_t *config, cam_wrapper_sensor_t sensor);
|
esp_err_t cam_wrapper_init(const cam_wrapper_config_t *config, cam_wrapper_sensor_t sensor);
|
||||||
@@ -119,6 +121,7 @@ esp_err_t cam_wrapper_init(const cam_wrapper_config_t *config, cam_wrapper_senso
|
|||||||
/* Acquire one frame into `out`. Returns ESP_OK on success, ESP_FAIL if the
|
/* Acquire one frame into `out`. Returns ESP_OK on success, ESP_FAIL if the
|
||||||
* driver could not hand out a frame (e.g. timeout or buffer starvation). */
|
* driver could not hand out a frame (e.g. timeout or buffer starvation). */
|
||||||
esp_err_t cam_wrapper_fb_get(cam_wrapper_fb_t *out, cam_wrapper_sensor_t sensor);
|
esp_err_t cam_wrapper_fb_get(cam_wrapper_fb_t *out, cam_wrapper_sensor_t sensor);
|
||||||
|
esp_err_t cam_wrapper_fb_get_latest(cam_wrapper_fb_t *out, uint32_t timeout_ms);
|
||||||
|
|
||||||
void cam_wrapper_fb_release(cam_wrapper_fb_t *fb);
|
void cam_wrapper_fb_release(cam_wrapper_fb_t *fb);
|
||||||
|
|
||||||
@@ -129,6 +132,26 @@ esp_err_t cam_wrapper_deinit(void);
|
|||||||
/* Name of the sensor the driver detected, or "unknown". Never NULL. */
|
/* Name of the sensor the driver detected, or "unknown". Never NULL. */
|
||||||
const char *cam_wrapper_sensor_name(void);
|
const char *cam_wrapper_sensor_name(void);
|
||||||
|
|
||||||
|
/* Keep the sensor's JPEG encoder; effect 2 removes chroma in the sensor ISP. */
|
||||||
|
esp_err_t cam_wrapper_set_grayscale(int enabled);
|
||||||
|
/* Runtime JPEG compression, 0..63, without restarting the camera pipeline. */
|
||||||
|
esp_err_t cam_wrapper_set_quality(unsigned quality);
|
||||||
|
/* OV5640 target bias: 0 = default, -1 / -2 select approximately -1.1 / -1.9 EV. */
|
||||||
|
esp_err_t cam_wrapper_set_exposure_bias(int ev);
|
||||||
|
/* 0 = auto shutter; nonzero holds that exposure in sensor lines, AGC stays on. */
|
||||||
|
esp_err_t cam_wrapper_set_shutter_lines(unsigned lines);
|
||||||
|
/* Keep AEC/AGC automatic, cap exposure lines, and disable night frame extension. */
|
||||||
|
esp_err_t cam_wrapper_set_exposure_limit(unsigned lines);
|
||||||
|
/* 0 keeps the driver timing; nonzero changes OV5640 horizontal total only. */
|
||||||
|
esp_err_t cam_wrapper_set_horizontal_timing(unsigned hts);
|
||||||
|
int cam_wrapper_exposure_lines(void);
|
||||||
|
/* OV5640 gain in 1/16 units; -1 when unavailable. */
|
||||||
|
int cam_wrapper_gain_raw(void);
|
||||||
|
float cam_wrapper_probe_frame_rate(void);
|
||||||
|
/* Silicon temperature, not ambient or camera-sensor temperature. */
|
||||||
|
esp_err_t cam_wrapper_temperature_init(void);
|
||||||
|
esp_err_t cam_wrapper_temperature_read(float *celsius);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -733,13 +733,14 @@ camera_fb_t *cam_take(TickType_t timeout)
|
|||||||
for (;;)
|
for (;;)
|
||||||
{
|
{
|
||||||
TickType_t elapsed = xTaskGetTickCount() - start; /* TickType_t is unsigned so rollover is safe */
|
TickType_t elapsed = xTaskGetTickCount() - start; /* TickType_t is unsigned so rollover is safe */
|
||||||
if (elapsed >= timeout) {
|
if (timeout && elapsed >= timeout) {
|
||||||
ESP_LOGW(TAG, "Failed to get frame: timeout");
|
ESP_LOGW(TAG, "Failed to get frame: timeout");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
TickType_t remaining = timeout - elapsed;
|
TickType_t remaining = timeout ? timeout - elapsed : 0;
|
||||||
|
|
||||||
if (xQueueReceive(cam_obj->frame_buffer_queue, (void *)&dma_buffer, remaining) == pdFALSE) {
|
if (xQueueReceive(cam_obj->frame_buffer_queue, (void *)&dma_buffer, remaining) == pdFALSE) {
|
||||||
|
if (!timeout) return NULL;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -750,6 +751,7 @@ camera_fb_t *cam_take(TickType_t timeout)
|
|||||||
if (dma_reset_counter < MAX_GDMA_RESETS) {
|
if (dma_reset_counter < MAX_GDMA_RESETS) {
|
||||||
ll_cam_dma_reset(cam_obj);
|
ll_cam_dma_reset(cam_obj);
|
||||||
dma_reset_counter++;
|
dma_reset_counter++;
|
||||||
|
if (!timeout) return NULL;
|
||||||
continue; /* retry with queue timeout */
|
continue; /* retry with queue timeout */
|
||||||
}
|
}
|
||||||
if (dma_reset_counter == MAX_GDMA_RESETS) {
|
if (dma_reset_counter == MAX_GDMA_RESETS) {
|
||||||
@@ -762,6 +764,7 @@ camera_fb_t *cam_take(TickType_t timeout)
|
|||||||
CAM_WARN_THROTTLE(warn_null_cnt,
|
CAM_WARN_THROTTLE(warn_null_cnt,
|
||||||
"Unexpected NULL frame on " CONFIG_IDF_TARGET);
|
"Unexpected NULL frame on " CONFIG_IDF_TARGET);
|
||||||
#endif
|
#endif
|
||||||
|
if (!timeout) return NULL;
|
||||||
vTaskDelay(1); /* immediate yield once resets are done */
|
vTaskDelay(1); /* immediate yield once resets are done */
|
||||||
continue; /* go to top of loop */
|
continue; /* go to top of loop */
|
||||||
}
|
}
|
||||||
@@ -802,6 +805,7 @@ skip_eoi_check:
|
|||||||
CAM_WARN_THROTTLE(warn_eoi_miss_cnt,
|
CAM_WARN_THROTTLE(warn_eoi_miss_cnt,
|
||||||
"NO-EOI - JPEG end marker missing");
|
"NO-EOI - JPEG end marker missing");
|
||||||
cam_give(dma_buffer);
|
cam_give(dma_buffer);
|
||||||
|
if (!timeout) return NULL;
|
||||||
continue; /* wait for another frame */
|
continue; /* wait for another frame */
|
||||||
} else if (cam_obj->psram_mode &&
|
} else if (cam_obj->psram_mode &&
|
||||||
cam_obj->in_bytes_per_pixel != cam_obj->fb_bytes_per_pixel) {
|
cam_obj->in_bytes_per_pixel != cam_obj->fb_bytes_per_pixel) {
|
||||||
@@ -818,6 +822,24 @@ skip_eoi_check:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
camera_fb_t *cam_take_latest(TickType_t timeout, uint32_t *skipped)
|
||||||
|
{
|
||||||
|
if (skipped) *skipped = 0;
|
||||||
|
camera_fb_t *latest = cam_take(timeout);
|
||||||
|
if (!latest) return NULL;
|
||||||
|
/* A bounded drain, not availability-check + blocking get: the producer can
|
||||||
|
* replace queued buffers on the other core. Retain the valid frame if a
|
||||||
|
* newer candidate is invalid or a GDMA-reset sentinel is encountered. */
|
||||||
|
for (size_t i = 1; i < cam_obj->frame_cnt; i++) {
|
||||||
|
camera_fb_t *next = cam_take(0);
|
||||||
|
if (!next) break;
|
||||||
|
cam_give(latest);
|
||||||
|
latest = next;
|
||||||
|
if (skipped) (*skipped)++;
|
||||||
|
}
|
||||||
|
return latest;
|
||||||
|
}
|
||||||
|
|
||||||
void cam_give(camera_fb_t *dma_buffer)
|
void cam_give(camera_fb_t *dma_buffer)
|
||||||
{
|
{
|
||||||
for (int x = 0; x < cam_obj->frame_cnt; x++) {
|
for (int x = 0; x < cam_obj->frame_cnt; x++) {
|
||||||
|
|||||||
@@ -401,6 +401,19 @@ camera_fb_t *esp_camera_fb_get()
|
|||||||
return fb;
|
return fb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
camera_fb_t *esp_camera_fb_get_latest(uint32_t timeout_ms, uint32_t *skipped)
|
||||||
|
{
|
||||||
|
if (skipped) *skipped = 0;
|
||||||
|
if (s_state == NULL) return NULL;
|
||||||
|
camera_fb_t *fb = cam_take_latest(pdMS_TO_TICKS(timeout_ms), skipped);
|
||||||
|
if (fb) {
|
||||||
|
fb->width = resolution[s_state->sensor.status.framesize].width;
|
||||||
|
fb->height = resolution[s_state->sensor.status.framesize].height;
|
||||||
|
fb->format = s_state->sensor.pixformat;
|
||||||
|
}
|
||||||
|
return fb;
|
||||||
|
}
|
||||||
|
|
||||||
void esp_camera_fb_return(camera_fb_t *fb)
|
void esp_camera_fb_return(camera_fb_t *fb)
|
||||||
{
|
{
|
||||||
if (s_state == NULL) {
|
if (s_state == NULL) {
|
||||||
|
|||||||
@@ -206,6 +206,9 @@ esp_err_t esp_camera_deinit(void);
|
|||||||
* @return pointer to the frame buffer
|
* @return pointer to the frame buffer
|
||||||
*/
|
*/
|
||||||
camera_fb_t* esp_camera_fb_get(void);
|
camera_fb_t* esp_camera_fb_get(void);
|
||||||
|
/* At most timeout_ms waiting for the first frame; pending frames are drained
|
||||||
|
* nonblocking, keeping the newest valid JPEG. skipped may be NULL. */
|
||||||
|
camera_fb_t* esp_camera_fb_get_latest(uint32_t timeout_ms, uint32_t *skipped);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Return the frame buffer to be reused again.
|
* @brief Return the frame buffer to be reused again.
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ void cam_stop(void);
|
|||||||
void cam_start(void);
|
void cam_start(void);
|
||||||
|
|
||||||
camera_fb_t *cam_take(TickType_t timeout);
|
camera_fb_t *cam_take(TickType_t timeout);
|
||||||
|
/* Hold only the newest validated frame, returning superseded buffers. */
|
||||||
|
camera_fb_t *cam_take_latest(TickType_t timeout, uint32_t *skipped);
|
||||||
|
|
||||||
void cam_give(camera_fb_t *dma_buffer);
|
void cam_give(camera_fb_t *dma_buffer);
|
||||||
|
|
||||||
|
|||||||
@@ -15,3 +15,6 @@ dist/
|
|||||||
# 截图不是源码,它们随界面一起变;要看就去 `pnpm app` 跑起来,或者自己跑 .dev/shoot.js。
|
# 截图不是源码,它们随界面一起变;要看就去 `pnpm app` 跑起来,或者自己跑 .dev/shoot.js。
|
||||||
/.dev/
|
/.dev/
|
||||||
/screenshots/
|
/screenshots/
|
||||||
|
|
||||||
|
# 测试运行时被 XDG_CONFIG_HOME 相对路径写出来的配置,不是仓库内容。
|
||||||
|
src-tauri/.config/
|
||||||
|
|||||||
+221
-19
@@ -1,5 +1,6 @@
|
|||||||
# desktop
|
# RMIC::2026
|
||||||
|
|
||||||
|
黑龙江大学 602 实验室 · Simba Robotics 的**视野链路终端**。
|
||||||
桌面端。**两种身份,两套页面,一个进程。**
|
桌面端。**两种身份,两套页面,一个进程。**
|
||||||
|
|
||||||
| 身份 | 画面 | 血量 | 装甲板 | 绑定 |
|
| 身份 | 画面 | 血量 | 装甲板 | 绑定 |
|
||||||
@@ -10,16 +11,173 @@
|
|||||||
界面是 WebView 里的 React(画面画在 canvas 上),后端是 Rust。**它不是把旧客户端套进
|
界面是 WebView 里的 React(画面画在 canvas 上),后端是 Rust。**它不是把旧客户端套进
|
||||||
一个窗口**:网络、重组、发现、配置、密码、录制全部重写成了可单测的模块。
|
一个窗口**:网络、重组、发现、配置、密码、录制全部重写成了可单测的模块。
|
||||||
|
|
||||||
|
> **名字与数据目录**:显示名是 `RMIC::2026`;安装包与可执行文件叫 `RMIC-2026`(Windows
|
||||||
|
> 文件名不允许 `:`)。配置、录制、日志仍落在旧的 `cam-desktop` 目录下——这是**故意的**,
|
||||||
|
> 换名字不该顺手把现场的密码哈希与绑定丢掉。见「配置」一节。
|
||||||
|
|
||||||
```text
|
```text
|
||||||
desktop/
|
desktop/
|
||||||
core/ cam-core:不含任何界面与 Tauri 的东西(80 个测试)
|
core/ cam-core:不含任何界面与 Tauri 的东西(83 个测试)
|
||||||
src-tauri/ Tauri 后端:推流、命令、录制(30 个测试)
|
src-tauri/ Tauri 后端:推流、命令、录制(32 个测试)
|
||||||
src/ React 前端:两套页面 + HUD
|
src/ React 前端:两套页面 + HUD
|
||||||
src/lib/ IPC 封装、WebSocket 客户端、状态存储、格式化
|
src/lib/ IPC 封装、WebSocket 客户端、状态存储、格式化、准星绘制、界面小节拍
|
||||||
src/components/ 视频窗格、血条、胜负横幅、瞄准器、装甲板表、队名编辑、录制面板、设置抽屉
|
src/components/ 取景器、装甲条、命中、结算、装甲板表、队名、录制、设置抽屉、启动序列、门、提示
|
||||||
src/pages/ 用户端 / 裁判端
|
src/pages/ 用户端 / 裁判端 / 固件维护 / 延迟校准
|
||||||
|
src/styles/ tokens(令牌)、base(原子)、chrome(外壳)、stage(舞台)、console、settings、maintenance
|
||||||
|
public/fonts/ 自托管的 HarmonyOS Sans SC 与 JetBrains Mono
|
||||||
|
src-tauri/icons/ 队标(由 simba-robotics.com/logo.svg 生成,含光学尺寸分级)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 界面:一套终端语言
|
||||||
|
|
||||||
|
这一栏不是装饰说明,而是"改界面之前该知道的几件事"。
|
||||||
|
|
||||||
|
### 配色:墨黑蓝打底,只有一支红
|
||||||
|
|
||||||
|
| 角色 | 值 | 用在哪 |
|
||||||
|
|---|---|---|
|
||||||
|
| 底色 | `--void #05070d` → `--panel-3 #172032` | 全部面积。五级低饱和冷蓝黑,靠 1px 线分层,不靠阴影 |
|
||||||
|
| 文字 | `--ink #e7ecf5` → `--ink-4 #3a4457` | 四档,第四档暗到不像真信息 |
|
||||||
|
| **强调** | `--primary #e8232b` | **只有**:焦点、选中、录制中、错误、临界血量、结算 |
|
||||||
|
| 状态 | `--error` / `--warn #f2b33d` / `--ok #3fc98a` | 各有语义,不与强调色混用 |
|
||||||
|
| 队伍 | `--team-0 #ff6b5e` / `--team-1 #4a9bff` | **硬件约束**:装甲板 LED 就是红与蓝 |
|
||||||
|
|
||||||
|
一条硬规则:**红只给必须被看见的东西**。开关合上是白的、滑杆填充是钢灰、分段控件的选中
|
||||||
|
记号是底沿一条 2px 线、分区编号是灰的、顶栏没有任何强调线。当红色出现时,它后面一定有一
|
||||||
|
件真事——这就是"只高亮真正重要的信息"落到代码里的样子。
|
||||||
|
|
||||||
|
队红刻意做得比强调红更暖更浅,否则两条血并列时会被误读成"系统在报警"。
|
||||||
|
|
||||||
|
### 形状:矩形,唯一的斜边给装甲条
|
||||||
|
|
||||||
|
全局是矩形:没有圆角、没有切角、没有药丸、没有阴影卡片。**唯一的斜边是装甲条那排平行四边形**
|
||||||
|
——它必须在一屏矩形里被一眼认成"这是血,不是别的东西"。别的元素一旦也开始斜切,那个形状
|
||||||
|
就不再携带信息了。
|
||||||
|
|
||||||
|
### 动效:严格照 Material 3 的曲线与时长
|
||||||
|
|
||||||
|
`styles/tokens.css` 里直接写着 M3 的七条缓动与 short/medium/long 四组时长:
|
||||||
|
|
||||||
|
| 令牌 | 曲线 | 语义 |
|
||||||
|
|---|---|---|
|
||||||
|
| `--m3-emphasized` | `cubic-bezier(0.2, 0, 0, 1)` | 需要存在感的转场 |
|
||||||
|
| `--m3-emphasized-decelerate` | `cubic-bezier(0.05, 0.7, 0.1, 1)` | **进入**(快起慢收) |
|
||||||
|
| `--m3-emphasized-accelerate` | `cubic-bezier(0.3, 0, 0.8, 0.15)` | **退出**(慢起快走) |
|
||||||
|
| `--m3-standard` / `-decelerate` / `-accelerate` | `(0.2,0,0,1)` / `(0,0,0,1)` / `(0.3,0,1,1)` | 常规位移 |
|
||||||
|
| `--m3-linear` | `(0,0,1,1)` | 恒速(仅用于跟踪与循环) |
|
||||||
|
|
||||||
|
旧名 `--e-out` / `--e-in` / `--d-1..5` 保留为**指向 M3 的别名**,所以没有一条规则落在自造
|
||||||
|
的贝塞尔上。两个例外是刻意的:`steps()` 用于录制红点与终端光标(离散闪烁),`linear` 用于
|
||||||
|
"没信号"那圈恒速转动的虚线环——M3 的曲线描述的是有始有终的转场,不是无限循环。
|
||||||
|
|
||||||
|
### 动效:每一页只有一个签名动作
|
||||||
|
|
||||||
|
重复使用同一种效果会让界面显得机械,所以只共享底层曲线与时长,**每页各有一个属于自己的
|
||||||
|
动作**:
|
||||||
|
|
||||||
|
| 界面 | 签名动作 | 它解决什么 |
|
||||||
|
|---|---|---|
|
||||||
|
| 启动序列 | 一行行亮起的自检 + 真实进度 | 让"还在等"与"卡住了"分得开 |
|
||||||
|
| 首启门 | 两张模块卡,悬停浮起并露出角括号 | 说明这件选择是要"装"进这台机器的 |
|
||||||
|
| 用户端 | 逐格熄灭的装甲条、四周泛红、六秒休眠 | 静止时屏幕上的东西越少越好 |
|
||||||
|
| 裁判端 | 中缝可拖、鼠标停在哪一路下方就看哪一路 | 不用再开第三个页面去"看某一边" |
|
||||||
|
| 设置 | 常驻实时预览 + 竖向分节导航 | 改准星/网格/装甲条时立刻看得见结果 |
|
||||||
|
| 固件维护 | 步骤轨 + 环形进度 + 终端日志 | 烧录的哪一步、到哪儿了,一眼可读 |
|
||||||
|
| 解锁 | 一次性的校验扫描 + 错误时的一次抖动 | 明确"现在这一步是在验证身份" |
|
||||||
|
|
||||||
|
`prefers-reduced-motion` 会把这些时长压到 0,不是关掉功能。
|
||||||
|
|
||||||
|
### 字体:中文一套、拉丁一套,逐字回退
|
||||||
|
|
||||||
|
| | 字体 | 承担什么 |
|
||||||
|
|---|---|---|
|
||||||
|
| 中文 | **HarmonyOS Sans SC**(400 / 500 / 700) | 叙述、标签、按钮文字。字面方正,11px 仍然立得住 |
|
||||||
|
| 拉丁与数字 | **JetBrains Mono**(400 / 500 / 700) | 全部读数:帧率、血量、地址、倍率、时间码。等宽,列里的数字跳动时不左右抖 |
|
||||||
|
|
||||||
|
两者都以 woff2 自托管在 `public/fonts/`,随 `frontendDist` 进安装包,**运行时不碰网络**
|
||||||
|
(CSP 里 `default-src 'self'` 也不允许)。中文由 `cn-font-split` 切成 82 个 unicode-range
|
||||||
|
分片 × 3 个字重,浏览器只下载页面真正用到的那几十个,而不是一上来吞 14 MB。
|
||||||
|
|
||||||
|
合到一句话里靠**逐字回退**,不做语言判断:`--font-ui` 把 JetBrains Mono 写在前面,它里面
|
||||||
|
没有汉字,浏览器就逐字往下找,落到 HarmonyOS。于是"血量 87"里数字是等宽的、汉字是
|
||||||
|
HarmonyOS 的,一条 `font-family` 同时满足两个要求。
|
||||||
|
|
||||||
|
启动序列会等 `document.fonts.ready`:先画成系统黑体、两百毫秒后再跳成 HarmonyOS,会让整块
|
||||||
|
布局重排一次——那一个抖动比多等一瞬难看得多。
|
||||||
|
|
||||||
|
### 队标
|
||||||
|
|
||||||
|
应用图标与启动页的标记是队标(`https://simba-robotics.com/logo.svg`),由它生成整套
|
||||||
|
`src-tauri/icons/`。原图是 1024×1024 的画布、标志只占中间一条,所以脚本把 viewBox 收到本体
|
||||||
|
上再居中;**小尺寸用更大的缩放比**(光学尺寸:256px 以上 82%、64–128px 88%、48px 以下 97%),
|
||||||
|
否则 16/24/32 上那些 1px 笔画会糊成一团。`icon.ico` 是手工拼的多尺寸容器——Pillow 的写入器
|
||||||
|
只能从一张主图缩出所有档位,而 Windows 任务栏恰恰只取最小的那几档。
|
||||||
|
|
||||||
|
笔画在 17px 以下就只剩 1px,所以顶栏只放字标 `RMIC::2026`(`::` 是界面里唯一被染红的两个
|
||||||
|
字符),完整队标留给启动页的 30px。
|
||||||
|
|
||||||
|
### 每页最顺手的那个操作
|
||||||
|
|
||||||
|
- **用户端不用点。** 操作手的手在遥控器上,所以这一页的交互全部收在画面上一个手势里:
|
||||||
|
滚轮缩放、拖动平移、双击复位。没有任何"瞄准再点"的按钮。六秒没有输入也没有挨打时,角上
|
||||||
|
的标签与代号自己淡下去;一有动静整块醒来。
|
||||||
|
- **裁判端让控制台跟着鼠标走。** 按下左格,下方列表就只显示那一边的装甲板(再按一次回到
|
||||||
|
全部,键盘 `1` / `2` / `0` 同样)。中间那条竖线可以拖——哪一路更重要就把它拉宽,双击回到
|
||||||
|
五五开。画面与下方控制台之间的横线也能拖:看回放的时候把播放器拉大。
|
||||||
|
- **设置是一块带预览的控制台。** 左侧竖向导航,右侧常驻一块实时预览;预览用的是画面里
|
||||||
|
**同一个**绘制函数(`lib/reticle.ts`),所以"预览里对、画面上偏"这种最坏情况不可能发生。
|
||||||
|
预览旁边那根血量滑杆是刻意加的:低血阈值与脉动只有真的把血拖下去才看得见。
|
||||||
|
|
||||||
|
## 版本与改动日志:算出来的,不是写出来的
|
||||||
|
|
||||||
|
版本号和改动日志都从 **git 历史**算,不靠人记着去改。
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node scripts/version.mjs --write # 计算并写回所有派生文件
|
||||||
|
node scripts/version.mjs --check # 只校验:有未归档的提交就退 1(CI 里跑这个)
|
||||||
|
node scripts/version.mjs --print # 把算出来的版本打到 stdout
|
||||||
|
```
|
||||||
|
|
||||||
|
### 规则
|
||||||
|
|
||||||
|
提交标题用 Conventional Commits(`type(scope): 说明`):
|
||||||
|
|
||||||
|
| 这批提交里有 | 升哪一位 |
|
||||||
|
|---|---|
|
||||||
|
| `BREAKING CHANGE:` 或 `type!:` | major |
|
||||||
|
| `feat` | minor |
|
||||||
|
| `fix` / `perf` | patch |
|
||||||
|
| 其它 | 不动版本,只加构建号 |
|
||||||
|
|
||||||
|
构建号是 `git rev-list --count HEAD`:单调递增、不会撞车、不用手工维护。正文里以 `- `
|
||||||
|
开头且本身写成 `type(scope): 说明` 的行算**额外条目**——这样一次 squash 提交也能把几件事
|
||||||
|
分别归类,不必为了日志好看去拆提交。
|
||||||
|
|
||||||
|
### 派生出来的东西
|
||||||
|
|
||||||
|
| 文件 | 谁用 |
|
||||||
|
|---|---|
|
||||||
|
| `version.json`(仓库根) | 固件 `build.rs` 读它,编译期注入 `RMIC_VERSION*` |
|
||||||
|
| `CHANGELOG.md` / `CHANGELOG.json` | 人读的那份与机器读的那份,同一次计算产出 |
|
||||||
|
| `desktop/src/lib/version.ts` | 界面上的版本标签与协议号 |
|
||||||
|
| `desktop/package.json`、`tauri.conf.json` 的 `version` | 装包文件名与 Windows 的「应用和功能」 |
|
||||||
|
|
||||||
|
建这套机制之前的人工整理记录保存在 [`docs/CHANGELOG-legacy.md`](../docs/CHANGELOG-legacy.md)。
|
||||||
|
|
||||||
|
### 固件与客户端怎么对上
|
||||||
|
|
||||||
|
同一条链贯穿到底:`git → version.json → 固件二进制 → Hello 报文 → 客户端界面`。
|
||||||
|
|
||||||
|
板子每 2 秒广播的 `Hello` 里带着**语义化版本、构建号、以及它编译进去的线路协议号**
|
||||||
|
(布局见 `protocol/README.md`)。客户端读出来之后:
|
||||||
|
|
||||||
|
- 设置面板的绑定下拉里显示 `固件 v0.2.0 · 34`;
|
||||||
|
- 裁判端每一格下方显示这块板子的固件版本;
|
||||||
|
- 协议对不上时,用户端画面正中给出**具体原因**("它是协议 2,本客户端是协议 1"),
|
||||||
|
而不是只表现为没有画面——它的视频帧会被严格解析丢弃,但那句解释正是靠通告读出来的。
|
||||||
|
|
||||||
|
**为什么值得占这 10 个字节**:一块烧错固件的板子,原来只表现为"没画面",现场会去查网线、
|
||||||
|
AP、供电。现在同一个现象会直接指向"重新烧固件"。
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -27,16 +185,37 @@ cd desktop
|
|||||||
pnpm install
|
pnpm install
|
||||||
pnpm app # = tauri dev:起 vite + 编 Rust + 开窗口
|
pnpm app # = tauri dev:起 vite + 编 Rust + 开窗口
|
||||||
pnpm build # 只构建前端(tsc --noEmit && vite build)
|
pnpm build # 只构建前端(tsc --noEmit && vite build)
|
||||||
pnpm tauri build # 出 .app 与 .dmg
|
pnpm tauri build # 默认打包 macOS 的 .app 与 .dmg;Windows 见下文
|
||||||
```
|
```
|
||||||
|
|
||||||
下面两条是 Rust 侧,用它验证不依赖窗口的部分:
|
下面两条是 Rust 侧,用它验证不依赖窗口的部分:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd desktop/core && cargo test # 80
|
cd desktop/core && cargo test # 83
|
||||||
cd desktop/src-tauri && cargo test # 30,其中几条真的会起 ffmpeg
|
cd desktop/src-tauri && cargo test # 32,其中几条真的会起 ffmpeg
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Windows x64 构建
|
||||||
|
|
||||||
|
需要 Node.js 22.12+(或 24)、pnpm 12、Rust stable MSVC,以及 Visual Studio Build Tools 的 C++ 桌面开发组件和 Windows SDK。应用窗口使用 WebView2 Runtime。
|
||||||
|
|
||||||
|
在 `desktop/` 中执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
pnpm tauri build --target x86_64-pc-windows-msvc --bundles nsis --ci -- --locked --config 'unstable.build-std=[]'
|
||||||
|
```
|
||||||
|
|
||||||
|
`--target` 覆盖本目录原来的 macOS target,`--bundles nsis` 选择 Windows 安装包;末尾的 Cargo 参数清空从根固件项目继承的 `build-std`。
|
||||||
|
|
||||||
|
- 程序:`src-tauri/target/x86_64-pc-windows-msvc/release/RMIC-2026.exe`(名字由 `tauri.conf.json` 的 `productName` 决定;直接 `cargo build` 出来的原始产物仍叫 `cam-desktop.exe`,那是内部构建物,不是交付物)。
|
||||||
|
- 安装包:同一 release 目录下的 `bundle/nsis/*-setup.exe`。
|
||||||
|
- 核心测试:进入 `core/` 后运行 `cargo +stable test --locked --target x86_64-pc-windows-msvc --config 'unstable.build-std=[]'`。
|
||||||
|
|
||||||
|
视频录制另需可从 `PATH` 找到的 `ffmpeg`,也可以在配置的 `[recording] ffmpeg` 中指定路径;基础图传显示不依赖它。
|
||||||
|
|
||||||
|
设备发现使用板卡的 Hello 广播;客户端除广播订阅外,还会按已发现设备的 IP 定向续订。Windows 开启 VPN / 虚拟网卡时,全局广播可能走虚拟接口;定向续订可避免“能发现并绑定,但始终等待画面”的情况。队伍端的绑定和画面过滤仍然生效。
|
||||||
|
|
||||||
### 演示模式:没有板子也能把整条链路跑起来
|
### 演示模式:没有板子也能把整条链路跑起来
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -261,8 +440,13 @@ IP,藏起来毫无意义;数据也可能已经收下并解码了,`.slint`/
|
|||||||
已知精度上限:两次读数之间发生两次伤害时只能看到净差。心跳 200 ms,所以这段窗口里的两次
|
已知精度上限:两次读数之间发生两次伤害时只能看到净差。心跳 200 ms,所以这段窗口里的两次
|
||||||
打击会被合并成一次——对"闪一下红"足够,对判定不够,判定看的是界面上的绝对血量。
|
打击会被合并成一次——对"闪一下红"足够,对判定不够,判定看的是界面上的绝对血量。
|
||||||
|
|
||||||
反馈有**两样**:整屏闪红、准星旁的伤害数字。**大号数字不放在画面正中**——第一版放了,
|
反馈有**两样**:画面四周泛红(颜色只从四条边渗进来,中间保持原样)、准星旁的伤害数字。
|
||||||
结果它正好压在瞄准点上,而被打中的那一下恰恰是最需要看着准星的时候。
|
**大号数字不放在画面正中**——第一版放了,结果它正好压在瞄准点上,而被打中的那一下恰恰是最
|
||||||
|
需要看着准星的时候。
|
||||||
|
|
||||||
|
**它不给方向。** 命中事件里的 `index` 是哪一条上报先发现了掉血,不是"从哪一侧打来的"(主板
|
||||||
|
一次广播四条、血值相同,谁先到达取决于 UDP)。把它画成方向箭头会是一条看起来很专业、实际上
|
||||||
|
完全错误的线索,比不画糟得多。
|
||||||
|
|
||||||
**没有"画面抖动"了。** 原来有第三样:被打中时整幅图(连瞄准器一起)晃一下。它去掉了,
|
**没有"画面抖动"了。** 原来有第三样:被打中时整幅图(连瞄准器一起)晃一下。它去掉了,
|
||||||
理由有两条:
|
理由有两条:
|
||||||
@@ -348,7 +532,7 @@ AP、板子,而不是二十行分配代码。现在它有一个直接对着这
|
|||||||
|
|
||||||
## 配置
|
## 配置
|
||||||
|
|
||||||
`~/.config/cam-desktop/config.toml`,`--config` 可改。
|
`~/.config/cam-desktop/config.toml`,`--config` 可改。**目录名沿用旧的 `cam-desktop`**:改名不该顺手把现场的密码哈希与绑定丢掉。要干净重来就删掉这个目录。
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
role = "team"
|
role = "team"
|
||||||
@@ -402,19 +586,25 @@ Tauri 的界面跑在 WKWebView 里,改一行 CSS 要走一次 cargo 构建。
|
|||||||
```sh
|
```sh
|
||||||
pnpm dev # 用户端
|
pnpm dev # 用户端
|
||||||
open 'http://127.0.0.1:1420/?preview=referee' # 裁判端(密码 1234)
|
open 'http://127.0.0.1:1420/?preview=referee' # 裁判端(密码 1234)
|
||||||
|
open 'http://127.0.0.1:1420/?preview=first-run' # 首启的身份选择
|
||||||
|
open 'http://127.0.0.1:1420/?preview=password' # 首启的设密码
|
||||||
```
|
```
|
||||||
|
|
||||||
|
后两个参数只是让对应的门禁强制显示出来,不用为了看一眼就去删配置文件。
|
||||||
|
|
||||||
两重门保证它进不了正式构建:`import.meta.env.DEV` 为真**且**没有 `__TAURI_INTERNALS__`,
|
两重门保证它进不了正式构建:`import.meta.env.DEV` 为真**且**没有 `__TAURI_INTERNALS__`,
|
||||||
并且调用点是 `await import(...)`——条件为假时整个模块不会被打进产物。
|
并且调用点是 `await import(...)`——条件为假时整个模块不会被打进产物。
|
||||||
|
|
||||||
## 测试
|
## 测试
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd desktop/core && cargo test # 80:重组、发现、两道闸门、帧率窗口、槽位、配置、密码、胜负判定
|
cd desktop/core && cargo test # 83:重组、发现、两道闸门、帧率窗口、槽位、配置、密码、胜负判定
|
||||||
cd desktop/src-tauri && cargo test # 30:帧头布局、录制、截图、演示画面、命令行
|
cd desktop/src-tauri && cargo test # 32:帧头布局、录制、截图、演示画面、命令行
|
||||||
cd protocol && cargo test # 15
|
cd protocol && cargo test # 20
|
||||||
```
|
```
|
||||||
|
|
||||||
|
前端解码队列测试:在 `desktop/` 运行 `pnpm test:stream`,5 项测试覆盖慢解码时保留最新帧、多路独立处理、关闭清理、坏帧恢复与设备重启。它不替代实际 WebView 的画面验证。
|
||||||
|
|
||||||
三条值得单独说的:
|
三条值得单独说的:
|
||||||
|
|
||||||
- **录制**那条真的起 ffmpeg、喂 20 帧、停,然后用 `ffprobe` 读回帧数与分辨率。只断言"文件
|
- **录制**那条真的起 ffmpeg、喂 20 帧、停,然后用 `ffprobe` 读回帧数与分辨率。只断言"文件
|
||||||
@@ -444,8 +634,11 @@ RUST_LOG=debug ./target/aarch64-apple-darwin/debug/cam-desktop | grep 'stream:'
|
|||||||
| `demo-*`(仅演示) | 合成协议数据报 | 走同一条真实路径 |
|
| `demo-*`(仅演示) | 合成协议数据报 | 走同一条真实路径 |
|
||||||
| tokio 任务 | WebSocket 服务、5 Hz 快照 | 异步 IO |
|
| tokio 任务 | WebSocket 服务、5 Hz 快照 | 异步 IO |
|
||||||
|
|
||||||
跨线程只有两样东西:`Arc` 与广播通道。帧在接收线程里编码一次(16 字节头 + JPEG),
|
帧在接收线程里编码一次(16 字节头 + JPEG),所有界面共用同一份 `Bytes`。
|
||||||
所有界面共用同一份 `Bytes`。
|
视频使用每路只保留最新帧的 watch 通道;状态与控制消息使用有序广播通道。
|
||||||
|
前端每路最多保留一次正在进行的解码和一张最新待解码帧。
|
||||||
|
|
||||||
|
每张本地 JPEG 前增加 `frameTiming` 文本消息,包含同机后端发布时刻;JPEG 二进制头仍为 16 字节。前端在解码前后检查本机队列年龄,超过 100 ms 的待解码帧或解码结果直接释放,恢复后优先显示最新画面。后端不重放超过 100 ms 的缓存帧,本地视频 socket 写入超时后重连。旧版没有时间标记时仍支持 JPEG,但只能按 JS 收到时刻判断本地年龄。这些时间不能用于测量 ESP 到电脑的无线传输延迟。
|
||||||
|
|
||||||
## 已知限制与没做的
|
## 已知限制与没做的
|
||||||
|
|
||||||
@@ -459,11 +652,20 @@ RUST_LOG=debug ./target/aarch64-apple-darwin/debug/cam-desktop | grep 'stream:'
|
|||||||
`ws://127.0.0.1:*`(端口是启动时随机分配的,写不死)。验证方式是"前端能不能连上后端"
|
`ws://127.0.0.1:*`(端口是启动时随机分配的,写不死)。验证方式是"前端能不能连上后端"
|
||||||
——见下面"测试"一节最后一条。
|
——见下面"测试"一节最后一条。
|
||||||
- **证书与公证**没做,`.dmg` 只在本机可用。
|
- **证书与公证**没做,`.dmg` 只在本机可用。
|
||||||
- 前端没有自动化测试。界面这一侧的验证手段是浏览器预览 + 真机运行;逻辑都在 `core/` 里
|
- 前端已有解码队列测试;布局、实际 WebView 解码和画面显示仍需浏览器预览及真机运行验证。
|
||||||
有测试。
|
|
||||||
|
|
||||||
## 排查
|
## 排查
|
||||||
|
|
||||||
|
需要测量相机到客户端的整条链路时,使用裁判配置启动校准模式:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\RMIC-2026.exe --config .\referee.toml --latency-report .\latency-run.ndjson
|
||||||
|
```
|
||||||
|
|
||||||
|
报告路径必须是新文件。让相机完整拍到左侧四个彩色角块,避开右侧回显,固定相机和窗口位置。标记每 50 ms 更新;程序沿正常接收、解码和 `VideoSurface` 绘制路径记录每个标记的首次有效回显,拒绝曝光混合而无法可靠解码的帧。NDJSON 包含相邻 rAF 夹定的时间、解码完成到画布绘制时间和标记分析耗时,不保存图像或配置。面板扫描和像素响应未测量,精确物理光学延迟需用高速相机复核。正常启动不运行标记识别;校准模式不会额外订阅一份 UDP 视频。
|
||||||
|
|
||||||
|
`pnpm test:latency` 验证标记边界、旋转/镜像、混合曝光拒绝及屏幕原色泛白情形。
|
||||||
|
|
||||||
| 现象 | 原因 |
|
| 现象 | 原因 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 起不来,提示绑不上 UDP 5600 | 同一端口上已经跑着另一个客户端(旧的 Slint 版或另一份本程序)。看 `~/.config/cam-desktop/startup-error.txt` |
|
| 起不来,提示绑不上 UDP 5600 | 同一端口上已经跑着另一个客户端(旧的 Slint 版或另一份本程序)。看 `~/.config/cam-desktop/startup-error.txt` |
|
||||||
|
|||||||
Generated
+26
@@ -55,6 +55,7 @@ dependencies = [
|
|||||||
"rand",
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"socket2",
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -297,6 +298,16 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.6.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
@@ -391,6 +402,21 @@ dependencies = [
|
|||||||
"wit-bindgen",
|
"wit-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "0.7.15"
|
version = "0.7.15"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ protocol = { path = "../../protocol" }
|
|||||||
|
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
socket2 = "0.6"
|
||||||
|
|
||||||
# 配置的序列化。这里与旧客户端(client/)的做法相反:那一版的配置只有十来个标量
|
# 配置的序列化。这里与旧客户端(client/)的做法相反:那一版的配置只有十来个标量
|
||||||
# 字段,手写 TOML 换来零依赖是划算的。现在配置里有 HUD 的嵌套结构和密码哈希,手写
|
# 字段,手写 TOML 换来零依赖是划算的。现在配置里有 HUD 的嵌套结构和密码哈希,手写
|
||||||
|
|||||||
@@ -619,9 +619,13 @@ const HEADER: &str = "\
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
fn home_dir() -> PathBuf {
|
fn home_dir() -> PathBuf {
|
||||||
|
// Windows 的 Tauri/服务进程里通常没有 `HOME`,只有 `USERPROFILE`。
|
||||||
|
// 只看 `HOME` 会落到 `.`,配置文件就变成相对于启动目录——安装版从开始菜单启动时
|
||||||
|
// 启动目录是 Program Files,配置写不进去,卸载后也留不下任何东西。
|
||||||
std::env::var_os("HOME")
|
std::env::var_os("HOME")
|
||||||
|
.filter(|p| !p.is_empty())
|
||||||
|
.or_else(|| std::env::var_os("USERPROFILE").filter(|p| !p.is_empty()))
|
||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
.filter(|p| !p.as_os_str().is_empty())
|
|
||||||
.unwrap_or_else(|| PathBuf::from("."))
|
.unwrap_or_else(|| PathBuf::from("."))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+207
-26
@@ -11,7 +11,8 @@
|
|||||||
//! 两个方向各司其职:
|
//! 两个方向各司其职:
|
||||||
//!
|
//!
|
||||||
//! - 板子每 2 秒广播一次 [`protocol::MsgType::Hello`],客户端被动收到就知道有谁在线。
|
//! - 板子每 2 秒广播一次 [`protocol::MsgType::Hello`],客户端被动收到就知道有谁在线。
|
||||||
//! - 客户端广播 [`protocol::MsgType::Subscribe`],板子收到后开始定向推流。
|
//! - 客户端广播 [`protocol::MsgType::Subscribe`],并向已发现的板子单播续订。
|
||||||
|
//! 多网卡 / VPN 可能把全局广播路由到虚拟网卡;单播按板子 IP 选择局域网接口。
|
||||||
//!
|
//!
|
||||||
//! 板子靠**源地址**记住客户端,所以客户端只要周期性续订就能维持推流,不存在任何
|
//! 板子靠**源地址**记住客户端,所以客户端只要周期性续订就能维持推流,不存在任何
|
||||||
//! 超时淘汰的竞态。
|
//! 超时淘汰的竞态。
|
||||||
@@ -59,7 +60,7 @@ use protocol::{
|
|||||||
/// 来自网络,而不是来自过小的缓冲。
|
/// 来自网络,而不是来自过小的缓冲。
|
||||||
const RECV_BUFFER: usize = 2048;
|
const RECV_BUFFER: usize = 2048;
|
||||||
|
|
||||||
/// 重新广播订阅的间隔。
|
/// 广播及定向续订的间隔。
|
||||||
///
|
///
|
||||||
/// 比板子判定"客户端掉线"的超时(6 秒)短得多,所以偶发丢一两个包不会中断推流。
|
/// 比板子判定"客户端掉线"的超时(6 秒)短得多,所以偶发丢一两个包不会中断推流。
|
||||||
const SUBSCRIBE_INTERVAL: Duration = Duration::from_millis(800);
|
const SUBSCRIBE_INTERVAL: Duration = Duration::from_millis(800);
|
||||||
@@ -74,11 +75,68 @@ const FRAME_TIMEOUT: Duration = Duration::from_millis(500);
|
|||||||
pub struct CameraInfo {
|
pub struct CameraInfo {
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
pub addr: SocketAddrV4,
|
pub addr: SocketAddrV4,
|
||||||
pub firmware: u16,
|
/// 板子自报的固件版本(来自它的 Hello)。
|
||||||
|
pub firmware: FirmwareInfo,
|
||||||
/// 最近一次收到它广播的时刻。
|
/// 最近一次收到它广播的时刻。
|
||||||
pub last_seen: Instant,
|
pub last_seen: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 板子自报的固件版本。
|
||||||
|
///
|
||||||
|
/// # 为什么不是一个 u16
|
||||||
|
///
|
||||||
|
/// 原来只有 `Hello.firmware: u16`,是个没有定义的数——显示出来也没人能解释。现在它是
|
||||||
|
/// 一组**能对得上**的数:语义化版本、构建号、以及板子编译进去的线路协议版本。
|
||||||
|
///
|
||||||
|
/// 最后一项是这套机制的重点:一块烧了别的协议版本的板子,视频帧会被严格解析挡掉,
|
||||||
|
/// 于是它在客户端看来等于不存在。但它的通告仍然读得出来,所以界面可以说
|
||||||
|
/// "板子是协议 2,客户端是协议 1",而不是只能报"没有画面"。
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub struct FirmwareInfo {
|
||||||
|
/// 打包的语义化版本(`major << 16 | minor << 8 | patch`)。0 = 固件太老,没带。
|
||||||
|
pub version: u32,
|
||||||
|
/// 自增构建号。0 = 未知。
|
||||||
|
pub build: u16,
|
||||||
|
/// 板子自报的线路协议版本。
|
||||||
|
pub protocol: u16,
|
||||||
|
/// 旧的两字节字段原值,只用于日志。
|
||||||
|
pub legacy: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FirmwareInfo {
|
||||||
|
pub fn from_hello(hello: &protocol::Hello) -> Self {
|
||||||
|
Self {
|
||||||
|
version: hello.version,
|
||||||
|
build: hello.build,
|
||||||
|
protocol: hello.protocol,
|
||||||
|
legacy: hello.firmware,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `"0.2.0"`;固件没带版本时返回 `None`(而不是 "0.0.0")。
|
||||||
|
pub fn version_text(&self) -> Option<String> {
|
||||||
|
(self.version != 0).then(|| protocol::format_version(self.version))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 这块板子的协议和本客户端一致吗。
|
||||||
|
///
|
||||||
|
/// 老固件没有这个字段,`Hello::decode` 会把帧头里的版本号填进来——那个值已经被
|
||||||
|
/// 严格解析校验过,所以到这一步一定匹配。
|
||||||
|
pub fn is_compatible(&self) -> bool {
|
||||||
|
self.protocol == protocol::PROTOCOL_VERSION
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 一行给人看的描述,日志与界面提示共用同一句。
|
||||||
|
pub fn describe(&self) -> String {
|
||||||
|
match (self.version_text(), self.build) {
|
||||||
|
(Some(version), 0) => format!("v{version}"),
|
||||||
|
(Some(version), build) => format!("v{version}.{build}"),
|
||||||
|
(None, 0) => "版本未知".to_string(),
|
||||||
|
(None, build) => format!("构建 {build}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl CameraInfo {
|
impl CameraInfo {
|
||||||
/// 多久没有广播了。
|
/// 多久没有广播了。
|
||||||
pub fn silence(&self) -> Duration {
|
pub fn silence(&self) -> Duration {
|
||||||
@@ -243,15 +301,21 @@ impl Discovery {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn note_hello(&self, id: u32, addr: SocketAddrV4, firmware: u16) -> bool {
|
/// 记下一次通告。
|
||||||
|
///
|
||||||
|
/// 返回值只在**新出现**或**版本/协议发生变化**时有意义:`Some(true)` 表示它是兼容的,
|
||||||
|
/// `Some(false)` 表示它自报的协议与本客户端不同。周期性的重复通告返回 `None`——
|
||||||
|
/// 它每 2 秒来一次,每次都打日志会把真正的事件淹掉。
|
||||||
|
fn note_hello(&self, id: u32, addr: SocketAddrV4, firmware: FirmwareInfo) -> Option<bool> {
|
||||||
let mut cameras = self.cameras.lock().expect("cameras mutex poisoned");
|
let mut cameras = self.cameras.lock().expect("cameras mutex poisoned");
|
||||||
|
|
||||||
match cameras.get_mut(&id) {
|
match cameras.get_mut(&id) {
|
||||||
Some(info) => {
|
Some(info) => {
|
||||||
|
let changed = info.firmware != firmware;
|
||||||
info.addr = addr;
|
info.addr = addr;
|
||||||
info.last_seen = Instant::now();
|
info.last_seen = Instant::now();
|
||||||
info.firmware = firmware;
|
info.firmware = firmware;
|
||||||
false
|
changed.then(|| firmware.is_compatible())
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
cameras.insert(
|
cameras.insert(
|
||||||
@@ -263,7 +327,7 @@ impl Discovery {
|
|||||||
last_seen: Instant::now(),
|
last_seen: Instant::now(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
true
|
Some(firmware.is_compatible())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -291,8 +355,7 @@ impl Discovery {
|
|||||||
|
|
||||||
/// 客户端身份。
|
/// 客户端身份。
|
||||||
///
|
///
|
||||||
/// 常量而非 MAC 派生:队伍客户端和裁判终端各自唯一,进程内也不需要区分自己,而一个
|
/// 旧客户端使用的固定标识,保留用于兼容判断。
|
||||||
/// 确定的常量让日志更好读。
|
|
||||||
pub const CLIENT_ID: u32 = 0x0C11_0001;
|
pub const CLIENT_ID: u32 = 0x0C11_0001;
|
||||||
|
|
||||||
/// 接收循环的可变状态:只由接收线程在跑循环时访问。
|
/// 接收循环的可变状态:只由接收线程在跑循环时访问。
|
||||||
@@ -318,6 +381,7 @@ pub struct Receiver {
|
|||||||
/// 身份可以运行时切换(同一个进程里从裁判端切到用户端),所以订阅里声明的角色
|
/// 身份可以运行时切换(同一个进程里从裁判端切到用户端),所以订阅里声明的角色
|
||||||
/// 也得能改——它决定板子要不要给这个客户端推流。
|
/// 也得能改——它决定板子要不要给这个客户端推流。
|
||||||
role: Mutex<SubscribeRole>,
|
role: Mutex<SubscribeRole>,
|
||||||
|
client_id: u32,
|
||||||
camera: Ipv4Addr,
|
camera: Ipv4Addr,
|
||||||
stop: Arc<AtomicBool>,
|
stop: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
@@ -327,6 +391,10 @@ impl Receiver {
|
|||||||
///
|
///
|
||||||
/// `only_sender` 为 `Some` 时只接受那一块板子的帧。
|
/// `only_sender` 为 `Some` 时只接受那一块板子的帧。
|
||||||
pub fn new(camera: Ipv4Addr, role: SubscribeRole, only_sender: Option<u32>) -> Self {
|
pub fn new(camera: Ipv4Addr, role: SubscribeRole, only_sender: Option<u32>) -> Self {
|
||||||
|
let client_id = loop {
|
||||||
|
let id = rand::random::<u32>();
|
||||||
|
if id != 0 && id != CLIENT_ID { break id; }
|
||||||
|
};
|
||||||
Self {
|
Self {
|
||||||
discovery: Arc::new(Discovery::new()),
|
discovery: Arc::new(Discovery::new()),
|
||||||
latest: Arc::new(LatestFrames::default()),
|
latest: Arc::new(LatestFrames::default()),
|
||||||
@@ -337,6 +405,7 @@ impl Receiver {
|
|||||||
discovery_all: false,
|
discovery_all: false,
|
||||||
}),
|
}),
|
||||||
role: Mutex::new(role),
|
role: Mutex::new(role),
|
||||||
|
client_id,
|
||||||
camera,
|
camera,
|
||||||
stop: Arc::new(AtomicBool::new(false)),
|
stop: Arc::new(AtomicBool::new(false)),
|
||||||
}
|
}
|
||||||
@@ -354,6 +423,15 @@ impl Receiver {
|
|||||||
) -> std::io::Result<(Self, UdpSocket)> {
|
) -> std::io::Result<(Self, UdpSocket)> {
|
||||||
let socket =
|
let socket =
|
||||||
UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, DEFAULT_PORT))?;
|
UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, DEFAULT_PORT))?;
|
||||||
|
// Absorb short scheduler/NIC bursts without losing JPEG fragments. The
|
||||||
|
// reader still drains continuously; decoded video uses latest frames.
|
||||||
|
let socket_ref = socket2::SockRef::from(&socket);
|
||||||
|
if let Err(err) = socket_ref.set_recv_buffer_size(1024 * 1024) {
|
||||||
|
log::warn!("could not enlarge the video receive buffer: {err}");
|
||||||
|
}
|
||||||
|
if let Ok(bytes) = socket_ref.recv_buffer_size() {
|
||||||
|
log::info!("video UDP receive buffer: {bytes} bytes");
|
||||||
|
}
|
||||||
socket.set_read_timeout(Some(Duration::from_millis(100)))?;
|
socket.set_read_timeout(Some(Duration::from_millis(100)))?;
|
||||||
|
|
||||||
let broadcast = socket.set_broadcast(true).is_ok();
|
let broadcast = socket.set_broadcast(true).is_ok();
|
||||||
@@ -439,8 +517,28 @@ impl Receiver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.discovery.note_hello(hello.sender_id, v4, hello.firmware) {
|
let firmware = FirmwareInfo::from_hello(&hello);
|
||||||
log::info!("found camera 0x{:08X} at {v4}", hello.sender_id);
|
|
||||||
|
if let Some(compatible) = self.discovery.note_hello(hello.sender_id, v4, firmware) {
|
||||||
|
if compatible {
|
||||||
|
log::info!(
|
||||||
|
"found camera 0x{:08X} at {v4} ({}, protocol {})",
|
||||||
|
hello.sender_id,
|
||||||
|
firmware.describe(),
|
||||||
|
firmware.protocol
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 这条日志值钱的地方在于:它把"没有画面"翻译成了一个具体的原因。
|
||||||
|
// 严格解析会把这些板子的视频帧全部丢掉,所以除了这里,没有任何地方
|
||||||
|
// 会留下"场上有一块协议不匹配的板子"的痕迹。
|
||||||
|
log::warn!(
|
||||||
|
"camera 0x{:08X} at {v4} speaks protocol {}, this client speaks {} - \
|
||||||
|
its video will be ignored; reflash the board with a matching firmware",
|
||||||
|
hello.sender_id,
|
||||||
|
firmware.protocol,
|
||||||
|
protocol::PROTOCOL_VERSION
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -473,11 +571,17 @@ impl Receiver {
|
|||||||
.entry(id)
|
.entry(id)
|
||||||
.or_insert_with(|| FrameAssembler::new(FRAME_TIMEOUT));
|
.or_insert_with(|| FrameAssembler::new(FRAME_TIMEOUT));
|
||||||
|
|
||||||
match assembler.push(frag) {
|
let dropped_before = assembler.dropped_frames();
|
||||||
Ok(frame) => {
|
let result = assembler.push(frag);
|
||||||
|
let dropped = assembler.dropped_frames() - dropped_before;
|
||||||
drop(ingest);
|
drop(ingest);
|
||||||
|
self.discovery.count(id, |s| {
|
||||||
|
s.datagrams += 1;
|
||||||
|
s.frames_dropped += dropped;
|
||||||
|
});
|
||||||
|
|
||||||
self.discovery.count(id, |s| s.datagrams += 1);
|
match result {
|
||||||
|
Ok(frame) => {
|
||||||
self.discovery
|
self.discovery
|
||||||
.note_frame(id, frame.width, frame.height, frame.data.len());
|
.note_frame(id, frame.width, frame.height, frame.data.len());
|
||||||
|
|
||||||
@@ -489,17 +593,7 @@ impl Receiver {
|
|||||||
sink(frame);
|
sink(frame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(protocol::DropReason::Superseded) => {
|
Err(_) => {}
|
||||||
// 被更新的帧追上了。这不是丢帧:半截的那张只是被超越,在健康链路上
|
|
||||||
// 这是帧被放弃的**正常**方式。
|
|
||||||
self.discovery.count(id, |s| s.datagrams += 1);
|
|
||||||
}
|
|
||||||
Err(protocol::DropReason::TimedOut) => {
|
|
||||||
self.discovery.count(id, |s| {
|
|
||||||
s.datagrams += 1;
|
|
||||||
s.frames_dropped += 1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,6 +615,25 @@ impl Receiver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 全局广播用于发现;已知板子还需按 IP 续订,避免广播被 VPN / 虚拟网卡接走。
|
||||||
|
fn subscription_targets(&self) -> Vec<SocketAddrV4> {
|
||||||
|
let mut targets = vec![SocketAddrV4::new(self.camera, DEFAULT_PORT)];
|
||||||
|
let only_sender = self.sender_limit();
|
||||||
|
|
||||||
|
for camera in self.discovery.online_cameras() {
|
||||||
|
if only_sender.is_some_and(|id| camera.id != id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Hello 的来源端口不一定是订阅监听端口;协议固定使用 DEFAULT_PORT。
|
||||||
|
let target = SocketAddrV4::new(*camera.addr.ip(), DEFAULT_PORT);
|
||||||
|
if !targets.contains(&target) {
|
||||||
|
targets.push(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
targets
|
||||||
|
}
|
||||||
|
|
||||||
/// 运行接收循环,直到 [`Receiver::shutdown`]。
|
/// 运行接收循环,直到 [`Receiver::shutdown`]。
|
||||||
///
|
///
|
||||||
/// 阻塞,所以放在单独的线程上。
|
/// 阻塞,所以放在单独的线程上。
|
||||||
@@ -543,12 +656,15 @@ impl Receiver {
|
|||||||
if last_subscribe.elapsed() >= SUBSCRIBE_INTERVAL {
|
if last_subscribe.elapsed() >= SUBSCRIBE_INTERVAL {
|
||||||
let subscribe = Subscribe {
|
let subscribe = Subscribe {
|
||||||
role: self.role(),
|
role: self.role(),
|
||||||
client_id: CLIENT_ID,
|
client_id: self.client_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(err) = socket.send_to(&subscribe.encode(), target) {
|
let encoded = subscribe.encode();
|
||||||
|
for target in self.subscription_targets() {
|
||||||
|
if let Err(err) = socket.send_to(&encoded, target) {
|
||||||
log::warn!("could not subscribe at {target}: {err}");
|
log::warn!("could not subscribe at {target}: {err}");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
last_subscribe = Instant::now();
|
last_subscribe = Instant::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -635,10 +751,58 @@ mod tests {
|
|||||||
let hello = Hello {
|
let hello = Hello {
|
||||||
sender_id: sender,
|
sender_id: sender,
|
||||||
firmware: 7,
|
firmware: 7,
|
||||||
|
version: protocol::pack_version(0, 2, 0),
|
||||||
|
build: 57,
|
||||||
|
protocol: protocol::PROTOCOL_VERSION,
|
||||||
};
|
};
|
||||||
rx.datagram(&hello.encode(MsgType::Hello), from(from_ip));
|
rx.datagram(&hello.encode(MsgType::Hello), from(from_ip));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discovered_boards_are_subscribed_directly_when_broadcast_uses_another_interface() {
|
||||||
|
let rx = Receiver::new(Ipv4Addr::BROADCAST, SubscribeRole::Referee, None);
|
||||||
|
let hello = Hello {
|
||||||
|
sender_id: 1,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
// Hello 可从其它端口发来,订阅仍需发往板子的固定监听端口。
|
||||||
|
rx.datagram(&hello.encode(MsgType::Hello), "192.168.1.139:12345".parse().unwrap());
|
||||||
|
feed_hello(&rx, 2, [192, 168, 1, 140]);
|
||||||
|
|
||||||
|
assert_eq!(rx.subscription_targets(), vec![
|
||||||
|
"255.255.255.255:5600".parse().unwrap(),
|
||||||
|
"192.168.1.139:5600".parse().unwrap(),
|
||||||
|
"192.168.1.140:5600".parse().unwrap(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// DHCP 地址变化后只使用新地址。
|
||||||
|
feed_hello(&rx, 1, [192, 168, 1, 141]);
|
||||||
|
assert_eq!(rx.subscription_targets(), vec![
|
||||||
|
"255.255.255.255:5600".parse().unwrap(),
|
||||||
|
"192.168.1.141:5600".parse().unwrap(),
|
||||||
|
"192.168.1.140:5600".parse().unwrap(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discovery_of_other_teams_does_not_directly_subscribe_to_their_video() {
|
||||||
|
let rx = Receiver::new(Ipv4Addr::BROADCAST, SubscribeRole::Team, Some(1));
|
||||||
|
rx.set_discovery_all(true);
|
||||||
|
feed_hello(&rx, 1, [192, 168, 1, 10]);
|
||||||
|
feed_hello(&rx, 2, [192, 168, 1, 20]);
|
||||||
|
|
||||||
|
assert_eq!(rx.discovery().cameras().len(), 2);
|
||||||
|
assert_eq!(rx.subscription_targets(), vec![
|
||||||
|
"255.255.255.255:5600".parse().unwrap(),
|
||||||
|
"192.168.1.10:5600".parse().unwrap(),
|
||||||
|
]);
|
||||||
|
rx.restrict_to(Some(2));
|
||||||
|
assert_eq!(rx.subscription_targets(), vec![
|
||||||
|
"255.255.255.255:5600".parse().unwrap(),
|
||||||
|
"192.168.1.20:5600".parse().unwrap(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delivers_the_latest_frame_of_the_bound_sender_only() {
|
fn delivers_the_latest_frame_of_the_bound_sender_only() {
|
||||||
let rx = Receiver::new(Ipv4Addr::UNSPECIFIED, SubscribeRole::Team, Some(1));
|
let rx = Receiver::new(Ipv4Addr::UNSPECIFIED, SubscribeRole::Team, Some(1));
|
||||||
@@ -761,4 +925,21 @@ mod tests {
|
|||||||
|
|
||||||
assert!(rx.discovery().cameras().is_empty());
|
assert!(rx.discovery().cameras().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn loss_statistics_include_superseded_frames_and_entirely_missing_ids() {
|
||||||
|
let rx = Receiver::new(Ipv4Addr::UNSPECIFIED, SubscribeRole::Referee, None);
|
||||||
|
feed_hello(&rx, 1, [192, 168, 1, 10]);
|
||||||
|
feed_frame(&rx, 1, 1, [192, 168, 1, 10]);
|
||||||
|
let partial = fragment_frame(1, 2, &jpeg(), 640, 480);
|
||||||
|
rx.datagram(&partial[0].encode(), from([192, 168, 1, 10]));
|
||||||
|
assert_eq!(rx.discovery().stats(1).frames_dropped, 0, "normal pending fragments are not drops");
|
||||||
|
feed_frame(&rx, 1, 4, [192, 168, 1, 10]);
|
||||||
|
let stats = rx.discovery().stats(1);
|
||||||
|
assert_eq!(stats.frames_shown, 2);
|
||||||
|
assert_eq!(stats.frames_dropped, 2, "partial 2 and entirely missing 3");
|
||||||
|
assert_eq!(stats.loss_percent(), 50.0);
|
||||||
|
for frag in partial { rx.datagram(&frag.encode(), from([192, 168, 1, 10])); }
|
||||||
|
assert_eq!(rx.discovery().stats(1).frames_dropped, 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,17 @@ pub struct CameraDto {
|
|||||||
pub addr: String,
|
pub addr: String,
|
||||||
pub online: bool,
|
pub online: bool,
|
||||||
pub silence_ms: u64,
|
pub silence_ms: u64,
|
||||||
pub firmware: u16,
|
/// 固件版本字符串(`"0.2.0"`)。固件太老、没带这个字段时为 `null`。
|
||||||
|
pub firmware_version: Option<String>,
|
||||||
|
/// 自增构建号。0 = 未知。
|
||||||
|
pub firmware_build: u16,
|
||||||
|
/// 板子自报的线路协议版本。
|
||||||
|
pub protocol: u16,
|
||||||
|
/// 本客户端能不能和它通信。
|
||||||
|
///
|
||||||
|
/// 为 `false` 时它不会有画面——帧被严格解析挡掉了——但界面仍然能说出原因,
|
||||||
|
/// 这就是这个字段存在的全部意义。
|
||||||
|
pub compatible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&CameraInfo> for CameraDto {
|
impl From<&CameraInfo> for CameraDto {
|
||||||
@@ -43,7 +53,10 @@ impl From<&CameraInfo> for CameraDto {
|
|||||||
addr: camera.addr.ip().to_string(),
|
addr: camera.addr.ip().to_string(),
|
||||||
online: camera.is_online(),
|
online: camera.is_online(),
|
||||||
silence_ms: camera.silence().as_millis() as u64,
|
silence_ms: camera.silence().as_millis() as u64,
|
||||||
firmware: camera.firmware,
|
firmware_version: camera.firmware.version_text(),
|
||||||
|
firmware_build: camera.firmware.build,
|
||||||
|
protocol: camera.firmware.protocol,
|
||||||
|
compatible: camera.firmware.is_compatible(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-1
@@ -3,7 +3,24 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>cam-desktop</title>
|
<meta name="color-scheme" content="dark" />
|
||||||
|
<title>RMIC::2026</title>
|
||||||
|
|
||||||
|
<!-- 中文三字重。这些文件是 cn-font-split 生成的(每种 82 个 unicode-range 分片),
|
||||||
|
放在 public/fonts 里随产物一起走;不经过打包器,所以用绝对路径直接 link。
|
||||||
|
放在 <head> 而不是 CSS 里 @import:@import 会串行阻塞,而这几份互不依赖。 -->
|
||||||
|
<link rel="stylesheet" href="/fonts/harmonyos-sans-sc/Regular.css" />
|
||||||
|
<link rel="stylesheet" href="/fonts/harmonyos-sans-sc/Medium.css" />
|
||||||
|
<link rel="stylesheet" href="/fonts/harmonyos-sans-sc/Bold.css" />
|
||||||
|
|
||||||
|
<!-- 首屏所有数字都是拉丁字形,先把它预取下来;中文字体是按需分片,预取哪一个都是猜。 -->
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="/fonts/jetbrains-mono/jetbrains-mono-latin-400-normal.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin
|
||||||
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name": "cam-desktop",
|
"name": "rmic-2026",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "ESP32 图传与装甲板系统的桌面端:用户端(血条 + 瞄准器)与裁判端(双路 + 装甲板 + 录制)",
|
"description": "RMIC::2026 —— ESP32 图传与装甲板系统的桌面端:用户端(装甲条 + 瞄准器)与裁判端(双路 + 装甲板 + 录制)",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc --noEmit && vite build",
|
"build": "tsc --noEmit && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"app": "tauri dev",
|
"app": "tauri dev",
|
||||||
"check": "tsc --noEmit"
|
"check": "tsc --noEmit",
|
||||||
|
"version:write": "node ../scripts/version.mjs --write",
|
||||||
|
"version:check": "node ../scripts/version.mjs --check",
|
||||||
|
"test:stream": "node --test scripts/test-stream.mjs",
|
||||||
|
"test:latency": "node --test scripts/test-latency-marker.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
|
"@tauri-apps/plugin-dialog": "^2.8.3",
|
||||||
"react": "^19",
|
"react": "^19",
|
||||||
"react-dom": "^19"
|
"react-dom": "^19"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+10
@@ -11,6 +11,9 @@ importers:
|
|||||||
'@tauri-apps/api':
|
'@tauri-apps/api':
|
||||||
specifier: ^2
|
specifier: ^2
|
||||||
version: 2.12.3
|
version: 2.12.3
|
||||||
|
'@tauri-apps/plugin-dialog':
|
||||||
|
specifier: ^2.8.3
|
||||||
|
version: 2.8.3
|
||||||
react:
|
react:
|
||||||
specifier: ^19
|
specifier: ^19
|
||||||
version: 19.3.0
|
version: 19.3.0
|
||||||
@@ -529,6 +532,9 @@ packages:
|
|||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-dialog@2.8.3':
|
||||||
|
resolution: {integrity: sha512-Hd/yBfk2R5v2MFrz/0VArSXeh8xKLLXGunqSiTtcHKn1zNZwaKujuA0VSwUvNfwC4hDoF1PveS+gBDNe3nHspw==}
|
||||||
|
|
||||||
'@types/babel__core@7.20.5':
|
'@types/babel__core@7.20.5':
|
||||||
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
||||||
|
|
||||||
@@ -1090,6 +1096,10 @@ snapshots:
|
|||||||
'@tauri-apps/cli-win32-ia32-msvc': 2.12.2
|
'@tauri-apps/cli-win32-ia32-msvc': 2.12.2
|
||||||
'@tauri-apps/cli-win32-x64-msvc': 2.12.2
|
'@tauri-apps/cli-win32-x64-msvc': 2.12.2
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-dialog@2.8.3':
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.12.3
|
||||||
|
|
||||||
'@types/babel__core@7.20.5':
|
'@types/babel__core@7.20.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.29.9
|
'@babel/parser': 7.29.9
|
||||||
|
|||||||
@@ -0,0 +1,680 @@
|
|||||||
|
/* Generated By cn-font-split@6.0.3 https://www.npmjs.com/package/cn-font-split
|
||||||
|
CreateTime: Fri, 06 Jun 2025 12:25:03 GMT;
|
||||||
|
Origin File Name Table:
|
||||||
|
256: Clock Tabular
|
||||||
|
257: Clock Proportional
|
||||||
|
258: Calculator Operators
|
||||||
|
259: Alternate “l”
|
||||||
|
260: Full Width Math Symbols
|
||||||
|
261: Punctuation Change
|
||||||
|
262: Red Circle Fig
|
||||||
|
263: Vert Figures
|
||||||
|
copyright: Copyright 2021-2025 Huawei Device Co., Ltd. All Rights Reserved.
|
||||||
|
fontFamily: HarmonyOS Sans SC
|
||||||
|
fontSubfamily: Bold
|
||||||
|
uniqueID: HarmonyOS Sans SC Bold Version 2.400
|
||||||
|
fullName: HarmonyOS Sans SC Bold
|
||||||
|
version: Version 2.400
|
||||||
|
postScriptName: HarmonyOS_Sans_SC_Bold
|
||||||
|
manufacturer: Hanyi Fonts
|
||||||
|
designer: Huawei Device Co., Ltd & Hanyi Fonts
|
||||||
|
preferredFamily: HarmonyOS Sans SC
|
||||||
|
preferredSubfamily: Bold
|
||||||
|
*/
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_53d21e.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+251,U+259,U+261,U+292,U+2bb-2bc,U+2ca-2cb,U+2d8-2dd,U+2f3,U+301,U+303,U+306-30a,U+30f,U+323,U+326-327,U+335,U+338,U+1e00-1e01,U+1e3e-1e3f,U+1e80-1e85,U+1ea0-1ef9,U+2004-2006,U+2009-200a,U+2010-2011,U+2013-201e,U+2020-2022,U+2025-2029,U+2030,U+2032-2037,U+2039-203b,U+2044,U+2052,U+2060-2064,U+2066-2069,U+2070,U+2074-2079,U+207f-2089,U+20a3-20a4,U+20a6-20ac,U+20b1,U+20b9-20ba,U+20bc-20bd,U+2103,U+2105,U+2109,U+2113,U+2116,U+2121-2122,U+2126,U+212e,U+215b-215e,U+2160-216b,U+2170-2179,U+2190-2193,U+2196-2199,U+2202,U+2206,U+2208,U+220f,U+2211-2212,U+2215,U+221a,U+221d-2220,U+2223,U+2225,U+2227-222b,U+222e,U+2234-2237,U+223d,U+2248,U+224c,U+2252,U+2260-2261,U+2264-2267,U+226e-226f,U+2295,U+2299,U+22a5,U+22bf,U+2312,U+2460-2469,U+2474-249b,U+2500-254b,U+2550-2573,U+2581-258f,U+2593-2595,U+25a0-25a1,U+25b2-25b3,U+25bc-25bd,U+25c6-25c7,U+25ca-25cb,U+25ce-25cf,U+25e2-25e5,U+2605-2606,U+2609,U+2640,U+2642,U+9f42-9f43,U+9f45-9f46,U+9f48-9f49,U+9f4c-9f4d,U+9f53,U+9f55-9f58,U+9f5a-9f5b,U+9f5d-9f5e,U+9f62,U+9f64-9f65,U+9f68-9f69,U+9f6b,U+9f6d-9f71,U+9f73-9f75,U+9f78-9f7e,U+9f81-9f82,U+9f8e-9f8f,U+9f91-9f93,U+9f96-9f98,U+9f9d-9f9e,U+9fa1-9fff,U+f9f1,U+fa0e-fa0f,U+fa11,U+fa13-fa14,U+fa1f,U+fa21,U+fa23-fa24,U+fa27-fa29,U+fe49-fe52,U+fe54-fe57,U+fe59-fe66,U+fe68-fe6b,U+ff01-ff03,U+ff05-ff0a,U+ff0c-ff1b,U+ff1f-ff3d,U+ff3f,U+ff41-ff5b,U+ff5d,U+ff5f-ff60,U+ffe6;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_70874c.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+9db2-9db3,U+9db5-9dba,U+9dbd-9dbf,U+9dc1,U+9dc3-9dd2,U+9dd4-9dd6,U+9dd8,U+9ddb-9de4,U+9de7-9dee,U+9df0-9df1,U+9df4-9df7,U+9dfb-9e0b,U+9e0d-9e14,U+9e16-9e19,U+9e1c,U+9e24,U+9e27,U+9e2e,U+9e30,U+9e34,U+9e3b-9e3c,U+9e40,U+9e4d,U+9e50,U+9e52-9e54,U+9e56,U+9e59,U+9e5d,U+9e5f-9e62,U+9e65,U+9e6e-9e6f,U+9e72,U+9e74-9e78,U+9e7b,U+9e80-9e81,U+9e83-9e84,U+9e86,U+9e89-9e8a,U+9e8c-9e91,U+9e94-9e96,U+9e98-9e9c,U+9e9e,U+9ea0-9ea4,U+9ea7-9ea8,U+9eaa-9eb3,U+9eb5-9eb7,U+9eb9-9eba,U+9ebf-9ec2,U+9ec5-9ec8,U+9eca-9ecb,U+9ed0,U+9ed3,U+9ed5-9ed7,U+9ed9-9eda,U+9ee1,U+9ee3-9ee4,U+9ee6,U+9eeb-9eee,U+9ef0-9ef1,U+9ef3,U+9ef5-9ef6,U+9ef8,U+9efa,U+9f00-9f08,U+9f0a,U+9f0c,U+9f0f,U+9f11-9f12,U+9f14,U+9f16,U+9f18,U+9f1a-9f1f,U+9f23-9f2b,U+9f2d-9f2e,U+9f30-9f33,U+9f35-9f36,U+9f38,U+9f3a,U+9f3c,U+9f3f-9f41;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_d2da49.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+9c2f-9c30,U+9c34,U+9c36,U+9c38,U+9c3a,U+9c3c-9c3d,U+9c3f-9c44,U+9c46-9c47,U+9c4a-9c51,U+9c53,U+9c55,U+9c59-9c5c,U+9c5e,U+9c60-9c66,U+9c68-9c6c,U+9c6e-9c76,U+9c79,U+9c7b,U+9c7d-9c7e,U+9c80,U+9c83-9c84,U+9c89-9c8a,U+9c8c,U+9c8f,U+9c93,U+9c96-9c99,U+9c9d,U+9caa,U+9cac,U+9caf,U+9cb9,U+9cbe-9cc2,U+9cc8-9cc9,U+9cd1-9cd2,U+9cda-9cdb,U+9ce0-9ce1,U+9ce3-9ce4,U+9ce6,U+9ce8,U+9cea-9cf2,U+9cf5,U+9cf7-9d05,U+9d08,U+9d0a-9d14,U+9d16-9d1a,U+9d1c,U+9d1e,U+9d20-9d22,U+9d24-9d25,U+9d27,U+9d29-9d2e,U+9d31-9d3a,U+9d3c-9d3e,U+9d40-9d41,U+9d43-9d50,U+9d54-9d5b,U+9d5e-9d5f,U+9d62-9d69,U+9d6b,U+9d6d-9d6e,U+9d70-9d71,U+9d73-9d86,U+9d88,U+9d8a-9d92,U+9d94-9d97,U+9d99,U+9d9b-9da4,U+9da6-9da8,U+9daa-9dae,U+9db0-9db1;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_e8003a.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+9ac8-9aca,U+9acd-9ace,U+9ad0,U+9ad7,U+9ad9-9ade,U+9ae0,U+9ae2-9ae5,U+9ae7-9aea,U+9aec,U+9aee,U+9af0-9af8,U+9afa,U+9afc-9b02,U+9b04-9b05,U+9b07,U+9b09-9b0e,U+9b10-9b12,U+9b14-9b1e,U+9b20-9b21,U+9b24,U+9b26,U+9b2a-9b2d,U+9b30,U+9b33-9b3a,U+9b3d-9b40,U+9b46,U+9b4a-9b4c,U+9b50,U+9b52-9b53,U+9b55-9b57,U+9b59,U+9b5b-9b6e,U+9b70-9b73,U+9b75-9b76,U+9b78-9b80,U+9b82,U+9b84-9b8d,U+9b8f,U+9b93-9b99,U+9b9b-9b9d,U+9b9f-9ba9,U+9bac,U+9baf-9bbf,U+9bc2-9bc6,U+9bc8,U+9bcb-9bd3,U+9bd5,U+9bd8-9bda,U+9bdc,U+9bde-9be0,U+9be3,U+9be5-9be6,U+9be9,U+9bec-9bef,U+9bf1-9bf3,U+9bf5-9bfc,U+9bfe,U+9c00-9c07,U+9c0a-9c0c,U+9c0e-9c11,U+9c14-9c1f,U+9c21-9c22,U+9c24,U+9c26-9c27,U+9c2a-9c2c,U+9c2e;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_bba9bc.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+98c7,U+98c9-98cd,U+98cf-98d0,U+98d4,U+98d6-98d7,U+98dc-98dd,U+98e1,U+98e3-98e6,U+98ec,U+98ee,U+98f0,U+98f3,U+98f5-98fb,U+98ff-9902,U+9904,U+9906-9907,U+990b,U+990e-990f,U+9914-9917,U+9919,U+991c-991d,U+991f-9920,U+9922-9927,U+9929-992b,U+992d,U+992f-9930,U+9932,U+9934,U+9936,U+9938-993a,U+993d,U+9940-9942,U+9944,U+9946-9947,U+994d-9951,U+9953,U+9956,U+9958-995b,U+995d,U+995f-9961,U+9964,U+9966,U+9973,U+9978-9979,U+997b,U+997e,U+9982-9983,U+9989,U+998c,U+998e,U+999a-99a4,U+99a6-99a7,U+99a9-99ab,U+99af-99b0,U+99b2,U+99b5-99c0,U+99c2-99cf,U+99d3,U+99d6-99d7,U+99da,U+99dc,U+99de,U+99e0-99e1,U+99e3-99ec,U+99ee-99f0,U+99f2-99fe,U+9a00,U+9a02-9a04,U+9a06-9a0c,U+9a10-9a15,U+9a17-9a18,U+9a1a-9a2a,U+9a2c,U+9a2f,U+9a31-9a35,U+9a39-9a3d,U+9a3f,U+9a46-9a49,U+9a4b-9a4c,U+9a4e,U+9a50-9a54,U+9a56,U+9a58-9a59,U+9a5c-9a5e,U+9a60-9a61,U+9a63,U+9a66-9a69,U+9a6b,U+9a72,U+9a83,U+9a89,U+9a8d-9a8e,U+9a94-9a95,U+9a99,U+9aa6,U+9aa9-9aae,U+9ab2-9ab5,U+9ab9,U+9abb,U+9abd-9abf,U+9ac3-9ac4,U+9ac6-9ac7;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_0ca5a9.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+96e1,U+96e4-96e7,U+96eb-96ee,U+96f0-96f1,U+96f4-96f5,U+96f8,U+96fa,U+96fc-96fd,U+96ff,U+9702-9703,U+9705,U+970a-970c,U+9710,U+9712,U+9714-9715,U+9717-971b,U+971d,U+971f-9726,U+9728-9729,U+972b-972c,U+972e-972f,U+9731,U+9733-9737,U+973a-973c,U+973f-9741,U+9743,U+9745-9747,U+9749-9751,U+9754-9755,U+9757-9758,U+975d,U+975f,U+9763-9764,U+9767,U+976a-9772,U+9775,U+9777-977b,U+977d-9784,U+9786-978a,U+978c,U+978e,U+9790,U+9793,U+9795-9797,U+9799-979c,U+979e-979f,U+97a1-97a2,U+97a4-97a5,U+97a7-97aa,U+97ac,U+97ae,U+97b0-97b1,U+97b3,U+97b5-97bc,U+97be-97c2,U+97c4-97c5,U+97c7-97c8,U+97ca,U+97cd-97d2,U+97d4-97d8,U+97da-97db,U+97df-97e5,U+97e8,U+97ee-97f2,U+97f4,U+97f7-97fa,U+97fc-97fe,U+9800,U+9804,U+9809,U+980b,U+980d,U+9814-9816,U+9819-981b,U+981d-9820,U+9822-9823,U+9825,U+9827-982c,U+982e-982f,U+9831-9836,U+983a,U+983c-9845,U+9847-984b,U+9850-9852,U+9854-9855,U+9857,U+985a,U+985c-985d,U+985f-9861,U+9863-9864,U+9866,U+9868-986a,U+986d-986e,U+9872,U+988b,U+988e,U+9892,U+9895,U+9899,U+98a3,U+98a9-98ad,U+98b0-98b2,U+98b4-98b5,U+98b7-98bb,U+98bd-98c3,U+98c5;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_22048b.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+9213,U+9216-921d,U+921f-9222,U+9224,U+9228-922d,U+922f,U+9231-9232,U+9235-9236,U+923b-923c,U+9241-9244,U+9246-9247,U+924a-924c,U+924e-9250,U+9252-9254,U+9256,U+9258-9259,U+925c-925d,U+925f-9261,U+9263,U+9265,U+9267-926b,U+926e-9273,U+9275-9277,U+9279,U+927c-927e,U+9281-9282,U+9284,U+9286-928d,U+928f-9290,U+9292,U+9294-9295,U+9297,U+9299,U+929b,U+929d-929f,U+92a1-92a2,U+92a4,U+92a7,U+92ad,U+92af-92b0,U+92b2,U+92b4-92b6,U+92b8-92ba,U+92bd-92c0,U+92c2,U+92c4,U+92c6,U+92c9-92cb,U+92cd-92ce,U+92d0-92d1,U+92d3-92dc,U+92de,U+92e0-92e2,U+92e7,U+92e9,U+92eb-92ed,U+92f2-92f5,U+92f7,U+92f9-92fb,U+92fd-9300,U+9302-9303,U+9305,U+9309-930f,U+9311,U+9313-9314,U+9316-9317,U+931c-931e,U+9321,U+9323-9325,U+9327,U+9329-932a,U+932c-932d,U+9330-9332,U+9334-9335,U+9337,U+9339-933d,U+933f,U+9342,U+9344-9345,U+9348-934a,U+934c,U+934e-9353,U+9355-9357,U+9359-935a,U+935c-9363,U+9366-9368,U+936b,U+936d-936f,U+9371-9374,U+9377-9379,U+937b-937d,U+937f-9381,U+9383,U+9385-9386,U+9388-9389,U+938b-938e,U+9390-9395,U+9397,U+9399-93a1,U+93a4-93a5,U+93a8,U+93ab,U+93ad,U+93af,U+93b1-93b2,U+93b4,U+93b6-93be,U+93c0-93c2,U+93c4-93c6,U+93c9,U+93cb,U+93ce-93d0,U+93d2-93d5,U+93d9-93db,U+93e0,U+93e3,U+93e5;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_3c428d.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+93e6-93e7,U+93e9-93f4,U+93f6,U+93f8,U+93fa-93fc,U+93fe-9402,U+9404-940a,U+940c-940f,U+9411,U+9415-9418,U+941a-941f,U+9421-9425,U+9429-942a,U+942c-942d,U+942f-9431,U+9434,U+9436-9437,U+9439,U+943b-943d,U+9440-9443,U+9445-9449,U+944b,U+944d-9450,U+9452-9453,U+9455-945f,U+9461-9462,U+9464,U+9466-946a,U+946c,U+946e-946f,U+9471,U+9473-9476,U+9478,U+947a-947b,U+9480,U+9482-9484,U+9491,U+9496,U+9498,U+94c7,U+94cf,U+94d3-94d4,U+94da,U+94e6,U+94fb,U+951c,U+9520,U+9527,U+9533,U+953d,U+9543,U+9548,U+954b,U+9555,U+955a,U+9560,U+956e,U+9574-9575,U+9578-957e,U+9581,U+9584-9585,U+9587-9588,U+958a,U+958d,U+9590-9591,U+9595-9597,U+9599-95a0,U+95a2,U+95a6-95a7,U+95aa,U+95ae-95b0,U+95b2-95b5,U+95b7-95b8,U+95ba,U+95c0-95c2,U+95c4-95c5,U+95c9,U+95cd-95cf,U+95d1-95d3,U+95d7-95db,U+95dd,U+95df-95e0,U+95e3-95e4,U+95e6-95e7,U+95ec,U+95ff,U+9607,U+9613,U+9618,U+961b,U+961e,U+9620,U+9623-9629,U+962b-962d,U+962f-9630,U+9637-963a,U+963e,U+9641,U+9643,U+964a,U+964e-964f,U+9651-9653,U+9656-9657,U+9659-965a,U+965c,U+965e,U+9660,U+9665-9666,U+966b,U+966d-966f,U+9671,U+9679-967c,U+967e-9684,U+9687,U+968c,U+9691-9693,U+9696,U+969a,U+969d-96a6,U+96a9,U+96ab-96af,U+96b2,U+96b5,U+96b7,U+96ba,U+96bf,U+96c2-96c3,U+96ca,U+96d0-96d1,U+96d3-96d4,U+96d7-96d8,U+96da,U+96dd,U+96df;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_d9776c.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+8f2e,U+8f30-8f32,U+8f34-8f37,U+8f39-8f3a,U+8f3c-8f3d,U+8f40-8f41,U+8f43,U+8f47-8f48,U+8f4a-8f4c,U+8f4f-8f53,U+8f55-8f5e,U+8f60,U+8f63,U+8f65,U+8f6a,U+8f80,U+8f8c,U+8f92,U+8f9d,U+8fa0-8fa2,U+8fa4-8fa5,U+8fa7,U+8faa,U+8fac,U+8fb3-8fb5,U+8fb7-8fb8,U+8fba-8fbc,U+8fbf-8fc0,U+8fc3,U+8fc6,U+8fc9-8fcd,U+8fcf,U+8fd2,U+8fd6-8fd7,U+8fda,U+8fe0-8fe1,U+8fe3,U+8fe7,U+8fec,U+8fef,U+8ff1-8ff2,U+8ff5-8ff6,U+8ffa-8ffc,U+8ffe-8fff,U+9007-9008,U+900c,U+900e,U+9013,U+9018,U+901c,U+9024-9025,U+9027-902b,U+9030-9031,U+9033-9034,U+9037,U+9039-903a,U+903d,U+903f-9040,U+9043,U+9045-9046,U+9048-9049,U+904c,U+9056,U+905a,U+905f,U+9061,U+9064,U+9066-9067,U+906a-906c,U+906f-9071,U+9073,U+9076,U+9079,U+907b,U+907e,U+9085-9086,U+9089,U+908c-908e,U+9092,U+9094,U+9096,U+9098,U+909a,U+909c,U+909e-90a0,U+90a4-90a5,U+90a7-90a9,U+90ab,U+90ad,U+90b2,U+90b7,U+90bc-90bd,U+90bf-90c0,U+90c2-90c3,U+90c6,U+90c8-90c9,U+90cb-90cd,U+90d2,U+90d4-90d6,U+90d8-90da,U+90de,U+90e0,U+90e3,U+90e5,U+90e9-90ea,U+90ec,U+90ee,U+90f0-90f3,U+90f6-90f7,U+90f9-90fc,U+90ff-9101,U+9103,U+9105,U+9107-9108,U+910a-9111,U+9113,U+9115,U+9117-9118,U+911a-911d,U+911f-9121,U+9124-9126,U+9128-912c,U+912e,U+9133,U+9135,U+9137-9138,U+913b-9142,U+9144-9145,U+9147,U+9151,U+9153-9156,U+9158-9159,U+915b-915c,U+915f-9160,U+9166-9168,U+916b,U+916d,U+9173,U+917a-917c,U+9180-9182,U+9184,U+9186,U+9188,U+918a,U+918e-918f,U+9193-9199,U+919d,U+919f-91a1,U+91a4-91a9,U+91b0-91b3,U+91b6-91b9,U+91bb-91bf,U+91c2,U+91c4,U+91c6,U+91c8,U+91d6,U+91da-91db,U+91de-91e2,U+91e5,U+91e8,U+91ea-91f4,U+91f6,U+91f8,U+91fb-91ff,U+9202-9203,U+9205-9207,U+920a-920c,U+920e-920f,U+9212;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_09158a.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+8cdf,U+8ce5,U+8ce8-8ce9,U+8ceb,U+8cee-8cf3,U+8cf5-8cf9,U+8cff-8d03,U+8d06-8d07,U+8d09,U+8d0b-8d0c,U+8d0e,U+8d11-8d12,U+8d14-8d15,U+8d18-8d1a,U+8d1c,U+8d20,U+8d51-8d52,U+8d57,U+8d5f,U+8d65,U+8d68-8d6a,U+8d6c,U+8d6e-8d6f,U+8d71-8d72,U+8d78-8d80,U+8d82-8d83,U+8d86-8d89,U+8d8c-8d90,U+8d92-8d93,U+8d96-8d98,U+8d9a-8d9e,U+8da0-8da2,U+8da4-8da7,U+8da9-8db0,U+8db6-8db7,U+8db9,U+8dbb,U+8dbd,U+8dc0-8dc2,U+8dc5,U+8dc7-8dca,U+8dcd,U+8dd0,U+8dd2-8dd5,U+8dd8-8dd9,U+8ddc,U+8de0,U+8de2,U+8de5-8de7,U+8de9,U+8ded-8dee,U+8df0-8df2,U+8df4,U+8df6,U+8dfc,U+8dfe-8e04,U+8e06-8e08,U+8e0b,U+8e0d-8e0e,U+8e11-8e13,U+8e15-8e1c,U+8e20-8e21,U+8e24-8e28,U+8e2b,U+8e2d,U+8e32-8e33,U+8e36-8e38,U+8e3b-8e3c,U+8e3e-8e3f,U+8e43,U+8e45-8e46,U+8e4d-8e50,U+8e53-8e54,U+8e56-8e58,U+8e5a-8e5f,U+8e61-8e62,U+8e65,U+8e67-8e68,U+8e6a-8e6b,U+8e6e,U+8e71,U+8e73,U+8e75,U+8e77-8e79,U+8e7b,U+8e7d-8e7e,U+8e80,U+8e82-8e84,U+8e86,U+8e88,U+8e8c,U+8e8e,U+8e96-8e99,U+8e9b,U+8e9d,U+8e9f-8ea0,U+8ea2-8ea4,U+8ea7-8ea9,U+8ead-8eae,U+8eb0-8eb1,U+8eb3-8eb9,U+8ebb-8ebf,U+8ec1-8ec9,U+8ecf-8ed1,U+8ed3,U+8ed5-8eda,U+8edc-8ede,U+8ee0-8ee3,U+8ee5-8eea,U+8eec-8ef1,U+8ef3-8ef7,U+8efd,U+8eff-8f02,U+8f04,U+8f06,U+8f08,U+8f0b-8f11,U+8f16-8f1a,U+8f20-8f24,U+8f27-8f28,U+8f2b-8f2d;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_a0f240.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+8a4a-8a4d,U+8a4f,U+8a51,U+8a53,U+8a56-8a57,U+8a59-8a5a,U+8a5c-8a5d,U+8a5f,U+8a64-8a65,U+8a67-8a68,U+8a6a,U+8a6f,U+8a74,U+8a76-8a78,U+8a7a-8a7b,U+8a7d-8a7e,U+8a80-8a83,U+8a88,U+8a8b,U+8a8e-8a90,U+8a94,U+8a96-8a97,U+8a99,U+8a9b-8a9d,U+8a9f,U+8aa2,U+8aa7,U+8aa9,U+8aab-8aaf,U+8ab1,U+8ab3-8ab5,U+8ab7-8ab8,U+8aba-8abb,U+8abd-8abe,U+8ac0-8ac1,U+8ac3,U+8ac5-8ac6,U+8ac8,U+8aca,U+8acc,U+8ace,U+8ad0,U+8ad3-8ad5,U+8ad8-8ada,U+8add,U+8adf-8ae0,U+8ae3,U+8ae5,U+8ae8-8aea,U+8aec,U+8aef-8af0,U+8af2,U+8af4-8af5,U+8af9,U+8afb,U+8afd,U+8aff,U+8b03,U+8b06,U+8b08-8b09,U+8b0b-8b0d,U+8b0f,U+8b11-8b13,U+8b15,U+8b18,U+8b1a,U+8b1c,U+8b1e-8b1f,U+8b21-8b25,U+8b27,U+8b29-8b2a,U+8b2d-8b32,U+8b34-8b38,U+8b3a-8b3d,U+8b3f-8b40,U+8b42-8b48,U+8b4a-8b4d,U+8b50-8b55,U+8b57,U+8b5b,U+8b5d-8b5e,U+8b60-8b65,U+8b67-8b6a,U+8b6d-8b6e,U+8b71-8b73,U+8b75-8b76,U+8b78-8b7c,U+8b7f,U+8b81-8b89,U+8b8b-8b8d,U+8b8f-8b91,U+8b94,U+8b97-8b9b,U+8b9d,U+8b9f,U+8bac,U+8bb1,U+8bbb,U+8bc7,U+8bd0,U+8bea,U+8c09,U+8c1e,U+8c38-8c40,U+8c42-8c45,U+8c4a-8c4b,U+8c4d,U+8c4f,U+8c51-8c53,U+8c56-8c59,U+8c5b-8c60,U+8c63-8c69,U+8c6d-8c72,U+8c74-8c77,U+8c7b-8c81,U+8c83-8c84,U+8c86-8c88,U+8c8b,U+8c8e-8c92,U+8c95-8c97,U+8c99-8c9a,U+8c9c,U+8c9f,U+8ca3-8ca6,U+8cad-8cae,U+8cb1,U+8cb5,U+8cb9,U+8cbe,U+8cc6,U+8cc9,U+8ccb-8cd0,U+8cd4,U+8cd6-8cd9,U+8cdb,U+8cdd;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_e7152d.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+883f,U+8841-8843,U+8847-8849,U+884b,U+884e-8852,U+8856,U+8858,U+885a,U+885c,U+885e-8860,U+8866-8867,U+886a,U+886d,U+886f,U+8871,U+8873-8876,U+8878-887c,U+8880,U+8883,U+8886-8887,U+8889-888a,U+888c,U+888e-8891,U+8893-8895,U+8897-889b,U+889d,U+889f-88a1,U+88a3,U+88a5-88aa,U+88ac,U+88ae-88b0,U+88b2-88b6,U+88b8-88bb,U+88bd-88c0,U+88c3-88c4,U+88c7-88c8,U+88ca-88cd,U+88d0-88d1,U+88d3,U+88d6-88d7,U+88da-88db,U+88de,U+88e0-88e1,U+88e6-88e7,U+88e9-88ef,U+88f2,U+88f5-88f7,U+88fa-88fb,U+88ff-8901,U+8903-8909,U+890b-890f,U+8911,U+8914-8918,U+891c-8920,U+8922-8924,U+8926-8929,U+892c-892f,U+8931,U+8935,U+8937,U+8939-893a,U+893c-8940,U+8942-8943,U+8945,U+8947-8948,U+894a-8955,U+8957-895c,U+8961-8963,U+8965,U+8967-8969,U+896b-896e,U+8970-8971,U+8973-897a,U+897c-897e,U+8980,U+8982,U+8984-8985,U+8987,U+8989-898a,U+898c-898e,U+8990-8992,U+8994-8995,U+8997,U+8999-89a0,U+89a2-89a5,U+89a7-89a9,U+89ab,U+89ad-89ae,U+89b0-89b1,U+89b3-89b6,U+89b8-89b9,U+89bb-89bc,U+89be,U+89c3,U+89cd,U+89d3,U+89d5,U+89d7-89d9,U+89db,U+89dd,U+89df-89e2,U+89e4,U+89e7-89ea,U+89ec-89ee,U+89f0-89f2,U+89f5,U+89f7,U+89f9-89ff,U+8a04-8a06,U+8a09,U+8a0b,U+8a0d,U+8a0f,U+8a11-8a12,U+8a14,U+8a19-8a1a,U+8a1c,U+8a1e,U+8a20-8a22,U+8a24,U+8a26-8a29,U+8a2b-8a2c,U+8a2e-8a30,U+8a32-8a33,U+8a35,U+8a37-8a39,U+8a3c-8a3d,U+8a3f-8a40,U+8a42-8a45,U+8a47,U+8a49;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_d8e0ef.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+8641-864c,U+8652-8653,U+8656-8659,U+865d,U+8660-8661,U+8663-8666,U+8668-866a,U+866d,U+8670,U+8672-8678,U+8683-8689,U+868e-8692,U+8694,U+8696-869b,U+869e-86a2,U+86a5-86a6,U+86ab,U+86ad-86ae,U+86b2-86b3,U+86b7-86b9,U+86bb-86bf,U+86c1-86c3,U+86c5,U+86c8,U+86cc-86cd,U+86d2-86d3,U+86d5-86d7,U+86da,U+86dc-86dd,U+86e0-86e3,U+86e5-86e8,U+86ea-86ec,U+86ef,U+86f5-86f7,U+86fc-86fd,U+86ff,U+8701,U+8704-8705,U+870b-870c,U+870e-8711,U+8714,U+8719,U+871b,U+871d,U+871f-8720,U+8724,U+8726-8728,U+872a-872d,U+872f-8730,U+8732-8733,U+8735-8736,U+8738-873a,U+873c-873d,U+8740-8746,U+874a-874b,U+874d,U+874f-8752,U+8754,U+8756,U+8758,U+875a-875e,U+8761-8762,U+8767,U+8769-876d,U+876f,U+8771-8773,U+8775,U+8777,U+8779-877a,U+877f-8781,U+8786-8787,U+8789-878a,U+878c,U+878e-8792,U+8794-8796,U+8798-879d,U+87a0-87a1,U+87a3-87a7,U+87a9-87aa,U+87ae,U+87b0-87b2,U+87b4,U+87b6-87b9,U+87bc,U+87be-87bf,U+87c1-87c3,U+87c5,U+87c7,U+87c9,U+87cc-87cd,U+87cf-87d0,U+87d4-87da,U+87dc-87df,U+87e1-87e2,U+87e4,U+87e6-87e9,U+87eb,U+87ed,U+87f0-87f1,U+87f3-87f5,U+87f7-87f8,U+87fa,U+87fc-87fd,U+87ff-8802,U+8804,U+8807-8809,U+880b-880c,U+880e-880f,U+8812,U+8817-881a,U+881c-881e,U+8820,U+8824-8830,U+8833-8835,U+8837-8838,U+883a,U+883d-883e;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_0cdc77.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+848d,U+848f,U+8491-8493,U+8495-8496,U+8498,U+849a-849b,U+849d,U+849f-84a0,U+84a2-84ae,U+84b0-84b1,U+84b3,U+84b5-84b7,U+84bb,U+84be,U+84c2-84c3,U+84c5,U+84c7-84c8,U+84cc,U+84ce-84cf,U+84d2,U+84d4-84d5,U+84d7-84dc,U+84de,U+84e1-84e2,U+84e4,U+84e8-84eb,U+84ed,U+84f1-84f3,U+84f5-84fb,U+84fe,U+8500-850b,U+850d-8510,U+8512,U+8515-8516,U+8518-8519,U+851b-851d,U+8520,U+8522,U+8524,U+8527-852a,U+852e-8536,U+853e-8540,U+8542,U+8544-8545,U+8547,U+854b-854d,U+854f-8551,U+8554,U+8557,U+855a-855d,U+855f-8561,U+8563,U+8565-8567,U+856b-856c,U+856e-8571,U+8573,U+8575-8576,U+8578,U+857c-857d,U+857f-8583,U+8586,U+8589,U+858b,U+858d-858e,U+8590,U+8592-8593,U+8595-859a,U+859d-859e,U+85a0-85a3,U+85a5,U+85a7,U+85ab-85ad,U+85b1-85b6,U+85b8,U+85bb-85c0,U+85c2-85c8,U+85ca-85cc,U+85d1-85d2,U+85d4,U+85d6-85db,U+85de-85e3,U+85e6-85e8,U+85eb-85f5,U+85f7-85f8,U+85fc-85fe,U+8600-8603,U+8608-8609,U+860c-8610,U+8612-8615,U+8617-8619,U+861b-861d,U+861f-8621,U+8623-8626,U+8628,U+862a-862c,U+862e-8637,U+8639,U+863b,U+863d-863e,U+8640;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_2f7c7d.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+81ac-81b2,U+81b4-81b9,U+81bc,U+81c4-81c5,U+81c7-81c8,U+81cb,U+81ce,U+81d0-81d1,U+81d3-81d7,U+81d9,U+81db-81df,U+81e1-81e2,U+81e4,U+81e6,U+81e9,U+81eb,U+81ee-81f2,U+81f5-81fa,U+81fd,U+81ff,U+8203,U+820b,U+820e-820f,U+8211,U+8213,U+8215-821a,U+821d,U+8220,U+8224-8227,U+8229,U+822e,U+8232,U+823a,U+823c-823d,U+823f-8243,U+8245-8246,U+8248,U+824a,U+824c-824e,U+8250-8257,U+825b-825e,U+8260-8263,U+8265,U+8267,U+8269-826a,U+826c-826d,U+8275-8277,U+827b-827c,U+8280-8281,U+8283,U+8285-8287,U+8289,U+828c,U+8290,U+8293-8296,U+829a-829b,U+829e,U+82a0,U+82a2-82a3,U+82a7,U+82b2,U+82b5-82b6,U+82ba,U+82bc,U+82bf-82c0,U+82c2-82c3,U+82c5-82c6,U+82c9,U+82d0,U+82d6,U+82d9-82da,U+82dd,U+82e2,U+82e8-82ea,U+82ec-82ee,U+82f0,U+82f2-82f3,U+82f5-82f6,U+82f8,U+82fa,U+82fc-8300,U+830a-830b,U+830d,U+8310,U+8312-8313,U+8316,U+8318-8319,U+831d-8326,U+8329-832a,U+832e,U+8330,U+8337,U+833b,U+833d-833f,U+8341-8342,U+8344-8345,U+8348,U+834b-834e,U+8353,U+8355-8359,U+835d,U+8362,U+8370-8376,U+8379-837a,U+837e-8384,U+8387-8388,U+838b-838d,U+838f-8391,U+8394-8395,U+8397,U+8399-839a,U+839d,U+839f,U+83a1,U+83a3-83a6,U+83ac-83af,U+83b5,U+83bb,U+83be-83bf,U+83c2-83c4,U+83c6,U+83c8-83c9,U+83cb,U+83cd-83ce,U+83d0-83d2,U+83d5,U+83d7,U+83d9-83db,U+83de,U+83e2-83e4,U+83e6-83e8,U+83eb-83ee,U+83f3,U+83f5-83f7,U+83fa-83fc,U+83fe-8400,U+8402,U+8405,U+8408-8409,U+8410,U+8412-8417,U+8419-841b,U+841e-8423,U+8429-842b,U+842d-8430,U+8432-8434,U+8436-8437,U+8439-843b,U+843e-8445,U+8447-8448,U+844a-8450,U+8453-8456,U+8458,U+845d-8460,U+8462,U+8465,U+8467-8468,U+846a,U+846e-8470,U+8472,U+8474,U+8479,U+847b-8481,U+8483-8486,U+848a;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_138611.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+7e8b,U+7e8e,U+7e90-7e92,U+7e95,U+7e97,U+7e99-7e9a,U+7e9d-7e9e,U+7eae,U+7eb4,U+7ebb-7ebc,U+7ed6,U+7ee4,U+7eec,U+7ef9,U+7f0a,U+7f10,U+7f1e,U+7f37,U+7f39,U+7f3b-7f41,U+7f43,U+7f46-7f4b,U+7f4d-7f4f,U+7f52-7f53,U+7f56,U+7f59,U+7f5b-7f5e,U+7f60,U+7f63-7f67,U+7f6b-7f6d,U+7f6f,U+7f73,U+7f76,U+7f78,U+7f7a-7f7d,U+7f7f-7f80,U+7f82-7f84,U+7f87,U+7f89,U+7f8d,U+7f8f-7f93,U+7f95-7f99,U+7f9b-7f9c,U+7fa0,U+7fa2,U+7fa6,U+7faa-7fae,U+7fb1,U+7fb3-7fb5,U+7fb7,U+7fba-7fbb,U+7fbe,U+7fc0,U+7fc2-7fc4,U+7fc6-7fc9,U+7fcb,U+7fcd,U+7fcf-7fd1,U+7fd3,U+7fd6-7fd7,U+7fd9-7fde,U+7fe2-7fe4,U+7fe7-7fe8,U+7fea,U+7fec-7fed,U+7fef,U+7ff2,U+7ff4-7ff8,U+7ffd-7fff,U+8002,U+8007-800a,U+800e-800f,U+8011,U+8013,U+801a-801b,U+801d-801f,U+8021,U+8023-8024,U+802b,U+802d,U+802f-8030,U+8032,U+8034,U+8039-803a,U+803c,U+803e,U+8040-8041,U+8044-8045,U+8047-8049,U+804e-8051,U+8053,U+8055,U+8057,U+8059,U+805b-805d,U+805f-8068,U+806b-806e,U+8074,U+8078,U+807a-807c,U+8081-8082,U+8088,U+808a,U+808d-8092,U+8094-8095,U+8097,U+8099,U+809e,U+80a3,U+80a6-80a8,U+80ac,U+80b0,U+80b3,U+80b5-80b6,U+80b8-80b9,U+80bb,U+80c5,U+80c7-80cb,U+80cf-80d5,U+80d8,U+80df-80e0,U+80e2-80e3,U+80e6,U+80ee,U+80f5,U+80f7,U+80f9,U+80fb,U+80fe-8101,U+8103-8104,U+8107,U+810b-810c,U+8115,U+8117,U+8119,U+811c-811d,U+811f-8122,U+8124-8128,U+812a,U+812d-812e,U+8130,U+8133-8135,U+8137,U+813a-813d,U+813f-8140,U+8142-8145,U+8147,U+8149,U+814d,U+814f,U+8152,U+8157-8158,U+815b-815f,U+8162-8164,U+8168,U+816a,U+816c,U+816f,U+8172,U+8175-8177,U+8184-8187,U+8189,U+818b-818e,U+8190,U+8192-8197,U+8199,U+819e-819f,U+81a1-81a2,U+81a4-81a5,U+81a7,U+81ab;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_16a8ba.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+7cab-7cad,U+7caf-7cb0,U+7cb4,U+7cb6-7cb8,U+7cba-7cbb,U+7cbf-7cc0,U+7cc2-7cc4,U+7cc6,U+7ccb,U+7cce-7cd4,U+7cd8,U+7cda-7cdb,U+7ce1-7ce6,U+7ce9-7cee,U+7cf0-7cf1,U+7cf3,U+7cf5,U+7cf7,U+7cfa,U+7cfc-7cfd,U+7cff,U+7d01,U+7d03,U+7d0c,U+7d0e-7d0f,U+7d11-7d12,U+7d16,U+7d18,U+7d1d-7d1f,U+7d23-7d26,U+7d28-7d2a,U+7d2c-7d2d,U+7d34-7d38,U+7d3b,U+7d3d-7d3e,U+7d41,U+7d45,U+7d47-7d4d,U+7d4f,U+7d51-7d54,U+7d56-7d5a,U+7d5c,U+7d5f-7d60,U+7d63-7d65,U+7d67,U+7d69-7d6d,U+7d6f-7d70,U+7d74-7d76,U+7d78,U+7d7a-7d80,U+7d82,U+7d84-7d85,U+7d87,U+7d89-7d8e,U+7d90,U+7d92,U+7d94-7d9b,U+7d9d,U+7d9f,U+7da1,U+7da4-7da5,U+7da7-7daa,U+7daf,U+7db3,U+7db6-7db7,U+7dbc,U+7dc0-7dc3,U+7dc5-7dc6,U+7dc8-7dc9,U+7dcc-7dd1,U+7dd3-7dd6,U+7dda-7ddc,U+7ddf,U+7de2,U+7de4-7de5,U+7de7,U+7dea-7deb,U+7ded-7dee,U+7df0,U+7df3,U+7df5,U+7df7-7df8,U+7dfa,U+7dfc-7e07,U+7e0c-7e0f,U+7e12-7e1a,U+7e1c,U+7e20-7e22,U+7e24-7e2a,U+7e2c,U+7e2f-7e30,U+7e33-7e34,U+7e38,U+7e3a,U+7e3c,U+7e3f-7e40,U+7e42,U+7e44,U+7e49-7e51,U+7e53,U+7e56-7e59,U+7e5b-7e5d,U+7e5f-7e60,U+7e63-7e68,U+7e6b-7e6c,U+7e71-7e72,U+7e74-7e78,U+7e7a-7e7b,U+7e7f-7e81,U+7e83-7e87,U+7e89;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_2fefd6.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+7ab5,U+7ab7-7ab9,U+7abb-7abe,U+7ac0-7ac3,U+7ac6,U+7ac9,U+7acc-7ad5,U+7ad7-7ad8,U+7ada-7add,U+7ae1-7ae2,U+7ae4,U+7ae7-7aec,U+7aee,U+7af0-7af5,U+7af7-7af8,U+7afb-7afc,U+7afe,U+7b00-7b02,U+7b05,U+7b07,U+7b09,U+7b0c-7b0e,U+7b10,U+7b12-7b13,U+7b16-7b18,U+7b1a,U+7b1c-7b1d,U+7b1f,U+7b21-7b23,U+7b27,U+7b29,U+7b2d,U+7b2f-7b30,U+7b32,U+7b34-7b37,U+7b39,U+7b3b,U+7b3d,U+7b3f-7b44,U+7b48,U+7b4a,U+7b4e,U+7b53,U+7b55,U+7b57,U+7b59,U+7b5c,U+7b5e-7b5f,U+7b61,U+7b63-7b66,U+7b68-7b6d,U+7b6f-7b70,U+7b73-7b74,U+7b76,U+7b78,U+7b7a,U+7b7c-7b7d,U+7b7f,U+7b81-7b84,U+7b86-7b8a,U+7b8c,U+7b8e,U+7b91-7b93,U+7b96,U+7b98-7b9b,U+7b9e-7ba0,U+7ba3-7ba5,U+7bae-7bb0,U+7bb2-7bb3,U+7bb5-7bb7,U+7bb9-7bbf,U+7bc2-7bc3,U+7bc5,U+7bc8,U+7bca,U+7bcd-7bd0,U+7bd2,U+7bd4-7bd8,U+7bdb-7bdc,U+7bde-7be0,U+7be2-7be3,U+7be7-7be8,U+7beb-7bed,U+7bef-7bf0,U+7bf2,U+7bf4-7bf6,U+7bf8-7bfb,U+7bfd,U+7bff,U+7c01-7c06,U+7c08-7c0a,U+7c0e,U+7c10,U+7c12-7c15,U+7c17-7c1d,U+7c20,U+7c22,U+7c24-7c25,U+7c28-7c29,U+7c2c-7c36,U+7c39-7c3d,U+7c42,U+7c44-7c4b,U+7c4e-7c5b,U+7c5d-7c5e,U+7c61-7c63,U+7c65-7c68,U+7c6b,U+7c6d,U+7c6f-7c72,U+7c75-7c7a,U+7c7e-7c88,U+7c8a-7c90,U+7c93-7c94,U+7c96,U+7c99-7c9b,U+7ca0-7ca1,U+7ca3,U+7ca6-7ca9;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_c37197.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+7607-7609,U+760e-760f,U+7611-7612,U+7614,U+7616,U+761a,U+761c-761d,U+7623,U+7628,U+762c,U+762e-762f,U+7631-7632,U+7636-7637,U+7639-763a,U+763d,U+7641,U+7644-7645,U+7648,U+764a-764b,U+764e-7651,U+7653,U+7655,U+7657,U+7659-765b,U+765d,U+7660,U+7666,U+7668,U+766a,U+7673-7677,U+7679-767a,U+767f-7680,U+7683,U+7685,U+7689-768a,U+768c-768d,U+768f-7690,U+7692,U+7694-7695,U+7697-7698,U+769b-76a3,U+76a5-76ad,U+76af,U+76b3,U+76b5-76b7,U+76b9,U+76bb-76be,U+76c0-76c1,U+76c4,U+76c7,U+76c9,U+76cb-76cc,U+76d3,U+76d5,U+76d9-76da,U+76dd,U+76e0,U+76e2,U+76e6,U+76e8-76ed,U+76f0,U+76f3,U+76f5-76f7,U+76fa-76fb,U+76fd,U+76ff-7703,U+7705-7706,U+770a,U+770c,U+770e-7718,U+771b-771d,U+7721,U+7723-7724,U+7727,U+772a-772c,U+772e,U+7730-7734,U+7739,U+773b,U+773d-773f,U+7742,U+7744-7746,U+7748-774e,U+7752-7759,U+775d,U+775f-7760,U+7764,U+7767,U+7769-776a,U+776d-7778,U+777a-777c,U+7781-7783,U+7786,U+7788-778b,U+778f-7790,U+7793-7797,U+7799-779d,U+77a1,U+77a3-77a4,U+77a6,U+77a8,U+77ab,U+77ae-77af,U+77b1-77b2,U+77b4,U+77b6-77ba,U+77be,U+77c0-77cc,U+77ce-77d6,U+77d8-77d9,U+77dd-77e1,U+77e4,U+77e6,U+77e8,U+77ea,U+77f0-77f2,U+77f4-77f5,U+77f7,U+77f9-77fc,U+7803-7808,U+780a-780b,U+780e-7810,U+7813,U+7815,U+7819,U+781b,U+781e,U+7820-7822,U+7824,U+7828,U+782a-782b,U+782e-782f,U+7831-7833,U+7835-7836,U+783d,U+783f,U+7841-7842,U+7844,U+7846,U+7848-784b,U+784d,U+784f,U+7851,U+7853-7854,U+7858-785c,U+785e-7863,U+7865-7867,U+7869,U+7870-7876,U+7878-787b,U+787d-7886,U+7888,U+788a-788b,U+788f-7890,U+7892;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_eeb3fd.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+7894-7896,U+7899,U+789d-789e,U+78a0,U+78a2,U+78a4,U+78a6,U+78a8,U+78aa-78ac,U+78ae-78af,U+78b5-78b7,U+78bb,U+78bd,U+78bf-78c0,U+78c2-78c4,U+78c6-78c8,U+78cc-78cf,U+78d1-78d3,U+78d6-78d8,U+78db-78e2,U+78e4-78e6,U+78e9-78eb,U+78ed-78ee,U+78f0-78f1,U+78f3,U+78f5-78f6,U+78f8-78f9,U+78fb-78fc,U+78fe-7900,U+7902-7904,U+7907-790d,U+790f-7912,U+7914-7918,U+791a-791d,U+7920-7923,U+7925,U+7927-7929,U+792d-7930,U+7932-7933,U+7935-7939,U+793d,U+793f,U+7942-7945,U+7947,U+794a-7952,U+7954,U+7958-7959,U+7961,U+7963-7964,U+7966,U+7969-796c,U+796e,U+7970-7976,U+7979,U+797b-797e,U+7982-7983,U+7986-7989,U+798b-798c,U+7990-7999,U+799b-79a5,U+79a8-79a9,U+79ab-79ad,U+79af,U+79b2,U+79b4-79b8,U+79bc,U+79c2,U+79c4-79c5,U+79c7,U+79ca,U+79cc,U+79ce-79d0,U+79d3-79d4,U+79d6-79d7,U+79d9-79de,U+79e0-79e2,U+79e5,U+79e8,U+79ea,U+79ec,U+79ee,U+79f1-79f7,U+79f9-79fa,U+79fc,U+79fe-79ff,U+7a01,U+7a04,U+7a07,U+7a09-7a0a,U+7a0c,U+7a0f-7a13,U+7a15-7a16,U+7a18-7a19,U+7a1b,U+7a1d,U+7a21-7a22,U+7a24-7a2d,U+7a2f-7a30,U+7a32,U+7a34-7a36,U+7a38,U+7a3a,U+7a3e,U+7a41-7a45,U+7a47-7a4b,U+7a4f-7a50,U+7a52-7a56,U+7a58-7a60,U+7a63-7a68,U+7a6a-7a6c,U+7a6e-7a6f,U+7a71-7a73,U+7a75,U+7a7b-7a7e,U+7a82,U+7a85,U+7a87,U+7a89-7a8c,U+7a8e-7a90,U+7a93-7a94,U+7a99-7a9b,U+7a9e,U+7aa1-7aa4,U+7aa7,U+7aab,U+7ab0-7ab2,U+7ab4;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_76f2de.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+739e,U+73a0-73a1,U+73a3-73a7,U+73aa,U+73ac-73ad,U+73b1,U+73b4-73b6,U+73b8-73b9,U+73bc-73bf,U+73c1,U+73c3-73c7,U+73cb-73cc,U+73ce,U+73d2-73d8,U+73da-73dd,U+73df,U+73e1-73e4,U+73e6,U+73e8,U+73ea-73ec,U+73ee-73f1,U+73f3-73fd,U+73ff-7402,U+7404,U+7407-7408,U+740b-740e,U+7411-7419,U+741c-7421,U+7423-7424,U+7427,U+7429,U+742b,U+742d,U+742f,U+7431-7432,U+7437-7439,U+743b,U+743d-743e,U+7440,U+7442-7448,U+744a,U+744c-7454,U+7456,U+7458,U+745d,U+7460-7462,U+7465-7468,U+746b-746c,U+746e,U+7471-7475,U+7478-747d,U+747f,U+7482,U+7484-7486,U+7488,U+748a,U+748c-748d,U+748f,U+7491-749b,U+749d,U+749f-74a2,U+74a4-74a5,U+74aa-74af,U+74b1-74b9,U+74bb-74bc,U+74be,U+74c0-74c9,U+74cb-74ce,U+74d0-74d1,U+74d3,U+74d5-74d9,U+74db,U+74dd,U+74df,U+74e1,U+74e5,U+74e7-74ed,U+74f0-74f3,U+74f5,U+74f8-74fe,U+7500,U+7502-7503,U+7505-750b,U+750e,U+7510,U+7512,U+7514,U+7516-7517,U+751b,U+751d-751e,U+7520-7521,U+7523-7524,U+7526-7527,U+752a,U+752e,U+7534,U+7536,U+7539,U+753c-753d,U+753f,U+7541-7544,U+7546-7547,U+7549-754a,U+754d,U+7550-7553,U+7555-7558,U+755e-7561,U+7563-7564,U+7567-7569,U+756c-756f,U+7571,U+7573,U+7575,U+7577,U+757a-757e,U+7580-7582,U+7584-7585,U+7588-7589,U+758c-758e,U+7590,U+7593,U+7595,U+7598,U+759b-759c,U+759e,U+75a2,U+75a6-75aa,U+75ad,U+75b6-75b7,U+75ba-75bb,U+75bf-75c1,U+75c6,U+75cb-75cc,U+75ce-75d1,U+75d3,U+75d7,U+75da,U+75dc-75dd,U+75df,U+75e1,U+75e5,U+75e9,U+75ec-75ef,U+75f2-75f3,U+75f5-75f8,U+75fa-75fb,U+75fd,U+7604,U+7606;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_666915.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+716a-716b,U+716d,U+716f-7171,U+7174-7177,U+7179,U+717b-717c,U+717e-7183,U+7185-7189,U+718b-718e,U+7190-7191,U+7193,U+7195-7196,U+719a-719e,U+71a1-71a7,U+71a9-71ab,U+71ad-71b0,U+71b2,U+71b4,U+71b6-71b8,U+71ba-71bd,U+71bf-71c0,U+71c2,U+71c4-71c7,U+71ca-71cd,U+71cf-71d1,U+71d3,U+71d6-71d8,U+71da-71db,U+71dd-71de,U+71e1-71e4,U+71e8-71ec,U+71ef-71f3,U+71f5-71f8,U+71fa,U+71fd,U+71ff-7205,U+7207-720c,U+720e-720f,U+7211-721a,U+721c,U+721e-7227,U+7229,U+722b,U+722e-722f,U+7233-7234,U+723c,U+7241-7245,U+7249-724b,U+724e-7251,U+7253-7255,U+7257,U+725a,U+725c,U+725e,U+7263-7265,U+7268,U+726a-726d,U+7270-7271,U+7273,U+7276-7278,U+727b-727c,U+7282-7283,U+7285-7289,U+728c,U+728e,U+7290-7291,U+7293-7295,U+7297-729a,U+729c-729e,U+72a0-72a1,U+72a3-72a6,U+72a8-72ab,U+72ae,U+72b1-72b3,U+72b5,U+72ba-72bf,U+72c5-72c7,U+72c9-72cc,U+72cf,U+72d1,U+72d3-72d6,U+72d8,U+72da-72dd,U+72df,U+72e2-72e7,U+72ea-72eb,U+72f5-72f6,U+72fe-7300,U+7302,U+7304-7309,U+730b-730d,U+730f-7312,U+7314,U+7318,U+731a,U+731f-7320,U+7323-7324,U+7326-7328,U+732d,U+732f-7330,U+7332-7333,U+7335,U+733a,U+733c-733d,U+7340,U+7342,U+7346-734c,U+734f,U+7351,U+7353-7356,U+7358-735f,U+7361-7367,U+7369,U+736e,U+7371,U+7373-7374,U+7376,U+7379,U+737d,U+737f,U+7381-7383,U+7385-7386,U+7388,U+738a,U+738c-738d,U+738f-7390,U+7392-7395,U+7397-739a,U+739c-739d;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_6938ca.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+6f6a-6f6c,U+6f71,U+6f73,U+6f75-6f76,U+6f79,U+6f7b,U+6f7d-6f7e,U+6f81-6f83,U+6f85,U+6f8a-6f8b,U+6f8f-6f96,U+6f98-6f9b,U+6f9d-6f9f,U+6fa2-6fa3,U+6fa5-6fa6,U+6fa8,U+6faa-6fad,U+6faf-6fb0,U+6fb2,U+6fb4-6fb5,U+6fb7-6fb8,U+6fba-6fbf,U+6fc4-6fc8,U+6fca-6fd0,U+6fd3-6fd7,U+6fd9-6fdd,U+6fe2-6fe3,U+6fe5-6fea,U+6fec-6fed,U+6ff2-6ff9,U+6ffb,U+6ffd,U+6fff-7004,U+7007-7008,U+700a,U+700c-700e,U+7010,U+7012-7014,U+7016-7017,U+7019,U+701c,U+701e,U+7021-7022,U+7024-7025,U+7029-7031,U+7033-7034,U+7036-7038,U+703a-703d,U+703f-7042,U+7045-704b,U+704d-704e,U+7050,U+7052-7054,U+7056-7057,U+7059-705c,U+705f-7062,U+7065-7068,U+706a,U+706e,U+7071-7074,U+7077,U+7079-707b,U+7081-7084,U+7086-7088,U+708b-708d,U+708f-7091,U+7093,U+7097-7098,U+709a-709b,U+709e-70aa,U+70b0,U+70b2,U+70b4-70b6,U+70ba,U+70be-70bf,U+70c4-70c7,U+70c9,U+70cb-70ce,U+70d0-70d7,U+70da,U+70dc-70de,U+70e0-70e3,U+70e5,U+70ea,U+70ee,U+70f0-70f3,U+70f5-70f6,U+70f8,U+70fa-70fc,U+70fe-7108,U+710b-710f,U+7111-7112,U+7114,U+7117,U+711b-7120,U+7122-7125,U+7127-712e,U+7132-7135,U+7137-7144,U+7147-7148,U+714b,U+714d,U+714f-7151,U+7153-7158,U+715a-715b,U+715d,U+715f-7161,U+7163;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_cf29b0.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+6c45,U+6c48,U+6c4b-6c4f,U+6c51-6c53,U+6c56,U+6c58,U+6c5a,U+6c62-6c63,U+6c65-6c67,U+6c6b-6c6f,U+6c71,U+6c73,U+6c75,U+6c77-6c78,U+6c7b-6c7c,U+6c7f-6c80,U+6c84,U+6c87,U+6c8a-6c8b,U+6c8d-6c8e,U+6c91,U+6c95-6c98,U+6c9a,U+6c9c-6c9e,U+6ca0,U+6ca2,U+6ca8,U+6cac,U+6caf-6cb0,U+6cb4-6cb7,U+6cba,U+6cc0,U+6cc2-6cc3,U+6cc6-6cc8,U+6ccb,U+6ccd-6ccf,U+6cd1-6cd2,U+6cd8-6cda,U+6cdc,U+6cdf,U+6ce4,U+6ce6-6ce7,U+6ce9,U+6cec-6ced,U+6cf2,U+6cf4,U+6cf9,U+6cff-6d00,U+6d02-6d03,U+6d05-6d06,U+6d08-6d0a,U+6d0d,U+6d0f-6d11,U+6d13-6d16,U+6d18,U+6d1c-6d1d,U+6d1f-6d24,U+6d26,U+6d28,U+6d2c-6d2d,U+6d2f-6d30,U+6d34,U+6d37-6d38,U+6d3a,U+6d3f-6d40,U+6d42,U+6d44,U+6d49,U+6d4c,U+6d50,U+6d55-6d58,U+6d5b,U+6d5d,U+6d5f,U+6d61-6d62,U+6d64-6d65,U+6d67-6d68,U+6d6b-6d6d,U+6d70-6d73,U+6d75-6d76,U+6d7a-6d7b,U+6d7d-6d81,U+6d83-6d84,U+6d86,U+6d8a-6d8b,U+6d8d,U+6d8f-6d90,U+6d92,U+6d96-6d9a,U+6d9c,U+6da2,U+6da5,U+6dac-6dad,U+6db0-6db1,U+6db3-6db4,U+6db6-6db7,U+6db9-6dbb,U+6dbd-6dbe,U+6dc1-6dc3,U+6dc8-6dca,U+6dcd-6dd0,U+6dd2-6dd5,U+6dd7,U+6ddb-6ddc,U+6ddf,U+6de2-6de3,U+6de7,U+6ded,U+6def-6df0,U+6df2,U+6df4,U+6df8,U+6dfd-6e04,U+6e06-6e08,U+6e0b,U+6e0f,U+6e12-6e13,U+6e15,U+6e18,U+6e1c,U+6e1e-6e1f,U+6e22,U+6e27-6e28,U+6e2a,U+6e2e,U+6e30-6e31,U+6e33,U+6e35-6e37,U+6e39,U+6e3b-6e3d,U+6e3f-6e42,U+6e45-6e49,U+6e4b-6e4c,U+6e4f-6e52,U+6e55,U+6e57,U+6e59-6e5a,U+6e5c-6e5d,U+6e60-6e62,U+6e64-6e66,U+6e68-6e6a,U+6e6c-6e6d,U+6e70-6e7d,U+6e80-6e82,U+6e84,U+6e87-6e88,U+6e8a-6e8e,U+6e91-6e95,U+6e97,U+6e99-6e9b,U+6e9e,U+6ea0-6ea1,U+6ea3-6ea4,U+6ea6,U+6ea8-6ea9,U+6eac-6eae,U+6eb0,U+6eb3,U+6eb5,U+6eb8-6eb9,U+6ebe-6ec0,U+6ec3,U+6ec6,U+6ec8-6eca,U+6ecd,U+6ed0,U+6ed2,U+6ed6,U+6ed8-6ed9,U+6edb-6edd,U+6ee3,U+6ee7,U+6eea-6eeb,U+6eed-6eee,U+6ef0-6ef1,U+6ef3,U+6ef5-6ef6,U+6efa-6efd,U+6f00,U+6f03-6f05,U+6f07-6f08,U+6f0a-6f0e,U+6f10,U+6f12,U+6f16-6f19,U+6f1b-6f1f,U+6f21,U+6f25-6f28,U+6f2e,U+6f30,U+6f34,U+6f37,U+6f39-6f3d,U+6f40,U+6f42-6f45,U+6f48-6f4a,U+6f4c,U+6f4e-6f50,U+6f52-6f53,U+6f55-6f57,U+6f5a,U+6f5d,U+6f5f-6f61,U+6f63,U+6f65,U+6f67-6f69;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_b8977e.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+6a56-6a57,U+6a5a,U+6a5c-6a5e,U+6a60,U+6a63-6a64,U+6a66-6a6a,U+6a6c-6a70,U+6a72-6a78,U+6a7a-6a7b,U+6a7d-6a7f,U+6a82-6a83,U+6a85-6a88,U+6a8a-6a8d,U+6a8f,U+6a92-6a93,U+6a95-6a96,U+6a98-6a9b,U+6a9d-6a9f,U+6aa1,U+6aa4-6aa8,U+6aaa,U+6aad-6ab2,U+6ab4-6ab7,U+6ab9-6aba,U+6abc-6abd,U+6abf-6ac2,U+6ac4-6ad2,U+6ad4-6ad9,U+6adc,U+6ae0-6ae4,U+6ae6,U+6ae9,U+6aed-6af0,U+6af2,U+6af4-6af7,U+6af9-6afa,U+6afc-6b03,U+6b05-6b09,U+6b0b-6b0e,U+6b10-6b11,U+6b13-6b15,U+6b17-6b1d,U+6b1f,U+6b25-6b26,U+6b28-6b31,U+6b33-6b36,U+6b38,U+6b3b-6b3c,U+6b3f-6b42,U+6b44-6b45,U+6b48,U+6b4a-6b4b,U+6b4d-6b4f,U+6b51-6b58,U+6b5a-6b5e,U+6b60,U+6b68-6b69,U+6b6b-6b71,U+6b73-6b76,U+6b7a,U+6b7d-6b7e,U+6b80,U+6b85,U+6b88,U+6b8c,U+6b8e-6b91,U+6b94-6b95,U+6b97,U+6b99,U+6b9c-6b9d,U+6b9f-6ba0,U+6ba2-6ba3,U+6ba5-6ba9,U+6bac,U+6bb0-6bb1,U+6bb6,U+6bb8-6bb9,U+6bbb,U+6bbd-6bbe,U+6bc3-6bc4,U+6bc7-6bca,U+6bcc,U+6bce,U+6bd0-6bd1,U+6bd8,U+6bda,U+6bdc-6be0,U+6be2-6be9,U+6bec-6bee,U+6bf0-6bf2,U+6bf4,U+6bf6-6bf8,U+6bfa-6bfc,U+6bfe,U+6c00-6c04,U+6c09-6c0b,U+6c0e,U+6c12,U+6c17,U+6c1c-6c1e,U+6c20,U+6c25,U+6c2d,U+6c31,U+6c36-6c37,U+6c3a-6c3c,U+6c3e-6c3f,U+6c43-6c44;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_8ea677.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+681e-6820,U+6822-6828,U+682b-6831,U+6834-6836,U+683a-683b,U+683f,U+6847,U+684b,U+684d,U+684f,U+6852,U+6856-685f,U+686a,U+686c-6873,U+6875,U+6878-687e,U+6880,U+6882,U+6884,U+6887-688e,U+6890-6892,U+6895-6896,U+6899-689c,U+689e,U+68a0-68a1,U+68a3-68a5,U+68a9-68ac,U+68ae,U+68b1-68b2,U+68b4,U+68b6-68bf,U+68c1,U+68c3,U+68c5-68c8,U+68cc,U+68ce-68d1,U+68d3-68d4,U+68d9,U+68db-68de,U+68e1-68e2,U+68e4-68e6,U+68e8-68eb,U+68ed,U+68ef,U+68f3-68f4,U+68f6-68f8,U+68fb,U+68fd-6900,U+6902-6904,U+6906-690a,U+690c,U+6911,U+6913-691e,U+6921-6923,U+6925-692c,U+692e-692f,U+6931-6933,U+6935-6938,U+693a-693c,U+693e,U+6940-6941,U+6943-6949,U+694b-6952,U+6955-6956,U+6958-6959,U+695b-695c,U+695f,U+6961-6962,U+6964-6965,U+6967,U+6969-696a,U+696c,U+696f-6970,U+6972-6974,U+6976,U+697a-697b,U+697d-697f,U+6981,U+6983,U+6985,U+698a-698c,U+698e-6993,U+6996-6997,U+6999-699a,U+699d-69a6,U+69a9,U+69ac,U+69af-69b0,U+69b2-69b3,U+69b5-69b6,U+69b8-69ba,U+69bc-69be,U+69c0,U+69c2-69c9,U+69cf,U+69d1-69d2,U+69d5-69da,U+69dc-69de,U+69e1-69e6,U+69e9,U+69eb-69ec,U+69ee-69f1,U+69f4-69fc,U+69fe,U+6a00,U+6a03-6a04,U+6a06-6a09,U+6a0b-6a10,U+6a12,U+6a14-6a16,U+6a1a-6a1d,U+6a20,U+6a22,U+6a24-6a27,U+6a29,U+6a2b-6a2e,U+6a30,U+6a32-6a33,U+6a36-6a37,U+6a3b-6a3c,U+6a3f-6a43,U+6a45-6a46,U+6a49-6a4a,U+6a4c-6a4f,U+6a51-6a55;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_0d19f4.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+653a,U+653c-653d,U+6540-6544,U+6546-6547,U+654a-654b,U+654d,U+6550,U+6552-6554,U+655a,U+655c,U+655f-6561,U+6564-6565,U+6567-656a,U+656d-656f,U+6571,U+6573,U+6576,U+6579-6581,U+6584-6586,U+6588-658a,U+658d-658f,U+6592,U+6594,U+6596,U+6598,U+659a,U+659d-659e,U+65a0,U+65a2-65a3,U+65a6,U+65a8,U+65aa,U+65ae,U+65b1-65b6,U+65b8,U+65ba-65bb,U+65be-65c0,U+65c7-65ca,U+65cd,U+65d0-65d1,U+65d3-65d5,U+65d8-65df,U+65e1,U+65e4,U+65ea-65eb,U+65f2-65f5,U+65f8-65f9,U+65fb-65ff,U+6601,U+6604-6605,U+6607-6609,U+660b,U+660d,U+6610-6612,U+6616-6618,U+661a-661c,U+661e,U+6621-6624,U+6626,U+6629-662c,U+662e,U+6630,U+6632-6633,U+6637-663b,U+663d,U+663f-6640,U+6644-6648,U+664a,U+664d-664e,U+6650-6651,U+6658-6659,U+665b-665c,U+665e,U+6660,U+6662-6663,U+6665,U+6667,U+6669-666d,U+6671-6673,U+6675,U+6678-6679,U+667b-667d,U+667f-6681,U+6683,U+6685-6686,U+668a-668b,U+668d-6690,U+6692-6695,U+6698-669c,U+669e-66a1,U+66a3-66a6,U+66a9-66aa,U+66ac-66ad,U+66af-66b0,U+66b2-66b3,U+66b5-66b8,U+66ba-66bd,U+66bf-66c3,U+66c5-66c6,U+66c8,U+66ca-66d5,U+66d7-66d8,U+66da,U+66de-66df,U+66e1-66e5,U+66e7-66e8,U+66ea-66eb,U+66ed-66ef,U+66f1,U+66f5-66f6,U+66fa-66fb,U+66fd,U+6701-6702,U+6704-6707,U+670c,U+670e-670f,U+6711-6713,U+6716,U+6718-671a,U+671c,U+671e,U+6720-6725,U+6729,U+672e,U+6730,U+6732-6733,U+6737-6739,U+673b-673c,U+673e-673f,U+6741,U+6744-6745,U+6747,U+674a-674b,U+674d,U+6752,U+6754-6755,U+6757-675b,U+675d,U+6762-6764,U+6766-6767,U+676b-676c,U+676e,U+6774,U+6776,U+6778-677b,U+677d,U+6780,U+6782-6783,U+6785-6786,U+6788,U+678a,U+678c-678f,U+6791-6794,U+6796,U+6799,U+679b,U+679f-67a1,U+67a4,U+67a6,U+67a9,U+67ac,U+67ae,U+67b1-67b2,U+67b4,U+67b9-67c0,U+67c2,U+67c5-67ce,U+67d5-67d7,U+67db,U+67df,U+67e1,U+67e3-67e4,U+67e6-67e8,U+67ea-67eb,U+67ed-67ee,U+67f2,U+67f6-67fa,U+67fc,U+67fe,U+6801-6804,U+6806,U+680d,U+6810,U+6812,U+6814-6815,U+6818-681c;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_87c494.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+6235,U+6238-623c,U+6242,U+6244-6246,U+624a,U+624f-6250,U+6255-6257,U+6259-625a,U+625c-6260,U+6262,U+6264-6265,U+6268,U+6271-6272,U+6274-6275,U+6277-6278,U+627a-627b,U+627d,U+6281-6283,U+6285-6288,U+628b-6290,U+6294,U+6299,U+629c-629e,U+62a3,U+62a6-62a7,U+62a9-62aa,U+62ad-62b0,U+62b2-62b4,U+62b6-62b8,U+62ba,U+62be,U+62c0-62c1,U+62c3,U+62cf,U+62d1,U+62d5,U+62de,U+62e0-62e1,U+62e4,U+62ea-62eb,U+62f0,U+62f2,U+62f5,U+62f8-62fb,U+6300,U+6303-6306,U+630a-630d,U+630f-6310,U+6312-6315,U+6317-6319,U+631c,U+6326-6327,U+6329,U+632c-632e,U+6330,U+6333-6338,U+633b-633c,U+633f-6341,U+6344,U+6347-6348,U+634a,U+6351-6354,U+6356-635d,U+6360,U+6364-6366,U+636a,U+636c,U+636f-6370,U+6373-6375,U+6378-6379,U+637c-637f,U+6381,U+6385-6386,U+638b,U+638d,U+6391,U+6393-6395,U+6397,U+639a,U+639c-639f,U+63a1,U+63a4,U+63a6,U+63ab,U+63af,U+63b1-63b2,U+63b5-63b6,U+63b9,U+63bb,U+63bd,U+63bf,U+63c1-63c3,U+63c5,U+63c7-63c8,U+63ca-63cc,U+63d1,U+63d3-63d5,U+63d7-63d9,U+63dc-63dd,U+63df,U+63e2,U+63e4-63e8,U+63eb-63ec,U+63ef-63f1,U+63f3,U+63f5,U+63f7,U+63fa-63fc,U+63fe,U+6403-6404,U+6406-640a,U+640e,U+6411-6412,U+6415,U+6418-641a,U+641d,U+641f,U+6422-6424,U+6427-6429,U+642b,U+642e-6433,U+6435,U+6437-6439,U+643b-643c,U+643e,U+6442-6443,U+6449,U+644b-6450,U+6453,U+6455-6457,U+6459-645b,U+645d,U+6460-6462,U+6464-6466,U+6468,U+646a-646c,U+646e,U+6470-6472,U+6474-6475,U+6477,U+647c-6481,U+6483,U+6486,U+6489-648f,U+6494,U+6497-6498,U+649b-649d,U+64a0-64a2,U+64a6-64a8,U+64aa,U+64af,U+64b1,U+64b4,U+64b6,U+64b9,U+64bd,U+64c3,U+64c6,U+64c8-64c9,U+64cc,U+64cf,U+64d1,U+64d3,U+64d5-64d6,U+64d9,U+64db-64dd,U+64df,U+64e3,U+64e5,U+64e7-64eb,U+64ed-64ee,U+64f3,U+64f5-64f6,U+64f8-64f9,U+64fd,U+64ff,U+6501-6503,U+6505,U+6507-6508,U+650a-650e,U+6510-6511,U+6513,U+6515,U+6517,U+651a,U+651e-6521,U+6526-6529,U+652d,U+6530-6533,U+6537;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_3b743f.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+5f60,U+5f63,U+5f67-5f68,U+5f6b,U+5f6e-5f6f,U+5f72,U+5f74-5f76,U+5f78,U+5f7a,U+5f7d-5f7e,U+5f83,U+5f86,U+5f8d-5f8f,U+5f93-5f94,U+5f96,U+5f9a-5f9b,U+5f9d,U+5f9f,U+5fa2-5fa7,U+5fab-5fac,U+5faf-5fb4,U+5fb6,U+5fb8,U+5fba-5fbb,U+5fbe-5fc2,U+5fc7-5fc8,U+5fca-5fcb,U+5fce,U+5fd3-5fd5,U+5fda-5fdc,U+5fde-5fdf,U+5fe2-5fe3,U+5fe5-5fe6,U+5fe8-5fe9,U+5fec,U+5fef-5ff0,U+5ff2-5ff4,U+5ff6-5ff7,U+5ff9-5ffa,U+5ffc,U+6007-6009,U+600b-600c,U+6010-6011,U+6013,U+6017-6018,U+601a,U+601e-601f,U+6022-6024,U+602c-602e,U+6030-6034,U+6036-603a,U+603d-603e,U+6040,U+6044-6045,U+6047-604a,U+604c,U+604e-604f,U+6051,U+6053-6054,U+6056-6058,U+605b-605c,U+605e-6061,U+6066,U+606e,U+6071-6072,U+6074-6075,U+6077,U+607e,U+6080-6082,U+6086-6088,U+608a-608b,U+608e-6091,U+6093,U+6095,U+6097-6099,U+609c,U+609e,U+60a1-60a2,U+60a4-60a5,U+60a7,U+60a9-60aa,U+60ae,U+60b0,U+60b3,U+60b7,U+60b9-60ba,U+60be-60c4,U+60c7-60c9,U+60cc-60d0,U+60d2-60d4,U+60d6-60d7,U+60d9,U+60db,U+60de,U+60e2-60e5,U+60ea,U+60f5,U+60f7-60f8,U+60fc-60ff,U+6102-6105,U+6107,U+610a-610c,U+6110-6114,U+6116-6119,U+611d-611e,U+6121-6122,U+6125,U+6129-612a,U+612c-6133,U+6135-6136,U+6138-613d,U+6140-6141,U+6143,U+6145-6147,U+6149,U+614f-6150,U+6152-6154,U+6156-6157,U+6159,U+615b-615c,U+615e,U+6160-6161,U+6164-6166,U+6169,U+616c-616d,U+616f,U+6171-6172,U+6174,U+6178-617b,U+617d,U+617f-6181,U+6183-6189,U+618c-618d,U+618f,U+6193,U+6195-6199,U+619b-619c,U+619e-61a3,U+61a5-61a6,U+61aa,U+61ad,U+61af-61b1,U+61b3-61b5,U+61b8-61bd,U+61bf-61c1,U+61c3-61c6,U+61ce-61d0,U+61d3,U+61d5-61de,U+61e0-61e2,U+61e4-61e5,U+61e7,U+61e9-61f1,U+61f3-61f4,U+61f9,U+61fb,U+61fd,U+6201-6205,U+6209,U+6213,U+6219,U+621c-621e,U+6220,U+6223,U+6226,U+6228,U+622b,U+622d,U+622f,U+6231;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_5825df.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+5cde-5ce0,U+5ce2-5ce3,U+5ce7,U+5ceb-5cec,U+5cee,U+5cf1-5cf3,U+5cf5,U+5cf7-5cfa,U+5cfc,U+5cfe-5d01,U+5d04-5d05,U+5d08-5d0c,U+5d0f-5d10,U+5d12-5d13,U+5d15,U+5d18-5d1a,U+5d1c-5d1d,U+5d1f-5d21,U+5d23,U+5d25,U+5d28,U+5d2a-5d2b,U+5d2f-5d32,U+5d35-5d3c,U+5d3f-5d46,U+5d48-5d49,U+5d4d-5d4f,U+5d51-5d57,U+5d59-5d5a,U+5d5c,U+5d5e-5d68,U+5d6a,U+5d6d-5d6e,U+5d70-5d73,U+5d75-5d80,U+5d83,U+5d85-5d86,U+5d88-5d96,U+5d98,U+5d9a-5d9c,U+5d9e-5d9f,U+5da1-5da6,U+5da8-5db3,U+5db5-5db6,U+5db9,U+5dbb,U+5dbe-5dc4,U+5dc6-5dca,U+5dcc,U+5dce-5dd1,U+5dd3,U+5dd5,U+5dd7-5dda,U+5ddc,U+5ddf-5de0,U+5de4,U+5dea,U+5dec-5ded,U+5df5-5df6,U+5df8,U+5dfa-5dfc,U+5dff-5e00,U+5e04,U+5e07,U+5e09-5e0b,U+5e0d-5e0e,U+5e12-5e13,U+5e17,U+5e1e-5e24,U+5e28-5e2a,U+5e2c,U+5e2f-5e30,U+5e32,U+5e34-5e35,U+5e39-5e3a,U+5e3e-5e3f,U+5e41,U+5e46-5e4b,U+5e4d-5e53,U+5e56,U+5e59-5e5a,U+5e5c-5e5d,U+5e60,U+5e64-5e6a,U+5e6d-5e71,U+5e75,U+5e77,U+5e81-5e83,U+5e85,U+5e88-5e89,U+5e8c-5e8e,U+5e92,U+5e98,U+5e9b,U+5e9d,U+5ea1-5ea4,U+5ea8-5eaa,U+5eac,U+5eae-5eb2,U+5eb4,U+5eba-5ebd,U+5ebf-5ec0,U+5ec3,U+5ec5-5ec7,U+5ecb-5ed0,U+5ed4-5ed5,U+5ed7-5ed9,U+5edc,U+5ede,U+5ee0,U+5ee4-5ee7,U+5eeb,U+5eed-5ef2,U+5ef5,U+5ef8-5ef9,U+5efb-5efd,U+5f05-5f07,U+5f09,U+5f0c-5f0e,U+5f10,U+5f16,U+5f19-5f1a,U+5f1c-5f1e,U+5f21-5f24,U+5f28,U+5f2b-5f2c,U+5f2e,U+5f30,U+5f32,U+5f34,U+5f36,U+5f38,U+5f3b,U+5f3d-5f3f,U+5f41-5f47,U+5f49-5f4b,U+5f4d,U+5f4f,U+5f51,U+5f54,U+5f59-5f5c,U+5f5f;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_1c96d4.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+5a4b-5a54,U+5a56-5a59,U+5a5b-5a61,U+5a63-5a65,U+5a68-5a69,U+5a6b-5a6c,U+5a6e-5a73,U+5a78-5a79,U+5a7b-5a7e,U+5a80-5a91,U+5a93-5a99,U+5a9c-5aa6,U+5aa8-5aa9,U+5aab-5ab1,U+5ab4,U+5ab6-5ab7,U+5ab9-5abb,U+5abf-5ac0,U+5ac3-5ac8,U+5aca,U+5acd-5ad1,U+5ad3,U+5ad5,U+5ad9-5adb,U+5add-5adf,U+5ae2,U+5ae4-5ae5,U+5ae7-5ae8,U+5aea,U+5aec-5af0,U+5af2-5af4,U+5af6-5af9,U+5afb-5aff,U+5b01-5b07,U+5b0a,U+5b0d-5b15,U+5b18,U+5b1a-5b20,U+5b22-5b23,U+5b25-5b29,U+5b2b-5b2f,U+5b31,U+5b33,U+5b35-5b36,U+5b39-5b3f,U+5b41-5b42,U+5b44-5b4b,U+5b4d-5b4f,U+5b52,U+5b56,U+5b5e,U+5b60-5b61,U+5b67-5b68,U+5b6d-5b6f,U+5b72,U+5b74,U+5b77,U+5b79,U+5b7b-5b7c,U+5b7e,U+5b82,U+5b86,U+5b8a,U+5b8d-5b8e,U+5b90-5b92,U+5b94,U+5b96,U+5b9f,U+5ba7-5ba9,U+5bac-5bad,U+5baf,U+5bb1-5bb2,U+5bb7,U+5bba-5bbc,U+5bc0-5bc1,U+5bc3,U+5bc8-5bcb,U+5bcd-5bcf,U+5bd1,U+5bd4-5bdc,U+5be0,U+5be3,U+5bea,U+5bed,U+5bef,U+5bf1-5bf4,U+5bf7,U+5bfd-5bfe,U+5c00,U+5c02-5c03,U+5c05,U+5c0c,U+5c10,U+5c12-5c13,U+5c17,U+5c19,U+5c1b,U+5c1e-5c21,U+5c23,U+5c26,U+5c28-5c2b,U+5c2d-5c30,U+5c32-5c33,U+5c35-5c36,U+5c43-5c44,U+5c47,U+5c4c,U+5c52-5c54,U+5c56-5c58,U+5c5a,U+5c5d,U+5c5f,U+5c67,U+5c69-5c6b,U+5c6d,U+5c70,U+5c72-5c78,U+5c7b-5c7e,U+5c80,U+5c83-5c87,U+5c89-5c8b,U+5c8e-5c8f,U+5c92-5c93,U+5c95,U+5c9d-5ca0,U+5ca4-5ca8,U+5caa,U+5cae-5cb0,U+5cb2,U+5cb4,U+5cb6,U+5cb9-5cbc,U+5cbe,U+5cc0,U+5cc2-5cc3,U+5cc5-5cca,U+5ccc-5cd1,U+5cd3-5cd8,U+5cda-5cdd;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_eac724.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+57b9-57c1,U+57c4-57ca,U+57cc-57cd,U+57d0-57d1,U+57d3,U+57d6-57d7,U+57db-57dc,U+57de,U+57e2-57e3,U+57e5-57ec,U+57ee,U+57f0-57f3,U+57f5-57f6,U+57fb-57fc,U+57fe-57ff,U+5801,U+5803-5804,U+5808-5809,U+580c,U+580e-5810,U+5812-5814,U+5817-5818,U+581a-581c,U+581f,U+5822-5823,U+5825-5829,U+582b-582e,U+5832-5833,U+5836-5843,U+5845-5849,U+584e,U+5850,U+5853,U+5855-5857,U+5859,U+585b-585d,U+585f-5861,U+5863-5864,U+5866-586a,U+586d-5874,U+5876-5878,U+587a-587d,U+587f,U+5882,U+5884,U+5886-5888,U+588b-5891,U+5894-5898,U+589b,U+589d,U+58a0-58a7,U+58aa-58ad,U+58af-58b2,U+58b4-58bb,U+58bd,U+58bf-58c0,U+58c2-58c4,U+58c6,U+58c8-58cd,U+58cf-58d0,U+58d2,U+58d4,U+58d6-58d7,U+58db-58dd,U+58e1,U+58e3,U+58e5-58e8,U+58ea,U+58ed,U+58f1-58f2,U+58f4-58f5,U+58f7-58f8,U+58fb-58fc,U+58fe-5901,U+5903,U+5905-5906,U+5908-590c,U+590e,U+5910-5913,U+5917-5918,U+591b,U+591d-591e,U+5921,U+5923,U+5926,U+5928,U+592c,U+5930,U+5932-5933,U+5935-5936,U+593b,U+593d,U+593f-5940,U+5943,U+5945-5946,U+594a,U+594c-594d,U+5952-5953,U+5959,U+595b-595f,U+5961,U+5963-5964,U+5966,U+5968,U+596b-596d,U+596f-5972,U+5975,U+5977,U+597a-597b,U+597e-5980,U+5985,U+5989,U+598b-598c,U+598e-5991,U+5994-5995,U+5998,U+599a-599c,U+599f-59a2,U+59a6-59a7,U+59ac-59ad,U+59b0-59b1,U+59b4-59b8,U+59ba,U+59bc-59bd,U+59bf-59c5,U+59c7-59c9,U+59cc,U+59ce-59cf,U+59d5-59d6,U+59d9,U+59db,U+59de-59e2,U+59e4,U+59e7,U+59e9-59eb,U+59ed-59f8,U+59fa,U+59fc-59fe,U+5a00,U+5a02,U+5a0a-5a0b,U+5a0d-5a10,U+5a12,U+5a14-5a17,U+5a19-5a1a,U+5a1d-5a1e,U+5a21-5a22,U+5a24,U+5a26-5a28,U+5a2a-5a30,U+5a33,U+5a35,U+5a37-5a3b,U+5a3d-5a3f,U+5a42-5a45,U+5a47-5a48;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_5ae301.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+50c4,U+50c6-50c8,U+50ca-50ce,U+50d0,U+50d2-50d4,U+50d7-50d9,U+50db-50dd,U+50df-50e4,U+50e9-50eb,U+50ef-50f0,U+50f2,U+50f4,U+50f6-50f8,U+50fa,U+50fc-50ff,U+5103,U+5105,U+510a,U+510c,U+510e-510f,U+5111,U+5113,U+5116-511e,U+5120,U+5122-5129,U+512b-5131,U+5133-5136,U+5138-5139,U+513d-513e,U+5142,U+514a,U+514e-5150,U+5153,U+5158-5159,U+515b,U+515d-5161,U+5163-5164,U+5166,U+516f,U+5172,U+517a,U+517e-517f,U+5183-5184,U+5186-5187,U+518b,U+518e-5190,U+5193-5194,U+5198,U+519a,U+519d-519f,U+51a1,U+51a3,U+51a6-51a9,U+51ad-51ae,U+51b4,U+51b8-51ba,U+51be-51bf,U+51c1-51c3,U+51c5,U+51c8,U+51ca,U+51ce,U+51d0,U+51d2-51da,U+51de-51df,U+51e2-51e3,U+51e5-51ea,U+51ec,U+51ee,U+51f2,U+51f4,U+51f7,U+51fe,U+5204-5205,U+5209,U+520b-520c,U+520f-5210,U+5213-5215,U+521c,U+521e-521f,U+5221-5223,U+5226-5227,U+522c,U+522f,U+5231-5232,U+5234-5235,U+523c,U+523e,U+5245-5246,U+5248-5249,U+524f,U+5252-5253,U+5255,U+5257-525a,U+525f-5260,U+5262-5264,U+5266,U+5268,U+526b-526d,U+5271,U+5273,U+5276-527c,U+527e,U+5280,U+5285-5286,U+528b,U+528e-528f,U+5292,U+5294-529a,U+529c,U+52a4-52a7,U+52ae-52b0,U+52b4-52bd,U+52c0,U+52c2,U+52c4-52c6,U+52c8,U+52ca,U+52cc-52cf,U+52d1,U+52d3-52d4,U+52d7,U+52da-52dc,U+52e0-52e1,U+52e3,U+52e5-52ef,U+52f2,U+52f4,U+52f6-52f7,U+52fc-52fd,U+5301-5304,U+5307,U+5309-530c,U+530e,U+5311-5314,U+5318,U+531b-531c,U+531e-531f,U+5322,U+5324-5325,U+5327-5329,U+532b-532c,U+5330,U+5332-5338,U+533c-533d,U+5342,U+5344,U+5346,U+534b-534d,U+5350,U+5358-5359,U+535b,U+535d,U+5365,U+5368,U+536a,U+536c-536d,U+5372,U+5376,U+537c,U+537e,U+5380-5381,U+5383,U+5387-5388,U+538a,U+538e-5394,U+5396-5397,U+539b-539c,U+539e,U+53a0-53a1,U+53a4,U+53a7,U+53aa-53ac,U+53af-53b1,U+53b3,U+53b5,U+53b7-53ba,U+53bc-53be,U+53c0,U+53c5-53c7,U+53ce-53d0,U+53d2-53d3,U+53d5,U+53da,U+53dc-53de,U+53e1,U+53e7,U+53f4,U+53fa,U+53fe-5400,U+5402,U+5405,U+5407,U+540b,U+5414,U+5418-541a,U+541c,U+5422,U+5424-5425,U+542a,U+5430,U+5437,U+543a,U+543d,U+543f,U+5441,U+5444-5445,U+5447,U+5449,U+544c-544f,U+545a,U+545d-5461,U+5463,U+5465,U+5467,U+5469-546f;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_306bba.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+5470,U+5474,U+5479-547a,U+547e-547f,U+5481,U+5483,U+5485,U+5487-548a,U+548d,U+5491,U+5493,U+5497-5498,U+549c,U+549e-54a2,U+54a5,U+54ae,U+54b0,U+54b2,U+54b5-54b7,U+54b9-54ba,U+54be,U+54c3,U+54c5,U+54ca-54cb,U+54d6,U+54d8,U+54db,U+54e0,U+54e3-54e4,U+54eb-54ec,U+54ef-54f1,U+54f4-54f9,U+54fb,U+54fe,U+5500,U+5502-5503,U+5505,U+5508,U+550a-550e,U+5512-5513,U+5516-5519,U+551c-551f,U+5521,U+5525-5526,U+5528-5529,U+552b,U+552d,U+5532,U+5534-5536,U+5539-553b,U+553d,U+5540,U+5542,U+5545,U+5547-5548,U+554b-554e,U+5551-5552,U+5554,U+5557-555b,U+555d,U+555f-5560,U+5562-5563,U+5568-5569,U+556b,U+556f-5574,U+5579-557a,U+557d,U+557f,U+5585,U+558c-558e,U+5590,U+5592-5593,U+5595-5597,U+559b,U+55a0-55a6,U+55a8-55a9,U+55ad,U+55af-55b0,U+55b4,U+55b6,U+55b8,U+55ba,U+55bc,U+55bf-55c3,U+55c8,U+55ca-55cb,U+55cf-55d0,U+55d5,U+55d7-55d9,U+55db,U+55de,U+55e0,U+55e2,U+55e7,U+55ed-55ee,U+55f0-55f1,U+55f4,U+55f8-55fc,U+55ff,U+5602-5605,U+5607,U+560a-560b,U+5610-5613,U+5615,U+5619-561a,U+561d,U+5620-5622,U+5625-5626,U+5628-562b,U+5633,U+5637,U+563a,U+563c-563e,U+5640-5644,U+5646-564b,U+564f-5652,U+5655-5656,U+565a-565b,U+565e-565f,U+5661,U+5663,U+5667,U+566d-566e,U+5670,U+5673,U+5675,U+5677,U+5679-567a,U+567d-567f,U+5681-5684,U+5688-568b,U+568d,U+5690-5692,U+5696-56a2,U+56a4,U+56a7,U+56a9-56ae,U+56b0-56b2,U+56b5,U+56b8-56bb,U+56bd-56bf,U+56c3-56c4,U+56c6-56c7,U+56c9,U+56cb-56d0,U+56d2-56d3,U+56d5-56d6,U+56d8-56d9,U+56dc,U+56e3,U+56e5-56e9,U+56ec,U+56ee-56ef,U+56f2-56f3,U+56f6-56f8,U+56fb-56fc,U+5700-5702,U+5705,U+570c,U+570e,U+5710-5711,U+5714-5715,U+5717,U+5719-571b,U+571d-571e,U+5720-5722,U+5724-5727,U+572b,U+5731-5732,U+5734-5738,U+573c-573d,U+573f,U+5741,U+5743-5746,U+5748-5749,U+574b,U+5752-5756,U+5758-5759,U+5762-5763,U+5765,U+5767,U+576c,U+576e,U+5771-5772,U+5774-5775,U+5778-577a,U+577d-5781,U+5787-578a,U+578d-5791,U+5794-579a,U+579d-579f,U+57a5,U+57a8,U+57aa,U+57ac,U+57af-57b1,U+57b3,U+57b6-57b7;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_6d65eb.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4d67-4db5,U+4e02,U+4e04-4e06,U+4e0f,U+4e12,U+4e17,U+4e20-4e21,U+4e23,U+4e29,U+4e2e-4e2f,U+4e31,U+4e33,U+4e35,U+4e37,U+4e3c,U+4e40-4e42,U+4e44,U+4e46,U+4e4a,U+4e51,U+4e55,U+4e57,U+4e5a-4e5b,U+4e62-4e65,U+4e67-4e68,U+4e6a-4e6f,U+4e72,U+4e74-4e7d,U+4e7f-4e81,U+4e83-4e85,U+4e87,U+4e8a,U+4e90,U+4e96-4e97,U+4e9c-4e9d,U+4ea3,U+4eaa,U+4eaf-4eb1,U+4eb4,U+4eb6-4eb9,U+4ebc-4ebe,U+4ec8,U+4ecc,U+4ecf-4ed0,U+4ed2,U+4eda-4edc,U+4ee0,U+4ee2,U+4ee6-4ee7,U+4ee9,U+4eed-4eef,U+4ef1,U+4ef4,U+4ef8-4efa,U+4efc,U+4efe,U+4f00,U+4f02-4f08,U+4f0b-4f0c,U+4f12-4f14,U+4f16,U+4f1c-4f1d,U+4f21,U+4f23,U+4f28-4f29,U+4f2c-4f2e,U+4f31,U+4f33,U+4f35,U+4f37,U+4f39,U+4f3b,U+4f3e-4f42,U+4f44-4f45,U+4f49-4f4c,U+4f52,U+4f56,U+4f61-4f62,U+4f66,U+4f68,U+4f6a-4f6b,U+4f6d-4f6e,U+4f71-4f72,U+4f75,U+4f77-4f7a,U+4f7d,U+4f80-4f82,U+4f85,U+4f87,U+4f8a,U+4f8c,U+4f8e,U+4f90,U+4f92-4f93,U+4f95,U+4f98-4f9a,U+4f9c,U+4f9e-4f9f,U+4fa1-4fa2,U+4fa4,U+4fab,U+4fad,U+4fb0-4fb4,U+4fb8-4fbe,U+4fc0,U+4fc2,U+4fc6-4fc9,U+4fcb-4fcd,U+4fd2-4fd6,U+4fd9,U+4fdb,U+4fe2,U+4fe4-4fe5,U+4fe7,U+4feb,U+4ff0,U+4ff2,U+4ff4-4ff7,U+4ff9,U+4ffb-4ffd,U+4fff,U+5001-5005,U+5007-5008,U+500a,U+500e,U+5010,U+5013,U+5015,U+5017,U+501b,U+501d-501e,U+5020,U+5022-5024,U+5027,U+502f-5039,U+503b,U+503d,U+503f-5042,U+5044-5046,U+504a-504b,U+504d,U+5050-5054,U+5056-5059,U+505b,U+505d-5064,U+5066-506b,U+506d-5073,U+5079-507a,U+507c-507d,U+5081-5084,U+5086-5087,U+5089-508c,U+508e-5090,U+5092-5095,U+5097,U+509a-50a1,U+50a4,U+50a6,U+50aa-50ab,U+50ae,U+50b0-50b1,U+50b6,U+50b8-50b9,U+50bc-50bd,U+50bf-50c1,U+50c3,U+30d7c,U+30de5,U+310a8,U+310b1,U+3114b;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_0dbc27.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4c74-4d66;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_135790.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4b68-4c73,U+2f80f;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_813dd1.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4a4f-4b67,U+2e774,U+2e779,U+2e8f4,U+2e932,U+2e9f4-2e9f5,U+2ea5d;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_ed0d20.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4923-4a4e,U+2dc4a,U+2e0c4,U+2e267-2e268,U+2e26a-2e26b,U+2e428,U+2e64b;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_3149e0.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+47d8-4922,U+2ce7c,U+2ce88,U+2ce93,U+2d075,U+2d079,U+2d6a6,U+2d8c7;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_71407d.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+46e9-47d7,U+2c542,U+2c613,U+2c618,U+2c621,U+2c629,U+2c62b-2c62d,U+2c62f,U+2c642,U+2c64a-2c64b,U+2c72c,U+2c72f,U+2c79f,U+2c7c1,U+2c7fd,U+2c88a,U+2c8d9,U+2c8de,U+2c8e1,U+2c8f3,U+2c907,U+2c90a,U+2c91d,U+2c9c0,U+2ca02,U+2ca0e,U+2ca7d,U+2caa9,U+2cb29,U+2cb2b,U+2cb2d-2cb2e,U+2cb31,U+2cb38-2cb39,U+2cb3b,U+2cb3f,U+2cb41,U+2cb4a,U+2cb4e,U+2cb5a-2cb5b,U+2cb63-2cb64,U+2cb69,U+2cb6c,U+2cb6f,U+2cb73,U+2cb76,U+2cb78,U+2cb7c,U+2cba4,U+2cbb1,U+2cbbf-2cbc0,U+2cbce,U+2cc56,U+2cc5f,U+2ccf5-2ccf6,U+2ccfd,U+2ccff,U+2cd02-2cd03,U+2cd0a,U+2cd8b,U+2cd8d,U+2cd8f-2cd90,U+2cd9f-2cda0,U+2cda8,U+2cdad-2cdae,U+2cdd5,U+2ce05,U+2ce18,U+2ce1a,U+2ce23,U+2ce26,U+2ce2a;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_0e127a.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+45c4-46e8,U+2b9ef,U+2ba74,U+2bac7,U+2bb5f,U+2bb62,U+2bb7c,U+2bb83,U+2bc1b,U+2bc30,U+2bd58,U+2bd77,U+2bd87,U+2bdf7,U+2be29,U+2c029-2c02a,U+2c0a9,U+2c0ca,U+2c1ae,U+2c1d5,U+2c1d9,U+2c1f9,U+2c27c,U+2c288,U+2c2a4,U+2c317,U+2c35b,U+2c361,U+2c364,U+2c3df,U+2c488,U+2c494,U+2c497;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_b028be.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+44a2-45c3,U+2aed0,U+2afa2,U+2b124-2b125,U+2b127-2b128,U+2b137-2b138,U+2b1ed,U+2b300,U+2b328,U+2b363,U+2b36f,U+2b372,U+2b37d,U+2b404,U+2b410,U+2b413,U+2b461,U+2b4e7,U+2b4ef,U+2b4f6,U+2b4f9,U+2b50d-2b50e,U+2b536,U+2b5ae-2b5af,U+2b5b3,U+2b5e7,U+2b5f4,U+2b61c-2b61d,U+2b624,U+2b626-2b628,U+2b62a,U+2b62c,U+2b695-2b696,U+2b6ad,U+2b6de,U+2b6e9,U+2b6ed,U+2b6f6,U+2b728,U+2b7a9,U+2b7c5,U+2b7e6,U+2b7f7,U+2b7f9,U+2b7fc,U+2b806,U+2b80a,U+2b81c,U+2b892,U+2b8b8;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_35c17d.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+434d-44a1,U+2a3ad,U+2a7dd,U+2a8fb,U+2a917,U+2aa07,U+2aa17,U+2aa30,U+2aa36,U+2aa58;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_abac36.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4236-434c,U+29f7e,U+29f83,U+29f8c;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_d829f0.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+40e9-4235,U+289c0,U+28a0f,U+28b46,U+28b49,U+28b4e,U+28c47,U+28c4d,U+28c4f,U+28c51,U+28c54,U+28e99,U+28f7b,U+294d0,U+295d7;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_0f358f.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+3f8b-40e8,U+27c35,U+27ff9,U+280bf,U+2812e,U+28263,U+28408,U+284c8,U+28678,U+28695,U+287e0;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_782503.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4e69,U+4f67,U+4f7e,U+4fdc,U+50e6,U+50ec,U+5196,U+51ab,U+51b1,U+5202,U+5233,U+5261,U+52ac,U+531a,U+5363,U+536e,U+5416,U+5432,U+5454,U+5472,U+5476,U+5478,U+54ad,U+54bb,U+54c2,U+54cf,U+54da,U+54de,U+5537,U+553f,U+5575,U+5581,U+55cc,U+55fe,U+5601,U+560c,U+561e,U+564d,U+566b,U+56dd,U+5709,U+572c,U+572e,U+576b,U+57cf,U+57d2,U+57f8,U+580d,U+581e,U+5844,U+5881,U+589a,U+5924,U+5981,U+5997,U+59a3,U+59b2,U+5ab8,U+5ae0,U+5b34,U+5b5b,U+5ba5,U+5c25,U+5c59,U+5c63,U+5d1e,U+5d24,U+5d6b,U+5e11,U+5e14,U+5e8b,U+5eb3,U+5ed1-5ed2,U+5ef4,U+5efe,U+5f40,U+5f58,U+5f73,U+5f8c,U+5fad,U+600d,U+6019,U+601b,U+602b,U+605a,U+6083,U+6092,U+6100,U+610e,U+623d,U+624c,U+62ca,U+634c,U+6369,U+638e,U+641b,U+6426,U+646d,U+6535,U+65c4,U+65d2,U+6661,U+66db,U+670a,U+6715,U+6753,U+6798,U+67c3,U+67f0,U+681d,U+6855,U+686b,U+68f9,U+6939,U+696e,U+6980,U+698d,U+69b1,U+69ca,U+69ed,U+69f2,U+69ff,U+6a18,U+6aab,U+6b39,U+6b82,U+6b84,U+6b9b,U+6bb3,U+6bf3,U+6bf9,U+6c05-6c06,U+6c0d,U+6c19-6c1a,U+6cb2,U+6cd6,U+6d07,U+6d1a,U+6d27,U+6d2b,U+6d33,U+6d91,U+6dab,U+6e53-6e54,U+6e8f,U+6ebb,U+6f36,U+6fb6,U+7035,U+7085,U+70bb,U+7178,U+71b5,U+7228,U+7256,U+725d,U+726e,U+727e,U+72b0,U+72b4,U+72ce,U+72f7,U+72fa-72fb,U+7313,U+7322,U+738e,U+739f,U+73e5,U+7481,U+74ba,U+74e0,U+750f,U+7540,U+754b,U+755a-755b,U+758b,U+7594,U+75b3,U+760c,U+7615,U+7630,U+7635,U+7640,U+7654,U+76a4,U+76cd,U+775a,U+7765,U+7768,U+77bd,U+77ec,U+781f,U+78a1,U+78d4,U+78d9,U+7905,U+7913,U+79b3,U+79d5,U+79eb,U+7a02,U+7a70,U+7a86,U+7b25,U+7b2a-7b2b,U+7b2e,U+7b31,U+7b38,U+7b47,U+7b62,U+7b6e,U+7b72,U+7b7b,U+7b85,U+7b9c,U+7bcc,U+7bea,U+7bfc,U+7c0b,U+7c0f,U+7c26,U+7c9e,U+7ce8,U+7dae,U+7e47,U+7e9b,U+7f36,U+7f7e,U+7fb0,U+7fbc,U+7fee,U+8016,U+8020,U+802a,U+8031,U+8043,U+8071,U+809f,U+80ab,U+80cd,U+80d7,U+8112,U+8118,U+8160,U+8169,U+8188,U+81a6,U+81c1,U+8234,U+823e,U+825f,U+8298,U+829f,U+82a4,U+82ca,U+82e1,U+82e4,U+82f4,U+82fb,U+8308,U+831a,U+8333,U+83c0,U+83d4,U+83df,U+83f8,U+8401,U+840f,U+841c,U+8451,U+845a,U+846d,U+8476,U+8478,U+84ba,U+84bd,U+851f,U+8548,U+8556,U+855e,U+8585,U+85a4,U+8627,U+8629,U+867a,U+868b,U+868d,U+86b5,U+86c4,U+86de,U+86e9,U+86f8,U+872e,U+8731,U+873e,U+8765,U+877b,U+877d,U+8785,U+87ab,U+87ea,U+87ee,U+880a,U+8813,U+8816,U+8832,U+8864,U+8872,U+887f,U+88a2,U+88b7,U+88c9,U+88ce,U+88f0,U+890a,U+892b,U+897b,U+89da,U+89f3,U+8a48,U+8c55,U+8c73,U+8c78,U+8d91,U+8dba,U+8dcf,U+8deb,U+8e14,U+8e3a,U+8e41,U+8e6f,U+8e90,U+8fb6,U+8fd3,U+8fe8,U+9011,U+902d,U+90be,U+915e,U+9161-9162,U+9179,U+9185,U+918c,U+9190,U+91a3,U+92c8,U+953a,U+961d,U+9654,U+96ce,U+96e9,U+972a,U+9730,U+9785,U+9954,U+9aba,U+9ac0-9ac1,U+9ae1,U+9aeb,U+9aed,U+9af9,U+9b1f,U+9b32,U+9b4d,U+9b51,U+9e87,U+9ee2,U+9f17,U+9f19,U+9f22,U+9f2c,U+9f2f;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_0d29b4.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+3e19-3f8a,U+26d4f,U+277c4;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_5a8449.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+3cc5-3e18,U+260b3,U+260c2,U+26220-26221,U+26246,U+26291,U+26351,U+26369,U+2640c,U+2648d,U+26548,U+26676,U+2677c,U+26b5c,U+26c0f,U+26c21;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_545620.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4e47,U+4e5c,U+4e8d,U+4e93,U+4ebb,U+4ec4,U+4edf,U+4ef5,U+4f32,U+4f5e,U+4f76,U+4f7b,U+4f89,U+5028,U+502c,U+5048,U+5080,U+50ee,U+5121,U+51c7,U+5208,U+520e,U+52f0,U+530f,U+535f,U+53f5,U+5412,U+5443,U+5464,U+5466,U+54a3,U+54a6,U+54bf,U+54d0,U+5533,U+5541,U+5549,U+5550,U+5556,U+555c,U+5576-5577,U+558f,U+5591,U+55b1,U+55dd,U+55e5,U+55f2,U+55f5,U+560f,U+5627,U+562c,U+564c,U+5657-5658,U+5664,U+567b,U+5685,U+56d4,U+56e1,U+571c,U+5769,U+577c,U+578c,U+5793,U+57e4,U+5800,U+5807,U+5889,U+58c5,U+593c,U+59a4,U+59af,U+59d8,U+5a09,U+5a0c,U+5a4a,U+5a62,U+5a67,U+5ad8,U+5aeb,U+5b53,U+5b6c,U+5c3b,U+5c50,U+5c7a,U+5ca2-5ca3,U+5cac,U+5d26-5d27,U+5d6c,U+5d9d,U+5db7,U+5df3,U+5e19,U+5ea0,U+5eb9,U+5ebe,U+5edb,U+5f01,U+5f2d,U+5fb5,U+5fbc,U+5fdd,U+5fea,U+5ff8,U+6029,U+6041-6042,U+60b1,U+6175,U+6215,U+6221-6222,U+623e,U+6308,U+6339,U+638a,U+63e0,U+643d,U+6441,U+647a,U+6485,U+64e2,U+6555,U+659b,U+65ab,U+65c3,U+65c6,U+65f0,U+6600,U+6677,U+668c,U+669d,U+66be,U+66e9,U+66f7,U+6775,U+678b,U+67c1,U+67d2,U+67e2,U+6832,U+6840,U+6874,U+6877,U+68f0,U+68fc,U+690b,U+6910,U+6934,U+6966,U+6978,U+6998,U+69a7,U+69c1,U+69ce,U+6a17,U+6a3d-6a3e,U+6a50,U+6a5b,U+6a84,U+6a97,U+6b8d,U+6baa,U+6bd6,U+6bfd,U+6c10,U+6c16,U+6c18,U+6c3d,U+6c54,U+6cd4,U+6ceb,U+6cee,U+6d04,U+6d0c,U+6d60,U+6ddd,U+6de0,U+6e6b,U+6eb1,U+6ebd,U+6ecf,U+6fde,U+7023,U+70b7,U+70ca,U+7110,U+7168,U+71b3,U+71f9,U+7230,U+723b,U+7284,U+728d,U+7292,U+72d2,U+72d9,U+7301,U+731e,U+7352,U+7360,U+73a2,U+73b3,U+740a,U+7441,U+7457,U+74ff,U+7511,U+75a3,U+75a5,U+75d8,U+75e3-75e4,U+75e7,U+7610,U+7619,U+7622,U+7633,U+7688,U+76b4,U+76e5,U+771a,U+772d,U+779f,U+7812,U+7818,U+7826,U+7829,U+782c,U+784c,U+7850,U+7887,U+789a,U+78b2,U+78f4,U+7957,U+7a39,U+7aa8,U+7ae6,U+7b04,U+7b0f,U+7b1e,U+7b24,U+7bac,U+7bb8,U+7bc1,U+7bd9,U+7c1f,U+7ccc,U+7f45,U+7f5f,U+7f71,U+7fa7,U+7faf,U+7fbf,U+7fd5,U+7fe5-7fe6,U+7ff3,U+801c,U+8052,U+8084,U+8093,U+80b1,U+80bc,U+80db,U+80dd,U+80e4,U+80ef,U+80f4,U+8132,U+815a,U+8167,U+81ca,U+81cc,U+81ec,U+8202,U+8210,U+8228,U+8244,U+8249,U+824f,U+828a,U+828e,U+82a1,U+82a8-82a9,U+82ab,U+82b4,U+82e3,U+8306,U+833a,U+8351,U+8378,U+83a8,U+83aa,U+83d8,U+83e1,U+83ea,U+83f0,U+8497,U+84c1,U+84ca,U+84fc,U+8538,U+853b,U+857b,U+858f,U+859c,U+85ff,U+8605,U+8662,U+867b,U+86a8-86a9,U+86b1,U+86b4,U+86c9,U+86ed,U+870a,U+8722,U+8725,U+8734,U+874c,U+8763,U+876e,U+879f,U+87b3,U+87ca,U+87e5,U+887d-887e,U+8888,U+88df,U+88fe,U+8919,U+8921,U+89e5,U+8c47,U+8c7a,U+8d67,U+8db5,U+8dbf,U+8dd7,U+8dec,U+8dfd,U+8e05,U+8e1f,U+8e59,U+8e69,U+8e85,U+900b,U+9021,U+902f,U+9044,U+9062,U+908b,U+9099,U+909b,U+90b4,U+90c7,U+90eb,U+90fe,U+9123,U+9143,U+9146,U+9172,U+918d,U+91aa,U+91ae,U+91ba,U+93ca,U+96b9,U+96d2,U+9708,U+975b,U+977c,U+992e,U+9955,U+9ab0,U+9ab6-9ab7,U+9ac2,U+9afb,U+9b23,U+9b3b,U+9ecd,U+9ecf,U+9ee7;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_bc06aa.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4e4d,U+4e5e,U+4ea8,U+4ed5,U+4f51,U+4f7c,U+4f83,U+4fae,U+4fd1,U+4fd8,U+4fef,U+4ffa,U+5018,U+5026,U+508d,U+50a3,U+50da,U+50e7,U+50fb,U+515c,U+5162,U+5189,U+5195,U+51a4,U+51f3,U+51f9,U+5203,U+5228,U+5254,U+5256,U+52ff,U+5319,U+5320,U+533e-533f,U+5351,U+5375,U+53db,U+53ed-53ee,U+541f,U+5435,U+543b,U+5475,U+5484,U+548b,U+5490,U+54c9,U+54e9,U+54fa,U+5543,U+5587,U+5589,U+5598,U+55a7,U+55d3,U+560e,U+576f,U+5777,U+57ae,U+57e0,U+5830,U+589f,U+58a9,U+5937,U+5962,U+5974,U+5984,U+59ae,U+59e8,U+59ec,U+5a1f,U+5a23,U+5a36,U+5a3c,U+5a49,U+5a9a,U+5ab3,U+5ac2,U+5ad6,U+5b5a,U+5b99,U+5b9b,U+5bb5,U+5bc5,U+5bc7,U+5bde,U+5be1,U+5be5,U+5c09,U+5c2c,U+5c51,U+5cb1,U+5cd9,U+5cea,U+5d0e,U+5d16,U+5d29,U+5d4c,U+5dcd,U+5deb,U+5e87,U+5e9a,U+5ed3,U+5f13,U+5f57,U+5f64,U+5f87,U+5f8a,U+5f98,U+5fe1,U+5ff1,U+6016,U+6021,U+606a,U+606d,U+6096,U+60bc,U+60d5,U+60df,U+60f9,U+611a,U+614c,U+6177,U+6241,U+6252,U+6254,U+625b,U+626f,U+627c,U+6292,U+6296,U+62b9,U+62c2,U+62cc,U+62ef,U+62f1,U+634f,U+63a0,U+63e3,U+6467,U+6495,U+64bc,U+64c2,U+6518,U+6577,U+6590,U+65a7,U+6627,U+6643,U+6674,U+6687,U+66ae,U+66d9,U+66dd,U+66f0,U+6714,U+673d,U+6789,U+6795,U+67d1,U+67ff,U+6813,U+6829,U+6893,U+68a7,U+68ad,U+68b3,U+68cd,U+68d5,U+68d8,U+68e0,U+690e,U+6930,U+6960,U+6977,U+6986,U+69bb,U+69d0,U+69fd,U+6a1f,U+6b47,U+6b49,U+6b6a,U+6bd7,U+6c13,U+6c1f,U+6c2e-6c2f,U+6c72,U+6c7e,U+6c81,U+6c90,U+6cbc,U+6ccc,U+6cd3,U+6cd7,U+6ce3,U+6d5a,U+6dcc,U+6de4,U+6df3,U+6e1d,U+6e23,U+6e2d,U+6e9c,U+6ea5,U+6ec7,U+6ed4-6ed5,U+6f2f,U+6f33,U+6f62,U+6fa1,U+7011,U+701a,U+707c,U+708a,U+70af,U+70d8,U+70f9,U+711a,U+7130,U+714e,U+716e,U+7194,U+71a0,U+71ac,U+71e5,U+7280-7281,U+72d0,U+72fc,U+7316,U+731c,U+7334,U+7357,U+7384,U+73ab,U+73ca,U+7422,U+7426,U+742a,U+745b-745c,U+745f,U+7470,U+7480,U+74a7,U+7529,U+7578,U+75d5,U+7696,U+76ce,U+7736-7737,U+77a7,U+77bb,U+77d7,U+77e2-77e3,U+77ee,U+780c,U+785d,U+788c,U+789f,U+78c5,U+78ca,U+7901,U+7941,U+796d,U+797a,U+79be,U+79e4,U+79f8,U+7a1a,U+7a3c,U+7a57,U+7a74,U+7a98,U+7a9f,U+7aff,U+7b1b,U+7bf7,U+7c07,U+7c3f,U+7c7d,U+7c9f,U+7ca5,U+7cb9,U+7cd9,U+7cdf,U+7e82,U+7f38,U+7f69,U+7f9e,U+7fdf,U+7fe9,U+800d,U+8018,U+803d,U+803f,U+80a2,U+80aa,U+80d6,U+8116,U+813e,U+8151,U+8155,U+817a,U+819d,U+81fb,U+8205,U+820c,U+821c,U+82ad,U+82af,U+82b9,U+82bd,U+82d1,U+82db,U+82df,U+82ef,U+8304,U+832c,U+8335,U+8354,U+839e,U+8403,U+840c,U+840e,U+8446,U+846b,U+849c,U+84b2,U+84c9,U+8517,U+851a,U+853d,U+8549,U+8587,U+85af,U+85fb,U+8611,U+8650,U+865e,U+868a,U+868c,U+86d9,U+8700,U+8774,U+8776,U+8862,U+88d4,U+88d8-88d9,U+88f8-88f9,U+8912,U+895f,U+8a79,U+8b6c,U+8c41,U+8c79,U+8d81,U+8d9f,U+8dcb,U+8dea,U+8e22,U+8e29,U+8e35,U+8e44,U+8e72,U+8e81,U+8eac,U+8eb2,U+8fe2,U+8fed,U+9017,U+901b,U+902e,U+9038,U+9050,U+906e,U+9091,U+90aa,U+90af,U+90dd,U+914b,U+916f,U+9175-9176,U+918b,U+946b,U+962e,U+9640,U+964c,U+9661,U+9672,U+968b,U+9699,U+96c0,U+9716,U+9756,U+9761,U+97a0,U+97f6,U+99a8,U+9ad3,U+9ae6,U+9e93,U+9ed4;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_cd2170.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+3b81-3cc4,U+2538a,U+2546a,U+25532,U+25562,U+255a8,U+255fd,U+256f1,U+2591a,U+25997,U+25bbe,U+25c7c,U+25cbb,U+25dd1,U+25ed7;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_fdc333.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+3a1a-3b80,U+246d8,U+247a4,U+249db,U+24a0f,U+24a7d,U+24a8c,U+24ac9,U+24b5b,U+24c1c,U+24c48,U+24cc8,U+24d21,U+24d50,U+24ea5,U+25128,U+251e2;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_f9818b.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4e15,U+4e1e,U+4e2b,U+4eb3,U+4ec9,U+4f0e,U+4f3d,U+4f58,U+4f64,U+4f8f,U+4fd0,U+4fdf,U+501c,U+5043,U+5055,U+510b,U+5140,U+51bc-51bd,U+51cb,U+5243,U+527d,U+52be,U+531d,U+5323,U+535e,U+536f,U+53a5,U+53f1,U+541d,U+5429,U+542d-542e,U+5431,U+547b,U+5486,U+548e,U+5492,U+54aa-54ab,U+54af,U+54c6,U+54ce,U+54fd,U+5506,U+5509,U+5511,U+552c,U+553e,U+557b,U+55b3,U+55c5,U+55d2,U+55e1,U+55e6,U+55f7,U+55fd,U+5600,U+5608,U+561f,U+5636,U+5639,U+563b,U+563f,U+5659,U+5662,U+568e,U+5693,U+572a,U+573b,U+574d,U+5768,U+5773,U+577b,U+57a0,U+57b8,U+57c2,U+586c,U+587e,U+5892,U+58d5,U+58f9,U+598a,U+5992-5993,U+59e3,U+5a20,U+5a29,U+5a6a,U+5a7a,U+5ac9,U+5ae3,U+5b17,U+5b62,U+5b7d,U+5b95,U+5ba6,U+5bb8,U+5c15,U+5c4e,U+5c8c,U+5cab,U+5cb7,U+5d34,U+5d3d,U+5d4a,U+5d82,U+5e1a-5e1b,U+5e44,U+5e7a,U+5ea5,U+5eff,U+5f0b,U+5f29,U+5f77,U+5f89,U+5f99,U+5f9c,U+5fa8,U+5fd0-5fd1,U+5fd6,U+5fff,U+6026,U+6035,U+6043,U+6059,U+6063,U+60b4,U+60b8,U+60bb,U+60d8,U+60f0,U+6115,U+612b,U+618e,U+6194,U+61a9,U+61ca,U+61e6,U+61f5,U+620a,U+6248,U+62a8,U+62d7,U+62fd,U+630e,U+637a,U+6390,U+63a3,U+63ac,U+63b0,U+63e9,U+642a,U+64a9,U+64ae,U+6525,U+652b,U+6538,U+655d,U+65bc,U+65cc,U+65ce,U+65d6,U+65ee-65ef,U+6631,U+6636,U+664c,U+665f,U+6666,U+6684,U+66b9,U+66f3,U+6773,U+6777,U+677c,U+6787,U+67b8,U+67d8,U+67de,U+67e9,U+6845,U+684e,U+6886,U+688f,U+68b5,U+693d,U+6963,U+696b,U+6979,U+6994,U+69b7,U+6a35,U+6aac,U+6b59,U+6c26,U+6c50,U+6c69,U+6c74,U+6c76,U+6c8c,U+6cae,U+6ce0,U+6cef,U+6d5c,U+6d8e,U+6d95,U+6db8,U+6dc5,U+6dd6,U+6dde,U+6de6,U+6dfc,U+6e1a,U+6e25,U+6e6e,U+6e9f,U+6ea7,U+6eb4,U+6ef9,U+6f15,U+6f29-6f2a,U+6f78,U+6fa7,U+6fc2,U+6fe1,U+7131,U+714a,U+715c,U+7166,U+7172,U+71b9,U+7317,U+731d,U+7329,U+7337,U+733e,U+7396,U+73b7,U+73e9,U+7425,U+7428,U+742c,U+742e,U+7455,U+7490,U+74f4,U+7519,U+7525,U+753a,U+75b5,U+75b9,U+75d4,U+75de,U+75e2,U+75fc,U+7638,U+7656,U+765c,U+7678,U+768e,U+76bf,U+76c2,U+76d4,U+76f9,U+7728-7729,U+7762,U+776c,U+777d,U+77a0,U+77cd,U+77f8,U+77fd,U+7825,U+7837,U+787c,U+78b4,U+78d0,U+795a,U+795f,U+79ed,U+7a1e,U+7a37,U+7a79,U+7a92,U+7abf,U+7afa,U+7b06,U+7b20,U+7b4f,U+7b60,U+7b75,U+7b8d,U+7bb4,U+7bdd,U+7be1,U+7cb3,U+7cbc,U+7cdc,U+7cef,U+7f44,U+7f54,U+7f8c,U+7f94,U+7fb2,U+7fb9,U+7fce,U+7fe1,U+8004,U+800b,U+8019,U+808b,U+809b,U+80f1,U+8146,U+816e,U+81c0,U+81c3,U+81c6,U+81fc,U+81fe,U+822b,U+82a5,U+82aa,U+82de,U+8309,U+8317,U+832f,U+8340,U+8343,U+8392,U+83a0,U+8431,U+8469,U+84af,U+8543,U+854a,U+8559,U+8568,U+85b0,U+85b9,U+8638,U+869c,U+86c0,U+86e4,U+8713,U+8718,U+87c0,U+87cb,U+87fe,U+8821-8822,U+8882,U+8892,U+88e8,U+88f1,U+8902,U+892a,U+8983,U+8c5a,U+8c82,U+8d73,U+8dbe,U+8e74,U+8e7c,U+8e87,U+8fc2,U+8fe4,U+8fe6,U+900d,U+9035,U+9041,U+9052,U+905b,U+9068,U+9074,U+9095,U+90a1,U+90c5,U+911e,U+9122,U+9149,U+9157,U+916a,U+916e,U+919b,U+93d6,U+9621,U+9631,U+9642,U+968d,U+96c9,U+9706,U+970f,U+97ed,U+99a5,U+9abc,U+9b03,U+9e9d,U+9ebe,U+9edb,U+9edd,U+9f10;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_09a9b3.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+3886-3a19,U+235cb,U+2365c,U+237c4,U+237f4,U+23a3c,U+23b72,U+23c7c,U+23c97-23c98,U+23e23,U+23e8c,U+241fe,U+242ee,U+24350,U+243f6;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_6927bf.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4edd-4ede,U+4ee1,U+4ee8,U+4eeb,U+4f09,U+4f22,U+4f43,U+4f57,U+4f5a,U+4f5d,U+4f6f,U+4fce,U+4fda,U+4ff3,U+4ff8,U+4ffe,U+500c,U+500f,U+5025,U+502d,U+504e,U+5088,U+50f3,U+5106,U+5241,U+525c,U+52ad,U+530d,U+5310,U+5345,U+539d,U+53df,U+53fb-53fc,U+5420-5421,U+544b,U+5477,U+5482,U+54a7,U+54cc,U+54e7,U+550f,U+5514,U+5530,U+5544,U+5555,U+557e,U+558b,U+5594,U+5599,U+559f,U+55b9,U+55c4,U+55d1,U+55d4,U+55d6,U+55df,U+55e4,U+55e8,U+55ea,U+55ef,U+5623,U+562d,U+564e,U+5654,U+5671,U+56d7,U+56eb,U+56f9,U+5704,U+576d,U+57a1,U+57d5,U+57ed,U+584d,U+5880,U+58ec,U+5914,U+5958,U+599e,U+59be,U+59dd,U+5a11,U+5a40,U+5a55,U+5abe,U+5ae1,U+5ae6,U+5b40,U+5b51,U+5b71,U+5b93,U+5bee,U+5c9c,U+5cc1,U+5ccb,U+5cd2,U+5d06,U+5d2e,U+5d47,U+5d4b,U+5d6f,U+5d99,U+5dfd,U+5e54,U+5e5b,U+5e61,U+5e96,U+5fd2,U+600f,U+6014,U+606b,U+608c,U+609a,U+60c6,U+60da,U+60f4,U+60fa,U+61ff,U+621b,U+621f,U+622e,U+62bb,U+62bf,U+62c8,U+634b,U+636d,U+6371,U+6387,U+63ae,U+63c4,U+63c6,U+63cd,U+63d6,U+63f6,U+6410,U+6414,U+6421,U+64c0,U+64d8,U+661d,U+6641,U+6657,U+66dc,U+6710,U+6748,U+6772,U+67b0,U+67b7,U+6841,U+6849,U+6954,U+695d,U+699b,U+69ab,U+6a28,U+6a47,U+6a58,U+6b46,U+6c21,U+6c24,U+6c35,U+6c4a,U+6c68,U+6c86,U+6c8f,U+6c94,U+6cc5,U+6d2e,U+6d35,U+6d39,U+6d63,U+6d6f,U+6d94,U+6dd9,U+6dec,U+6e4e,U+6e98,U+6ec2,U+6ed3,U+6f09,U+6f66,U+6f8d,U+6fb9,U+6fc9,U+6fe0,U+6fef,U+705e,U+7094,U+7119,U+712f,U+71e7,U+723f,U+726f,U+729f,U+7325,U+7350,U+73c8,U+73d9,U+73de,U+741a,U+7430,U+74e4,U+753e,U+7548,U+7566,U+7579,U+757f,U+7583,U+759d,U+75b8,U+75bd,U+75c2,U+75cd,U+75f1,U+75ff,U+7634,U+7699,U+76c5,U+76f1,U+7708,U+7719,U+777e,U+778c,U+7791,U+77b3,U+77dc,U+781d,U+7823,U+7827,U+782d,U+7830,U+783c,U+7889,U+7893,U+78a3,U+78ec,U+7949,U+795c,U+79e3,U+7a14,U+7a17,U+7a88,U+7a95,U+7aa0,U+7afd,U+7b08,U+7b33,U+7b90,U+7b95,U+7be6,U+7bfe,U+7c0c,U+7c2a,U+7c91,U+7cb2,U+7cc5,U+7da6,U+7f58,U+7f61,U+7fb8,U+7fca,U+8006,U+8012,U+8026,U+8037,U+807f,U+80bd,U+80ed,U+814b,U+8153,U+8171,U+8174,U+8198,U+8200,U+8214,U+821b,U+8222,U+826e,U+828d,U+82be,U+82c4,U+82d2,U+82d5,U+82dc,U+82eb,U+82f7,U+830c,U+832d,U+8331,U+8334,U+834f,U+83c5,U+83d6,U+83fd,U+840b,U+8418,U+8438,U+843c,U+8473,U+847a,U+84a1,U+84d6,U+84ff,U+852b,U+8564,U+85d0,U+8693,U+86a3-86a4,U+86a7,U+86aa,U+86af,U+86b6,U+86c6,U+86d0,U+86f9,U+8703,U+8707-8709,U+870d,U+8737,U+873b,U+8759,U+8760,U+8782,U+87af,U+87c6,U+87d1-87d2,U+87e0,U+8815,U+8839,U+8869,U+8913,U+8936,U+8a07,U+8a3e,U+8b07,U+8c49,U+8c4c,U+8c62,U+8c89,U+8d6d,U+8d84,U+8d94,U+8dc6,U+8dce,U+8ddb,U+8dfa,U+8e09,U+8e1d,U+8e2e,U+8e31,U+8e39,U+8e3d,U+8e42,U+8e47,U+8e49,U+8e4b,U+8e6d,U+8e76,U+9004-9005,U+9016,U+9036,U+9051,U+907d,U+9082,U+9088,U+9097,U+90b0,U+90d7,U+90dc,U+90e2,U+90ef,U+9104,U+914a,U+9150,U+9169,U+919a,U+91b4,U+938f,U+965f,U+9697,U+96b0,U+96bc,U+970e,U+9739,U+973e,U+9798,U+97a3,U+9997,U+9aef,U+9e82,U+9edc,U+9edf-9ee0,U+9f3e;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_f7ca8a.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4e0c,U+4e28,U+4e36,U+4e3f,U+4ea0,U+4ec2,U+4ef3,U+4f74,U+4f91,U+4f94,U+4fc5,U+502e,U+50ba,U+50d6,U+50ed,U+5107,U+5155,U+5182,U+51f5,U+5216,U+525e,U+5281-5282,U+5290,U+5293,U+52d6,U+52f9,U+5369,U+53b6,U+54a9,U+54ff,U+552a,U+553c,U+5588,U+55b5,U+55c9,U+55cd,U+5686,U+570a,U+572f,U+574c,U+5776,U+57a4,U+57f4,U+57fd,U+580b,U+5819,U+5820,U+5865,U+58bc,U+5902,U+5925,U+59d2,U+5ab5,U+5b16,U+5b32,U+5b65,U+5b80,U+5b84,U+5be4,U+5c1c,U+5c22,U+5c6e,U+5c88,U+5c8d,U+5cb5,U+5d3e,U+5ddb,U+5e80,U+5ee8,U+5f50,U+5f56,U+5f61,U+5f82,U+5fc4,U+5fc9,U+5fe4,U+5fed,U+600a,U+605d,U+6067,U+609b,U+609d,U+60dd,U+6106,U+614a,U+615d,U+619d,U+6224-6225,U+6243,U+62f6,U+6343,U+63ad,U+63be,U+63ce,U+63de,U+63f2,U+640b-640c,U+6420,U+6434,U+643f,U+6496,U+64d0,U+64d7,U+64e4,U+6509,U+652e,U+6534,U+6549,U+656b,U+6603,U+6634,U+674c,U+676a,U+67b5,U+67d9,U+67dd,U+6833,U+6844,U+6883,U+6957,U+6971,U+69d4,U+6a8e,U+6a91,U+6aa0,U+6b37,U+6b43,U+6bea,U+6c15,U+6c2a,U+6c46,U+6c5c,U+6cd0,U+6d0e,U+6d7c,U+6e2b,U+6eb2,U+6eb7,U+6f2d,U+6f72,U+6f89,U+6f8c,U+7039,U+706c,U+70b1,U+70c0,U+7113,U+71d4,U+71e0,U+721d,U+727f,U+728b,U+728f,U+72c1,U+72c3,U+72e8,U+72f3-72f4,U+730a,U+7331,U+7338-7339,U+734d,U+736c,U+736f,U+73e7,U+746d,U+74a9,U+74de,U+750d,U+7513,U+752f,U+754e,U+7592,U+75c3-75c4,U+75e6,U+760a,U+761b,U+7625,U+762d,U+763c,U+7643,U+764d,U+7704,U+7707,U+7722,U+7735,U+7743,U+7747,U+7780,U+778d,U+77a2,U+77b5,U+77e7,U+7809,U+7811,U+7839,U+7847,U+784e,U+786a,U+786d,U+78a5,U+78c9,U+78f2,U+791e,U+7924,U+793b,U+7946,U+7953,U+7967,U+798a,U+7a03,U+7a78,U+7a80,U+7aac,U+7ab3,U+7b0a,U+7b45,U+7b4c,U+7b58,U+7b9d,U+7ba2,U+7bda,U+7be5,U+7c40,U+7ca2,U+7cc8,U+7cd7,U+7cf8,U+7e3b,U+7f68,U+7f9d,U+8025,U+8028-8029,U+8035,U+8080,U+809c,U+80ad,U+80c2,U+80e9,U+80ec,U+80f2,U+810e,U+811e,U+812c,U+8182,U+8201,U+8204,U+8221,U+822d,U+822f,U+8233,U+824b,U+825a,U+8268,U+8274,U+827d,U+827f,U+8284,U+828f,U+8291,U+82b0,U+82e0,U+8307,U+831b,U+833c,U+8347,U+837d,U+839b,U+83a9,U+83b0,U+83dd,U+83e5,U+83f9,U+8406,U+8459,U+845c,U+8488,U+848e,U+84b4,U+84b9,U+84cd,U+84d0,U+84f0,U+850c,U+8579-857a,U+85a8,U+85b7,U+863c,U+864d,U+867c,U+86b0,U+86ba,U+86d1,U+86d8,U+871e,U+8723,U+8729,U+8753,U+8764,U+8770,U+8788,U+878b,U+8793,U+8797,U+87ac-87ad,U+87b5,U+87bd,U+87d3,U+87db,U+8803,U+881b,U+883c,U+8844,U+88bc,U+88d2,U+88fc,U+8930,U+895e,U+8966,U+89d6,U+89eb,U+8b26,U+8c85,U+8c8a,U+8c94,U+8c98,U+8dbc,U+8de3,U+8e23,U+8e40,U+8e94,U+8e9e,U+8ece,U+8fd5,U+9058,U+90b6,U+90db,U+914e-914f,U+9164,U+9174,U+9191,U+91a2,U+91ad,U+91af,U+91b5,U+928e,U+936a,U+943e,U+955f,U+9622,U+963c-963d,U+966c,U+9674,U+9688,U+96b3,U+9794,U+97ab,U+9998,U+9ab1,U+9ad1,U+9adf,U+9b08,U+9b0f,U+9b2f,U+9b43,U+9b48,U+9eb4,U+9ee5,U+9ef9,U+9efb-9efc,U+9f37,U+9f3d,U+9fa0;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_bf7217.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+3702-3885,U+2295e,U+2298f,U+22ad3,U+22aec,U+22b38,U+22ba5,U+22cdf,U+22d80,U+22dec,U+22f96,U+23519;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_5b1e0f.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4e01,U+4e39,U+4e43,U+4e4f,U+4e58-4e59,U+4e95,U+4ea1,U+4ea6,U+4eab,U+4eae,U+4ec1,U+4ef2,U+4f0a,U+4f0f-4f11,U+4f2f-4f30,U+4f34,U+4f38,U+4f3c,U+4f69,U+4f73,U+4faf,U+4fb5,U+4fd7,U+4ff1,U+5012,U+5021,U+504f,U+5085,U+5141,U+5144,U+5146,U+5192,U+51a0,U+51b0,U+51b6-51b7,U+51dd,U+51e1,U+5200,U+5211,U+5237,U+523a,U+5272,U+52a3,U+52c3,U+52c7,U+52d2,U+52d8,U+52e4,U+5353,U+539a,U+53e5,U+53eb,U+5401,U+541b,U+5427,U+542b,U+5448,U+54e5,U+54ea,U+5510,U+552f,U+5531,U+554a,U+559d,U+5609,U+5740,U+5750,U+5761,U+5766,U+57c3,U+5802,U+5806,U+5821,U+5851,U+5854,U+585e,U+586b,U+58a8,U+58c1,U+5915,U+5949,U+594f,U+5954,U+5960,U+5976,U+5999,U+59a5,U+59b9,U+59bb,U+59c6,U+59d0-59d1,U+59d3,U+5a5a,U+5a92,U+5b54,U+5b5f,U+5b64,U+5b85,U+5b87-5b88,U+5b8b,U+5b97,U+5b9c,U+5bbf,U+5bc4,U+5bd2,U+5be8,U+5c01,U+5c04,U+5c0a,U+5c16,U+5c24,U+5c3e,U+5c4b,U+5c65,U+5cfb,U+5d07,U+5de1,U+5de7,U+5e1d,U+5e7c,U+5ec9,U+5ef7,U+5f04,U+5f1f,U+5f31,U+5f6d,U+5f70,U+5f79,U+5f92,U+5faa,U+5fbd,U+5fd8-5fd9,U+5fe0,U+5ffd,U+6015,U+602a,U+6050,U+6062,U+6069,U+6070,U+6084,U+6089,U+60a3,U+60a8,U+60b2,U+60dc,U+6162,U+6167,U+6170,U+61c2,U+622a,U+6234,U+626d,U+6291,U+62b1,U+62bc-62bd,U+62c6,U+62d2-62d4,U+62d6,U+62f3,U+6311,U+6316,U+6350,U+6355,U+6377,U+6389,U+638c,U+6398,U+63cf,U+63e1,U+63ed,U+63f4,U+640f,U+642c-642d,U+6458,U+6469,U+6478,U+64a4,U+64cd,U+6500,U+653b,U+654f,U+6562,U+6566,U+656c,U+658c,U+65c1,U+65e6,U+65e8,U+65ec,U+65f1,U+65fa,U+660c,U+6628,U+6668,U+667a,U+6696,U+66b4,U+66f9,U+66fc,U+66ff,U+670b,U+6717,U+672b,U+675c,U+676d,U+6790,U+679a,U+679d,U+67b6,U+67ec,U+67f1,U+67f3-67f4,U+683d,U+6842,U+684c,U+6851,U+6885,U+68af-68b0,U+68ee,U+695a,U+699c,U+6b20,U+6b23,U+6b8a,U+6b96,U+6bc5,U+6bcd,U+6bd2,U+6beb,U+6c0f,U+6c1b,U+6c57,U+6c60,U+6c6a,U+6c89,U+6cc9,U+6cdb,U+6ce5,U+6cf0,U+6cf3,U+6d17,U+6d1b,U+6d1e,U+6d3d,U+6d59,U+6d66,U+6d69-6d6a,U+6d6e,U+6d82,U+6db2,U+6db5,U+6de1,U+6dee,U+6df7,U+6dfb,U+6e20-6e21,U+6e58,U+6eaa,U+6ecb,U+6ed1,U+6f0f,U+6f20,U+6f2b,U+6f58,U+704c,U+7070,U+708e,U+70ad,U+70b8,U+7126,U+714c,U+718a,U+719f,U+71c3,U+71d5,U+7206,U+7236,U+7238,U+7259,U+725b,U+7262,U+7267,U+72e0,U+731b,U+73bb,U+73cd,U+73e0,U+741b,U+7434,U+745e,U+7483,U+74dc,U+74e6,U+74f7,U+7518,U+7532,U+755c,U+756a,U+7586,U+7591,U+75ab,U+75be,U+75db,U+764c,U+7687,U+76c8,U+76df,U+76f2,U+76fe,U+7761,U+77db,U+786c,U+7891,U+78c1,U+78e8,U+795d,U+7965,U+7981,U+79d2,U+79df,U+79e6,U+79e9,U+7a00,U+7a3b,U+7a3f,U+7a46,U+7a7f,U+7a97,U+7adf,U+7aef,U+7af9,U+7b11,U+7b26,U+7bb1,U+7bc7,U+7c4d,U+7c89,U+7c97,U+7f8a,U+7fbd,U+7ffb,U+8000,U+8010,U+8015,U+8017,U+8033,U+8058,U+805a,U+8089,U+8096,U+809d,U+80a5,U+80a9,U+80af,U+80de,U+80e1,U+8150,U+817f,U+81a8,U+81e3,U+8212,U+827e,U+82ac,U+82b3,U+82d7,U+82e5,U+8302,U+8336,U+8352,U+8377,U+83ab,U+83f2,U+8461,U+8463,U+8499,U+84c4,U+852c,U+8584,U+864e,U+86cb,U+8861,U+8863,U+8870,U+8877,U+8881,U+888b,U+88c1-88c2,U+88d5,U+8986,U+8c46,U+8c6a,U+8c8c,U+8d64,U+8d6b,U+8d74,U+8da3,U+8dd1,U+8ddd,U+8ddf,U+8de8,U+8df3,U+8e0f,U+8f9b,U+8fd4,U+8fea-8feb,U+8ff7,U+9003,U+900f,U+9014,U+906d,U+9075,U+907f-9080,U+90a6,U+90ca,U+90ce,U+90ed,U+9192,U+91ce,U+963b,U+9644,U+966a,U+9675-9677,U+9686,U+9694,U+96c5,U+96d5,U+96e8,U+96ea,U+96f6-96f7,U+9707,U+971e,U+9732,U+978b,U+9910,U+9aa8,U+9ebb,U+9ece,U+9ed8;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_971f00.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+3569-3701,U+21681,U+216df,U+21a18,U+21a25,U+21b36,U+21deb,U+21ed5,U+2231a;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_df3eef.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+3001-3002,U+3008-3011,U+3014-3017,U+4e00,U+4e03,U+4e09-4e0b,U+4e0d,U+4e14,U+4e16,U+4e2d,U+4e3b,U+4e45,U+4e4b,U+4e4e,U+4e5d,U+4e5f,U+4e88,U+4e8b-4e8c,U+4e92,U+4e94,U+4e9b,U+4ea4,U+4eac,U+4eba,U+4ec0,U+4eca-4ecb,U+4ecd,U+4ed6,U+4ed8,U+4ee3-4ee5,U+4ef6,U+4efb,U+4efd,U+4f01,U+4f0d,U+4f46,U+4f4d-4f4f,U+4f55,U+4f5c,U+4f7f,U+4f8b,U+4f9b,U+4f9d,U+4fbf,U+4fc3-4fc4,U+4fdd,U+4fe1,U+500d,U+5019,U+503c,U+5047,U+505a,U+505c,U+5065,U+50cf,U+5143,U+5145,U+5148-5149,U+514d,U+5165,U+5168,U+516b-516d,U+5171,U+5175-5178,U+517c,U+518d,U+5206-5207,U+520a,U+5217,U+521d,U+5224,U+5229,U+5230,U+5238,U+523b,U+524d,U+526f,U+529b,U+529f-52a0,U+52a9-52aa,U+5305,U+5316-5317,U+5341,U+5347-5348,U+534a,U+5357,U+535a,U+5361,U+5370-5371,U+5382,U+539f,U+53bb,U+53c8,U+53ca-53cb,U+53cd,U+53d6-53d7,U+53e3-53e4,U+53e6,U+53ec,U+53ef-53f0,U+53f2-53f3,U+53f8,U+5404,U+5409,U+540d,U+5411,U+5426,U+5438,U+544a,U+5462,U+5468,U+5473,U+547c-547d,U+548c,U+54c1,U+54c8,U+552e,U+5546,U+5584,U+559c,U+5668,U+56db,U+56e0,U+56fa,U+571f,U+5728,U+5730,U+5733,U+5747,U+578b,U+57ce,U+57df,U+57f9-57fa,U+5883,U+589e,U+58eb,U+590f,U+5916,U+591a,U+591c,U+5927,U+5929-592a,U+592e,U+5931,U+5947,U+5957,U+5973,U+5979,U+597d,U+5982,U+59cb,U+59d4,U+5a01,U+5b50,U+5b57-5b58,U+5b63,U+5b69,U+5b89,U+5b8c,U+5b8f,U+5b98,U+5b9a,U+5ba2-5ba4,U+5bb3,U+5bb9,U+5bc6,U+5bcc,U+5bdf,U+5c0f,U+5c11,U+5c1a,U+5c31,U+5c3c,U+5c45,U+5c55,U+5c71,U+5cb8,U+5ddd-5dde,U+5de5-5de6,U+5dee,U+5df1-5df2,U+5df4,U+5e02,U+5e0c,U+5e38,U+5e45,U+5e55,U+5e73-5e74,U+5e8f,U+5e95,U+5e97,U+5e9c,U+5ea6-5ea7,U+5ead,U+5eb7,U+5ef6,U+5efa,U+5f0f,U+5f15,U+5f62,U+5f71,U+5f80,U+5f85,U+5f88,U+5f8b,U+5f90,U+5f97,U+5fae,U+5fb7,U+5fc3,U+5fc5,U+5feb,U+600e,U+601d,U+6025,U+6027,U+606f,U+60c5,U+60e0,U+60f3,U+610f,U+611f,U+6210-6211,U+6216,U+623f-6240,U+624b,U+6253,U+6276,U+6279,U+627e-6280,U+628a,U+6293,U+6295,U+6297,U+62c9,U+62cd,U+62db,U+62ec,U+62ff,U+6301,U+6307,U+6309,U+632f,U+6388,U+6392,U+63a2,U+63a5,U+63a7-63a8,U+63aa,U+63d0,U+641e,U+64ad,U+652f,U+6536,U+6539,U+653e-653f,U+6545,U+6548,U+6551,U+6563,U+6574,U+6587,U+6599,U+65a4,U+65af-65b0,U+65b9,U+65bd,U+65c5,U+65cf,U+65e5,U+65e9,U+660e,U+6613,U+661f-6620,U+6625,U+662f,U+665a,U+666e-666f,U+66f2,U+66f4,U+66fe,U+6700,U+6708-6709,U+670d,U+671b,U+671d,U+671f,U+6728,U+672a,U+672c,U+674e,U+6750-6751,U+675f,U+6797,U+67d0,U+67d3,U+6821,U+6839,U+683c,U+6848,U+68c9,U+690d,U+6a21,U+6b21,U+6b3e,U+6b4c,U+6b62-6b66,U+6b7b,U+6bb5,U+6bcf,U+6bd4,U+6bdb,U+6c11,U+6c34,U+6c38,U+6c42,U+6c5f,U+6c7d,U+6c99,U+6cb3,U+6cb9,U+6cbb,U+6cbf,U+6cd5,U+6ce2,U+6d0b,U+6d25,U+6d2a,U+6d32,U+6d3b,U+6d3e,U+6d41,U+6d77,U+6d88,U+6df1,U+6e05,U+6e2f,U+6e56,U+6e90,U+6f14,U+6f6e,U+6fb3,U+6fc0,U+706b,U+70c8,U+7136,U+7164,U+7167,U+7247-7248,U+724c,U+7269,U+7279,U+72af,U+7387,U+7389,U+738b,U+73ed,U+7403,U+7406,U+751a,U+751f,U+7528,U+7530-7531,U+7533,U+7537,U+754c,U+7559,U+7565,U+75c5,U+767b,U+767d-767e,U+7684,U+76ae,U+76ca,U+76db,U+76ee,U+76f4,U+76f8,U+770b,U+773c,U+7763,U+77e5,U+77ed,U+77f3,U+7814,U+7834,U+793a,U+793e,U+7956,U+795e,U+7968,U+798f,U+79c0,U+79d1,U+79fb,U+7a0b,U+7a76,U+7a7a,U+7a81,U+7acb,U+7ad9,U+7ae0,U+7ae5,U+7b2c,U+7b49,U+7b54,U+7b56,U+7b97,U+7ba1,U+7c73,U+7cbe,U+7cfb,U+7d20,U+7d22,U+7e41,U+7f3a,U+7f6a,U+7f6e,U+7f72,U+7f8e,U+8001,U+8003,U+8005,U+800c,U+80a1,U+80b2,U+80fd,U+81ea,U+81f3,U+821e,U+822a,U+822c,U+8239,U+826f,U+8272,U+82b1,U+82e6,U+82f1,U+8349,U+83dc,U+843d,U+8457,U+85cf,U+878d,U+8840,U+884c,U+8857,U+88ab,U+897f,U+8981,U+89d2,U+89e3,U+8a00,U+8b66,U+8c61,U+8d70,U+8d77,U+8d85,U+8d8a,U+8db3,U+8def,U+8eab,U+8fc5,U+8fce,U+8fd1,U+8ff0,U+8ffd,U+9000-9001,U+9010,U+901a,U+901f-9020,U+9047,U+904d,U+9053,U+90a3,U+90e8,U+90fd,U+914d,U+9152,U+91c7,U+91cd,U+91cf,U+91d1,U+9632,U+963f,U+964d,U+9650,U+9662,U+9664,U+969c,U+96c4,U+96c6,U+9700,U+9752,U+975e,U+9760,U+9762,U+9769,U+97f3,U+98df,U+9996,U+9999,U+9ad8,U+9f13,U+fe10-fe19,U+fe30-fe31,U+fe33-fe44;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_8867bc.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+27e8-27e9,U+2e81,U+2e84,U+2e88,U+2e8b-2e8c,U+2e97,U+2ea7,U+2eaa,U+2eae,U+2eb3,U+2eb6-2eb7,U+2ebb,U+2eca,U+2ff0-2ffb,U+3000,U+3003,U+3005-3007,U+3012-3013,U+3018-301b,U+301d-301e,U+3021-3029,U+303e,U+3105-3129,U+31c0-31e3,U+3220-3229,U+3231,U+32a3,U+338e-338f,U+339c-339e,U+33a1,U+33c4,U+33ce,U+33d1-33d2,U+33d5,U+3400-3568,U+20087,U+20089,U+200cc,U+20164,U+201ee,U+20242,U+20302,U+20542,U+20545,U+20587,U+20676,U+207ad,U+207b0,U+20a44,U+20ab1,U+20b6f,U+20bbe,U+20cd0,U+20d1f,U+20dff,U+20ef3,U+20f1f,U+2139a,U+21413,U+2144d,U+215d7;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_fdb231.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+20-7e,U+a0-17f,U+18f,U+192,U+1a0-1a1,U+1af-1b0,U+1cd-1dc,U+1e5,U+1e7,U+1e9,U+1ef-1f0,U+1f9-1ff,U+218-21b,U+21f,U+237,U+2c6-2c7,U+2c9,U+300,U+302,U+304,U+30c,U+ff04,U+ff0b,U+ff1c-ff1e,U+ff3e,U+ff40,U+ff5c,U+ff5e,U+ffe0-ffe5;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_d95e9e.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+50b4,U+50e8,U+5110,U+51aa,U+5244,U+528c,U+52f1,U+532d,U+53b4,U+54bc,U+551a,U+5645,U+5666,U+5672,U+568c,U+56a6,U+56b3,U+56c5,U+5852,U+58da,U+5969,U+5abc,U+5b24,U+5b4c,U+5c68,U+5d2c,U+5d81,U+5df0,U+5df9,U+5e6c,U+5f12,U+5f33,U+6128,U+6192,U+61ae,U+61cd,U+61df,U+6207,U+6214,U+6227,U+6451,U+6463,U+649f,U+6504,U+68d6,U+68ec,U+69aa,U+69bf,U+69e7,U+6a48,U+6ae7,U+6aeb,U+6af3,U+6e5e,U+6e63,U+6f59,U+6f77,U+6fa9,U+6ffc,U+7027,U+7197,U+736a-736b,U+7380,U+7602,U+761e,U+7649,U+7667,U+76b8,U+7798,U+78fd,U+79b0,U+7a6d,U+7ab6,U+7c5c,U+7c69-7c6a,U+7cdd,U+7cf4,U+7cf6,U+7cf9,U+7d06-7d07,U+7d32,U+7d86,U+7d88,U+7dc4,U+7dd9,U+7de6,U+7df6,U+7df9,U+7e0b,U+7e11,U+7e2d,U+7e32,U+7e36,U+7e52,U+7e62,U+7e6f,U+7e8a,U+7f86,U+7f8b,U+802e,U+8079,U+8156,U+8161,U+8181,U+81da,U+81e0,U+8264,U+826b,U+8452,U+851e,U+8526,U+8546,U+8552,U+8558,U+8562,U+8577,U+859f,U+85f6,U+861e,U+863a,U+86fa,U+87e3,U+87ef,U+8806,U+8810,U+8933,U+8946,U+8949,U+895d,U+89a1,U+89af,U+89bf,U+89f6,U+8a01,U+8a75,U+8a84,U+8ad1,U+8ad7,U+8b6b,U+8b7e,U+8b9e,U+8d10,U+8db2,U+8e60,U+8e92,U+8ee4,U+8ef9-8efa,U+8f05,U+8f07,U+8f0a,U+8f61-8f62,U+91c3,U+91d9,U+9201,U+9225,U+9233,U+923d,U+9255,U+929a,U+92a9,U+92b1,U+92cf,U+92dd,U+92df,U+92e3,U+92e6,U+92f6,U+9301,U+9306-9308,U+9312,U+931f,U+9343,U+9346,U+9364,U+9369,U+9370,U+9384,U+9387,U+93bf,U+93dd,U+93f7,U+93f9,U+9412,U+9414,U+9420,U+9426,U+9428,U+944a,U+9465,U+9479,U+958c,U+95ab,U+95bd,U+95e5,U+9689,U+96c8,U+979d,U+97bd,U+97dd,U+980a,U+980f,U+9856,U+9859,U+9862,U+98eb,U+98f1,U+9931,U+9933,U+9937,U+993c,U+994a,U+995c,U+99d4,U+99d8,U+9a0d,U+9a36,U+9a42,U+9a4f,U+9b83,U+9b92,U+9b9a,U+9b9e,U+9baa,U+9bc1,U+9bdd,U+9bea,U+9c08,U+9c12,U+9c28-9c29,U+9c35,U+9c39,U+9c3e,U+9c5d,U+9c5f,U+9c67,U+9c6d,U+9c7a,U+9d1f,U+9d2f-9d30,U+9d42,U+9d53,U+9d5c,U+9d6f,U+9d87,U+9d93,U+9d98,U+9da5,U+9dbc,U+9dd9-9dda,U+9de5-9de6,U+9def,U+9e0c,U+9e7a,U+9ecc,U+9ef2,U+9ef7,U+9f09,U+9f34,U+9f4e-9f4f,U+9f54,U+9f59;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_8a2aad.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4e08,U+4e18-4e19,U+4e32,U+4e38,U+4e52-4e53,U+4e73,U+4e7e,U+4e9f,U+4ead,U+4ed4,U+4ed7,U+4ed9,U+4ef0,U+4f50,U+4fcf,U+502a,U+5076,U+50ac,U+50b2,U+5112,U+5154,U+5180,U+51f0,U+51fd,U+524a,U+526a,U+52ab,U+52c9,U+52df,U+52fe,U+5306,U+5308,U+532a,U+5339,U+5349,U+5378,U+537f,U+5384,U+53c9,U+53d4,U+5410,U+5439,U+543e,U+5440,U+5496,U+54ac,U+54b1,U+54c0,U+54e8,U+54ed,U+5561,U+5564-5566,U+5580,U+558a,U+55bb,U+561b,U+5634,U+56ca,U+572d,U+573e,U+574a,U+574e,U+5751,U+5764,U+576a,U+5782-5783,U+57cb,U+57d4,U+5824,U+582a,U+5835,U+584c,U+5858,U+5885,U+5893,U+58e4,U+5948,U+594e,U+5951,U+59a8,U+59da,U+59fb,U+59ff,U+5a03,U+5a1c,U+5a46,U+5ac1,U+5acc,U+5ae9,U+5b55,U+5b5c-5b5d,U+5bb0,U+5bb4,U+5bc2,U+5bd3,U+5bf8,U+5bfa,U+5c39-5c3a,U+5c3f,U+5c48,U+5c60,U+5c6f,U+5c90,U+5d14,U+5d1b,U+5df7,U+5dfe,U+5e06,U+5e15,U+5e37,U+5e3d,U+5e62,U+5e7b,U+5e7d,U+5eb8,U+5eca,U+5ed6,U+5f0a,U+5f17-5f18,U+5f6a,U+5f6c,U+5f7c,U+5fcc-5fcd,U+6012,U+6028,U+6068,U+6094,U+609f-60a0,U+60d1,U+6101,U+6109,U+6127,U+6148,U+614e,U+6155,U+6168,U+61be,U+61c8,U+6208,U+6212,U+6247,U+626e,U+6284,U+62ab,U+62d0,U+62d8,U+62da,U+62fc,U+62fe,U+632a-632b,U+633a,U+6349,U+6367,U+6380,U+638f,U+63a9,U+63d2,U+6454,U+6492,U+649e,U+64b0,U+64c5,U+64e6,U+655e,U+6572,U+6591,U+659c,U+65a5,U+65ed,U+6602,U+660f,U+6614,U+662d,U+6664,U+6670,U+6676,U+6691,U+66a8,U+6749,U+674f,U+67af,U+67cf,U+67d4,U+67ef,U+682a,U+6843,U+6846,U+6850,U+6854,U+6876,U+68a8,U+68d2,U+68da,U+68f5,U+6905,U+6912,U+6a0a,U+6a61,U+6b3a,U+6b67,U+6b79,U+6bb7,U+6bbf,U+6bef,U+6c27-6c28,U+6c41,U+6c55,U+6c5b,U+6c5d,U+6c70,U+6c82-6c83,U+6c9b,U+6cab,U+6cb8,U+6cca,U+6ce1,U+6cf5,U+6d74,U+6d78,U+6daf,U+6dc4,U+6dcb,U+6dd1,U+6dd8,U+6deb,U+6df9,U+6e24,U+6e34,U+6e5b,U+6ea2,U+6eb6,U+6ef4,U+6f02,U+6f06,U+6f6d,U+6f84,U+7092,U+70ac,U+70b3,U+70e4,U+70ef,U+710a,U+7199,U+722c,U+723d,U+725f,U+7261,U+7272,U+72ac,U+72c2,U+72d7,U+73b2,U+7433,U+751c,U+752b,U+7538,U+754f,U+7554,U+758f,U+75b2,U+75bc,U+7624,U+7626,U+7686,U+76c6,U+76d2,U+76ef,U+76fc,U+7709,U+7720,U+775b,U+7766,U+7779,U+7784,U+77ac,U+7802,U+780d,U+7838,U+786b,U+788e,U+7897-7898,U+78a7,U+78b0,U+78b3,U+78cb,U+78f7,U+79b9,U+79bd,U+79c9,U+79e7,U+7a0d,U+7a3d,U+7a84,U+7ae3,U+7aed,U+7b52,U+7bad,U+7c92,U+7c98,U+7cca,U+7d2b,U+7f50,U+7f55,U+7fc1,U+7fc5,U+7fd4,U+7fe0,U+7ff0,U+7ffc,U+8036,U+804a,U+8086-8087,U+808c,U+809a,U+80ba,U+80c3,U+80ce,U+80f8,U+8102,U+8106,U+810a,U+8154,U+8170,U+8179,U+8180,U+818f,U+819c,U+81c2,U+81ed,U+821f,U+8236,U+8247,U+8258,U+8292,U+829d,U+8305,U+8328,U+832b,U+8389,U+838e,U+83c7,U+83ca,U+83cc,U+83f1,U+8404,U+840d,U+845b,U+846c,U+8482,U+84b8,U+84ec,U+8513,U+8521,U+857e,U+859b,U+85aa,U+85e4,U+8679,U+86c7,U+8702,U+871c,U+87ba,U+87f9,U+884d,U+886b,U+8896,U+88b1,U+8944,U+8a93,U+8c6b,U+8dcc,U+8e48,U+8eba,U+8f9c,U+8fa3,U+8fa8,U+8fb0-8fb1,U+8fc4,U+9006,U+901d,U+9022,U+903c,U+9042,U+904f,U+9063,U+90a2,U+90b1,U+90b5,U+9102,U+916c,U+9177,U+9187,U+9189,U+961c,U+964b,U+96a7,U+96c1,U+970d,U+971c,U+9738,U+978d,U+97ad,U+9b3c,U+9b41-9b42,U+9b44-9b45,U+9b4f,U+9b54,U+9e7f,U+9e9f,U+9f0e,U+9f3b;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_82438c.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4fc1,U+5000,U+5096,U+50c9,U+5114-5115,U+526e,U+5274,U+5399,U+53c4,U+55f6,U+5635,U+5638,U+565d,U+5665,U+566f,U+56c0,U+56c8,U+57b5,U+5816,U+581d,U+584f,U+58d9,U+58e2,U+5b00,U+5b19,U+5b21,U+5b2a,U+5d0d,U+5d33,U+5e43,U+5ee1,U+5ee9,U+614d,U+616a,U+6173,U+61e8,U+6261,U+6331,U+645c,U+6476,U+64fc,U+651b,U+6894,U+6898,U+6a05,U+6a34,U+6a81,U+6a89,U+6abe,U+6add-6ade,U+6ae8,U+6aec,U+6af8,U+6b0f,U+6b5f,U+6b7f,U+6ba4,U+6c0c,U+6de5,U+6f35,U+6f7f,U+6fae,U+7026,U+7032,U+7162,U+716c,U+733b,U+7341,U+743a,U+74a6,U+74d4,U+74da,U+7646,U+7658,U+7669,U+766d,U+7670,U+7725,U+7864,U+7868,U+78b8,U+78e7,U+7906,U+7a61,U+7b67,U+7bcb,U+7bf3,U+7c00,U+7c1e,U+7cf2,U+7d08,U+7d3c,U+7d3f,U+7d4e,U+7d5b,U+7d5d,U+7d83,U+7d9e,U+7da3,U+7db0,U+7db9,U+7dc7,U+7de1,U+7df1,U+7e0a,U+7e1e-7e1f,U+7e35,U+7e7e,U+7e98,U+7fb6,U+802c,U+8141,U+8278,U+8407,U+8435,U+8494,U+84e7,U+84ef,U+84f4,U+84fd,U+8541,U+8555,U+85ce,U+85ea,U+87bb,U+87f6,U+889e,U+8a0c,U+8a10,U+8a4e,U+8a58,U+8a6c,U+8a7f,U+8a86,U+8a91,U+8a9a,U+8ab6,U+8ac2,U+8acf,U+8adb,U+8ade,U+8ae1-8ae2,U+8ae4,U+8afc,U+8b04-8b05,U+8b59,U+8b92,U+8b95-8b96,U+8b9c,U+8c9b,U+8cb2-8cb3,U+8cba,U+8cfb,U+8cfe,U+8d04,U+8e55,U+8e91,U+8e95,U+8edb,U+8eeb,U+8f1c,U+8f1e,U+8f26,U+8f33,U+8f54,U+9015,U+9090,U+90df,U+90e4,U+9134,U+9136,U+91c5,U+91d3-91d5,U+91e4,U+91e7,U+91f7,U+9204,U+9227,U+9230,U+9239,U+923f,U+9248,U+9266,U+926d,U+927a,U+927f,U+9283,U+92a0,U+92a3,U+92a6,U+92aa-92ab,U+92e8,U+92ee,U+92f1,U+9319,U+931b,U+9338,U+9340,U+93a6,U+93a9,U+93c3,U+93cc-93cd,U+93e8,U+9403,U+9413,U+9419,U+943f,U+944c,U+9454,U+946d,U+9477,U+9582,U+958e,U+95b6,U+95b9,U+95bc,U+95be,U+95c3,U+95c8,U+95cb,U+95d0,U+9768,U+97c9,U+97d9,U+9807,U+980e,U+9826,U+984e,U+9853,U+9873-9874,U+98ae,U+98bc,U+98e0,U+98ed,U+9908,U+9949,U+99d1,U+99d9,U+99e2,U+9a05,U+9a43-9a44,U+9ad5,U+9b29,U+9b2e,U+9b4e,U+9b58,U+9b74,U+9b8e,U+9b90,U+9bab,U+9bc0,U+9bc7,U+9bd6-9bd7,U+9be1-9be2,U+9bff,U+9c09,U+9c13,U+9c23,U+9c33,U+9c45,U+9ce7,U+9d07,U+9d52,U+9dbb,U+9dc0,U+9dc2,U+9df3,U+9df8,U+9e15,U+9e1d,U+9efd,U+9eff,U+9f5c,U+9f66,U+9f6a,U+9f76-9f77;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_2c89c1.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+37e,U+384-38a,U+38c,U+38e-3a1,U+3a3-3ce,U+3d1-3d2,U+3d6,U+400-45f,U+462-463,U+46b,U+472-475,U+490-493,U+497,U+49a-49d,U+4a2-4a3,U+4ae-4b3,U+4b8-4bb,U+4ca,U+4d8-4d9,U+4e8-4e9,U+1f45,U+54f3,U+5515,U+5adc,U+5e58,U+5fee,U+6499,U+6516,U+6bff,U+6d5e,U+7020,U+7044,U+75b0,U+7603,U+799a,U+7e70,U+8014,U+80d9,U+8159,U+8183,U+81a3,U+81aa,U+8464,U+8811,U+89dc,U+8a92,U+8b56,U+8cb0,U+8cd5,U+8ce7,U+8e93,U+8ea6,U+8fee,U+9139,U+93aa,U+940b,U+95bf,U+986c,U+9a2d,U+9a38,U+9beb,U+9bf4,U+9c20,U+9c37,U+9d1d,U+9e88,U+9f47,U+9f60;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_bee5fd.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+50af,U+50c2,U+5108,U+5137,U+513b,U+528a,U+5504,U+559e,U+560d,U+561c,U+5660,U+5680,U+5694-5695,U+56a8,U+56b6,U+56c1,U+5707,U+5770,U+57e1,U+580a,U+584b,U+58ce,U+58e0,U+5950,U+597c,U+5aa7,U+5ad7,U+5b08,U+5b0b,U+5b76,U+5b7f,U+5cf4,U+5da0,U+5da7,U+5db4,U+5dcb,U+5ec4,U+60f2,U+60fb,U+6134,U+613e,U+6142,U+615f,U+616b,U+61cc,U+61e3,U+61fa,U+6229,U+636b,U+64a3,U+64b3,U+64bb,U+64c4,U+64ef,U+6523,U+66b1,U+66c7,U+66d6,U+689f,U+690f,U+69e8,U+6a9c,U+6aa3,U+6ad3,U+6adf,U+6aea,U+6af1,U+6b1e,U+6b9e,U+6bae,U+6c2c,U+6c33,U+6c39,U+6df6,U+6f1a,U+6fa0,U+7005,U+7028,U+7043,U+7069,U+7146,U+71dc,U+71f4,U+7296,U+72fd,U+7319,U+7370,U+7449,U+74a3,U+75d9,U+75fe,U+763b,U+765f,U+7664,U+76b0,U+77bc,U+78ad,U+78e3,U+7931,U+7a1f,U+7a4c,U+7c11,U+7c23,U+7c5f,U+7d02,U+7d13,U+7d15,U+7d31,U+7d3a,U+7d40,U+7dbe,U+7dcb,U+7dd7-7dd8,U+7df2,U+7e09,U+7e39,U+7e48,U+7e88,U+7fa5,U+8075,U+81cf,U+81d2,U+82bb,U+83a2,U+83a7,U+858c,U+8594,U+85ba,U+8604,U+861a,U+866f,U+8706,U+8716,U+875f,U+8768,U+8784,U+87c4,U+87c8,U+87ce,U+8814,U+8823,U+8831,U+8938,U+893b,U+8960,U+8964,U+8998,U+89a6,U+89ac,U+89b2,U+89f4,U+8a03,U+8a15-8a16,U+8a36,U+8a41,U+8a46,U+8a52,U+8a54,U+8a5b,U+8a61,U+8a70,U+8a85,U+8aa5,U+8acd,U+8aeb,U+8aed,U+8af6,U+8b14,U+8b16,U+8b2b,U+8b3e,U+8b4e,U+8cc5,U+8cda,U+8e4c,U+8e89,U+8e9a,U+8ea1,U+8ea5,U+8eaa,U+8ed4,U+8ef2,U+8efb,U+8f42,U+8f64,U+9106,U+9148,U+91d2,U+91f5,U+91f9,U+9200,U+9210-9211,U+922e,U+9238,U+924d,U+925e,U+9278,U+92bc,U+92c3,U+92c7,U+92e5,U+92ef,U+9315,U+9341,U+9347,U+9354,U+9358,U+9398,U+93a3,U+93a7,U+93b0,U+93b5,U+93d1,U+93dc,U+93e4,U+93f5,U+9410,U+9427,U+943a,U+9463,U+9481,U+95ac-95ad,U+95de,U+9658,U+973d,U+9742,U+9744,U+97c3,U+97de,U+981c,U+9821,U+9837,U+9865,U+9870,U+98af,U+98b6,U+98c6,U+98c8,U+98e9,U+98f4,U+9909,U+9911,U+991b,U+991e,U+993f,U+9948,U+994c,U+9962,U+9a0f,U+9a16,U+9a2e,U+9a41,U+9a64,U+9acf,U+9ad6,U+9bad,U+9bca,U+9bdb,U+9be4,U+9be7,U+9bf0,U+9bfd,U+9c0d,U+9c25,U+9c2d,U+9c31,U+9c48,U+9c52,U+9c56,U+9c78,U+9ce9,U+9cf6,U+9d06,U+9d23,U+9d61,U+9d6a,U+9d89,U+9d9a,U+9dd3,U+9df2,U+9e1a-9e1b,U+9e85,U+9ea9,U+9f5f,U+9f67,U+9f6c,U+9f95;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_9ad5e8.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4e99,U+4f47,U+50e5,U+5102,U+513a,U+513c,U+5157,U+5191,U+5284,U+54e2,U+55b2,U+55c6-55c7,U+55da,U+55e9,U+5616,U+562e,U+5630,U+5653,U+56ea,U+579c,U+585a,U+5879,U+58ae,U+59cd,U+5acb,U+5af5,U+5afa,U+5b38,U+5c5c,U+5d22,U+5d87,U+5d97,U+5db8,U+5dd2,U+5dd4,U+5e40,U+5edd,U+60b5,U+611c,U+6137,U+615a,U+619a,U+61ab,U+633e,U+6384,U+6425,U+6440,U+645f,U+6473,U+649a,U+64f0,U+64f7,U+64fb,U+6506,U+6519,U+6582,U+6595,U+66c4,U+67f5,U+68e7,U+69f3,U+6a62,U+6ab3,U+6ab8,U+6abb,U+6ada-6adb,U+6b12,U+6bab,U+6c08,U+6d79,U+6e19,U+6ece,U+6ef7-6ef8,U+6f2c,U+6f41,U+6f6f,U+6f97,U+6fd8,U+6ffa,U+705d,U+70f4,U+71c9,U+71fc,U+71fe,U+7258,U+729b,U+72a2,U+7377,U+737a,U+737c,U+73a8,U+743f,U+744b,U+7489,U+74bd,U+74bf,U+74cf,U+750c,U+7515,U+760d,U+7627,U+7647,U+7662,U+766c,U+7672,U+769a,U+7787,U+792a-792c,U+798e,U+79b1,U+79c8,U+7b8b,U+7be4,U+7c0d,U+7c2b,U+7c37,U+7c6c,U+7c6e,U+7cc9,U+7d1c,U+7d33,U+7d46,U+7d73,U+7dac,U+7dde,U+7e08,U+7e10,U+7e1d,U+7e43,U+7e45,U+7e6e,U+7e93,U+7f4c,U+7f88,U+7ffa,U+811b,U+81be-81bf,U+81cd,U+82e7,U+8396,U+83f4,U+8477,U+849e,U+84c0,U+854e,U+858a,U+85f9-85fa,U+8622,U+865c,U+86fb,U+8778,U+87a2,U+880d,U+884a,U+8956,U+89b7,U+8a1b,U+8a25,U+8a6b,U+8a6d-8a6e,U+8a7c,U+8ab9,U+8ac9,U+8adc,U+8ae6,U+8af3,U+8afa,U+8b01,U+8b10,U+8b17,U+8b28,U+8b2c,U+8b33,U+8b4f,U+8cc1,U+8cd2,U+8ce1,U+8d05,U+8d0d,U+8d16-8d17,U+8e63,U+8e7a,U+8e8a,U+8efc,U+8efe,U+8f12,U+8f25,U+8f45-8f46,U+8fae,U+902c,U+9114,U+9116,U+913a,U+9183,U+91c1,U+91e9,U+91fa,U+9208,U+920d,U+9226,U+9237,U+923e,U+9249,U+9251,U+9257,U+925a,U+9262,U+926c,U+927b,U+9291,U+9293,U+9296,U+92a5,U+92ac,U+92bb,U+92cc,U+92e4,U+92f0,U+9310,U+932e,U+9333,U+9365,U+936c,U+9376,U+937a,U+93a2,U+93ac,U+93b3,U+93d7-93d8,U+93de,U+93e2,U+942b,U+942e,U+9432,U+9460,U+947e,U+9586,U+958f,U+95a1,U+95a8,U+95cc,U+95d4-95d5,U+975a,U+9766,U+9830,U+9839,U+990c,U+9912,U+9921,U+993e,U+9943,U+9957,U+995e,U+99b1,U+99b4,U+99df,U+99ed,U+9a2b,U+9a3e,U+9a40,U+9a4d,U+9a5b,U+9a65,U+9a6a,U+9aaf,U+9b22,U+9b77,U+9b81,U+9bc9,U+9be8,U+9c54,U+9c58,U+9d15,U+9d1b,U+9d26,U+9d51,U+9d60,U+9d72,U+9da9,U+9daf,U+9dd7,U+9dfa,U+9e1e,U+9f72;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_c2b336.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4f96,U+4fb6,U+4fe0,U+5098,U+50ad,U+50d5,U+510d,U+516a,U+51dc,U+522a,U+52fb,U+5331,U+5379,U+53ad,U+5436,U+555e,U+5614,U+562f,U+5687,U+56a5,U+56c2,U+56d1,U+582f,U+5862,U+589c,U+58b3,U+58fa,U+59e6,U+5a41,U+5a6d,U+5b0c,U+5be2,U+5bf5,U+5c37,U+5c4d,U+5ce9,U+5dbc,U+5de3,U+5e57,U+5eec,U+5f5e,U+5fa0,U+6065,U+60b6,U+60bd,U+60f1,U+6144,U+618a,U+6190,U+61f6,U+61fc,U+61fe,U+63c0,U+6417,U+646f,U+647b,U+6493,U+64be-64bf,U+64f1-64f2,U+650f,U+6522,U+652a,U+6583,U+65ac,U+665d,U+6688-6689,U+66e0,U+66ec,U+6727,U+68f2,U+6953,U+6968,U+6a3a,U+6ae5,U+6afb,U+6b16,U+6bad,U+6baf,U+6bb2,U+6bc6,U+6c2b,U+6cdd,U+6d36,U+6d87,U+6dea,U+6df5,U+6e26,U+6e4a,U+6f23,U+6f70,U+6f80,U+6fc1,U+6ff0,U+6ffe,U+7006,U+7009,U+700f,U+7018,U+701d,U+7055,U+7064,U+7152,U+7192,U+71be,U+71c1,U+71d9,U+71ed,U+71fb,U+720d,U+7463,U+7469,U+746f,U+7587,U+758a,U+760b,U+7613,U+7621,U+7661,U+766e,U+7671,U+7681,U+76ba,U+76de,U+775c,U+779e,U+77ef,U+786f,U+78ef,U+79aa,U+79bf,U+7a08,U+7a1c,U+7a62,U+7aaa,U+7aba,U+7ac4-7ac5,U+7ac7,U+7b4d,U+7b8f,U+7be9,U+7c3e,U+7cde,U+7d09,U+7d43,U+7d5e,U+7d62,U+7d79,U+7d81,U+7d8f,U+7d91,U+7db4,U+7db8,U+7dba-7dbb,U+7dbd,U+7ddd,U+7def,U+7e37,U+7e46,U+7e55,U+7e5a,U+7e6d,U+7e79,U+7e7d,U+7e8f,U+7f75,U+7fa8,U+7ff9,U+8073,U+807e,U+8123,U+814e,U+81a9,U+834a,U+8466,U+8525,U+852d,U+8553,U+856a,U+8588,U+863f,U+879e,U+87ec,U+87fb,U+8805,U+881f,U+883b,U+896a,U+8993,U+8a1d,U+8a23,U+8a60,U+8a63,U+8aa1,U+8aa3,U+8aa6,U+8aa8,U+8ac4,U+8ad2,U+8af1,U+8af7,U+8b0a,U+8b0e,U+8b20,U+8b41,U+8b74,U+8b8e,U+8c4e,U+8c8d,U+8c9e,U+8caf,U+8cb6,U+8cbd,U+8cca,U+8cd1,U+8cdc,U+8ce4,U+8cec,U+8d13,U+8e34,U+8ec0,U+8ed2,U+8f1f,U+8f3e,U+8f4d,U+905c-905d,U+9087,U+9132,U+919e,U+91ac,U+91d7-91d8,U+9209,U+9215,U+921e,U+9223,U+923a,U+9240,U+92a8,U+92c5,U+92f8,U+9318,U+931a,U+9320,U+9328,U+934d,U+9382,U+938a,U+93df,U+93fd,U+9433,U+9438,U+9470,U+9472,U+947c,U+947f,U+9594,U+95a5,U+9695,U+96b4,U+96b8,U+96cb,U+96db,U+9711,U+97cc,U+97dc,U+9824,U+985b,U+986b,U+9871,U+98e2,U+98ea,U+9903,U+9913,U+991a,U+9935,U+993b,U+9945,U+99ad,U+99c1,U+99d2,U+99dd,U+99f1,U+99ff,U+9a01,U+9a4a,U+9a62,U+9bd4,U+9c32,U+9c3b,U+9c49,U+9c57,U+9c77,U+9d09,U+9d3f,U+9ef4,U+9f4b,U+9f9c;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_aacef2.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4e1f,U+5006,U+5009,U+5074-5075,U+5078,U+50f1,U+5109,U+5147,U+514c,U+51cd,U+51f1,U+524e,U+525d,U+5270,U+528d,U+52f3,U+52f8,U+53b2,U+53e2,U+5442,U+5451,U+5586,U+559a,U+55aa,U+5606,U+5617,U+5674,U+5875,U+588a,U+58be,U+58d8,U+58df,U+58e9,U+593e,U+599d,U+5b30,U+5c5b,U+5c62,U+5ca1,U+5d50,U+5d84,U+5dba,U+5dbd,U+5dd6,U+5e25,U+5e5f,U+5ec1-5ec2,U+5eda,U+5edf,U+5f14,U+5f4c,U+5f4e,U+5f65,U+6046,U+6085,U+6158,U+617c,U+61a4,U+61b6,U+61c7,U+61f2,U+61f8,U+6200,U+62cb,U+62dd,U+6399,U+6488,U+6490,U+64ab,U+64b2,U+64cb,U+64e0-64e1,U+64ec,U+64fe,U+6514,U+651c,U+652c,U+6558,U+6575,U+6736,U+687f,U+68d7,U+68df,U+69d3,U+6a01,U+6a1e,U+6a38,U+6ac3,U+6b04,U+6b3d,U+6bbc,U+6bc0,U+6d29,U+6dbc,U+6de9,U+6dfa,U+6e3e,U+6e6f,U+6ebc,U+6ec4,U+6ecc,U+6eec,U+6eef,U+6ef2,U+6f11,U+6f3f,U+6f51,U+6f86-6f87,U+6fb1,U+6feb,U+7015,U+701f,U+703e,U+7051,U+7058,U+7165,U+7169,U+71e6,U+721b,U+723a,U+727d,U+72a7,U+72f9,U+7336,U+7343-7345,U+7375,U+7378,U+7464,U+746a,U+74ca,U+7501,U+775e,U+77da,U+78a9,U+78da,U+797f,U+798d,U+79a6,U+7a4e,U+7aa9,U+7aaf,U+7ac8,U+7aca,U+7c60,U+7cb5,U+7d0b,U+7d17,U+7d68,U+7da2,U+7db1,U+7dbf,U+7dd2,U+7de0,U+7de3,U+7dec,U+7e1b,U+7e2b,U+7e61,U+7e69,U+7e96,U+7e9c,U+7f77,U+8070,U+8076,U+8105,U+8108,U+816b,U+8178,U+819a,U+81d8,U+81e5,U+8259,U+8266,U+8332,U+8490,U+84bc,U+84ee,U+8514,U+8523,U+8569,U+856d,U+8591,U+85a6,U+8606,U+860a-860b,U+8755,U+8766,U+8836,U+8932,U+896f,U+8972,U+89d4,U+8a1f,U+8a50,U+8a73,U+8a87,U+8a98,U+8ae7,U+8b19,U+8b39,U+8b5a,U+8b5c,U+8b5f,U+8b6f,U+8c48,U+8c54,U+8c93,U+8caa,U+8cc2-8cc4,U+8cc8,U+8ce2,U+8ce6,U+8ced,U+8cf4,U+8d1b,U+8e30,U+8e64,U+8e8b,U+8ecb,U+8ef8,U+8f13-8f14,U+8f29,U+8f3b,U+8f3f,U+8f5f,U+8faf,U+9059,U+908f,U+9112,U+919c,U+91c0,U+91d0,U+91e3,U+9214,U+9234,U+925b,U+9264,U+9298,U+929c,U+92b3,U+92c1,U+934b,U+935b,U+93c7-93c8,U+9444,U+9583,U+9592,U+9598,U+95a3,U+95a9,U+95b1,U+95bb,U+95c7,U+95d6,U+95e1,U+9670,U+96b1,U+9727,U+978f,U+97cb,U+97fb,U+9801,U+9803,U+980c,U+9811,U+9838,U+9846,U+984f,U+98b3,U+98c4,U+98fc,U+9905,U+992c,U+994b,U+9952,U+99ae,U+99b3,U+99d5,U+9a0e,U+9a37,U+9a45,U+9a55,U+9a5f,U+9ad2,U+9b27-9b28,U+9b31,U+9b91,U+9ce5,U+9cf4,U+9d28,U+9d3b,U+9d5d,U+9db4,U+9df9,U+9e79,U+9e7c,U+9f21,U+9f52,U+9f90,U+9f94;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_0ff13d.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4e82,U+5016,U+502b,U+5049,U+5091,U+50b5,U+50b7,U+50be,U+50d1,U+50de,U+5100-5101,U+511f,U+5132,U+518a,U+525b,U+5291,U+52c1,U+52f5,U+5433,U+5553,U+55ac,U+55ce,U+570f,U+5713,U+584a,U+58c7,U+58d3,U+58de,U+58ef,U+58fd,U+5920,U+5922,U+5967,U+596a,U+596e,U+5a1b,U+5a66,U+5abd,U+5b43,U+5b6b,U+5bae,U+5bec,U+5c0b,U+5cef,U+5cf6,U+5cfd,U+5d11,U+5d17,U+5e33,U+5e63,U+5e6b,U+5eab,U+5ec8,U+5ee2,U+5ef3,U+5f48,U+5f7f,U+5f91,U+5fb9,U+60e1,U+6163,U+616e,U+6176,U+617e,U+6182,U+6191,U+61b2,U+61f7,U+6232,U+6368,U+6372,U+6383,U+639b,U+63da,U+63f9,U+6416,U+6436,U+64a5,U+64c1,U+64c7,U+64fa,U+651d,U+6524,U+6557,U+65c2,U+6649,U+66a2,U+66ab,U+66c9,U+68c4,U+68ca,U+694a,U+69cd,U+69ea,U+6a11,U+6a13,U+6a4b,U+6a6b,U+6a94,U+6b61,U+6b78,U+6b98,U+6bba,U+6c59,U+6dda,U+6de8,U+6e09,U+6e67,U+6e9d,U+6eab,U+6ec5,U+6efe,U+6f01,U+6f22,U+6f32,U+6f38,U+6f54,U+6f5b,U+6f64,U+6fc3,U+6fe4,U+6ff1,U+700b,U+7063,U+707d,U+70cf,U+7149,U+7159,U+71c8,U+71d2,U+7210,U+7240,U+7246,U+7274,U+755d,U+7562,U+7570,U+75e0,U+7642,U+7652,U+7665,U+76c3,U+76dc,U+76e4,U+76e7,U+7843,U+78bc,U+7919,U+791f,U+7955,U+79ae,U+7a40,U+7aae,U+7b46,U+7bc9,U+7c21,U+7c43,U+7c4c,U+7c64,U+7cfe,U+7d10,U+7d14,U+7d19,U+7d1b,U+7d21,U+7d2e,U+7d30,U+7d39,U+7d55,U+7d61,U+7d72,U+7da0,U+7db5,U+7de9,U+7df4,U+7e2e,U+7e31,U+7e3e,U+7e5e,U+7e6a,U+7e73,U+7e8d,U+7f70,U+7feb,U+8056,U+8085,U+812b,U+8139,U+8166,U+8173,U+81a0,U+81bd,U+81c9,U+820a,U+8271,U+838a,U+840a,U+8449,U+84cb,U+85a9,U+85cd,U+865b,U+8667,U+87f2,U+885d,U+89ba,U+89bd,U+89f8,U+8a02,U+8a34,U+8a3a,U+8a5e,U+8a62,U+8a69,U+8a95,U+8a9e,U+8aa0,U+8aa4,U+8ab0,U+8ab2,U+8abc,U+8aee,U+8af8,U+8afe,U+8b00,U+8b02,U+8b1d,U+8b7d,U+8c6c,U+8c9d,U+8ca2,U+8ca9,U+8cab,U+8cb4,U+8cbc,U+8cc0,U+8cd3,U+8cde,U+8ce0,U+8cfa,U+8d08,U+8d0a,U+8d0f,U+8d95,U+8d99,U+8da8,U+8de1,U+8e10,U+8e8d,U+8ecc,U+8edf,U+8f09,U+8f1b,U+8f1d,U+8f2a,U+8f2f,U+8f44,U+8f4e,U+8fad,U+9055,U+905e,U+9072,U+9077,U+907a,U+907c,U+9081,U+90f5,U+9127,U+912d,U+9130,U+91cb,U+91e6,U+9285,U+92d2,U+92ea,U+9304,U+9326,U+932b,U+932f,U+9375,U+937e,U+9396,U+93e1,U+9451,U+947d,U+9589,U+95c6,U+95ca,U+95e2,U+965d,U+9663,U+9678,U+96d6,U+96dc,U+96de,U+9748,U+975c,U+97a6,U+97d3,U+9802,U+9806,U+9812-9813,U+9817,U+983b,U+98ef,U+98f2,U+98fd-98fe,U+99d0,U+99db,U+9a19,U+9a30,U+9a5a,U+9b06,U+9b5a,U+9b6f,U+9bae,U+9cf3,U+9d6c,U+9e7d,U+9e97,U+9ea5,U+9f15,U+9f4a,U+9f61;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_00397b.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4e26,U+4e9e,U+4f15,U+4f48,U+4f54,U+4f86,U+4fb7,U+4fec,U+500b,U+5011,U+5099,U+50a2,U+50b3,U+50c5,U+50f9,U+5104,U+512a,U+5152,U+5167,U+5169,U+5225,U+5247,U+524b,U+5275,U+5283,U+5287,U+5289,U+52d5,U+52d9,U+52dd-52de,U+52e2,U+532f,U+5340,U+5354,U+537b,U+537d,U+53c3,U+54e1,U+5538,U+554f,U+55ab,U+55ae,U+5678,U+56b4,U+570b,U+570d,U+5712,U+5716,U+5718,U+57f7,U+5805,U+5831,U+5834,U+59b3,U+5b78,U+5be6-5be7,U+5be9,U+5beb,U+5bf6,U+5c07-5c08,U+5c0d-5c0e,U+5c46,U+5c64,U+5c6c,U+5e2b,U+5e36,U+5e79,U+5e7e,U+5ee3,U+5f35,U+5f37,U+5f9e,U+5fa9,U+611b,U+614b,U+61c9,U+6230,U+6236,U+63db,U+63ee,U+640d,U+64ca,U+64d4,U+64da,U+64f4,U+654e,U+6578,U+65b7,U+65e3,U+6642,U+66f8,U+6703,U+6771,U+67fb,U+689d,U+696d,U+6975,U+69ae,U+69cb,U+6a02,U+6a19,U+6a23,U+6a39,U+6a5f,U+6aa2,U+6b0a,U+6b50,U+6b72,U+6b77,U+6c23,U+6c7a,U+6c92,U+6cc1,U+6e1b,U+6e2c,U+6e96,U+6eff,U+6fa4,U+6fdf,U+7121,U+71b1,U+71df,U+722d,U+7232,U+723e,U+7260,U+72c0,U+734e,U+7368,U+7372,U+737b,U+73fe,U+74b0,U+7522,U+756b,U+7576,U+767c,U+76e1,U+76e3,U+771e,U+774f,U+77ad,U+78ba,U+790e,U+7926,U+7a05,U+7a2e,U+7a31,U+7a4d,U+7a69,U+7af6,U+7bc0,U+7bc4,U+7ce7,U+7d00,U+7d04-7d05,U+7d0d,U+7d1a,U+7d42,U+7d44,U+7d50,U+7d66,U+7d71,U+7d93,U+7d9c,U+7dab,U+7dad,U+7db2,U+7dca,U+7de8,U+7dfb,U+7e23,U+7e3d,U+7e54,U+7e7c,U+7e8c,U+7e94,U+7f85,U+7fa3,U+7fa9,U+7fd2,U+805e,U+806f,U+8072,U+8077,U+807d,U+8129,U+81e8,U+8207-8209,U+83d3,U+83ef,U+842c,U+84c6,U+85dd,U+85e5,U+8607,U+862d,U+8655,U+865f,U+8846,U+8853,U+8855,U+885b,U+88cf,U+88dc-88dd,U+88fd,U+8988,U+898b,U+898f,U+8996,U+89aa,U+89c0,U+8a08,U+8a0a,U+8a0e,U+8a13,U+8a17-8a18,U+8a2a,U+8a2d,U+8a31,U+8a3b,U+8a55,U+8a66,U+8a71-8a72,U+8a8c-8a8d,U+8aaa,U+8abf,U+8ac7,U+8acb,U+8ad6,U+8b1b,U+8b49,U+8b58,U+8b70,U+8b77,U+8b80,U+8b8a,U+8b93,U+8c50,U+8ca0-8ca1,U+8ca7-8ca8,U+8cac,U+8cb7-8cb8,U+8cbb,U+8cbf,U+8cc7,U+8ce3,U+8cea,U+8cfc-8cfd,U+8eca,U+8ecd,U+8f03,U+8f15,U+8f38,U+8f49,U+8fa6,U+8fb2,U+8ff4,U+9019,U+9023,U+9032,U+904a-904b,U+904e,U+9054,U+9060,U+9069,U+9078,U+9084,U+908a,U+9109,U+91ab,U+91dd,U+9245,U+9280,U+92b7,U+92fc,U+9322,U+9336,U+93ae,U+9435,U+9577,U+9580,U+958b,U+9593,U+95a4,U+95dc,U+9673,U+967d,U+968a,U+968e,U+969b,U+96a8,U+96aa,U+96bb,U+96d9,U+96e2-96e3,U+96f2,U+96fb,U+97c6,U+97ff,U+9805,U+9808,U+9810,U+9818,U+982d,U+984c-984d,U+9858,U+985e,U+9867,U+986f,U+98a8,U+98db,U+990a,U+9918,U+9928,U+99ac,U+9a57,U+9ad4,U+9b25,U+9ebc,U+9ec3,U+9ed2,U+9ede,U+9ee8,U+9f63,U+9f8d;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_e7daea.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4f25,U+4f27,U+4f65,U+4fa9-4faa,U+4fe3,U+4fe6,U+50a7,U+51eb,U+51fc,U+523d,U+5240,U+5250,U+538d,U+53c1,U+5452-5453,U+5456,U+549d,U+54d3-54d4,U+54dd,U+5527,U+556d-556e,U+55eb,U+55f3,U+5624,U+568f,U+56af,U+5739,U+575c,U+57b2,U+57b4,U+57da,U+57ef,U+5942,U+59ab,U+5aaa,U+5ad2,U+5ad4,U+5af1,U+5c98,U+5d03,U+5d5b,U+5d5d,U+5def,U+5e0f,U+5e91,U+5eea,U+5ffe,U+6004,U+6079,U+60ab,U+60ad,U+6120,U+61b7,U+6217,U+622c,U+6266,U+629f,U+6332,U+63b8,U+63bc,U+64b8,U+64ba,U+66a7,U+679e,U+67a7,U+67b3,U+67fd,U+6800,U+680c,U+6860,U+68c2,U+691f,U+6924,U+6987,U+6989,U+6a2f,U+6a79,U+6a7c,U+6aa9,U+6b24,U+6b81,U+6b87,U+6b92-6b93,U+6bc2,U+6c07,U+6c29,U+6ca9,U+6cf6,U+6d4d,U+6da0,U+6e0c,U+6e86,U+6edf,U+6f4b,U+6f74,U+6fd1,U+7080,U+7145,U+7198,U+72b8,U+72f2,U+732c,U+737e,U+73d0,U+7477,U+748e,U+74d2,U+7596,U+75a0,U+75c8,U+75e8,U+763f,U+765e,U+7726,U+7817,U+781c,U+7856,U+7877,U+789b,U+7a51,U+7b15,U+7b5a,U+7ba6-7ba7,U+7baa,U+7c74,U+7c9c-7c9d,U+7ccd,U+7e9f,U+7ea8,U+7ec0,U+7ecb,U+7ed0,U+7ed4,U+7ed7,U+7ee1,U+7ee6,U+7ee8,U+7ef1,U+7efa-7efb,U+7efe,U+7f01,U+7f0d,U+7f11,U+7f17,U+7f1b,U+7f1f,U+7f22,U+7f26,U+7f31,U+7f35,U+8027,U+80fc,U+816d,U+81bb,U+8279,U+82c1,U+82cc,U+82d8,U+8315,U+835c,U+8366,U+8368-8369,U+839c,U+83b3-83b4,U+83b8,U+83bc,U+84d1,U+84e0,U+85ae,U+85c1,U+85d3,U+86cf,U+86d4,U+86f3-86f4,U+8748,U+877c,U+886e,U+88e2-88e3,U+89c7,U+89de,U+8a8a,U+8ba6-8ba7,U+8bb5,U+8bcc,U+8bce,U+8bd3,U+8bd6,U+8bdf,U+8be8,U+8bee,U+8bf3,U+8bf9,U+8c00,U+8c04,U+8c07,U+8c14,U+8c16-8c17,U+8c1d,U+8c20-8c21,U+8c25,U+8c2f-8c30,U+8c36,U+8d30,U+8d36,U+8d3d,U+8d40,U+8d59,U+8d5c,U+8dde,U+8df8,U+8e2f,U+8e51,U+8e70,U+8f6b,U+8f6d,U+8f73,U+8f78,U+8f87,U+8f8b,U+8f8e-8f8f,U+8f94,U+8f9a,U+8ff3,U+9026,U+90ba,U+90c4,U+90cf-90d0,U+917d,U+933e,U+9486-9487,U+948c-948d,U+948f-9490,U+94a3,U+94aa-94ab,U+94ad,U+94af,U+94b2,U+94ba,U+94bd,U+94bf,U+94c8,U+94ca,U+94cc-94cd,U+94d1-94d2,U+94d5-94d6,U+94d9,U+94df,U+94e9-94ea,U+94ef-94f0,U+94f3,U+94f7,U+94fc-94fd,U+9507,U+9515,U+951b,U+951d,U+9531,U+9546,U+954a,U+954f,U+9554,U+9558-9559,U+955d-955e,U+9566-9567,U+956b,U+9571-9572,U+95e9,U+95f1,U+95f3,U+9604,U+9608-960b,U+960f,U+9612,U+9615,U+9617,U+9765,U+97af,U+97ea,U+9878,U+987c,U+9880,U+988f,U+989a-989b,U+989e,U+98a2,U+98a7,U+98d1,U+98d5,U+9963,U+996c,U+9980,U+998a,U+9991,U+9994-9995,U+9a78,U+9a7d,U+9a88,U+9a93,U+9a9b,U+9aa0,U+9aa2,U+9ac5,U+9acc,U+9b47,U+9b49,U+9c82,U+9c87,U+9c90-9c91,U+9c9b,U+9c9e,U+9ca5,U+9ca7,U+9ca9,U+9cad-9cae,U+9cb1,U+9cb5,U+9cc3,U+9cc7,U+9cca,U+9cce-9ccf,U+9cd3,U+9cd5,U+9cd9,U+9e28-9e29,U+9e2c,U+9e37,U+9e41-9e42,U+9e46-9e47,U+9e58,U+9e5a,U+9e5e,U+9e6c,U+9efe,U+9f0b,U+9f39,U+9f83,U+9f85,U+9f87-9f8a,U+9f8c;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_269445.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4e2c,U+4f1b,U+507e,U+5181,U+522d,U+523f,U+52a2,U+5326,U+537a,U+53a3,U+5423,U+5459,U+54b4,U+54d5,U+54d9,U+54dc,U+5523,U+55be,U+56df,U+5786,U+57d8,U+5941,U+5a08,U+5b37,U+5c66,U+5cbd,U+5d74,U+5e31,U+5e3b,U+5e42,U+5e5e,U+5f11,U+5f2a,U+6003,U+603c,U+610d,U+6126,U+61d4,U+6206,U+620b,U+6322,U+63b4,U+63f8,U+6445,U+6484,U+6769,U+67a8,U+680a,U+684a,U+6861,U+6864,U+6920,U+69e0,U+6a65,U+6bf5,U+6cf7,U+6cfa,U+6d48,U+6e16,U+6ed7,U+6ee0,U+6f24,U+6f46,U+709d,U+7173,U+717a,U+72ad,U+72ef,U+7303,U+7321,U+75ac,U+75d6,U+7605,U+7617,U+76b2,U+770d,U+7857,U+78b9,U+7962,U+7a06,U+7aad,U+7b3e,U+7ba8,U+7c16,U+7cc1,U+7cc7,U+7d77,U+7ea1,U+7ea5,U+7ea9,U+7ec1,U+7ee0,U+7ef2,U+7f02,U+7f07,U+7f0b-7f0c,U+7f0f,U+7f12,U+7f21,U+7f23,U+7f27,U+7f2f,U+7f32-7f33,U+7f74,U+8022,U+804d,U+80b7,U+80e8,U+80ea,U+8114,U+8136,U+817d,U+8223,U+823b,U+8288,U+82c8,U+8311,U+835b,U+836c-836e,U+83b6,U+8411,U+8487,U+8489,U+848c,U+84e3,U+8539,U+866e,U+867f,U+86f1-86f2,U+877e,U+88e5,U+89cb-89cc,U+89cf,U+89ef,U+8ba0,U+8bd4,U+8bdc,U+8bf6,U+8bfc,U+8c02,U+8c18,U+8c2b,U+8c2e,U+8c33,U+8c35,U+8d33,U+8d46-8d47,U+8d4d,U+8d55,U+8db1,U+8dd6,U+8e2c,U+8e9c,U+8f75,U+8f77,U+8f79-8f7a,U+8f7e,U+8f81-8f82,U+917e,U+948b,U+9494,U+94ac,U+94b6-94b8,U+94d7-94d8,U+94de,U+94e5,U+94eb,U+94f4,U+94f9,U+950a,U+950d-950e,U+9513-9514,U+9516,U+9518,U+951e,U+9529-952c,U+9538,U+953c,U+953e-953f,U+9544-9545,U+954e,U+9561,U+9564-9565,U+9568-956a,U+956c,U+95f6,U+95fc,U+9603,U+960c-960d,U+9667,U+96e0,U+9792,U+97b2,U+97b4,U+9883,U+989f,U+98a1,U+98a5,U+98e7,U+990d,U+9967,U+9969,U+996b,U+9987,U+9993,U+9a75,U+9a7a,U+9a80,U+9a92,U+9a96,U+9a98,U+9a9f,U+9aa3,U+9c86,U+9c8b,U+9c8e,U+9c92,U+9c94-9c95,U+9c9a,U+9ca0-9ca1,U+9ca3,U+9ca6,U+9cb0,U+9cb4,U+9cba,U+9cbc-9cbd,U+9cc6,U+9ccb,U+9cd0,U+9cd4,U+9cd8,U+9ce2,U+9e2b,U+9e31-9e32,U+9e36,U+9e38-9e3a,U+9e48,U+9e4b,U+9e4e,U+9e55,U+9e5b,U+9e63,U+9e68-9e6a,U+9e71,U+9e7e,U+9ebd,U+9ec9,U+9ee9-9eea,U+9f0d,U+9f44,U+9f51,U+9f80,U+9f86;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_36ad1b.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4eb5,U+4f2b,U+4fac,U+4fea,U+506c,U+507b,U+50a5,U+50a9,U+5156,U+51a2,U+520d,U+52d0,U+53a9,U+53ae,U+53fd,U+5457,U+545b,U+5494,U+5499,U+549b,U+54a4,U+54d2,U+551b,U+5520,U+5522,U+556c,U+55bd,U+55ec,U+565c,U+567c,U+56f5,U+5785,U+579b,U+57a7,U+57a9,U+57ad,U+57d9,U+57dd,U+5811,U+59a9-59aa,U+59f9,U+5a06,U+5a32,U+5a75-5a76,U+5b6a,U+5b73,U+5c49,U+5c99,U+5cbf,U+5cc4,U+5ce4-5ce5,U+5d02,U+5d58,U+5fcf,U+6002,U+6005-6006,U+603f,U+6078,U+607b,U+607d,U+607f,U+60af,U+60ec-60ee,U+61d1,U+626a,U+62a1,U+631b,U+631e,U+6376,U+637b,U+63b3,U+63ff,U+6448,U+64b5,U+64b7,U+64de,U+6619,U+6635,U+6654,U+67a5,U+67ad,U+680e,U+683e,U+6867,U+6901,U+692d,U+6988,U+69df,U+6b9a,U+6c32,U+6ca3-6ca4,U+6d43,U+6d52,U+6d54,U+6d9e,U+6e11,U+6ee2,U+704f,U+7096,U+70c3,U+70e9,U+70ec,U+7116,U+7118,U+724d,U+7266,U+728a,U+72c8,U+72cd,U+72de,U+72f0,U+736d,U+7391,U+73c9,U+73cf,U+740f,U+74ee-74ef,U+7572,U+75b1,U+75b4,U+75c9,U+7618,U+762a,U+7663,U+766f,U+7691,U+7751,U+77fe,U+7800,U+783b,U+783e,U+789c,U+7980,U+7a23,U+7b3a,U+7ba9,U+7bab,U+7bd1,U+7bd3,U+7c41,U+7c7c,U+7cbd,U+7ea3,U+7eb0,U+7ebe,U+7ec2,U+7ec9,U+7ecc,U+7edb,U+7eeb,U+7eef,U+7ef6,U+7f03-7f04,U+7f08,U+7f19,U+7f1c,U+7f25,U+7f2c,U+7f30,U+7f42,U+7f9f,U+7ff1,U+8069,U+80ae,U+80c4,U+810d,U+8113,U+817c,U+8191,U+8297,U+82cb,U+830f,U+8314,U+835a,U+835e,U+8360,U+8364-8365,U+836a,U+8385,U+8424,U+84df,U+84e5,U+8537,U+853a,U+8572,U+8616,U+864f,U+866c,U+8671,U+869d,U+86ac,U+86ca,U+86ce,U+86f0,U+8715,U+8717,U+87a8,U+8845,U+8884-8885,U+88c6,U+891b,U+8934,U+8941,U+89ca,U+89ce,U+89d0-89d1,U+8ba3,U+8baa-8bab,U+8bb9,U+8bc2-8bc3,U+8bc5,U+8bcb,U+8bcf,U+8bd2,U+8bd8,U+8bdb,U+8be0-8be1,U+8be4,U+8be9,U+8bf0,U+8c0c,U+8c0f,U+8c11-8c12,U+8c15,U+8c1f,U+8c27,U+8c29-8c2a,U+8c32,U+8d32,U+8d45,U+8d49,U+8d53,U+8d58,U+8d5d,U+8db8,U+8dc4,U+8df7,U+8df9,U+8e0c,U+8e7f,U+8e8f,U+8f71-8f72,U+8f7c,U+8f95,U+8f98,U+8fab,U+909d,U+90d3,U+90e6-90e7,U+92ae,U+9485,U+948e,U+9492,U+9495,U+9497,U+949a,U+949c-949d,U+94a1,U+94a4,U+94b4-94b5,U+94b9,U+94bc,U+94c2,U+94c4,U+94c6,U+94c9,U+94cb,U+94db,U+94e0-94e1,U+94e3-94e4,U+94e7,U+94f1,U+9502-9503,U+9506,U+9509,U+950f,U+9511-9512,U+9517,U+951f,U+9528,U+9534,U+9536-9537,U+9542,U+9549,U+9552-9553,U+9556-9557,U+9562-9563,U+956f-9570,U+9573,U+95eb,U+95f0,U+95fe,U+9602,U+9606,U+9611,U+9616,U+9619-961a,U+9649,U+96f3,U+9701,U+972d,U+9753,U+9791,U+97eb,U+9889-988a,U+988c,U+9893-9894,U+98a6,U+98d2-98d3,U+98d9-98da,U+98e8,U+9968,U+996f,U+9974,U+9977,U+997d,U+9984,U+998b,U+9990,U+9a6e,U+9a77,U+9a81,U+9a87,U+9a8a,U+9a90,U+9a9c-9a9e,U+9aa1,U+9aa7,U+9acb,U+9c85,U+9c88,U+9ca2,U+9ca8,U+9cab,U+9cb2-9cb3,U+9cb6-9cb7,U+9cc5,U+9ccd,U+9cdc-9cdd,U+9cdf,U+9e20,U+9e22,U+9e2a,U+9e2f,U+9e33,U+9e3e,U+9e44,U+9e49,U+9e4c,U+9e51,U+9e57,U+9e5c,U+9e66-9e67,U+9e6b,U+9e6d,U+9e73,U+9eb8,U+9f8b,U+9f9b;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_a04e21.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4e98,U+4ec7,U+4ed1,U+4f1e,U+4f63,U+4fa0,U+4fa3,U+4fa5,U+4fe8,U+50bb,U+50f5,U+51c4,U+51db,U+51ff,U+5220,U+5300,U+532e,U+5364,U+538c,U+5413,U+5450,U+5455,U+545c,U+548f,U+54bd,U+54d1,U+54d7,U+54df,U+5507,U+5567,U+5578,U+5618,U+56a3,U+56f1,U+5742,U+575e-5760,U+5815,U+5978,U+59d7,U+5a04-5a05,U+5a07,U+5a34,U+5ba0,U+5bdd,U+5c27,U+5c34,U+5c7f,U+5c96,U+5ce6,U+5ce8,U+5dc5,U+5e18,U+5e27,U+5e3c,U+5eb5,U+5f26,U+5f5d,U+5f95,U+601c,U+6064,U+607a,U+60e7,U+60eb,U+6123,U+6151,U+61d2,U+62a0,U+62e2-62e3,U+62e7,U+631a,U+631d,U+631f-6320,U+6342,U+6346,U+6361,U+6363,U+63b7,U+63ba,U+6400-6402,U+6405,U+6512,U+655b,U+658b,U+6593,U+65a9,U+65f7,U+663c,U+6652,U+6655-6656,U+672d,U+67ab,U+67e0,U+6805,U+6808-6809,U+6816-6817,U+6862,U+6866,U+6868,U+68f1,U+6984,U+69db,U+6a31,U+6a71,U+6a90,U+6b7c,U+6ba1,U+6bb4,U+6bd9,U+6be1,U+6c22,U+6c79,U+6c93,U+6ca5-6ca6,U+6cbe,U+6cde,U+6cf8,U+6cfb,U+6cfe,U+6d3c,U+6d4a,U+6d4f,U+6d9f,U+6da1,U+6da3,U+6da7,U+6da9,U+6e0d-6e0e,U+6e83,U+6e85,U+6eaf,U+6ee4,U+6ee6,U+6f13,U+6f4d,U+709c,U+70bd,U+70c1,U+70db,U+70e8,U+70eb,U+718f,U+72b7,U+72f8,U+7315,U+73ae,U+73ba,U+73d1,U+73f2,U+7405,U+7410,U+7487,U+7574,U+759f,U+75a1,U+75ae-75af,U+75d2,U+75ea-75eb,U+75f4,U+762b,U+763e,U+766b,U+7682,U+76b1,U+76cf,U+772f,U+7741,U+7792,U+77e9,U+77eb,U+77f6,U+781a,U+783a,U+796f,U+7977,U+7985,U+79c3,U+79c6,U+79fd,U+7a8d,U+7a9c,U+7aa5-7aa6,U+7ad6,U+7b03,U+7b5b,U+7b5d,U+7bf1,U+7caa,U+7cd5,U+7eab-7ead,U+7eb6,U+7ec5,U+7eca,U+7ece,U+7ed1,U+7eda,U+7ede,U+7ee2,U+7ee5,U+7eee,U+7ef0,U+7ef7,U+7efd,U+7f00,U+7f09,U+7f0e,U+7f15,U+7f20,U+7f24,U+7f28,U+7f2a-7f2b,U+7f2d-7f2e,U+7f81,U+7fa1,U+7fd8,U+8038,U+803b,U+804b,U+80b4,U+80be,U+80e7,U+80eb,U+8110,U+814c,U+817b,U+82b8,U+82c7,U+82ce,U+82d4,U+830e,U+8327,U+835f,U+8367,U+836b,U+83b9-83ba,U+841d,U+8426,U+8471,U+84e6,U+8511,U+853c,U+8681-8682,U+86ee,U+8721,U+8747,U+8749,U+874e,U+889c,U+89c5,U+8ba5,U+8bb3-8bb4,U+8bb6-8bb7,U+8bbd,U+8bc0,U+8bd9,U+8be3,U+8be7,U+8beb-8bec,U+8bf2,U+8bf5,U+8bfd,U+8bff,U+8c06,U+8c0d-8c0e,U+8c19-8c1c,U+8c23-8c24,U+8c2c,U+8c34,U+8d1e,U+8d26,U+8d31,U+8d3b-8d3c,U+8d48,U+8d4a,U+8d4e,U+8d50,U+8d61,U+8e0a,U+8e52,U+8eaf,U+8f76,U+8f84,U+8f8a,U+8f8d,U+8f97,U+8f99,U+8fe9,U+8ff8,U+90ac,U+90f8,U+915d,U+9171,U+9489-948a,U+9499,U+949b,U+94a0,U+94a5,U+94a8,U+94ae,U+94b0,U+94b3,U+94be,U+94c0,U+94ce,U+94d0,U+94e2,U+94e8,U+94ec,U+94ee,U+94f2,U+94f5,U+94ff,U+9504,U+9508,U+950c,U+951a,U+9522,U+9524-9525,U+952d,U+952f-9530,U+9532,U+9535,U+9539,U+9540-9541,U+954c-954d,U+9550-9551,U+955b,U+956d,U+9576,U+95f5,U+95f7,U+95fa,U+9600,U+9647,U+9668,U+96b6,U+96bd,U+96cf,U+97e7,U+97ec,U+9885,U+988d,U+9890,U+98a0,U+98a4,U+996a,U+9975,U+997a,U+997f,U+9981,U+9985,U+998d,U+998f,U+9992,U+9a6d,U+9a6f,U+9a73-9a74,U+9a79,U+9a7c,U+9a7f,U+9a85-9a86,U+9a8b,U+9a8f,U+9aa5,U+9b13,U+9c7f,U+9c9f,U+9ca4,U+9cb8,U+9cbb,U+9cc4,U+9ccc,U+9cd6-9cd7,U+9cde,U+9e25-9e26,U+9e35,U+9e3d,U+9e43,U+9e4a,U+9f9f;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_b1143d.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4e11,U+4e1b,U+4e22,U+4e27,U+4ec6,U+4ed3,U+4eff,U+4f26,U+4f2a,U+4f5b,U+4fa6-4fa8,U+4fca,U+4fde,U+4fe9,U+4fed,U+5077,U+5151,U+5179,U+517d,U+5188,U+51af,U+51bb,U+51c0,U+51c9,U+51cc,U+51d1,U+51e4,U+51ed,U+51ef,U+51f6,U+522e,U+5239,U+5251,U+5265,U+5269,U+529d,U+52b2,U+52cb,U+535c,U+5362,U+5367,U+5389,U+5395,U+5398,U+53a2,U+53a8,U+53d9,U+53e0,U+53f9,U+540a,U+5415,U+541e,U+5446,U+54a8,U+54b8,U+54c4,U+54f2,U+5524,U+5582,U+55b7,U+5631,U+566a,U+5706,U+5708,U+5723,U+575b,U+575d,U+5784,U+5792,U+57a6,U+57ab,U+5899,U+58f3,U+58f6,U+5938-5939,U+5986,U+5988,U+59dc,U+5a18,U+5a31,U+5a74,U+5baa-5bab,U+5bff,U+5c18,U+5c1d,U+5c38,U+5c4f,U+5c61,U+5c82,U+5c9a,U+5ca9,U+5cad,U+5cb3,U+5d2d,U+5de2,U+5de9,U+5e05,U+5e1c,U+5e78,U+5e90,U+5e99,U+5e9e,U+5f03,U+5f25,U+5f2f,U+5f39,U+5f66,U+5fa1,U+5fc6,U+5fe7,U+604b,U+6052,U+6073,U+6076,U+607c,U+60a6,U+60ac,U+60e8-60e9,U+60ef,U+6108,U+6124,U+621a,U+6251,U+6263,U+626b,U+6270,U+629a-629b,U+62ac,U+62dc,U+62df,U+62e6,U+62e8,U+6321,U+6323-6324,U+6328,U+633d,U+635e,U+63fd,U+641c,U+643a,U+6447,U+644a,U+6491,U+654c,U+65cb,U+6606,U+664b,U+6682,U+6697,U+6734-6735,U+6740,U+6746,U+6760,U+67a2-67a3,U+67aa,U+67dc,U+680b,U+680f,U+6869,U+68a6,U+68cb,U+6982,U+6a2a,U+6b32,U+6bc1,U+6c64,U+6c9f,U+6ca7,U+6caa,U+6cc4,U+6cea,U+6cfc,U+6d12,U+6d45-6d47,U+6d51,U+6d53,U+6d8c,U+6d9b,U+6d9d,U+6da4,U+6dc0,U+6e0a,U+6e14,U+6e17,U+6e7f,U+6e89,U+6eda,U+6ede,U+6ee5,U+6ee8-6ee9,U+6f47,U+6f9c,U+6fd2,U+706d,U+7076,U+707f,U+7089,U+70ae,U+70bc,U+70c2,U+70e6-70e7,U+7115,U+7237,U+7275,U+727a,U+72b9,U+72ed-72ee,U+72f1,U+730e,U+732b,U+739b,U+73a9,U+743c,U+7476,U+74f6,U+7545,U+75c7,U+76d7,U+7750,U+77a9,U+7801,U+7816,U+7845,U+7855,U+788d,U+78b1,U+7978,U+7984,U+7a83,U+7a91,U+7a9d,U+7b0b,U+7b3c,U+7b4b,U+7bee,U+7ca4,U+7cd6,U+7ea0,U+7ea4,U+7eb1-7eb2,U+7eb5,U+7eb9,U+7ebd,U+7ed2,U+7ed8,U+7ee3,U+7eea,U+7ef3,U+7ef5,U+7ef8,U+7f05-7f06,U+7f14,U+7f18,U+7f1a,U+7f1d,U+7f34,U+7f62,U+8042,U+806a,U+80a0,U+80a4,U+80bf-80c1,U+80f6,U+8109,U+810f,U+8138,U+814a,U+817e,U+8206,U+820d,U+8230-8231,U+8273,U+829c,U+82a6,U+82cd,U+82f9,U+8346,U+8350,U+8361,U+83b1-83b2,U+8427,U+848b,U+84dd,U+8574,U+865a,U+866b,U+867e,U+8680,U+8695,U+8854,U+886c,U+88ad,U+88e4,U+89e6,U+8bbc,U+8bc8,U+8bca,U+8bd1,U+8bde,U+8be6,U+8bf1,U+8bf8,U+8bfa,U+8c05,U+8c0a,U+8c10,U+8c13,U+8c26,U+8c28,U+8c2d,U+8c31,U+8c37,U+8d1d,U+8d24,U+8d29-8d2a,U+8d2c,U+8d2e,U+8d3e-8d3f,U+8d41-8d43,U+8d4b-8d4c,U+8d4f,U+8d54,U+8d56,U+8d5a,U+8d60,U+8d62-8d63,U+8dfb,U+8e2a,U+8f67,U+8f69,U+8f70,U+8f74,U+8f7f,U+8f85,U+8f88,U+8f90-8f91,U+8f96,U+8f9e-8f9f,U+8fa9,U+8fc1,U+8fc8,U+8fdf,U+900a,U+9012,U+903b,U+903e,U+9065,U+90b9,U+90bb,U+90c1,U+9178,U+917f,U+91ca,U+9493,U+949e,U+94a6-94a7,U+94a9,U+94bb,U+94c3,U+94c5,U+94dc-94dd,U+94ed,U+94f8,U+94fa,U+94fe,U+9501,U+9505,U+950b,U+9510,U+9521,U+9523,U+953b,U+955c,U+95ea,U+95ef,U+95f2,U+95f8-95f9,U+95fd,U+9601,U+9605,U+960e,U+9610,U+9634,U+9655,U+9690,U+96c7,U+96fe,U+9709,U+97e6,U+97f5,U+9875-9877,U+987d,U+9882,U+9887-9888,U+9891,U+9896-9897,U+989c,U+98d8,U+9965,U+9971-9972,U+9976,U+997c,U+9988,U+9a70-9a71,U+9a76,U+9a7e,U+9a82,U+9a84,U+9a91,U+9a97,U+9a9a,U+9aa4,U+9c8d,U+9e1f,U+9e23,U+9e2d,U+9e3f,U+9e45,U+9e64,U+9e70,U+9f20,U+9f7f,U+9f9a;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_c728d8.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4e10,U+4e56,U+4ea2,U+4ea5,U+4ec3,U+4f36,U+4f3a,U+4f5f,U+4f6c,U+4f70,U+4f84,U+4f88,U+4f8d,U+4f97,U+5014,U+501a,U+5029,U+504c,U+516e,U+5197,U+51a5,U+51f8,U+5201,U+524c,U+527f,U+5288,U+52fa,U+5315,U+5321,U+5352,U+5366,U+53e8-53e9,U+5406,U+540f,U+543c,U+5471,U+5480,U+5495,U+549a,U+54b3,U+54c7,U+54e6,U+54ee,U+54fc,U+5501,U+556a,U+557c,U+5583,U+55dc,U+55e3,U+5632,U+5669,U+566c,U+5676,U+56b7,U+56bc,U+56da,U+56e4,U+56ff,U+5703,U+5729,U+57a2-57a3,U+58d1,U+5919,U+592d,U+592f,U+5944,U+5955,U+595a,U+5983,U+598d,U+5996,U+59ca,U+59e5,U+5a13,U+5a25,U+5a77,U+5a7f,U+5a9b,U+5ab2,U+5b09,U+5b70,U+5b75,U+5b7a,U+5bd0,U+5bf0,U+5c41,U+5c79,U+5c91,U+5c94,U+5ced,U+5d69,U+5e16,U+5e4c,U+5eb6,U+5f08,U+5f1b,U+5f27,U+5f3c,U+5ffb,U+6020,U+602f,U+604d,U+6055,U+606c,U+608d,U+60cb,U+60e6,U+60f6,U+618b,U+61a7-61a8,U+61ac,U+61cb,U+620c-620e,U+6233,U+6249,U+6273,U+6289,U+62c4,U+62c7,U+62ce,U+62d9,U+62ed-62ee,U+62f4,U+62f7,U+6345,U+634d-634e,U+6382,U+6396,U+63c9,U+63ea,U+6413,U+6452,U+645e,U+6479,U+6482,U+6487,U+64ac,U+64ce,U+64d2,U+6556,U+659f,U+65a1,U+660a,U+6615,U+664f,U+667e,U+66e6,U+6726,U+6756,U+675e,U+67c4,U+67da,U+6853,U+6897,U+68a2,U+68e3,U+68fa,U+693f,U+6942,U+695e,U+6995,U+69a8,U+69ad,U+69b4,U+69cc,U+6a44,U+6a59,U+6a80,U+6b83,U+6b86,U+6b89,U+6bcb,U+6bd3,U+6c30,U+6c40,U+6c5e,U+6c85,U+6cad,U+6cb1,U+6cbd,U+6cf1,U+6d19,U+6d31,U+6d85,U+6d93,U+6daa,U+6dae,U+6dbf,U+6dc6-6dc7,U+6e32,U+6e3a,U+6e43-6e44,U+6e4d,U+6e5f,U+6eba,U+6ec1,U+6f31,U+6f3e,U+6f5e,U+6f7a,U+6f7c,U+6f88,U+6f8e,U+6fee,U+701b,U+7078,U+7095,U+7099,U+70ab,U+70d9,U+70f7,U+70fd,U+7109,U+715e,U+717d,U+7184,U+71a8,U+71ce,U+71ee,U+722a,U+7235,U+7239,U+7252,U+72c4,U+72e1,U+72e9,U+733f,U+73c0,U+73c2,U+7409,U+7435-7436,U+7459-745a,U+747e,U+748b,U+749c,U+749e,U+74a8,U+74e2-74e3,U+7504,U+752c-752d,U+7599-759a,U+75a4,U+75ca,U+75f0,U+75f9,U+7600-7601,U+761f-7620,U+7629,U+768b,U+7693,U+7738,U+773a,U+776b,U+777f,U+7785,U+778e,U+77a5,U+77aa,U+77b0,U+77bf,U+7852,U+78be,U+78d5,U+78fa,U+7934,U+7940,U+7948,U+795b,U+7960,U+79a7,U+79ba,U+7a20,U+7a96,U+7b19,U+7b28,U+7b50,U+7b71,U+7b77,U+7b94,U+7bc6,U+7c27,U+7c38,U+7c95,U+7cb1,U+7ce0,U+7d0a,U+7d6e,U+7f79,U+7f9a,U+7fcc,U+8046,U+8098,U+80da,U+80e5,U+80f0,U+80f3,U+80fa,U+812f,U+8148,U+8165,U+818a,U+819b,U+81b3,U+81ba,U+81e7,U+8235,U+8237-8238,U+828b,U+8299,U+82ae,U+82b7,U+82d3,U+8301,U+831c,U+8338-8339,U+837b-837c,U+8386,U+8393,U+8398,U+83bd,U+83c1,U+83cf,U+83e0,U+83e9,U+8475,U+84bf,U+84d3,U+85c9,U+85d5,U+85dc,U+85e9,U+8654,U+86db,U+86df,U+86fe,U+8712,U+871a,U+873f,U+8757,U+8783,U+8859,U+888d,U+88a4,U+88f3-88f4,U+8910,U+891a,U+8925,U+8d66,U+8db4,U+8dda,U+8de4,U+8e1e,U+8e4a,U+8e66,U+8e6c,U+8fe5,U+901e,U+9083,U+90b3,U+90b8,U+90e1,U+90f4,U+9119,U+912f,U+9131,U+914c,U+915a,U+9163,U+9165,U+9170,U+91c9,U+91dc,U+962a,U+965b,U+9685,U+9698,U+96cc-96cd,U+96ef,U+96f9,U+9704,U+9713,U+9773-9774,U+9776,U+9ab8,U+9e8b,U+9e92,U+9eef;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_8ea124.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+4e07,U+4e0e,U+4e13,U+4e1a,U+4e1c-4e1d,U+4e24-4e25,U+4e2a,U+4e30,U+4e34,U+4e3a,U+4e3d-4e3e,U+4e48-4e49,U+4e4c,U+4e50,U+4e54,U+4e60-4e61,U+4e66,U+4e70-4e71,U+4e86,U+4e89,U+4e8e-4e8f,U+4e91,U+4e9a,U+4ea7,U+4ea9,U+4eb2,U+4ebf,U+4ec5,U+4ece,U+4eea,U+4eec,U+4ef7,U+4f17-4f1a,U+4f1f-4f20,U+4f24,U+4f53,U+4f59,U+4f60,U+4fee,U+501f,U+503a,U+503e,U+507f,U+50a8,U+513f,U+514b,U+515a,U+5170,U+5173-5174,U+517b,U+5185,U+518c,U+5199,U+519b-519c,U+51ac,U+51b2-51b3,U+51b5,U+51c6,U+51cf,U+51e0,U+51fa-51fb,U+5212,U+5218-521b,U+522b,U+5236,U+5242,U+5267,U+529e,U+52a1,U+52a8,U+52b1,U+52b3,U+52bf,U+533a-533b,U+5343,U+534e-534f,U+5355-5356,U+5360,U+536b,U+5373-5374,U+5377,U+5385-5386,U+538b,U+53a6,U+53bf,U+53c2,U+53cc,U+53d1,U+53d8,U+53ea,U+53f6-53f7,U+5403,U+5408,U+540c,U+540e,U+5417,U+5428,U+542c,U+542f,U+5434,U+5458,U+54cd,U+56de,U+56e2,U+56ed,U+56f0,U+56f4,U+56fd-56fe,U+573a,U+574f,U+5757,U+575a,U+58ee,U+58f0,U+5904,U+5907,U+590d,U+591f,U+592b,U+5934,U+593a,U+594b,U+5956,U+5965,U+5987,U+5b59,U+5b66,U+5b81,U+5b83,U+5b9d-5b9e,U+5ba1,U+5bb6,U+5bbd-5bbe,U+5bf9,U+5bfb-5bfc,U+5c06,U+5c14,U+5c3d,U+5c40,U+5c42,U+5c4a,U+5c5e,U+5c81,U+5c97,U+5c9b,U+5ce1,U+5cf0,U+5de8,U+5e01,U+5e03,U+5e08,U+5e10,U+5e26,U+5e2d-5e2e,U+5e72,U+5e76,U+5e7f,U+5e84,U+5e86,U+5e8a,U+5e93-5e94,U+5e9f,U+5f00,U+5f02,U+5f20,U+5f3a,U+5f52-5f53,U+5f55,U+5f69,U+5f7b,U+5f81,U+5f84,U+5fd7,U+5ff5,U+6000-6001,U+603b,U+60ca,U+613f,U+620f,U+6218,U+6237,U+624d-624e,U+6258,U+6267,U+6269,U+626c,U+6298,U+62a2,U+62a4-62a5,U+62b5,U+62c5,U+62e5,U+62e9,U+6302,U+6325,U+635f,U+6362,U+636e,U+6444,U+6446,U+6559,U+6570,U+6597,U+65ad,U+65d7,U+65e0,U+65e2,U+65e7,U+65f6,U+663e,U+6653,U+672f,U+6731,U+673a,U+6742-6743,U+6761,U+6765,U+6768,U+676f-6770,U+677e-677f,U+6781,U+6784,U+679c,U+67e5,U+6807,U+6811,U+6837-6838,U+6863,U+6865,U+6881,U+68c0,U+697c,U+6b22,U+6b27,U+6b8b,U+6bd5,U+6c14,U+6c47,U+6c49,U+6c61,U+6c88,U+6ca1,U+6ce8,U+6cfd,U+6d01,U+6d4b,U+6d4e,U+6d89,U+6da6,U+6da8,U+6e10,U+6e29,U+6e38,U+6e7e,U+6ee1,U+6f5c,U+706f,U+7075,U+707e,U+70b9,U+70df,U+70ed,U+7231,U+72b6,U+72ec,U+732a,U+732e,U+73af-73b0,U+7535,U+753b,U+7597,U+76d0-76d1,U+76d6,U+76d8,U+771f,U+7740,U+77ff,U+7840,U+786e,U+793c,U+79bb,U+79c1,U+79cb,U+79cd,U+79d8,U+79ef-79f0,U+7a0e,U+7a33,U+7a77,U+7ade,U+7b14,U+7b51,U+7b79,U+7b7e,U+7b80,U+7c7b,U+7cae,U+7d27,U+7d2f,U+7ea2,U+7ea6-7ea7,U+7eaa,U+7eaf,U+7eb3,U+7eb7-7eb8,U+7eba,U+7ebf,U+7ec3-7ec4,U+7ec6-7ec8,U+7ecd,U+7ecf,U+7ed3,U+7ed5,U+7ed9,U+7edc-7edd,U+7edf,U+7ee7,U+7ee9,U+7eed,U+7ef4,U+7efc,U+7eff,U+7f13,U+7f16,U+7f29,U+7f51,U+7f57,U+7f5a,U+7fa4,U+804c,U+8054,U+8083,U+80c6,U+80cc,U+80dc,U+8111,U+811a,U+8131,U+81f4,U+8270,U+827a,U+8282,U+82cf,U+8303,U+8363,U+836f,U+83b7,U+8425,U+8428,U+8651,U+867d,U+8865,U+8868,U+88c5,U+89c1-89c2,U+89c4,U+89c6,U+89c8-89c9,U+8a89,U+8ba1-8ba2,U+8ba4,U+8ba8-8ba9,U+8bad-8bb0,U+8bb2,U+8bb8,U+8bba,U+8bbe-8bbf,U+8bc1,U+8bc4,U+8bc6,U+8bc9,U+8bcd,U+8bd5,U+8bd7,U+8bda,U+8bdd,U+8be2,U+8be5,U+8bed,U+8bef,U+8bf4,U+8bf7,U+8bfb,U+8bfe,U+8c01,U+8c03,U+8c08,U+8c0b,U+8c22,U+8d1f,U+8d21-8d23,U+8d25,U+8d27-8d28,U+8d2b,U+8d2d,U+8d2f,U+8d34-8d35,U+8d37-8d3a,U+8d44,U+8d5b,U+8d5e,U+8d75-8d76,U+8d8b,U+8dc3,U+8df5,U+8f66,U+8f68,U+8f6c,U+8f6e-8f6f,U+8f7b,U+8f7d,U+8f83,U+8f86,U+8f89,U+8f93,U+8fb9,U+8fbd-8fbe,U+8fc7,U+8fd0,U+8fd8-8fd9,U+8fdb-8fde,U+8ff9,U+9002,U+9009,U+9057,U+9093,U+90ae,U+90d1,U+91cc,U+9274,U+9488,U+949f,U+94a2,U+94b1,U+94c1,U+94f6,U+9500,U+9519,U+9526,U+952e,U+9547,U+957f,U+95e8,U+95ed-95ee,U+95f4,U+95fb,U+9614,U+961f,U+9633,U+9635-9636,U+9645-9646,U+9648,U+9669,U+968f,U+96be,U+9759,U+97e9,U+9879-987b,U+987e-987f,U+9881,U+9884,U+9886,U+9898,U+989d,U+98ce,U+98de,U+996d-996e,U+9970,U+9986,U+9a6c,U+9a7b,U+9a8c,U+9c7c,U+9c81,U+9c9c,U+9e21,U+9e4f,U+9ea6,U+9ec4,U+9ed1,U+9f50,U+9f84,U+9f99;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family:"HarmonyOS Sans SC";
|
||||||
|
src:local("HarmonyOS Sans SC Bold"),local("HarmonyOS Sans SC"),url("./Bold_c614c1.woff2") format("woff2");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range:U+3041-3094,U+309b-309e,U+30a1-30fe;
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user