本网站为个人网站，主要用来记录自己在学习计算机编程语言过程中遇到的问题和编写的代码以及解决问题的办法，方便后期用来测试使用与分享
前因逛模之屋的时候发现，里面居然有live2d模型的分享，遂下载下来想放到网站上体验，但是发现模型版本是Live2D Cubism 4，而原始插件Pio只支持到Live2D Cubism 2，于是便想将Live2D SDK更新一下，支持最新的模型。实施方案修改模型发现逻辑，同时识别 model.json（Cubism 2）和 *.model3.json（Cubism 3/4），并将画布从 canvas改为 div容器用 pixi-live2d-display 替换 loadlive2d()修改模型发现逻辑 foreach($load as $key => $value){ $single = substr($value, 26); // 优先检测 Cubism 3/4 的 model3.json $model3_files = glob($value . "/*.model3.json"); if(!empty($model3_files)){ $models[$single] = ucfirst($single) . " (Cubism 3/4)"; } // 再检测 Cubism 2 的 model.json else if(file_exists($value . "/model.json")){ $models[$single] = ucfirst($single); } };下载SDKcdn感觉不如本地稳定文件用途pixi.min.jsPixiJS 渲染引擎live2d.min.jsCubism 2 运行库（pio/kurumi 模型需要）live2dcubismcore.min.jsCubism 4 运行库（Nahida 模型需要）pixi-live2d-display.min.js统一加载器，同时支持 Cubism 2/3/4下载链接替换旧api重写 pio.js，用 pixi-live2d-display 的 PIXI.live2d.Live2DModel.from() 替换旧的 loadlive2d()，同时保留所有原有交互功能。主要修改部分：// 加载模型（支持 Cubism 2 model.json 和 Cubism 3/4 *.model3.json） const loadModel = async (modelUrl) => { if (!window.PIXI || !PIXI.live2d) { console.error("pixi-live2d-display 未加载"); return; } // 首次加载时创建 PIXI.Application if (!current.app) { const width = current.container.clientWidth || 280; const height = current.container.clientHeight || 250; current.app = new PIXI.Application({ width: width, height: height, backgroundAlpha: 0, view: current.container.tagName === "CANVAS" ? current.container : undefined }); // 如果容器不是 canvas，将 PIXI 创建的 canvas 插入容器 if (current.container.tagName !== "CANVAS") { current.container.innerHTML = ""; current.container.appendChild(current.app.view); current.app.view.style.width = width + "px"; current.app.view.style.height = height + "px"; } } // 移除旧模型 if (current.model) { current.app.stage.removeChild(current.model); current.model.destroy(); current.model = null; } try { const model = await PIXI.live2d.Live2DModel.from(modelUrl, { autoInteract: false }); // 自适应缩放 const scaleX = current.app.view.width / model.width; const scaleY = current.app.view.height / model.height; const scale = Math.min(scaleX, scaleY); model.scale.set(scale); // 居中 model.x = (current.app.view.width - model.width * scale) / 2; model.y = (current.app.view.height - model.height * scale) / 2; current.model = model; current.app.stage.addChild(model); } catch (e) { console.error("Live2D 模型加载失败:", e); } }; Bug修复后台没有配置"交互提示扩展"，PHP 端 json_decode 得到的是 null，传到 JS 后 prop.content 就是 undefined，访问 .referer 就报错了。加上 prop.content = prop.content || {};function getCanvas() 和 function getLoader() 定义在方法内部，第二次访问页面时会报 Cannot redeclare 致命错误。设置 anchor 为中心点，让模型以中心为基准定位，解决偏移问题。Math.min 会让模型按较短的边来缩放，确保整个模型都在画布内，不会有任何部分被裁切。如果有空白区域也只是某一侧留白，不会影响显示。表情添加由于没有表情相关的代码，需要新加，以下是特性对比特性Cubism 2Cubism 3/4眼球物理（眼珠晃动惯性）无有 - 眼球有弹性和延迟表情系统 (Expression)无独立表情文件有 - .exp3.json 独立表情嘴型同步 (LipSync)简单有 - ParamMouthOpenY自动眨眼 (EyeBlink)需手动实现内置 - ParamEyeLOpen/ParamEyeROpen高级物理引擎简单单摆多层级弹簧物理 (120fps)// 触发表情（仅 Cubism 3/4 模型支持） const triggerExpression = (name) => { if (!current.model) return; const model = current.model; // pixi-live2d-display 的 expression() 方法 if (model.internalModel && model.internalModel.motionManager) { // Cubism 4 模型 if (model.internalModel.settings && model.internalModel.settings.expressions) { const expressions = model.internalModel.settings.expressions; if (expressions.length === 0) return; if (name) { model.expression(name); } else { // 随机触发 const randIdx = Math.floor(Math.random() * expressions.length); model.expression(randIdx); } return true; } // Cubism 2 model.json 中的 expressions if (model.settings && Array.isArray(model.settings.expressions) && model.settings.expressions.length > 0) { const randIdx = Math.floor(Math.random() * model.settings.expressions.length); model.expression(randIdx); return true; } } return false; }; // 获取可用的表情名称列表 const getExpressionNames = () => { if (!current.model) return []; const model = current.model; const names = []; if (model.internalModel && model.internalModel.settings && model.internalModel.settings.expressions) { model.internalModel.settings.expressions.forEach((exp) => { const expName = exp.Name || exp.File.replace(/\.exp3?\.json$/, ""); names.push(expName); }); } if (model.settings && Array.isArray(model.settings.expressions)) { model.settings.expressions.forEach((exp) => { names.push(exp.Name || exp.file || exp); }); } return names; };model3.json 的 FileReferences 里没有 Expressions 字段，只有 Moc、Textures、Physics、DisplayInfo。需要把 Expressions 和 Motions 配置补进 model3.json。"DisplayInfo": "Nahida_1080.cdi3.json", "Expressions": [ {"Name": "Angry", "File": "Angry.exp3.json"}, {"Name": "Happy1", "File": "Happy1.exp3.json"}, {"Name": "Sad1", "File": "Sad1.exp3.json"}, {"Name": "Sad2", "File": "Sad2.exp3.json"}, {"Name": "Shy", "File": "Shy.exp3.json"}, {"Name": "shy_normal", "File": "shy_normal.exp3.json"}, {"Name": "Wink", "File": "Wink.exp3.json"}, {"Name": "Halfeyes", "File": "Halfeyes.exp3.json"}, {"Name": "StarEye", "File": "StarEye.exp3.json"}, {"Name": "HandChange", "File": "HandChange.exp3.json"}, {"Name": "kusa", "File": "kusa.exp3.json"}, {"Name": "black", "File": "black.exp3.json"}, {"Name": "mouthchange", "File": "mouthchange.exp3.json"} ]结语修复之后就可以使用啦
前言在B站看到一个项目仅用几十KB就构建了一个Minecraft服务端，运行更是只需要少得可怜的资源（esp32-c3都能运行），于是记录一下在Alpine Linux上的移植过程。开始因为使用的是 Alpine Linux，它底层使用 musl 而不是通常的 glibc (Ubuntu) 或 uClibc (Buildroot)。这需要一个专门针对 arm-linux-musleabihf 的交叉编译器，否则编译出的程序在板子上运行时会报错（例如 File not found 或段错误）。因此我们需要获取 musl 交叉编译工具链，不过我建议在宿主机手动下载然后传到wsl，不然这速度太慢了。 # 下载工具链 wget https://musl.cc/arm-linux-musleabihf-cross.tgz # 解压 tar -xf arm-linux-musleabihf-cross.tgz echo 'export PATH=$PATH:/home/luckfox/Desktop/arm-linux-musleabihf-cross/bin' >> ~/.bashrc source ~/.bashrc arm-linux-musleabihf-gcc --version搭建编译环境，运行setup_env.sh#!/bin/bash # Setup script for BareIron development environment on WSL/Ubuntu # Installs Java 21 and Node.js set -e echo "[1/3] Updating package lists..." sudo apt update echo "[2/3] Installing dependencies (curl, wget, tar, build-essential)..." sudo apt install -y curl wget tar sudo build-essential echo "[3/3] Installing Java 21..." # Check if OpenJDK 21 is available in default repos if apt-cache search openjdk-21-jdk | grep -q openjdk-21-jdk; then sudo apt install -y openjdk-21-jdk else echo "OpenJDK 21 not found in default repositories. Adding PPA.." sudo add-apt-repository -y ppa:openjdk-r/ppa sudo apt update sudo apt install -y openjdk-21-jdk fi # Verify Java installation java -version echo "Installing Node.js (Latest LTS)..." # Using NodeSource for up-to-date Node.js if ! command -v node &> /dev/null; then curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt install -y nodejs else echo "Node.js is already installed." fi # Verify Node installation node -v echo "--------------------------------------------------------" echo "Setup Complete!" echo "--------------------------------------------------------" echo "1. Java 21 and Node.js have been installed." echo "" echo "Note: You need to manually install or configure your cross-compilation toolchain." echo "For Luckfox Pico (Alpine), ensure 'arm-linux-musleabihf-gcc' is in your PATH." echo "" echo "Then you can proceed with:" echo " ./extract_registries.sh" echo " ./build_luckfox.sh --type alpine" 准备注册表数据：运行 ./extract_registries.sh 。（没权限自己添加），手动下载mc 1.21.8 server.jar，放到notchian文件夹，然后继续执行./extract_registries.sh添加交叉编译编译脚本build_luckfox.sh#!/bin/bash # Build script for Luckfox Pico (RV1103) # Target Architecture: ARMv7l (Cortex-A7) set -e # Default settings BUILD_TYPE="buildroot" # Options: buildroot, ubuntu, alpine OUTPUT_NAME="bareiron_luckfox" # Check for registries if [ ! -f "include/registries.h" ]; then echo "Error: 'include/registries.h' is missing." echo "Please run './extract_registries.sh' first (requires Minecraft server jar and Java 21+)." exit 1 fi # Help function print_usage() { echo "Usage: $0 [options]" echo "Options:" echo " --type <type> Target system type: 'buildroot' (default), 'ubuntu', or 'alpine'" echo " --help Show this help message" } # Parse arguments while "$#" -gt 0 ; do case $1 in --type) BUILD_TYPE="$2"; shift ;; --help) print_usage; exit 0 ;; *) echo "Unknown parameter: $1"; print_usage; exit 1 ;; esac shift done echo "Building for Luckfox Pico ($BUILD_TYPE)..." # Set compiler based on type if [ "$BUILD_TYPE" == "buildroot" ]; then # Toolchain for Buildroot (uClibc) DEFAULT_CC_PATH="/home/buildroot/luckfox-pico/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin/arm-rockchip830-linux-uclibcgnueabihf-gcc" CC="${CROSS_COMPILE:-$DEFAULT_CC_PATH}" if ! command -v "$CC" &> /dev/null && [ ! -x "$CC" ]; then echo "Error: Compiler not found at '$CC'" echo "Please set CROSS_COMPILE environment variable to your toolchain path." exit 1 fi elif [ "$BUILD_TYPE" == "ubuntu" ]; then # Toolchain for Ubuntu (glibc) DEFAULT_CC_PATH="arm-none-linux-gnueabihf-gcc" CC="${CROSS_COMPILE:-$DEFAULT_CC_PATH}" if ! command -v "$CC" &> /dev/null; then echo "Error: Compiler '$CC' not found in PATH." echo "Please install 'gcc-arm-linux-gnueabihf' or set CROSS_COMPILE." exit 1 fi elif [ "$BUILD_TYPE" == "alpine" ]; then # Toolchain for Alpine (musl) # User needs to download a musl toolchain, e.g., from musl.cc or bootlin DEFAULT_CC_PATH="arm-linux-musleabihf-gcc" CC="${CROSS_COMPILE:-$DEFAULT_CC_PATH}" # Try to find compiler in common locations if not in PATH if ! command -v "$CC" &> /dev/null; then # Check user-provided path from common locations POTENTIAL_PATH="/home/luckfox/Desktop/arm-linux-musleabihf-cross/bin/$DEFAULT_CC_PATH" if [ -x "$POTENTIAL_PATH" ]; then CC="$POTENTIAL_PATH" fi fi if ! command -v "$CC" &> /dev/null && [ ! -x "$CC" ]; then echo "Error: Compiler '$CC' not found in PATH." echo "For Alpine, you need a musl-based toolchain (e.g., arm-linux-musleabihf-gcc)." echo "Please download one (e.g. from musl.cc) and add it to PATH or set CROSS_COMPILE." exit 1 fi else echo "Error: Invalid build type '$BUILD_TYPE'. Use 'buildroot', 'ubuntu', or 'alpine'." exit 1 fi echo "Using compiler: $CC" # Clean previous build rm -f "$OUTPUT_NAME" # Compile flags # -O2: Optimization # -Iinclude: Include directory # -lpthread: Threading support (needed for glibc/uclibc, usually implicit in musl but harmless) # -lm: Math library # -static: For Alpine/Musl, static linking is often preferred for portability, # but if you have the libraries on the board, dynamic is fine. # We will default to dynamic to match other builds, but you can uncomment -static below if needed. # LDFLAGS="-static" $CC src/*.c -O2 -Iinclude -o "$OUTPUT_NAME" -lpthread -lm if [ -f "$OUTPUT_NAME" ]; then echo "Build successful! Binary: $OUTPUT_NAME" file "$OUTPUT_NAME" else echo "Build failed." exit 1 fi运行前修改项目中include/globals.h，在嵌入式系统（如 ESP32）上， task_yield() 通常用于让出 CPU 时间片给其他任务，但在 Linux/PC 上，如果这个宏定义为空（或者只是一个空操作），程序就会在一个死循环里全速空转，导致单核 CPU 占用率飙升到 100%。Linux/Alpine : 使用 usleep(1000) (休眠 1 毫秒)#ifndef H_GLOBALS #define H_GLOBALS #include <stdint.h> #include <unistd.h> #ifdef ESP_PLATFORM #define WIFI_SSID "your-ssid" #define WIFI_PASS "your-password" void task_yield (); #else #ifdef _WIN32 #define task_yield() Sleep(1) #else #define task_yield() usleep(1000) #endif #endif #define true 1 #define false 0 // TCP port, Minecraft's default is 25565 #define PORT 25565 // How many players to keep in memory, NOT the amount of concurrent players // Even when offline, players who have logged on before take up a slot #define MAX_PLAYERS 16 // How many mobs to allocate memory for #define MAX_MOBS (MAX_PLAYERS) // Manhattan distance at which mobs despawn #define MOB_DESPAWN_DISTANCE 256 // Server game mode: 0 - survival; 1 - creative; 2 - adventure; 3 - spectator #define GAMEMODE 0 // Max render distance, determines how many chunks to send #define VIEW_DISTANCE 2 // Time between server ticks in microseconds (default = 1s) #define TIME_BETWEEN_TICKS 1000000 // Calculated from TIME_BETWEEN_TICKS #define TICKS_PER_SECOND ((float)1000000 / TIME_BETWEEN_TICKS) // Initial world generation seed, will be hashed on startup // Used in generating terrain and biomes #define INITIAL_WORLD_SEED 0xA103DE6C // Initial general RNG seed, will be hashed on startup // Used in random game events like item drops and mob behavior #define INITIAL_RNG_SEED 0xE2B9419 // Size of each bilinearly interpolated area ("minichunk") // For best performance, CHUNK_SIZE should be a power of 2 #define CHUNK_SIZE 8 // Terrain low point - should start a bit below sea level for rivers/lakes #define TERRAIN_BASE_HEIGHT 60 // Cave generation Y level #define CAVE_BASE_DEPTH 24 // Size of every major biome in multiples of CHUNK_SIZE // For best performance, should also be a power of 2 #define BIOME_SIZE (CHUNK_SIZE * 8) // Calculated from BIOME_SIZE #define BIOME_RADIUS (BIOME_SIZE / 2) // How many visited chunk coordinates to "remember" // The server will not re-send chunks that the player has recently been in // Must be at least 1, otherwise chunks will be sent on each position update #define VISITED_HISTORY 4 // How many player-made block changes to allow // Determines the fixed amount of memory allocated to blocks #define MAX_BLOCK_CHANGES 20000 // If defined, writes and reads world data to/from disk (or flash). // This is a synchronous operation, and can cause performance issues if // frequent random disk access is slow. Data is still stored in and // accessed from memory - reading from disk is only done on startup. // When targeting ESP-IDF, LittleFS is used to manage flash reads and // writes. Flash is typically *very* slow and unreliable, which is why // this option is disabled by default when targeting ESP-IDF. #ifndef ESP_PLATFORM #define SYNC_WORLD_TO_DISK #endif // The minimum interval (in microseconds) at which certain data is written // to disk/flash. Bounded on the low end by TIME_BETWEEN_TICKS. By default, // applies only to player data. Block changes are written as soon as they // are made, but in much smaller portions. Set DISK_SYNC_BLOCKS_ON_INTERVAL // to make this apply to block changes as well. #define DISK_SYNC_INTERVAL 15000000 // Whether to sync block changes to disk on an interval, instead of syncing // on each change. On systems with fast random disk access, this shouldn't // be necessary. // #define DISK_SYNC_BLOCKS_ON_INTERVAL // Time in microseconds to spend waiting for data transmission before // timing out. Default is 15s, which leaves 5s to prevent starving other // clients from Keep Alive packets. #define NETWORK_TIMEOUT_TIME 15000000 // Size of the receive buffer for incoming string data #define MAX_RECV_BUF_LEN 256 // If defined, sends the server brand to clients. Doesn't do much, but will // show up in the top-left of the F3/debug menu, in the Minecraft client. // You can change the brand string in the "brand" variable in src/globals.c #define SEND_BRAND // If defined, rebroadcasts ALL incoming movement updates, disconnecting // movement from the server's tickrate. This makes movement much smoother // on very low tickrates, at the cost of potential network instability when // hosting more than just a couple of players. When disabling this on low // tickrates, consider disabling SCALE_MOVEMENT_UPDATES_TO_PLAYER_COUNT too. #define BROADCAST_ALL_MOVEMENT // If defined, scales the frequency at which player movement updates are // broadcast based on the amount of players, reducing overhead for higher // player counts. For very many players, makes movement look jittery. // It is not recommended to use this if BROADCAST_ALL_MOVEMENT is disabled // on low tickrates, as that might drastically decrease the update rate. #define SCALE_MOVEMENT_UPDATES_TO_PLAYER_COUNT // If defined, calculates fluid flow when blocks are updated near fluids // Somewhat computationally expensive and potentially unstable #define DO_FLUID_FLOW // If defined, allows players to craft and use chests. // Chests take up 15 block change slots each, require additional checks, // and use some terrible memory hacks to function. On some platforms, this // could cause bad performance or even crashes during gameplay. #define ALLOW_CHESTS // If defined, enables flight for all players. As a side-effect, allows // players to sprint when starving. // #define ENABLE_PLAYER_FLIGHT // If defined, enables the item pickup animation when mining a block/ // Does not affect how item pickups work! Items from broken blocks still // get placed directly in the inventory, this is just an animation. // Relatively inexpensive, though requires sending a few more packets // every time a block is broken. #define ENABLE_PICKUP_ANIMATION // If defined, players are able to receive damage from nearby cacti. #define ENABLE_CACTUS_DAMAGE // If defined, logs unrecognized packet IDs // #define DEV_LOG_UNKNOWN_PACKETS // If defined, logs cases when packet length doesn't match parsed byte count #define DEV_LOG_LENGTH_DISCREPANCY // If defined, log chunk generation events // #define DEV_LOG_CHUNK_GENERATION // If defined, allows dumping world data by sending 0xBEEF (big-endian), // and uploading world data by sending 0xFEED, followed by the data buffer. // Doesn't implement authentication, hence disabled by default. // #define DEV_ENABLE_BEEF_DUMPS #define STATE_NONE 0 #define STATE_STATUS 1 #define STATE_LOGIN 2 #define STATE_TRANSFER 3 #define STATE_CONFIGURATION 4 #define STATE_PLAY 5 extern ssize_t recv_count; extern uint8_t recv_buffer[MAX_RECV_BUF_LEN]; extern uint32_t world_seed; extern uint32_t rng_seed; extern uint16_t world_time; extern uint32_t server_ticks; extern char motd[]; extern uint8_t motd_len; #ifdef SEND_BRAND extern char brand[]; extern uint8_t brand_len; #endif extern uint16_t client_count; typedef struct { short x; short z; uint8_t y; uint8_t block; } BlockChange; #pragma pack(push, 1) typedef struct { uint8_t uuid[16]; char name[16]; int client_fd; short x; uint8_t y; short z; short visited_x[VISITED_HISTORY]; short visited_z[VISITED_HISTORY]; #ifdef SCALE_MOVEMENT_UPDATES_TO_PLAYER_COUNT uint16_t packets_since_update; #endif int8_t yaw; int8_t pitch; uint8_t grounded_y; uint8_t health; uint8_t hunger; uint16_t saturation; uint8_t hotbar; uint16_t inventory_items[41]; uint16_t craft_items[9]; uint8_t inventory_count[41]; uint8_t craft_count[9]; // Usage depends on player's flags, see below // When no flags are set, acts as cursor item ID uint16_t flagval_16; // Usage depends on player's flags, see below // When no flags are set, acts as cursor item count uint8_t flagval_8; // 0x01 - attack cooldown, uses flagval_8 as the timer // 0x02 - has not spawned yet // 0x04 - sneaking // 0x08 - sprinting // 0x10 - eating, makes flagval_16 act as eating timer // 0x20 - client loading, uses flagval_16 as fallback timer // 0x40 - movement update cooldown // 0x80 - craft_items lock (for storing pointers) uint8_t flags; } PlayerData; typedef struct { uint8_t type; short x; // When the mob is dead (health is 0), the Y coordinate acts // as a timer for deleting and deallocating the mob uint8_t y; short z; // Lower 5 bits: health // Middle 1 bit: sheep sheared, unused for other mobs // Upper 2 bits: panic timer uint8_t data; } MobData; #pragma pack(pop) union EntityDataValue { uint8_t byte; int pose; }; typedef struct { uint8_t index; // 0 - Byte // 21 - Pose int type; union EntityDataValue value; } EntityData; extern BlockChange block_changes[MAX_BLOCK_CHANGES]; extern int block_changes_count; extern PlayerData player_data[MAX_PLAYERS]; extern int player_data_count; extern MobData mob_data[MAX_MOBS]; #endif执行命令chmod +x build_luckfox.sh ./build_luckfox.sh --type alpine传输到板子root目录上，配置后台运行编写openrc/bareiron#!/sbin/openrc-run name="BareIron Minecraft Server" description="Minimalist Minecraft server for embedded systems" # Path to the executable command="/root/bareiron_luckfox" # Background the process command_background=true # PID file location pidfile="/run/${RC_SVCNAME}.pid" # Working directory (where world.bin is stored) directory="/root" # Redirect output to log file (optional, helps debugging) output_log="/var/log/bareiron.log" error_log="/var/log/bareiron.err" depend() { # Start after networking is ready need net # Use DNS if available use dns logger } start_pre() { # Ensure the binary exists and is executable if [ ! -x "$command" ]; then eerror "Executable not found or not executable: $command" return 1 fi einfo "Starting $name..." } 编写install_service.sh#!/bin/sh # Installation script for BareIron OpenRC service on Alpine Linux # Run this on your Luckfox Pico SERVICE_NAME="bareiron" SERVICE_SCRIPT="openrc/bareiron" TARGET_DIR="/etc/init.d" BINARY_NAME="bareiron_luckfox" BINARY_PATH="/root/$BINARY_NAME" # Check if running as root if [ "$(id -u)" -ne 0 ]; then echo "Error: This script must be run as root." exit 1 fi # Check if binary exists on the board if [ ! -f "$BINARY_PATH" ]; then echo "Warning: Binary '$BINARY_PATH' not found." echo "Make sure you have copied '$BINARY_NAME' to /root/" fi echo "Installing service script..." if [ ! -f "$SERVICE_SCRIPT" ]; then echo "Error: Service script '$SERVICE_SCRIPT' not found in current directory." echo "Please upload the 'openrc' folder to the board first." exit 1 fi cp "$SERVICE_SCRIPT" "$TARGET_DIR/$SERVICE_NAME" chmod +x "$TARGET_DIR/$SERVICE_NAME" echo "Service installed to $TARGET_DIR/$SERVICE_NAME" echo "Adding service to default runlevel (autostart on boot)..." rc-update add "$SERVICE_NAME" default echo "---------------------------------------------------" echo "Installation Complete!" echo "---------------------------------------------------" echo "To start the server now:" echo " rc-service $SERVICE_NAME start" echo "" echo "To stop the server:" echo " rc-service $SERVICE_NAME stop" echo "" echo "To check status:" echo " rc-service $SERVICE_NAME status" echo "" echo "To view logs:" echo " tail -f /var/log/bareiron.log"执行命令，然后删除openrc/bareiron和install_service.shchmod +x install_service.sh ./install_service.sh启动： rc-service bareiron start停止： rc-service bareiron stop查看日志： tail -f /var/log/bareiron.log如何游玩下载 : 直接搜索 "HMCL 下载" 或访问其 GitHub/官网。使用方法 :打开 HMCL。点击“账户” -> 添加离线账户 (随便起个名字)。点击“下载” -> 选择 1.21.8 版本（必须与服务器版本一致）。下载完成后，点击“启动游戏”。进入游戏后，选择“多人游戏” -> “添加服务器” -> 输入Luckfox 板子的 IP 地址 -> 连接。配置内网穿透使用樱花frp创建tcp隧道重新配置内外穿透配置域名，具体看设置 SRV 解析多人服务器，填入域名（一般延迟80ms）enjoy yourself！
前言之前买了一个luckfox pico plus，有一说一，当时不知道为什么脑子抽了买个这玩意，60多💴，拿来不知道有啥用，就跟着教程随便摸索了一下，记录一下摸索过程。开始摸索先看了几遍幸狐科技给的wiki文档，有一说一，还挺通俗易懂，luckfox pico plus使用RV1103处理器，基于单核 ARM Cortex-A7 32 位架构，计算性能可达 0.5 TOPS，板载内存64MB DDR2与板载存储128MB spi nand flash，跟着教程也装了ubuntu（卡的要死）、Buildroot，不过用起来感觉不怎么样，看见教程有Alpine Linux 移植，就跟着进行了移植（幸狐这wiki搬运教程也太不上心了，我看到好多错的地方）。移植开始docker运行Alpine Linux容器我用的windows桌面端docker，所以运行前执行docker run --rm --privileged multiarch/qemu-user-static --reset --persistent yes在wsl的ubuntu22.04换行中，执行以下代码，将临时文件挂载到系统，用来存储新的rootfsdd if=/dev/zero of=rootfs.ext4 bs=1M count=100 mkfs.ext4 rootfs.ext4 mkdir -p /home/luckfox/my-rootfs sudo mount rootfs.ext4 /home/luckfox/my-rootfs然后执行启动armv7alpine容器docker run -it --name armv7alpine --net=host -v /tmp/my-rootfs:/my-rootfs arm32v7/alpine容器内操作在容器里，将Alpine Linux的官方源替换成阿里源sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories apk update安装openrc用来管理进程apk add openrc rc-update add devfs boot rc-update add procfs boot rc-update add sysfs boot添加安全终端 ：允许 root 用户从 ttyFIQ0 登录。删除tty1到tty6开始的行，这一步一定要做，否则串口启动会卡死在找ttyX上。执行完最好vi /etc/inittab看看ttyFIQ0::respawn:/sbin/agetty --autologin root ttyFIQ0 vt100前面有没有#echo ttyFIQ0 > /etc/securetty # 删除 tty1-tty6 的配置 sed -i '/tty[1-6]/d' /etc/inittab # 添加 ttyFIQ0 自动登录配置 sed -i '/# Put a agetty on the serial port/a ttyFIQ0::respawn:/sbin/agetty --autologin root ttyFIQ0 vt100' /etc/inittab配置静态ipcat <<EOF > /etc/network/interfaces auto lo iface lo inet loopback auto eth0 iface eth0 inet static address 192.168.1.100 netmask 255.255.255.0 gateway 192.168.1.1 EOF添加开机启动rc-update add networking default修改dnscat <<EOF > /etc/resolv.conf nameserver 114.114.114.114 nameserver 8.8.8.8 EOF设置板子名字，方便ssh连接echo luckfox > /etc/hostname安装配置ssh，设置登录密码apk add openssh vi /etc/ssh/sshd_config sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/g' /etc/ssh/sshd_config rc-update add sshd default passwd创建本地启动服务脚本cat <<EOF > /etc/local.d/crond.start #!/bin/bash # 网络校时并启动 crond 定时任务 ntpd -d -q && crond # 关闭板载 LED 灯 (如果你不想关灯，可以把这行去掉) echo 0 > /sys/class/leds/work/brightness EOF chmod +x /etc/local.d/crond.start rc-update add local default配置开机启动ntpd实现网络校时cat <<EOF > /etc/ntp.conf server ntp.aliyun.com iburst server ntp1.aliyun.com iburst server ntp2.aliyun.com iburst EOF rc-update add local default apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ echo "Asia/Shanghai" > /etc/timezone && apk del tzdata安装需要用到的软件包，最好手动vi /etc/passwd看看第一行改成 /bin/bash了吗apk add util-linux sqlite btop bash bash-completion sed -i 's/\/bin\/ash/\/bin\/bash/g' /etc/passwd将 rootfs 打包，在宿主机的 /home/luckfox/my-rootfs/ 目录下看到一个 alpine.tar.gz 文件。cd / for d in bin etc lib root sbin usr; do tar c "$d" | tar x -C /my-rootfs; done for dir in dev proc run sys var tmp media mnt opt srv; do mkdir -p /my-rootfs/${dir}; done cd /my-rootfs/ && tar czf alpine.tar.gz *修改官方SDK根目录下的build.sh文件function build_sysdrv() { echo "============Start building sysdrv============" mkdir -p ${RK_PROJECT_OUTPUT_IMAGE} build_uboot build_kernel build_rootfs build_recovery if [ -z $RK_CUSTOM_ROOTFS ]; then rootfs_tarball="$RK_PROJECT_PATH_SYSDRV/rootfs_${RK_LIBC_TPYE}_${RK_CHIP}.tar" else rootfs_tarball="$RK_CUSTOM_ROOTFS" fi rootfs_out_dir="$RK_PROJECT_OUTPUT/rootfs_${RK_LIBC_TPYE}_${RK_CHIP}" if ! [ -d $RK_PROJECT_OUTPUT ]; then mkdir -p $RK_PROJECT_OUTPUT fi if [ -f $rootfs_tarball ]; then if [ -d $rootfs_out_dir ]; then rm -rf $rootfs_out_dir fi if [ -z $RK_CUSTOM_ROOTFS ]; then tar xf $rootfs_tarball -C $RK_PROJECT_OUTPUT else mkdir -p $rootfs_out_dir tar xf $rootfs_tarball -C $rootfs_out_dir fi else msg_error "Not found rootfs tarball: $rootfs_tarball" exit 1 fi msg_info "If you need to add custom files, please upload them to <Luckfox Sdk>/output/out/rootfs_${RK_LIBC_TPYE}_${RK_CHIP}." finish_build } function __PACKAGE_ROOTFS() { local rootfs_tarball _target_dir _install_dir if [ -z $RK_CUSTOM_ROOTFS ]; then rootfs_tarball="$RK_PROJECT_PATH_SYSDRV/rootfs_${RK_LIBC_TPYE}_${RK_CHIP}.tar" else rootfs_tarball="$RK_CUSTOM_ROOTFS" fi if [ -f $rootfs_tarball ]; then if [ -z $RK_CUSTOM_ROOTFS ]; then tar xf $rootfs_tarball -C $RK_PROJECT_OUTPUT else if [ ! -d $RK_PROJECT_PACKAGE_ROOTFS_DIR ]; then mkdir $RK_PROJECT_PACKAGE_ROOTFS_DIR fi tar xf $rootfs_tarball -C $RK_PROJECT_PACKAGE_ROOTFS_DIR fi else msg_error "Not found rootfs tarball: $rootfs_tarball" exit 1 fi if [ "$LF_TARGET_ROOTFS" == "buildroot" ] || [ "$LF_TARGET_ROOTFS" == "busybox" ]; then build_get_sdk_version cat >$RK_PROJECT_PACKAGE_ROOTFS_DIR/bin/sdkinfo <<EOF #!/bin/sh echo Build Time: $(date "+%Y-%m-%d-%T") echo SDK Version: ${GLOBAL_SDK_VERSION} EOF chmod a+x $RK_PROJECT_PACKAGE_ROOTFS_DIR/bin/sdkinfo __COPY_FILES $RK_PROJECT_PATH_APP/root $RK_PROJECT_PACKAGE_ROOTFS_DIR fi __COPY_FILES $RK_PROJECT_PATH_MEDIA/root $RK_PROJECT_PACKAGE_ROOTFS_DIR __COPY_FILES $SDK_ROOT_DIR/external $RK_PROJECT_PACKAGE_ROOTFS_DIR if [ -d "$RK_PROJECT_PACKAGE_ROOTFS_DIR/usr/share/iqfiles" ]; then ( cd $RK_PROJECT_PACKAGE_ROOTFS_DIR/etc ln -sf ../usr/share/iqfiles ./ ) fi if [ -f $RK_PROJECT_FILE_ROOTFS_SCRIPT ]; then chmod a+x $RK_PROJECT_FILE_ROOTFS_SCRIPT cp -f $RK_PROJECT_FILE_ROOTFS_SCRIPT $RK_PROJECT_PACKAGE_ROOTFS_DIR/etc/init.d fi }修改SDK根目录下的.BoardConfig.mk文件,在文件末尾添加以下内容export RK_CUSTOM_ROOTFS=../sysdrv/custom_rootfs/alpine.tar.gz删除output/out/rootfs_uclibc_rv1106目录,确保我们的rootfs生效rm -rf output/out/rootfs_uclibc_rv1106编译镜像，然后烧录cd project ./build.sh lunch ./build.sh手动挂载tf卡ls /dev/mmcblk* mkdir -p /mnt/sdcard mount -t vfat /dev/mmcblk1p1 /mnt/sdcard mount -a df -h添加开机自启vi /etc/fstab/dev/mmcblk1p1 /mnt/sdcard vfat defaults,noatime 0 0配置sshrc-service sshd status ssh-keygen -A mkdir -p /var/empty chown root:root /var/empty chmod 755 /var/empty rc-service sshd start后续单独编译Rootfs，编译后 ：在 output/image/ 目录下会生成一个新的 rootfs.img 。烧录时 ：使用瑞芯微的烧录工具，只勾选 rootfs 分区，选择这个新的 rootfs.img 进行下载。./build.sh rootfs添加内外穿透借助樱花FRP，下载安装配置cd /usr/local/bin # 替换 <下载地址> 为实际链接 wget -O frpc https://nya.globalslb.net/natfrp/client/frpc/0.51.0-sakura-12.3/frpc_linux_armv7 chmod +x frpc ./frpc -v vi /etc/init.d/frpc#!/sbin/openrc-run name="frpc" description="SakuraFrp Client" # 你的 frpc 路径 command="/usr/local/bin/frpc" # 你的启动参数 (把你的 Token 填在这里) # 例如: command_args="-f wdnmdtoken666666:12345" command_args="YOUR_TOKEN_HERE" # 让 frpc 在后台运行 command_background="yes" # PID 文件路径 (用于管理进程) pidfile="/run/frpc.pid" depend() { # 依赖网络服务，要在网络启动后再启动 need net after firewall }chmod +x /etc/init.d/frpc rc-service frpc start rc-update add frpc default添加虚拟内存SWAPdd if=/dev/zero of=/mnt/sdcard/swapfile bs=1M count=256 chmod 600 /mnt/sdcard/swapfile mkswap /mnt/sdcard/swapfile swapon /mnt/sdcard/swapfile free -h vi /etc/fstab /mnt/sdcard/swapfile none swap sw 0 0设置crond定时任务(6小时校正一次)mkdir -p /var/spool/cron/crontabs ln -s /usr/bin/vi /bin/vi crontab -e# do daily/weekly/monthly maintenance # min hour day month weekday command 0 */6 * * * ntpd -d -q
说明为什么我要部署这个项目？答案很简单：听歌不想花钱。在没有部署之前，我是用的酷我提供的API。【感谢酷我】但是近半年来，酷我的API越来越不稳定了。所以，在此基础上，我使用了GD Studio's Online Music Platform API，本文也是根据此api获取的音乐播放链接。开始之前你需要准备：一个能访问github的电脑。安装了node.js 24版本或以上配置好了npm镜像以及下载了pnpm模块后端的部署首先，后端部署比较简单只需要访问将项目源码下载到你的电脑，然后按照教程，在项目目录里执行pnpm i然后等待模块儿全部安装完毕当你看到如上图一样的就表示你安装成功了，接下来就可以执行# 默认端口 3000 node app.js执行后就会显示访问后端，你就会看到到此，后端部署完毕【当然，到这里还是使用的网易云音乐的api，无法获取付费音乐，如何修改我们放在第三步来说明】前端的部署前端我试了很多的网易云项目，如AlgerMusicPlayer、SPlayer、YesPlayMusic等。YesPlayMusic界面比较素（太素了，啥也没有），所以没有使用；AlgerMusicPlayer没有手机端适配，没有使用；最终选择了SPlayer作为前端。同样的，将项目下载下来，在项目目录执行pnpm install安装完依赖后，复制 .env.example 为 .env 并按需修改，如：将 .env 文件中的 VITE_API_URL 改为第一步得到的 API 地址VITE_API_URL = http://localhost:3000;【最后不能/】执行编译打包之前，需要再添加一句SKIP_NATIVE_BUILD=true所以最后你的.env文件样式可能是# WEB 端口 VITE_WEB_PORT=14558 # API 端口 VITE_SERVER_PORT=25884 # API 地址 - 结尾不要加 / VITE_API_URL=http://localhost:3000 SKIP_NATIVE_BUILD=true 执行编译打包pnpm build文件输出在out/renderer 目录，本地测试可以按照下面的方法测试cd out/renderer npx http-server -p 4000访问http://127.0.0.1:4000/，就可以发现已经部署成功了！修改后端Api因为版权限制，无法听vip音乐，除非你账号本来就有vip【不过既然你有vip了还部署这个干嘛】目前网站默认关闭试听，需要在设置里打开而且试听会有提醒，需要去掉。我们需要做的有这几点：默认打开播放试听去除试听弹窗修改音乐播放链接使用的api实施计划默认打开播放试听在SPlayer里，src/stores/setting.ts中的第552行playSongDemo: false, -> playSongDemo: true,这样就实现了默认播放试听【如果你不打开，它不让你播放】去除试听弹窗在SPlayer里，src/core/player/SongManager.ts中的第552行【？行数还一样了】if (isTrial) window.$message.warning("当前歌曲仅可试听"); -> // if (isTrial) window.$message.warning("当前歌曲仅可试听");注释掉就行了。修改完不要忘记再次执行pnpm build npx http-server -p 4000修改音乐播放链接使用的api回到api-enhanced，首先安装axios【跨域请求第三方api】pnpm add axios修改文件module/song_url_v1.js【这次改的有点多】//module/song_url_v1.js // 歌曲链接 - v1 // 此版本不再采用 br 作为音质区分的标准 // 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断 // 当unblock为true时, 会尝试使用unblockmusic-utils进行解锁, 同时音质设置不会生效, 但仍然为必须传入参数 const logger = require('../util/logger.js') const createOption = require('../util/option.js') const axios = require('axios') module.exports = async (query, request) => { const { matchID, } = require('@neteasecloudmusicapienhanced/unblockmusic-utils') require('dotenv').config() if (query.unblock === 'true') { try { const result = await matchID(query.id, query.source) logger.info('Starting unblock(uses modules unblock):', query.id, result) const useProxy = process.env.ENABLE_PROXY || 'false' let proxyUrl = '' if (result.data.url && result.data.url.includes('kuwo')) { proxyUrl = useProxy === 'true' && process.env.PROXY_URL ? process.env.PROXY_URL + result.data.url : result.data.url } return { status: 200, body: { code: 200, msg: 'Warning: Customizing unblock sources is not supported on this endpoint. Please use `/song/url/match` instead.', data: [ { id: Number(query.id), url: result.data.url, type: 'flac', level: query.level, freeTrialInfo: 'null', fee: 0, proxyUrl: proxyUrl || '', }, ], }, cookie: [], } } catch (e) { console.error('Error in unblocking music:', e) } } const ids = String(query.id).split(',') let br = 999 const level = query.level || 'lossless' if (level === 'standard') br = 128 else if (level === 'higher') br = 192 else if (level === 'exhigh') br = 320 else if (level === 'lossless' || level === 'hires' || level === 'sky' || level === 'jyeffect' || level === 'jymaster') br = 999 const tasks = ids.map(async (id) => { try { const res = await axios.get('https://music-api.gdstudio.xyz/api.php', { params: { types: 'url', source: 'netease', id: id, br: br, }, }) const data = res.data return { id: Number(id), url: data.url, br: data.br * 1000, size: data.size, md5: null, code: 200, expi: 1200, type: data.url ? data.url.split('.').pop() : 'mp3', gain: 0, fee: 0, payed: 0, flag: 0, canExtend: false, freeTrialInfo: null, level: level, encodeType: data.url ? data.url.split('.').pop() : 'mp3', } } catch (e) { return { id: Number(id), url: null, br: 0, size: 0, code: 200, freeTrialInfo: null, } } }) const result = await Promise.all(tasks) result.sort((a, b) => { return ids.indexOf(String(a.id)) - ids.indexOf(String(b.id)) }) return { status: 200, body: { code: 200, data: result, }, } } 修改文件module/song_url.js//module/song_url.js // 歌曲链接 const createOption = require('../util/option.js') const axios = require('axios') module.exports = async (query, request) => { const ids = String(query.id).split(',') let br = 999 const queryBr = parseInt(query.br || 999000) if (queryBr < 192000) br = 128 else if (queryBr < 320000) br = 192 else if (queryBr < 740000) br = 320 else if (queryBr < 999000) br = 740 const tasks = ids.map(async (id) => { try { const res = await axios.get('https://music-api.gdstudio.xyz/api.php', { params: { types: 'url', source: 'netease', id: id, br: br, }, }) const data = res.data return { id: Number(id), url: data.url, br: data.br * 1000, size: data.size, md5: null, code: 200, expi: 1200, type: data.url ? data.url.split('.').pop() : 'mp3', gain: 0, fee: 0, payed: 0, flag: 0, canExtend: false, freeTrialInfo: null, level: 'standard', encodeType: data.url ? data.url.split('.').pop() : 'mp3', } } catch (e) { return { id: Number(id), url: null, br: 0, size: 0, code: 200, freeTrialInfo: null, } } }) const result = await Promise.all(tasks) result.sort((a, b) => { return ids.indexOf(String(a.id)) - ids.indexOf(String(b.id)) }) return { status: 200, body: { code: 200, data: result, }, } } 总结引入 axios : 使用 axios 库来请求外部 API ( music-api.gdstudio.xyz )。替换获取逻辑 :移除了原有的调用网易云官方接口 ( /api/song/enhance/player/url ) 的代码。改为并行请求外部 API，支持同时获取多个歌曲 ID 的链接。保留了原有的响应数据结构 ( { code: 200, data: [...] } )，确保对现有客户端的兼容性。参数映射 :旧版 ( song_url.js ) : 将传入的 br (码率，如 320000) 映射为 API 支持的档位 (128, 192, 320, 740, 999)。新版 ( song_url_v1.js ) : 将传入的 level (音质等级，如 standard, exhigh, lossless) 映射为 API 支持的 br 档位。standard -> 128higher -> 192exhigh -> 320lossless , hires , sky , jyeffect , jymaster -> 999注意事项部署在服务器时，你不能进行以下操作单独前端启用 HTTPS或单独后端启用 HTTPS，这会报错【现代浏览器会阻止这种混合内容（Mixed Content）请求】，你将无法在前端获取任何内容。写在最后不用重复造轮子太爽了，本站右上角的音乐使用的就是此API.enjoy yourself!后续将SPlayer与api-enhanced合并打包为了镜像，一键部署前后端，将SPlayer打包后的out文件夹放在api-enhanced根目录。需要修改的地方：SPlayer：.envapi-enhanced：Dockerfile.dockerignoreserver.js#.env # WEB 端口 VITE_WEB_PORT=14558 # API 端口 VITE_SERVER_PORT=25884 # API 地址 - 结尾不要加 / VITE_API_URL= SKIP_NATIVE_BUILD=true #Dockerfile FROM node:lts-alpine RUN apk add --no-cache tini ENV NODE_ENV=production USER node WORKDIR /app # 先只拷贝 package.json 和 yarn.lock（如果有），利用 Docker 缓存机制加速安装 COPY --chown=node:node package.json yarn.lock* ./ # 配置国内淘宝镜像源，极大地加快下载速度 RUN yarn config set registry https://registry.npmmirror.com && yarn install --production --network-timeout=100000 # 然后再拷贝其余代码 COPY --chown=node:node . ./ EXPOSE 3000 CMD [ "/sbin/tini", "--", "node", "app.js" ] #.dockerignore /** !/module !/plugins !/public !/static !/util !/app.js !/server.js !/package.json !/index.js !/generateConfig.js !/main.js !/data !/.env !/pnpm-lock.yaml !/out/renderer//server.js //140行添加 app.use(express.static(path.join(__dirname, 'out/renderer'))) //340行添加 app.use((req, res, next) => { if (!req.path.includes('.')) { res.sendFile(path.join(__dirname, 'out/renderer/index.html')) } else { res.status(404).end() } })最后在api-enhanced根目录执行build_and_export.bat@echo off chcp 65001 >nul setlocal :: Define Variables set IMAGE_NAME=ncm-api-splayer set TAR_FILE=%IMAGE_NAME%.tar echo ======================================================== echo NCM API Enhanced + SPlayer Docker Build Script echo ======================================================== :: 1. Build Docker Image echo [1/3] Building Docker Image... docker build -t %IMAGE_NAME% . if %errorlevel% neq 0 ( echo [ERROR] Docker build failed! pause exit /b %errorlevel% ) :: 2. Export Image echo [2/3] Exporting image to %TAR_FILE% ... docker save -o %TAR_FILE% %IMAGE_NAME% if %errorlevel% neq 0 ( echo [ERROR] Image export failed! pause exit /b %errorlevel% ) :: 3. Complete echo [3/3] Success! echo. echo Image saved to: %CD%\%TAR_FILE% echo File size: for %%I in ("%TAR_FILE%") do echo %%~zI bytes echo. echo ======================================================== echo Next steps: echo 1. Upload %TAR_FILE% to your server echo 2. Run on server: docker load -i %TAR_FILE% echo 3. Run container: docker run -d -p 3000:3000 --restart=always %IMAGE_NAME% echo ======================================================== pause 文件存放在api-enhanced根目录ncm-api-splayer.tar，上传到服务器导入镜像后执行docker run -d -p 4000:3000 ncm-api-splayer4000可改为任意端口，直接访问ip:4000即可部署完毕。
首先进SYSETM依次点击后等待下载完成点击去目录后显示文件总数123，即为下载完全下载完成后点击START进入游戏游戏界面左滑即可呼出菜单可执行的操作：保存：保存当前进度加载：读取存储的进度，与LOAD功能相同跳过场景：跳过当前场景跳过章节：跳过本章chapter
首先点击去下载然后点击下载数据，等数据下载完成下载完毕后如果显示37即为下载完成全部数据退出应用后重新进入即可使用点击错题本即可查看不会的单词点击单词即可显示出单词意思
起因原ATRI文本文件大小为1.6MB，而watch gt这破表加载300KB的文本文件时，不管是import的形式，还是storage的形式，亦或是file.read的形式，都会卡到重启。于是，在此背景下研究了一个分块加载文本的代码格式举个例子我有如下一个json的文本列表，我就会在达到某一个条件时加载其中符合条件的那个。PS：之前试过使用require分块加载，但是还是会报错，不知道是系统原因还是啥原因，人家小米全import加载都不会报错(jsonDataList: [ 'b999.json', 'b101.json', 'b102.json', 'b103.json', 'b111.json', 'b112.json', 'b113.json', 'b114.json', 'b121.json', 'b122.json', 'b123.json', 'b124.json', 'b200.json', 'b201.json', 'b202.json', 'b203.json', 'b204.json', 'b205.json', 'b206.json', 'b207.json', 'b301.json', 'b302.json', 'b303.json', 'b304.json', 'b401.json', 'b402.json', 'b403.json', 'b404.json', 'b405.json', 'b406.json', 'b407.json', 'b501.json', 'b601.json', 'b701.json' ],如下代码为ATRI加载文本文件使用的方法PS:原计划是将文本放在本地的，但是我不会读取读取本地文件，开发文档也没讲，寄！async loadData(chapter) { const num = this.jsonDataList[chapter]; // 将回调转换为Promise return new Promise((resolve, reject) => { file.readText({ uri: `internal://files/${num}`, success: (data) => { try { const parsedData = JSON.parse(data.text); resolve(parsedData); // 正确传递解析后的数据 } catch (e) { reject("JSON解析失败"); } }, fail: (_, code) => { reject(`文件读取失败，错误码：${code}`); } }); }); },最近在做一个可查找的词典，但是由于词库过大，加载200KB的索引会卡死，于是将索引分为26份，按照首字母（全部转化为小写）进行查找，不必将所有索引全部加载，实现按需查找的功能，并且也不会爆内存，完美！下面给出我写的下载模块，因为watch gt不支持数据块传输，所以只能使用全部下载字节流的方式了：import fetch from '@blueos.network.fetch' import file from '@blueos.storage.file' import prompt from '@blueos.window.prompt' export default { data: { indexData: null, jsonDataList: ['output_part_1.json'], }, async fetchPromise(options) { return new Promise((resolve, reject) => { fetch.fetch({ ...options, success: resolve, fail: reject, }); }); }, async writeTextPromise(options) { return new Promise((resolve, reject) => { file.writeText({ ...options, success: resolve, fail: (data, code) => reject({ data, code }), }); }); }, async writeArrayBufferPromise(options) { return new Promise((resolve, reject) => { file.writeArrayBuffer({ ...options, success: resolve, fail: (data, code) => reject({ data, code }), }); }); }, async getFile(fileName) { return new Promise((resolve, reject) => { file.get({ uri: `internal://files/${fileName}`, success: () => { resolve(true); }, fail: (_, code) => { reject(`handling fail, code = ${code}`); }, }); }); }, async downloadFile(data) { prompt.showToast({ message: "开始下载" }); let totalNum = data.length; let hasDownload = 0; for (let i = 0; i < totalNum; i++) { const fileName = data[i]; let isInFile = false; try { isInFile = await this.getFile(fileName); } catch (error) { console.log(error); } if (isInFile) hasDownload++; if (!isInFile) { try { let fileUri = `internal://files/${fileName}`; let baseUrl = 'https://chorblack.top' const chunkResponse = await this.fetchPromise({ url: `${baseUrl}/app/dictionary/${fileName}`, responseType: "arraybuffer", }); const textData = chunkResponse.data; await this.writeArrayBufferPromise({ uri: fileUri, buffer: new Uint8Array(textData), }); prompt.showToast({ message: `还剩${data.length - i - 1}` }) hasDownload++; } catch (error) { console.error(`下载失败: ${fileName}`, error); } } } if (hasDownload==totalNum) prompt.showToast({ message: "下载完成" }); } }下面提供分块传输的方法： <text class="title">{{ title }}</text>
准备工具：视频文件Python一个电脑（可以联网）预备工具下载步骤：抽取视频帧图片找到你想要开始的视频时间，如图1中所示为1:01，即61秒；找到你想要结束的视频时间，如图2中所示为1:03，即63秒；图1图2打开python代码：提取保存png.py图3将开始时间和结束时间填入所示的框内，如图3所示，运行代码，在同目录下会出现一个文件夹：output，如图4所示图4运行代码后在output中生成了一系列图片，如图5所示图5找到图片帧截取位置准备一个图片截取工具，如画图、ps等，如图6、图7所示，推荐ps，本文将使用ps进行演示；图6图7首先查看图片的分辨率大小，如图8所示，本文使用的视频分辨率为2560×1080图8由于要截取的图片我需要保持宽度不变，那么由计算得知：$$\frac{390}{450} = \frac{x}{1080}$$得出x约等于936从ps边界线处拉出两条参考线，如图9所示，先确认第一条线的位置，再在第一条线的坐标上加上刚才的x值，本文为936，即为第二条线的坐标值；如果你的图像中心对称，你也可以通过计算得出第一条线的横坐标a：$$a = \frac{2560}{2} - \frac{936}{2} = 812$$图9计算完成后，点击第一条线，右击，选择"编辑所选参考线"，将得到的a填入，再将a加上半个x，得到第二条线的坐标值，填入看看效果，如图10所示；图10确认完成后，将python代码：切割图片png.py复制到output文件夹内，打开代码，在所选框中填入如下四个数值：以左上角为坐标原点，右方向为x正方向，下方向为y正方向，切割图片起始横坐标a、切割图片起始纵坐标（本文为0）、裁剪宽度x、裁剪长度（本文为1080），运行代码，在output下生成了一个output文件夹，如图11、图12所示：图11图12图片分辨率、大小修改选取图片帧范围，本文选取了frame_001852.png\~frame_001890.png，然后打开网址：resize-images进行图片分辨率修改，如图14所示：Why：由于性能因素以及rpk包限制，建议将图片分辨率设置为与表盘分辨率一致；图14将所选图片拖入后，点击右侧"添加尺寸"，按照图15所示，设置格式为png、宽度为390、高度为450、图片布局选择自适应，确认选项后点击开始；图15为保证图片命名具有规律性，点击重命名，自定义命名、数字、补零等，如图16所示，确认后点击下载zip将图片包下载下来并解压：图16解压之后，点击compress-images进行图片压缩，由此来减少rpk包的大小，如图17所示（我要是能装上原版大小图片的包我还会压缩？）图17同样的，将上次重置分辨率的图片全部拖进去，选择无损，点击开始，如果你压缩后包小于2MB，恭喜你，不需要进有损压缩了，如果压缩后包大小仍然大于2MB，点击返回，选择自定义，数值越大图片越糊，选择合适的大小，将图片包控制在2MB左右，如图18所示：小技巧：如果你某些部分不需要太高分辨率，可以进行分块压缩，需要好的显示的可以使用无损压缩，对于如特效等不需要仔细显示的可以使用数值大一点的自定义压缩。图18这样，我们所需要的图像帧资源就得到了编辑表盘代码将图像资源放入对应的表盘文件夹中，在data中找到frames项，填写图片个数与图片路径，本文使用的是52张图片，路径规则如图19所示：Ps：duration项控制着图片帧的播放速率，本文使用的是50ms，即20帧；图19到此，你就可以成功将表盘运行起来了，当然你仍需要进行一些配置来保证安装，更改manifest.json的数据：更改package，即包名，命名格式可以是域名反转或中间；更改屏幕尺寸，watch gt为390，watch 3为466；更换id和name，id是表盘唯一标识，id相同则无法安装，因此有一个独特的id能保证你安装成功，name则为表盘显得的名称，图21中的name也需更改；previewImage为预览图，显示在表盘切换界面；图20图21到此为止，表盘制作成功，其他内容请查看蓝河开发文档：vivo蓝河应用开发感谢观看，如果觉得有用的话，请投喂我哦~编写者：澪编写时间：2025.4.12QQ：1562818788博客主页