25 changed files with 17 additions and 516 deletions
-2
View File
@@ -1,8 +1,6 @@
# Firmware build and release output.
/target/
/version/
/output/
/.venv-customer-fs/
# Local legacy-board packages retained when switching to the new-board branch.
/artifacts/legacy-no-gnss/
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.
+1 -1
View File
@@ -70,7 +70,7 @@ ota_ready_hxgnss_fs576
- `customer_fs.bin`:从原 256 KiB LittleFS 中提取全部 14 个音频文件,以 4096 字节块、
144 个块重新生成 576 KiB LittleFS;
- 新文件系统镜像 SHA-256:
`45350804d65fc6ded24caf9b215b71e89f9160d10d4998c5d57d4f4496d6000d`。
`51ad32f8196aeab36a62ed436ac65f495ce204436f8ef57efccafc87313b0d3c`。
### 3.2 运行身份
@@ -3,8 +3,9 @@
#include <string.h>
#include "ql_rtos.h"
#include "ql_application.h"
#include "ql_audio.h"
#include "ql_iic.h"
#include "ql_audio.h"
#include "ql_dev.h"
#include "ql_iic.h"
#include "ql_gpio.h"
#include "app_inc.h"
#include "ql_fs.h"
@@ -48,6 +49,7 @@ static volatile u32 g_nfc_tx_mode_deadline = 0;
#define NFC_TYPE2_READ_SIZE 16
#define NFC_TYPE2_MEMORY_SIZE 160
#define NFC_TYPE2_NDEF_OFFSET 16
#define NFC_IMEI_DIGITS 15
#define NFC_IRQ_TX 0x40
#define NFC_IRQ_RX 0x20
@@ -173,6 +175,7 @@ static void nfc_type2_build_ndef(void)
{
char device_id[32] = {0};
char serial_number[32] = {0};
char imei[32] = {0};
char json[128];
int json_len;
u8 *ndef;
@@ -184,10 +187,17 @@ static void nfc_type2_build_ndef(void)
if (hal_os_get_device_sn(serial_number) != 0) {
strcpy(serial_number, "unknown");
}
if (ql_dev_get_imei(imei, sizeof(imei)) != QL_DEV_SUCCESS ||
imei[0] == '\0') {
strcpy(imei, "unknown");
} else {
imei[NFC_IMEI_DIGITS] = '\0';
}
json_len = snprintf(json, sizeof(json),
"{\"device_id\":\"%s\",\"serial_number\":\"%s\"}",
device_id, serial_number);
"{\"device_id\":\"%s\",\"serial_number\":\"%s\","
"\"imei\":\"%s\"}",
device_id, serial_number, imei);
if (json_len < 0) {
json_len = 0;
} else if (json_len >= (int)sizeof(json)) {
Binary file not shown.
-215
View File
@@ -1,215 +0,0 @@
# customer_fs.bin 双系统生成工具
本目录提供 `EG800AKCN_11LG` 项目的 `customer_fs.bin` 自动生成和校验工具,支持
Windows 与 macOS。
当前 `customer_fs` 分区大小为 576 KiB,设备 kernel 只支持 LittleFS 磁盘格式
`0x00020000`(disk 2.0)。所有生成入口都会在输出前执行兼容性检查,避免再次生成
kernel 无法挂载的 disk 2.1 镜像。
## 1. 源文件目录
文件系统的源文件统一存放在:
```text
audio_prompts/customer_fs/
```
当前目录中的 14 个音频文件,全部从已经过实机验证、能够正常挂载的 Windows
`customer_fs.bin` 中提取,并经过重新打包和逐文件回读验证。
设备代码从 `U:/` 根目录访问文件,因此所有文件必须直接放在上述目录中。例如:
```text
正确:audio_prompts/customer_fs/network_connect_zh.mp3
错误:audio_prompts/customer_fs/subdir/network_connect_zh.mp3
```
生成工具会拒绝隐藏文件和子目录,避免把 `.DS_Store` 等无关文件打进正式镜像。
## 2. 如何增删改文件
生成工具采用“根据源目录全量重建”的方式,不是在旧 bin 上追加内容。
- 添加:把新文件直接放进 `audio_prompts/customer_fs/`;
- 替换:用新文件覆盖同名文件;
- 编辑:使用音频工具修改原文件并保存到原位置;
- 删除:从源目录删除文件,下一次生成的 bin 中将不再包含该文件;
- 改名:直接修改源文件名,但必须同步确认业务代码使用的新文件名。
部分音频文件名已经写入业务代码。删除或改名后,如果代码仍然播放原文件,串口会打印:
```text
tips file missing
```
因此,生成工具允许删减文件,但删减前仍需确认业务代码不再使用它。
## 3. Windows 一键生成
在项目根目录执行:
```bat
tools\customer_fs\build_windows.bat
```
Windows 版本使用项目自带并已通过实机验证的工具:
```text
ql-cross-tool\win32\mklfs-v2.exe
```
同时使用 SDK 自带的 Python 执行兼容性校验,不需要另外安装 Python 依赖。
也可以指定自定义源目录和输出文件:
```bat
tools\customer_fs\build_windows.bat 源目录 输出文件
```
例如:
```bat
tools\customer_fs\build_windows.bat ^
audio_prompts\customer_fs ^
output\customer_fs\customer_fs.bin
```
## 4. macOS 一键生成
在项目根目录执行:
```bash
./tools/customer_fs/build_macos.sh
```
首次运行时,脚本会自动创建:
```text
.venv-customer-fs
```
并安装项目固定的依赖版本:
```text
littlefs-python==0.19.0
```
新版 `littlefs-python` 默认会生成 disk 2.1 镜像,不能直接使用它自带的命令行生成。
本项目的 `generate_macos.py` 会显式设置:
```text
disk_version = 0x00020000
```
从而生成当前设备 kernel 可以识别的 disk 2.0 镜像。
也可以指定自定义源目录和输出文件:
```bash
./tools/customer_fs/build_macos.sh 源目录 输出文件
```
例如:
```bash
./tools/customer_fs/build_macos.sh \
audio_prompts/customer_fs \
output/customer_fs/customer_fs.bin
```
## 5. 默认输出位置
Windows 和 macOS 默认都输出到:
```text
output/customer_fs/customer_fs.bin
```
生成时会先写入临时文件:
```text
customer_fs.bin.tmp
```
只有生成和全部校验成功后,临时文件才会替换正式输出。生成或校验失败时,已有正式输出
不会被覆盖。
## 6. 自动门禁检查
每次生成都会检查:
| 检查项 | 要求 |
| --- | ---: |
| 镜像大小 | 589824 字节 |
| LittleFS 磁盘格式 | `0x00020000` |
| Block Size | 4096 字节 |
| Block Count | 144 |
| 第一个 superblock | 必须存在且参数正确 |
| 第二个 superblock | 必须存在且参数正确 |
| 源目录 | 不能为空 |
| 源文件层级 | 只能是根目录文件 |
| 隐藏文件 | 不允许 |
| 文件名 | 必须能在生成镜像中找到 |
也可以单独校验一个镜像:
```bash
python3 tools/customer_fs/verify_customer_fs.py \
--image output/customer_fs/customer_fs.bin \
--source audio_prompts/customer_fs
```
校验成功会输出镜像大小、LittleFS 版本、块参数、源文件数量和 SHA-256。
## 7. 放入正式固件工程
生成并校验成功后,需要显式复制到 SDK 打包位置。
macOS:
```bash
cp output/customer_fs/customer_fs.bin \
ql-config/config/EG800AKCN_11LG/customer_fs.bin
```
Windows:
```bat
copy /Y output\customer_fs\customer_fs.bin ^
ql-config\config\EG800AKCN_11LG\customer_fs.bin
```
替换前建议备份原文件并记录新镜像的 SHA-256。
`customer_fs.bin` 不是当前 APP-only OTA 的升级内容。修改该镜像后,需要重新生成并烧录
包含分区表、kernel、customer_app 和 customer_fs 的完整工厂包,不能只下发 APP OTA。
## 8. 实机验收
Windows 生成工具已经过实机验证。macOS 工具已经通过结构校验、重新挂载和逐文件回读,
首次用于正式版本前仍应至少烧录一台设备验收。
启动日志应直接出现:
```text
[U] mount successfully
```
不能出现:
```text
[U] mount failed (-22), will format fs partition
```
还需要逐一验证镜像中的音频能够通过 `U:/文件名` 正常读取和播放,并记录本次发布使用的
`customer_fs.bin` SHA-256。
## 9. 注意事项
- 不要用十六进制工具把 disk 2.1 镜像的版本字段直接改成 disk 2.0;
- 不要直接使用 `python -m littlefs create`,它当前默认生成 disk 2.1;
- 不要把 `.venv-customer-fs`、`output` 或 `.DS_Store` 提交到仓库;
- Windows 和 macOS 生成镜像的整体 SHA-256 可能不同,这是块分配顺序不同造成的;
- 两个平台产物必须满足相同的 disk 2.0、4096 字节块和 144 块约束;
- 修改、删除或重命名业务音频前,必须同步检查代码中的文件名引用。
-38
View File
@@ -1,38 +0,0 @@
#!/bin/sh
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
ROOT_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)
SOURCE_DIR=${1:-"$ROOT_DIR/audio_prompts/customer_fs"}
OUTPUT_IMAGE=${2:-"$ROOT_DIR/output/customer_fs/customer_fs.bin"}
VENV_DIR=${CUSTOMER_FS_VENV:-"$ROOT_DIR/.venv-customer-fs"}
PYTHON_BOOTSTRAP=${PYTHON3:-python3}
TEMP_IMAGE="${OUTPUT_IMAGE}.tmp"
cleanup() {
rm -f -- "$TEMP_IMAGE"
}
trap cleanup EXIT INT TERM
if [ ! -x "$VENV_DIR/bin/python" ]; then
"$PYTHON_BOOTSTRAP" -m venv "$VENV_DIR"
fi
if ! "$VENV_DIR/bin/python" -c 'import littlefs; raise SystemExit(0 if littlefs.__version__ == "0.19.0" else 1)' 2>/dev/null; then
"$VENV_DIR/bin/python" -m pip install --requirement "$SCRIPT_DIR/requirements.txt"
fi
mkdir -p -- "$(dirname -- "$OUTPUT_IMAGE")"
rm -f -- "$TEMP_IMAGE"
"$VENV_DIR/bin/python" "$SCRIPT_DIR/generate_macos.py" \
--source "$SOURCE_DIR" \
--output "$TEMP_IMAGE"
"$VENV_DIR/bin/python" "$SCRIPT_DIR/verify_customer_fs.py" \
--image "$TEMP_IMAGE" \
--source "$SOURCE_DIR"
mv -f -- "$TEMP_IMAGE" "$OUTPUT_IMAGE"
trap - EXIT INT TERM
printf 'customer_fs ready: %s\n' "$OUTPUT_IMAGE"
-56
View File
@@ -1,56 +0,0 @@
@echo off
setlocal EnableExtensions
set "SCRIPT_DIR=%~dp0"
for %%I in ("%SCRIPT_DIR%..\..") do set "ROOT_DIR=%%~fI"
if "%~1"=="" (
set "SOURCE_DIR=%ROOT_DIR%\audio_prompts\customer_fs"
) else (
set "SOURCE_DIR=%~f1"
)
if "%~2"=="" (
set "OUTPUT_IMAGE=%ROOT_DIR%\output\customer_fs\customer_fs.bin"
) else (
set "OUTPUT_IMAGE=%~f2"
)
set "TEMP_IMAGE=%OUTPUT_IMAGE%.tmp"
set "MKLFS=%ROOT_DIR%\ql-cross-tool\win32\mklfs-v2.exe"
set "PYTHON=%ROOT_DIR%\ql-cross-tool\win32\owtoolchain\Python\python.exe"
if not exist "%MKLFS%" (
echo customer_fs generation failed: mklfs-v2.exe not found at "%MKLFS%"
exit /b 1
)
if not exist "%PYTHON%" (
echo customer_fs generation failed: bundled Python not found at "%PYTHON%"
exit /b 1
)
if not exist "%SOURCE_DIR%\" (
echo customer_fs generation failed: source directory not found at "%SOURCE_DIR%"
exit /b 1
)
for %%I in ("%OUTPUT_IMAGE%") do if not exist "%%~dpI" mkdir "%%~dpI"
if exist "%TEMP_IMAGE%" del /q "%TEMP_IMAGE%"
"%MKLFS%" -c "%SOURCE_DIR%" -s 589824 -i "%TEMP_IMAGE%"
if errorlevel 1 goto :failed
"%PYTHON%" "%SCRIPT_DIR%verify_customer_fs.py" ^
--image "%TEMP_IMAGE%" ^
--source "%SOURCE_DIR%"
if errorlevel 1 goto :failed
move /Y "%TEMP_IMAGE%" "%OUTPUT_IMAGE%" >nul
if errorlevel 1 goto :failed
echo customer_fs ready: "%OUTPUT_IMAGE%"
exit /b 0
:failed
if exist "%TEMP_IMAGE%" del /q "%TEMP_IMAGE%"
echo customer_fs generation failed; existing output was not changed.
exit /b 1
-76
View File
@@ -1,76 +0,0 @@
#!/usr/bin/env python3
"""Generate a disk-2.0 LittleFS customer_fs image on macOS."""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from littlefs import LittleFS, UserContext
IMAGE_SIZE = 589824
BLOCK_SIZE = 4096
BLOCK_COUNT = 144
DISK_VERSION = 0x00020000
def fail(message: str) -> None:
raise SystemExit(f"customer_fs generation failed: {message}")
def collect_source_files(source: Path) -> list[Path]:
if not source.is_dir():
fail(f"source directory does not exist: {source}")
entries = sorted(source.iterdir(), key=lambda path: path.name.encode("utf-8"))
if not entries:
fail(f"source directory is empty: {source}")
invalid = [entry.name for entry in entries if not entry.is_file() or entry.name.startswith(".")]
if invalid:
fail(
"source must contain only visible root-level files; invalid entries: "
+ ", ".join(invalid)
)
return entries
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--source", required=True, type=Path)
parser.add_argument("--output", required=True, type=Path)
args = parser.parse_args()
files = collect_source_files(args.source)
context = UserContext(buffsize=IMAGE_SIZE)
fs = LittleFS(
context=context,
mount=False,
block_size=BLOCK_SIZE,
block_count=BLOCK_COUNT,
read_size=16,
prog_size=16,
cache_size=256,
lookahead_size=32,
block_cycles=500,
disk_version=DISK_VERSION,
)
fs.format()
fs.mount()
try:
for source_file in files:
with source_file.open("rb") as source_handle:
with fs.open("/" + source_file.name, "wb") as image_handle:
while chunk := source_handle.read(64 * 1024):
image_handle.write(chunk)
finally:
fs.unmount()
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_bytes(context.buffer)
print(f"generated {args.output} from {len(files)} files")
return 0
if __name__ == "__main__":
sys.exit(main())
-1
View File
@@ -1 +0,0 @@
littlefs-python==0.19.0
-121
View File
@@ -1,121 +0,0 @@
#!/usr/bin/env python3
"""Validate a customer_fs LittleFS image for the EG800AKCN_11LG kernel."""
from __future__ import annotations
import argparse
import hashlib
import struct
import sys
from pathlib import Path
DEFAULT_SIZE = 589824
DEFAULT_DISK_VERSION = 0x00020000
DEFAULT_BLOCK_SIZE = 4096
DEFAULT_BLOCK_COUNT = 144
SUPERBLOCK_MAGIC_OFFSETS = (8, DEFAULT_BLOCK_SIZE + 8)
def fail(message: str) -> None:
raise SystemExit(f"customer_fs verification failed: {message}")
def source_files(source: Path) -> list[Path]:
if not source.is_dir():
fail(f"source directory does not exist: {source}")
entries = sorted(source.iterdir(), key=lambda path: path.name.encode("utf-8"))
if not entries:
fail(f"source directory is empty: {source}")
invalid = [entry.name for entry in entries if not entry.is_file() or entry.name.startswith(".")]
if invalid:
fail(
"source must contain only visible root-level files; invalid entries: "
+ ", ".join(invalid)
)
return entries
def verify_superblock(
image: bytes,
magic_offset: int,
disk_version: int,
block_size: int,
block_count: int,
) -> None:
if image[magic_offset : magic_offset + 8] != b"littlefs":
fail(f"LittleFS magic is missing at 0x{magic_offset:X}")
actual_version, actual_block_size, actual_block_count = struct.unpack_from(
"<III", image, magic_offset + 12
)
if actual_version != disk_version:
fail(
f"disk version at 0x{magic_offset:X} is 0x{actual_version:08X}; "
f"expected 0x{disk_version:08X}"
)
if actual_block_size != block_size:
fail(
f"block size at 0x{magic_offset:X} is {actual_block_size}; "
f"expected {block_size}"
)
if actual_block_count != block_count:
fail(
f"block count at 0x{magic_offset:X} is {actual_block_count}; "
f"expected {block_count}"
)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--image", required=True, type=Path)
parser.add_argument("--source", type=Path, help="also check root-level source filenames")
parser.add_argument("--size", type=int, default=DEFAULT_SIZE)
parser.add_argument(
"--disk-version", type=lambda value: int(value, 0), default=DEFAULT_DISK_VERSION
)
parser.add_argument("--block-size", type=int, default=DEFAULT_BLOCK_SIZE)
parser.add_argument("--block-count", type=int, default=DEFAULT_BLOCK_COUNT)
args = parser.parse_args()
if not args.image.is_file():
fail(f"image does not exist: {args.image}")
image = args.image.read_bytes()
if len(image) != args.size:
fail(f"image size is {len(image)}; expected {args.size}")
if args.block_size * args.block_count != args.size:
fail("block size multiplied by block count does not equal image size")
for magic_offset in (8, args.block_size + 8):
verify_superblock(
image,
magic_offset,
args.disk_version,
args.block_size,
args.block_count,
)
files: list[Path] = []
if args.source:
files = source_files(args.source)
missing = [path.name for path in files if path.name.encode("utf-8") not in image]
if missing:
fail("source filenames missing from image: " + ", ".join(missing))
digest = hashlib.sha256(image).hexdigest()
print("customer_fs verified")
print(f" image: {args.image}")
print(f" size: {len(image)}")
print(f" disk_version: 0x{args.disk_version:08X}")
print(f" block_size: {args.block_size}")
print(f" block_count: {args.block_count}")
if files:
print(f" source_files: {len(files)}")
print(f" sha256: {digest}")
return 0
if __name__ == "__main__":
sys.exit(main())
+1 -1
View File
@@ -42,7 +42,7 @@ EXPECTED = {
}
FBF_HEADER_ALLOWANCE = 8192
MINIMUM_SYSTEM_MARGIN = 64 * 1024
EXPECTED_FS_SHA256 = "45350804d65fc6ded24caf9b215b71e89f9160d10d4998c5d57d4f4496d6000d"
EXPECTED_FS_SHA256 = "51ad32f8196aeab36a62ed436ac65f495ce204436f8ef57efccafc87313b0d3c"
def fail(message: str) -> None: