Эх сурвалжийг харах

Merge branch 'master' of https://gitee.com/openLuat/LuatOS

alienwalker 1 сар өмнө
parent
commit
a316662f31

+ 20 - 1
components/fatfs/luat_lib_fatfs.c

@@ -131,6 +131,7 @@ static int fatfs_mount(lua_State *L)
 			LLOGD("init sdcard at spi=%d cs=%d", spit->spi_id, spit->spi_cs);
 			diskio_open_spitf(0, (void*)spit);
 		}
+	#ifdef LUAT_USE_SDIO
 	}else if(fatfs_mode == DISK_SDIO){
 		luat_fatfs_sdio_t *fatfs_sdio = luat_heap_malloc(sizeof(luat_fatfs_sdio_t));
 		if (fatfs_sdio == NULL) {
@@ -145,13 +146,16 @@ static int fatfs_mount(lua_State *L)
 
 		LLOGD("init FatFS at sdio");
 		diskio_open_sdio(0, (void*)fatfs_sdio);
+	#endif
+	#if defined(LUA_USE_LINUX) || defined(LUA_USE_WINDOWS) || defined(LUA_USE_MACOSX)
 	}else if(fatfs_mode == DISK_RAM){
 		LLOGD("init ramdisk at FatFS");
 		diskio_open_ramdisk(0, luaL_optinteger(L, 3, 64*1024));
+	#endif
 	}else if(fatfs_mode == DISK_USB){
 
 	}else{
-		LLOGD("fatfs_mode error");
+		LLOGD("fatfs_mode error %d", fatfs_mode);
 		lua_pushboolean(L, 0);
 		lua_pushstring(L, "fatfs_mode error");
 		return 2;
@@ -176,6 +180,21 @@ static int fatfs_mount(lua_State *L)
 			if (re == FR_OK) {
 				re = f_mount(fs, mount_point, 1);
 				LLOGD("remount again %d", re);
+				if (re == FR_OK) {
+					LLOGI("sd/tf mount success after auto format");
+				}
+				else {
+					LLOGE("sd/tf mount failed again %d after auto format", re);
+					lua_pushboolean(L, 0);
+					lua_pushstring(L, "mount error");
+					return 2; 
+				}
+			}
+			else {
+				LLOGE("sd/tf format failed %d", re);
+				lua_pushboolean(L, 0);
+				lua_pushstring(L, "format error");
+				return 2;
 			}
 		}
 	}

+ 6 - 6
components/multimedia/vtool/src/luat_vtool_mp4box.c

@@ -812,7 +812,7 @@ int luat_vtool_mp4box_close(mp4_ctx_t* ctx) {
         LLOGE("ctx is NULL");
         return -1;
     }
-    LLOGI("开始关闭mp4文件 %s", ctx->path);
+    // LLOGI("开始关闭mp4文件 %s", ctx->path);
     // 刷新缓冲,确保文件大小正确
     buffered_flush(ctx);
     // 然后, 把文件关掉, 重新打开
@@ -823,7 +823,7 @@ int luat_vtool_mp4box_close(mp4_ctx_t* ctx) {
     luat_fs_fseek(ctx->fd, 0, SEEK_END);
     // 把mdat的box大小更新一下
     ret = luat_fs_ftell(ctx->fd);
-    LLOGI("文件当前长度 %d", ret);
+    LLOGI("mp4 file size before mdat %d", ret);
     long int pos = ctx->mdat_offset;
     size_t mdat_len = ret - ctx->mdat_offset;
     // LLOGI("mdat 长度更新为 %d 目标偏移量 %d sizeof(int) %d sizeof(long int) %d", mdat_len, ctx->mdat_offset, sizeof(int), sizeof(long int));
@@ -832,7 +832,7 @@ int luat_vtool_mp4box_close(mp4_ctx_t* ctx) {
         LLOGE("seek mdat offset failed %d", ret);
     }
     ret = luat_fs_ftell(ctx->fd);
-    LLOGD("当前fd偏移量位置 %d 期望 %d", ret, ctx->mdat_offset);
+    // LLOGD("当前fd偏移量位置 %d 期望 %d", ret, ctx->mdat_offset);
     if (ret != (int)ctx->mdat_offset) {
         LLOGE("seek mdat offset failed %d", ret);
         ret = -1;
@@ -915,7 +915,7 @@ clean:
             luat_fs_fseek(ctx->fd, 0, SEEK_END);
             luat_fs_fflush(ctx->fd);
             ret = luat_fs_ftell(ctx->fd);
-            LLOGI("写入完成, 文件最终长度 %d", ret);
+            LLOGI("mp4 file final size %d", ret);
             ret = 0;
             luat_fs_fclose(ctx->fd);
         }
@@ -928,7 +928,7 @@ clean:
         LLOGE("文件句柄为空!!!");
         ret = -10;
     }
-    LLOGD("释放mp4资源, 释放内存");
+    // LLOGD("释放mp4资源, 释放内存");
     // 释放全部资源
     if (ctx->sps) {
         luat_heap_free(ctx->sps);
@@ -970,6 +970,6 @@ clean:
     }
     clean_box(&ctx->box_moov);
     luat_heap_free(ctx);
-    LLOGI("mp4文件关闭完成, box写入结束, 文件已关闭");
+    LLOGI("mp4 file closed, box write finished, file closed");
     return ret;
 }

+ 26 - 29
module/Air8101/demo/wlan/airkiss/main.lua → module/Air8101/demo/config_wifi_network/airkiss/airkiss_task.lua

@@ -1,42 +1,45 @@
--- @module airkiss
--- @release 2025.05.27
--- 运行环境:本demo可直接在Air8101开发板上运行。
--- 执行逻辑:先执行airkiss配网,获取IP成功后, 将配网信息存入fskv,重启后自动连接。
+--[[
+@module airkiss_task
+@summary airkiss 配网功能模块
+@version 1.0
+@date    2025.12.8
+@author  拓毅恒
+@usage
+用法实例
 
--- LuaTools需要PROJECT和VERSION这两个信息
-PROJECT = "AirKiss"
-VERSION = "1.0.0"
+启动 AirKiss 配网功能
+- 运行 airkiss_task 任务,先检查是否有已保存的配网信息
+- 如有保存的信息则直接连接,否则启动airkiss配网
+- 配网成功后将信息保存到fskv并重启设备
 
-if wdt then
-    --添加硬狗防止程序卡死,在支持的设备上启用这个功能
-    wdt.init(9000)--初始化watchdog设置为9s
-    sys.timerLoopStart(wdt.feed, 3000)--3s喂一次狗
-end
+注:本demo无需额外配置,直接在 main.lua 中 require "airkiss_task" 即可加载运行。
+]]
 
+-- 订阅IP_READY事件,获取IP成功后触发
 sys.subscribe("IP_READY", function(ip)
     log.info("wlan", "ip ready", ip)
     -- 联网成功, 可以发起http, mqtt, 等请求了
 end)
 
+-- 订阅SC_RESULT事件,配网成功后触发
 sys.subscribe("SC_RESULT", function(ssid, password)
-    log.info("why", ssid, password)
+    log.info("airkiss", "配网成功", ssid, password)
 end)
 
-fskv.init()  -- 初始化fskv, 用于存储配网信息
-
 local function start_airkiss()
-    sys.wait(500) -- 这里等500ms只是方便看日志,非必须
-    wlan.init() -- 初始化wifi协议栈
+    -- 初始化fskv, 用于存储配网信息
+    fskv.init()
+    -- 初始化wifi协议栈
+    wlan.init()
 
     -- 获取上次保存的配网信息, 如果存在就直接联网, 不需要配网了
     -- 注意, fskv保存的数据是掉电存储的, 刷脚本/刷固件也不会清除
     -- 如需完全清除配置信息, 可调用 fskv.clear() 全清
     if fskv.get("wlan_ssid") then
         wlan.connect(fskv.get("wlan_ssid"), fskv.get("wlan_passwd"))
-        return -- 等联网就行了
+        return
     end
 
-    -- 以下是smartconfig之 AirKiss 配网
     -- 配网时选用 AirKiss 模式
     -- 仅支持2.4G的wifi, 5G wifi是不支持的
     -- 配网时, 手机应靠近模块, 以便更快配网成功
@@ -47,19 +50,18 @@ local function start_airkiss()
         if ret == false then
             log.info("smartconfig", "timeout")
             wlan.smartconfig(wlan.STOP)
-            sys.wait(3000) -- 再等3s重新配网, 或者直接reboot也行
+            sys.wait(3000)
         else
             -- 获取配网后, ssid和passwd会有值
             log.info("smartconfig", ssid, passwd)
-            -- 获取IP成功, 将配网信息存入fskv, 做持久化存储
+            -- 获取IP成功, 将配网信息存入fskv, 掉电也能保存
             log.info("fskv", "save ssid and passwd")
             fskv.set("wlan_ssid", ssid)
             fskv.set("wlan_passwd", passwd)
 
-            -- -- 这里建议重启, 当然这也不是强制的
+            -- 重启后将使用配网信息自动连接
             log.info("wifi", "wait 3s to reboot")
             sys.wait(3000)
-            -- -- 重启后有配网信息, 所以就自动连接
             rtos.reboot()
             break
         end
@@ -67,9 +69,4 @@ local function start_airkiss()
 
 end
 
-sys.taskInit(start_airkiss) -- 启动配网任务
-
--- 用户代码已结束---------------------------------------------
--- 结尾总是这一句
-sys.run()
--- sys.run()之后后面不要加任何语句!!!!!
+sys.taskInit(start_airkiss)

+ 57 - 0
module/Air8101/demo/config_wifi_network/airkiss/main.lua

@@ -0,0 +1,57 @@
+--[[
+@module main
+@summary LuatOS用户应用脚本文件入口,总体调度AirKiss配网应用逻辑
+@version 1.0
+@date    2025.12.8
+@author  拓毅恒
+@usage
+演示功能概述
+本demo演示如何通过AirKiss协议实现WIFI配网功能,配网成功后自动保存配网信息并重启设备,重启后自动连接WIFI网络。
+
+更多说明参考本目录下的readme.md文件
+]]
+
+-- LuaTools需要PROJECT和VERSION这两个信息
+PROJECT = "AirKiss"
+VERSION = "1.0.0"
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+if wdt then
+    --添加硬狗防止程序卡死,在支持的设备上启用这个功能
+    wdt.init(9000)--初始化watchdog设置为9s
+    sys.timerLoopStart(wdt.feed, 3000)--3s喂一次狗
+end
+
+
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
+-- 可以使用合宙的iot.openluat.com平台进行远程升级
+-- 也可以使用客户自己搭建的平台进行远程升级
+-- 远程升级的详细用法,可以参考fota的demo进行使用
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+-- end, 3000)
+
+-- 加载 AirKiss 配网功能模块
+require "airkiss_task"
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!

+ 94 - 0
module/Air8101/demo/config_wifi_network/airkiss/readme.md

@@ -0,0 +1,94 @@
+## 功能模块介绍
+
+1、main.lua:主程序入口,负责初始化系统环境和加载airkiss_task模块;
+
+2、airkiss_task.lua:AirKiss配网功能实现模块,演示如何使用AirKiss协议进行WIFI配网;
+
+## 演示功能概述
+
+1、airkiss_task:演示如何通过AirKiss协议实现WIFI配网功能
+   - 启动AirKiss配网,支持手机APP发送WIFI信息
+   - 配网成功后将WIFI信息保存到fskv持久化存储
+   - 重启后自动读取配网信息并连接WIFI网络
+   - 支持IP获取成功的事件通知
+
+AirKiss配网的主要功能特性:
+
+1、**简单易用**:只需几行代码即可实现完整的配网流程;
+
+2、**持久化存储**:配网信息保存到fskv,掉电不丢失,刷脚本/固件也不会清除;
+
+3、**自动重连**:设备重启后自动读取配网信息并连接WIFI;
+
+4、**事件通知**:通过"IP_READY"和"SC_RESULT"消息通知配网和联网状态;
+
+5、**超时处理**:配网超时后自动重新尝试;
+
+## 演示硬件环境
+
+1、Air8101核心板一块
+
+2、TYPE-C USB数据线一根
+
+3、Air8101核心板和数据线的硬件接线方式为
+
+- Air8101核心板通过TYPE-C USB口供电;(核心板背面的功耗测试开关拨到OFF一端)
+
+- 如果测试发现软件频繁重启,重启原因值为:poweron reason 0,可能是供电不足,此时再通过直流稳压电源对核心板的vbat管脚进行4V供电,或者VIN管脚进行5V供电;
+
+- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
+
+## 演示软件环境
+
+1、Luatools下载调试工具
+
+2、[Air8101 固件](https://docs.openluat.com/air8101/luatos/firmware/)
+
+## 演示核心步骤
+
+1、搭建好硬件环境,确保开发板能正常供电;
+
+2、AirKiss配网操作步骤:
+
+- 将Air8101核心板通过TYPE-C USB数据线连接到电脑;
+
+- 使用Luatools将demo脚本下载到Air8101核心板;
+
+- 下载完成后,Air8101核心板会自动重启并开始执行AirKiss配网程序;
+
+- 微信公众号或者微信小程序搜索"AirKiss 配网",本demo选用"巴法科技(深圳)有线公司"的"一键配网"小程序来进行AirKiss 配网;
+
+- 在小程序中选择"AirKiss 配网",输入家庭WIFI密码,然后点击"下一步";
+
+- 将手机靠近Air8101核心板,保持手机屏幕亮屏,等待配网完成;
+
+- 配网成功后,设备会自动保存配网信息并重启;
+
+- 重启后,设备会自动连接到刚才配置的WIFI网络;
+
+3、通过串口调试工具,可以查看设备的运行日志,包括配网状态和联网信息:
+
+```lua
+[2025-05-27 14:30:21.571][000000000.689] I/user.main AirKiss 1.0.0
+[2025-05-27 14:30:22.124][000000001.135] I/user.wlan 启动airkiss
+[2025-05-27 14:30:35.236][000000014.255] I/user.airkiss 配网成功 MyWiFi 12345678
+[2025-05-27 14:30:35.237][000000014.256] I/user.smartconfig MyWiFi 12345678
+[2025-05-27 14:30:35.238][000000014.257] I/user.fskv save ssid and passwd
+[2025-05-27 14:30:35.239][000000014.258] I/user.wifi wait 3s to reboot
+[2025-05-27 14:30:38.240][000000017.265] I/user.main AirKiss 1.0.0
+[2025-05-27 14:30:38.756][000000017.777] I/user.wlan ip ready 192.168.1.100
+```
+
+4、注意事项:
+
+- AirKiss配网仅支持2.4G WIFI,不支持5G WIFI;
+
+- 有些中文WIFI会无法配网成功,请将WiFi名称修改为英文;
+
+- 配网时请确保手机和设备距离较近,信号良好;
+
+- 配网信息保存在fskv中,如需清除配置信息,可以调用fskv.clear()函数;
+
+- 如果需要重新配网,可以先调用fskv.clear()清除配置信息,然后重启设备;
+
+- 配网超时时间为3分钟,超时后会自动重新尝试配网;

+ 9 - 7
module/Air8101/project/wifi_ap_read_file/readme.md

@@ -22,7 +22,7 @@ HTTPSVR 文件管理系统是一种基于Air8101模组的轻量级文件服务
 #### 1、系统控制方式
 
 - **自动启动模式**:在task_control.lua中设置`AUTO_START=true`,系统开机后会自动创建AP热点、初始化SD卡并启动HTTP文件服务器
-- **手动控制模式**:默认设置`AUTO_START=false`,通过短按GPIO5来控制系统的启动和停止
+- **手动控制模式**:默认设置`AUTO_START=false`,通过杜邦线短接GPIO5与GND(地)然后断开,来控制系统的启动和停止
 
 #### 2、初始化AP热点
 
@@ -66,12 +66,12 @@ HTTPSVR 文件管理系统是一种基于Air8101模组的轻量级文件服务
 
 **手动控制模式操作步骤:**
 1. 烧录固件后上电,设备初始化按键功能,等待按键初始化完毕日志中会输出"系统已就绪,等待按键触发"的打印
-2. 通过拉低GPIO5,设备自动创建WiFi热点并启动文件服务器
+2. 使用杜邦线短接GPIO5与GND(地)然后断开,设备自动创建WiFi热点并启动文件服务器
 3. 使用手机或电脑连接到`LuatOS_FileHub`热点(密码:12345678)
 4. 打开浏览器,输入`http://192.168.4.1/explorer.html`访问文件管理系统
 5. 使用用户名`admin`和密码`123456`登录
 6. 浏览文件列表,点击文件可直接下载
-7. 再次拉低GPIO5可停止文件服务器服务
+7. 再次使用杜邦线短接GPIO5与GND(地)然后断开,可停止文件服务器服务
 
 **自动启动模式操作步骤:**
 1. 修改task_control.lua文件,将`AUTO_START`值设置为`true`
@@ -83,11 +83,13 @@ HTTPSVR 文件管理系统是一种基于Air8101模组的轻量级文件服务
 
 ## 演示硬件环境
 
+![](https://docs.openluat.com/air8101/luatos/app/image/8101-AirMICROSD_1000.jpg)
+
 1、Air8101核心板一块
 
 2、TYPE-C USB数据线一根
 
-3、micro SD卡一张(可选,用于扩展存储)
+3、AirMICROSD_1000配件板一个+micro SD卡一张(可选,用于扩展存储)
 
 ## 演示软件环境
 
@@ -108,7 +110,7 @@ HTTPSVR 文件管理系统是一种基于Air8101模组的轻量级文件服务
 **两种启动模式效果说明:**
 
 - **自动启动模式**(默认AUTO_START=true):上电后系统会自动创建AP热点、初始化SD卡并启动HTTP服务器,无需手动拉低GPIO5即可使用文件管理系统功能。
-- **手动控制模式**(需修改AUTO_START=false):上电后系统仅初始化按键功能,日志显示"系统已就绪,等待按键触发",需要手动拉低GPIO5才会启动文件管理系统服务。
+- **手动控制模式**(需修改AUTO_START=false):上电后系统仅初始化按键功能,日志显示"系统已就绪,等待按键触发",需要手动使用杜邦线短接GPIO5与GND(地)然后断开才会启动文件管理系统服务。
 
 手动控制模式下的启动日志示例:
 
@@ -116,7 +118,7 @@ HTTPSVR 文件管理系统是一种基于Air8101模组的轻量级文件服务
 [000000000.497] I/user.main 系统已就绪,等待按键触发
 ```
 
-5、拉低GPIO5,启动文件管理系统。
+5、使用杜邦线短接GPIO5与GND(地)然后断开,启动文件管理系统。
 
 ```lua
 系统启动,创建AP:
@@ -157,7 +159,7 @@ SD卡挂载完毕,开始启动系统:
 
 12、如需要访问SD卡中的文件,点击网页左上角的 `TF/SD目录` ,即可切换到SD卡目录中
 
-13、在手动控制模式下,再次拉低GPIO5按键,可停止文件管理系统服务,日志会输出停止状态信息
+13、在手动控制模式下,再次使用杜邦线短接GPIO5与GND(地)然后断开,可停止文件管理系统服务,日志会输出停止状态信息
 
 ## 系统参数说明
 

+ 5 - 5
module/Air8101/project/wifi_ap_read_file/task_control.lua

@@ -10,18 +10,18 @@
 
 -- 控制模式配置:
 -- 1. 自动启动模式:设置AUTO_START=true(默认),系统开机后自动创建AP热点、初始化SD卡并启动HTTP文件服务器
--- 2. 手动控制模式:设置AUTO_START=false,通过拉低GPIO5控制系统的启停
+-- 2. 手动控制模式:设置AUTO_START=false,通过杜邦线短接GPIO5与GND(地)然后断开控制系统的启停
 
 -- 手动控制操作指南:
 -- - 功能:控制远程文件管理系统的启动与停止
 -- - 引脚:GPIO5
--- - 触发方式:短按(下降沿触发)
+-- - 触发方式:将GPIO5与GND(地)引脚用杜邦线短接然后断开,通过双边沿触发
 -- - 防抖处理:因杜邦线测试时脉冲无法控制,所以暂无设计。实际设计板子时,根据自己的需求可以更改防抖配置以及打开防抖
--- - 状态切换:短按一次切换一次系统运行状态
+-- - 状态切换:每次短接GPIO5与GND后断开,切换一次系统运行状态
 
 -- 使用示例:
 -- 1. 自动启动系统:在代码中将AUTO_START设置为true(默认)
--- 2. 手动控制系统:在代码中将AUTO_START设置为false,通过拉低GPIO5切换系统状态
+-- 2. 手动控制系统:在代码中将AUTO_START设置为false,通过杜邦线短接GPIO5与GND然后断开,切换系统状态
 -- 3. 查看状态:通过日志查看系统启动和停止的状态信息
 
 -- 注意:本模块需要在main.lua中通过require方式引入,无需额外调用接口
@@ -69,7 +69,7 @@ local function press_key()
     sys.publish("PRESS", true)
 end
 gpio.setup(5, press_key, gpio.PULLUP, gpio.BOTH)
--- gpio.debounce(0, 100, 1) -- 实际设计板子时,根据自己的需求可以更改防抖配置以及打开防抖
+-- gpio.debounce(5, 100, 1) -- 实际设计板子时,根据自己的需求可以更改防抖配置以及打开防抖
 
 local function config_services()
     -- 根据配置决定是否自动启动服务

+ 102 - 102
script/libs/exeasyui.lua

@@ -167,7 +167,7 @@ local function configure_font_backend(opts)
     elseif opts.type == "hzfont" and hzfont then
         local cache_size = tonumber(opts.cache_size) or 256
         cache_size = (cache_size == 128 or cache_size == 256 or cache_size == 512 or cache_size == 1024 or cache_size == 2048) and
-        cache_size or 256
+            cache_size or 256
         local ok = hzfont.init(opts.path, cache_size)
         if ok then
             FontAdapter._backend = "hzfont"
@@ -213,7 +213,7 @@ function ui.hw_init(opts)
     extp.init(tp_config)
 
     -- 设置消息发布状态
-    if tp_config.message_enabled then
+    if tp_config and tp_config.message_enabled then
         if type(tp_config.message_enabled) == "table" then
             for msg_type, enabled in pairs(tp_config.message_enabled) do
                 if type(msg_type) == "string" and type(enabled) == "boolean" then
@@ -232,14 +232,14 @@ function ui.hw_init(opts)
     end
 
     -- 设置滑动阈值
-    if tp_config.swipe_threshold then
+    if tp_config and tp_config.swipe_threshold then
         if type(tp_config.swipe_threshold) == "number" and tp_config.swipe_threshold > 0 then
             extp.set_swipe_threshold(tp_config.swipe_threshold)
         end
     end
 
     -- 设置长按阈值
-    if tp_config.long_press_threshold then
+    if tp_config and tp_config.long_press_threshold then
         if type(tp_config.long_press_threshold) == "number" and tp_config.long_press_threshold > 0 then
             extp.set_long_press_threshold(tp_config.long_press_threshold)
         end
@@ -1321,11 +1321,11 @@ function dropdown_panel:draw(ctx)
     if not self.visible then return end
     local owner = self.owner
     if not owner then return end
-    local ax, ay = self:get_absolute_position()
-    local dark = (current_theme == "dark")
-    local bg_color  = dark and COLOR_WIN11_DARK_BUTTON_BG or COLOR_WIN11_LIGHT_BUTTON_BG
+    local ax, ay       = self:get_absolute_position()
+    local dark         = (current_theme == "dark")
+    local bg_color     = dark and COLOR_WIN11_DARK_BUTTON_BG or COLOR_WIN11_LIGHT_BUTTON_BG
     local border_color = dark and COLOR_WIN11_DARK_BUTTON_BORDER or COLOR_WIN11_LIGHT_BUTTON_BORDER
-    ctx:fill_rect(ax, ay, self.w, self.h, bg_color )
+    ctx:fill_rect(ax, ay, self.w, self.h, bg_color)
     ctx:stroke_rect(ax, ay, self.w, self.h, border_color)
     local startIdx = self.scroll_offset + 1
     local endIdx = math.min(#owner.options, startIdx + (self.visible_count or owner.max_visible_items or 5) - 1)
@@ -1374,7 +1374,7 @@ function dropdown_panel:draw(ctx)
         local thumbY
         if self.max_scroll_offset > 0 then
             thumbY = scrollBarY +
-            math.floor((self.scroll_offset / self.max_scroll_offset) * (scrollBarHeight - thumbHeight))
+                math.floor((self.scroll_offset / self.max_scroll_offset) * (scrollBarHeight - thumbHeight))
         else
             thumbY = scrollBarY
         end
@@ -1579,9 +1579,9 @@ end
 
 function combo_box:draw(ctx)
     if not self.visible then return end
-    local ax, ay = self:get_absolute_position()
-    local bg_color  = self.pressed and COLOR_GRAY or self.colors.bg
-    ctx:fill_rect(ax, ay, self.w, self.h, bg_color )
+    local ax, ay   = self:get_absolute_position()
+    local bg_color = self.pressed and COLOR_GRAY or self.colors.bg
+    ctx:fill_rect(ax, ay, self.w, self.h, bg_color)
     ctx:stroke_rect(ax, ay, self.w, self.h, self.colors.border)
     local textPadding = 8
     local arrowSpace = 20
@@ -1997,9 +1997,9 @@ function keyboard:new(opts)
         { text = "7",      chars = { "7" }, type = "number" },
         { text = "8",      chars = { "8" }, type = "number" },
         { text = "9",      chars = { "9" }, type = "number" },
-        { text = "delete", chars = {},    type = "delete" },
+        { text = "delete", chars = {},      type = "delete" },
         { text = "0",      chars = { "0" }, type = "number" },
-        { text = "EN",     chars = {},    type = "letter" }
+        { text = "EN",     chars = {},      type = "letter" }
     }
 
     -- 根据模式设置按键映射
@@ -2348,13 +2348,13 @@ end
 function keyboard:draw(ctx)
     if not self.visible then return end
 
-    local ax, ay = self:get_absolute_position()
-    local dark = (current_theme == "dark")
-    local bg_color  = dark and COLOR_WIN11_DARK_BUTTON_BG or COLOR_WIN11_LIGHT_BUTTON_BG
+    local ax, ay       = self:get_absolute_position()
+    local dark         = (current_theme == "dark")
+    local bg_color     = dark and COLOR_WIN11_DARK_BUTTON_BG or COLOR_WIN11_LIGHT_BUTTON_BG
     local border_color = dark and COLOR_WIN11_DARK_BUTTON_BORDER or COLOR_WIN11_LIGHT_BUTTON_BORDER
 
     -- 绘制键盘背景
-    ctx:fill_rect(ax, ay, self.w, self.h, bg_color )
+    ctx:fill_rect(ax, ay, self.w, self.h, bg_color)
     ctx:stroke_rect(ax, ay, self.w, self.h, border_color)
 
     -- 绘制顶部控制栏(返回按钮和预览区)
@@ -2383,20 +2383,20 @@ function keyboard:draw(ctx)
 end
 
 function keyboard:draw_top_bar(ctx, ax, ay)
-    local dark = (current_theme == "dark")
-    local bg_color  = dark and COLOR_WIN11_DARK_BUTTON_BG or COLOR_WIN11_LIGHT_BUTTON_BG
-    local border_color = dark and COLOR_WIN11_DARK_BUTTON_BORDER or COLOR_WIN11_LIGHT_BUTTON_BORDER
-    local text_color = dark and COLOR_WHITE or COLOR_BLACK
-    local button_bg_color  = bg_color 
+    local dark            = (current_theme == "dark")
+    local bg_color        = dark and COLOR_WIN11_DARK_BUTTON_BG or COLOR_WIN11_LIGHT_BUTTON_BG
+    local border_color    = dark and COLOR_WIN11_DARK_BUTTON_BORDER or COLOR_WIN11_LIGHT_BUTTON_BORDER
+    local text_color      = dark and COLOR_WHITE or COLOR_BLACK
+    local button_bg_color = bg_color
 
     -- 返回按钮
-    local backBtnX = ax + 10
-    local backBtnY = ay + 5
-    local backBtnW = 60
-    local backBtnH = 35
+    local backBtnX        = ax + 10
+    local backBtnY        = ay + 5
+    local backBtnW        = 60
+    local backBtnH        = 35
     -- 检查返回按钮是否被按下
-    local backBtnbg_color  = (self.enable_click_effect and self._backButtonPressed) and COLOR_GRAY or button_bg_color 
-    ctx:fill_rect(backBtnX, backBtnY, backBtnW, backBtnH, backBtnbg_color )
+    local backBtnbg_color = (self.enable_click_effect and self._backButtonPressed) and COLOR_GRAY or button_bg_color
+    ctx:fill_rect(backBtnX, backBtnY, backBtnW, backBtnH, backBtnbg_color)
     ctx:stroke_rect(backBtnX, backBtnY, backBtnW, backBtnH, border_color)
     local back_text = "返回"
     local back_style = { size = 12 }
@@ -2427,7 +2427,7 @@ function keyboard:draw_top_bar(ctx, ax, ay)
         -- 输入预览区:有边框,高35px
         local previewAreaY = backBtnY
         local previewAreaH = backBtnH
-        ctx:fill_rect(previewX, previewAreaY, previewW, previewAreaH, button_bg_color )
+        ctx:fill_rect(previewX, previewAreaY, previewW, previewAreaH, button_bg_color)
         ctx:stroke_rect(previewX, previewAreaY, previewW, previewAreaH, border_color)
         -- 左对齐绘制,左边距10px
         local previewtext_color = (previewText == "") and COLOR_GRAY or text_color
@@ -2471,18 +2471,18 @@ end
 function keyboard:draw_preview_area(ctx, ax, ay)
     if not self.input then return end
 
-    local previewY = ay + 5      -- 和返回按键平行
+    local previewY      = ay + 5 -- 和返回按键平行
     local previewHeight = 35     -- 和返回按键高度一致
-    local previewX = ax + 80     -- 预览框起始位置(返回键后)
-    local previewW = self.w - 90 -- 预览框宽度
+    local previewX      = ax + 80 -- 预览框起始位置(返回键后)
+    local previewW      = self.w - 90 -- 预览框宽度
 
-    local dark = (current_theme == "dark")
-    local bg_color  = dark and COLOR_WIN11_DARK_BUTTON_BG or COLOR_WIN11_LIGHT_BUTTON_BG
-    local border_color = dark and COLOR_WIN11_DARK_BUTTON_BORDER or COLOR_WIN11_LIGHT_BUTTON_BORDER
-    local text_color = dark and COLOR_WHITE or COLOR_BLACK
+    local dark          = (current_theme == "dark")
+    local bg_color      = dark and COLOR_WIN11_DARK_BUTTON_BG or COLOR_WIN11_LIGHT_BUTTON_BG
+    local border_color  = dark and COLOR_WIN11_DARK_BUTTON_BORDER or COLOR_WIN11_LIGHT_BUTTON_BORDER
+    local text_color    = dark and COLOR_WHITE or COLOR_BLACK
 
     -- 绘制预览区背景
-    ctx:fill_rect(previewX, previewY, previewW, previewHeight, bg_color )
+    ctx:fill_rect(previewX, previewY, previewW, previewHeight, bg_color)
     ctx:stroke_rect(previewX, previewY, previewW, previewHeight, border_color)
 
     -- 绘制预览文本
@@ -2507,15 +2507,15 @@ function keyboard:draw_preview_area(ctx, ax, ay)
 end
 
 function keyboard:draw_key(ctx, key)
-    local dark = (current_theme == "dark")
-    local bg_color  = dark and COLOR_WIN11_DARK_BUTTON_BG or COLOR_WIN11_LIGHT_BUTTON_BG
-    local border_color = dark and COLOR_WIN11_DARK_BUTTON_BORDER or COLOR_WIN11_LIGHT_BUTTON_BORDER
-    local text_color = dark and COLOR_WHITE or COLOR_BLACK
-    local presse_dbg_color  = COLOR_GRAY
+    local dark             = (current_theme == "dark")
+    local bg_color         = dark and COLOR_WIN11_DARK_BUTTON_BG or COLOR_WIN11_LIGHT_BUTTON_BG
+    local border_color     = dark and COLOR_WIN11_DARK_BUTTON_BORDER or COLOR_WIN11_LIGHT_BUTTON_BORDER
+    local text_color       = dark and COLOR_WHITE or COLOR_BLACK
+    local presse_dbg_color = COLOR_GRAY
 
-    local btnbg_color  = (self.enable_click_effect and key.pressed) and presse_dbg_color  or bg_color 
+    local btnbg_color      = (self.enable_click_effect and key.pressed) and presse_dbg_color or bg_color
 
-    ctx:fill_rect(key.x, key.y, key.w, key.h, btnbg_color )
+    ctx:fill_rect(key.x, key.y, key.w, key.h, btnbg_color)
     ctx:stroke_rect(key.x, key.y, key.w, key.h, border_color)
 
     -- -- 绘制按键文本
@@ -2809,15 +2809,15 @@ end
 
 -- 绘制候选字符区
 function keyboard:draw_candidate_area(ctx, ax, ay)
-    local candidateY = ay + 50 -- 候选区Y坐标(预览区下方10px)
-    local candidateHeight = 50
+    local candidateY       = ay + 50 -- 候选区Y坐标(预览区下方10px)
+    local candidateHeight  = 50
     local candidateBtnSize = 30
 
-    local dark = (current_theme == "dark")
-    local bg_color  = dark and COLOR_WIN11_DARK_BUTTON_BG or COLOR_WIN11_LIGHT_BUTTON_BG
-    local border_color = dark and COLOR_WIN11_DARK_BUTTON_BORDER or COLOR_WIN11_LIGHT_BUTTON_BORDER
-    local text_color = dark and COLOR_WHITE or COLOR_BLACK
-    local presse_dbg_color  = COLOR_GRAY
+    local dark             = (current_theme == "dark")
+    local bg_color         = dark and COLOR_WIN11_DARK_BUTTON_BG or COLOR_WIN11_LIGHT_BUTTON_BG
+    local border_color     = dark and COLOR_WIN11_DARK_BUTTON_BORDER or COLOR_WIN11_LIGHT_BUTTON_BORDER
+    local text_color       = dark and COLOR_WHITE or COLOR_BLACK
+    local presse_dbg_color = COLOR_GRAY
 
     -- 候选按键固定10个,从左到右排列
     for i = 1, 10 do
@@ -2826,12 +2826,12 @@ function keyboard:draw_candidate_area(ctx, ax, ay)
 
         -- 根据是否有候选字符决定显示内容
         if i <= #self.currentCandidates then
-            local char = self.currentCandidates[i]
+            local char        = self.currentCandidates[i]
             -- 检查候选按键是否被按下
-            local isPressed = (self._pressedCandidateIndex == i)
-            local btnbg_color  = (self.enable_click_effect and isPressed) and presse_dbg_color  or bg_color 
+            local isPressed   = (self._pressedCandidateIndex == i)
+            local btnbg_color = (self.enable_click_effect and isPressed) and presse_dbg_color or bg_color
 
-            ctx:fill_rect(btnX, btnY, candidateBtnSize, candidateBtnSize, btnbg_color )
+            ctx:fill_rect(btnX, btnY, candidateBtnSize, candidateBtnSize, btnbg_color)
             ctx:stroke_rect(btnX, btnY, candidateBtnSize, candidateBtnSize, border_color)
 
             -- 绘制候选字符文本
@@ -2843,7 +2843,7 @@ function keyboard:draw_candidate_area(ctx, ax, ay)
             ctx:draw_text(char, textX, textY, text_color, textStyle)
         else
             -- 没有候选字符时显示空按钮
-            ctx:fill_rect(btnX, btnY, candidateBtnSize, candidateBtnSize, bg_color )
+            ctx:fill_rect(btnX, btnY, candidateBtnSize, candidateBtnSize, bg_color)
             ctx:stroke_rect(btnX, btnY, candidateBtnSize, candidateBtnSize, border_color)
         end
     end
@@ -2997,30 +2997,30 @@ end
 
 -- 绘制左侧音节选择区
 function keyboard:draw_left_syllable_panel(ctx, ax, ay)
-    local syllableBtnSize = 30    -- 每个音节按钮大小(30x30)
-    local syllableAreaX = ax      -- 左侧预留区域X坐标
-    local syllableAreaY = ay + 95 -- 从按键区域上方开始(与大格子对齐)
+    local syllableBtnSize     = 30 -- 每个音节按钮大小(30x30)
+    local syllableAreaX       = ax -- 左侧预留区域X坐标
+    local syllableAreaY       = ay + 95 -- 从按键区域上方开始(与大格子对齐)
 
     -- 大格子高度是90px,4个大格子总高度360px
     -- 12个小格子,每个30px,总共360px,正好对齐
     -- 每3个小格子对齐一个大格子(90px = 3 * 30px)
-    local keySize = 90              -- 大格子高度
-    local totalHeight = 4 * keySize -- 4个大格子的总高度 = 360px
-
-    local dark = (current_theme == "dark")
-    local bg_color  = dark and COLOR_WIN11_DARK_BUTTON_BG or COLOR_WIN11_LIGHT_BUTTON_BG
-    local border_color = dark and COLOR_WIN11_DARK_BUTTON_BORDER or COLOR_WIN11_LIGHT_BUTTON_BORDER
-    local text_color = dark and COLOR_WHITE or COLOR_BLACK
-    local selecte_dbg_color  = COLOR_SKY_BLUE
+    local keySize             = 90  -- 大格子高度
+    local totalHeight         = 4 * keySize -- 4个大格子的总高度 = 360px
+
+    local dark                = (current_theme == "dark")
+    local bg_color            = dark and COLOR_WIN11_DARK_BUTTON_BG or COLOR_WIN11_LIGHT_BUTTON_BG
+    local border_color        = dark and COLOR_WIN11_DARK_BUTTON_BORDER or COLOR_WIN11_LIGHT_BUTTON_BORDER
+    local text_color          = dark and COLOR_WHITE or COLOR_BLACK
+    local selecte_dbg_color   = COLOR_SKY_BLUE
     local selected_text_color = COLOR_WHITE
-    local presse_dbg_color  = COLOR_GRAY
+    local presse_dbg_color    = COLOR_GRAY
 
     -- 12个小格子,每个30px,总共360px,正好等于4个大格子的高度
-    local start_y = syllableAreaY
+    local start_y             = syllableAreaY
 
     -- 1. 最上面的上一页切换按键(↑)- 第一个大格子的第一个小格子位置
-    local topBtnY = start_y
-    ctx:fill_rect(syllableAreaX, topBtnY, syllableBtnSize, syllableBtnSize, bg_color )
+    local topBtnY             = start_y
+    ctx:fill_rect(syllableAreaX, topBtnY, syllableBtnSize, syllableBtnSize, bg_color)
     ctx:stroke_rect(syllableAreaX, topBtnY, syllableBtnSize, syllableBtnSize, border_color)
     -- 使用 draw_arrow_icon 绘制箭头图标
     draw_arrow_icon(syllableAreaX, topBtnY, syllableBtnSize, syllableBtnSize, "up", text_color)
@@ -3037,18 +3037,18 @@ function keyboard:draw_left_syllable_panel(ctx, ax, ay)
             local syllable = self.syllableCandidates[idx]
             local isSelected = (idx == self.selectedSyllableIndex)
             local isPressed = (self._pressedSyllableIndex == idx)
-            local btnbg_color 
+            local btnbg_color
             if self.enable_click_effect and isPressed then
-                btnbg_color  = presse_dbg_color 
+                btnbg_color = presse_dbg_color
             elseif isSelected then
-                btnbg_color  = selecte_dbg_color 
+                btnbg_color = selecte_dbg_color
             else
-                btnbg_color  = bg_color 
+                btnbg_color = bg_color
             end
             local btntext_color = (isSelected or (self.enable_click_effect and isPressed)) and selected_text_color or
-            text_color
+                text_color
 
-            ctx:fill_rect(syllableAreaX, btnY, syllableBtnSize, syllableBtnSize, btnbg_color )
+            ctx:fill_rect(syllableAreaX, btnY, syllableBtnSize, syllableBtnSize, btnbg_color)
             ctx:stroke_rect(syllableAreaX, btnY, syllableBtnSize, syllableBtnSize, border_color)
             ctx:draw_text_in_rect_centered(syllableAreaX, btnY, syllableBtnSize, syllableBtnSize, syllable, {
                 color = btntext_color,
@@ -3056,45 +3056,45 @@ function keyboard:draw_left_syllable_panel(ctx, ax, ay)
             })
         else
             -- 空按钮
-            ctx:fill_rect(syllableAreaX, btnY, syllableBtnSize, syllableBtnSize, bg_color )
+            ctx:fill_rect(syllableAreaX, btnY, syllableBtnSize, syllableBtnSize, bg_color)
             ctx:stroke_rect(syllableAreaX, btnY, syllableBtnSize, syllableBtnSize, border_color)
         end
     end
 
     -- 3. 最下面的下一页切换按键(↓)- 第4个大格子的第3个小格子位置(最后一个)
     local bottomBtnY = start_y + 11 * syllableBtnSize -- 第12个小格子(索引12)
-    ctx:fill_rect(syllableAreaX, bottomBtnY, syllableBtnSize, syllableBtnSize, bg_color )
+    ctx:fill_rect(syllableAreaX, bottomBtnY, syllableBtnSize, syllableBtnSize, bg_color)
     ctx:stroke_rect(syllableAreaX, bottomBtnY, syllableBtnSize, syllableBtnSize, border_color)
     draw_arrow_icon(syllableAreaX, bottomBtnY, syllableBtnSize, syllableBtnSize, "down", text_color)
 end
 
 -- 绘制候选字选择区
 function keyboard:draw_pinyin_candidates(ctx, ax, ay)
-    local candidateY = ay + 50 -- 候选区Y坐标
-    local candidateHeight = 50
+    local candidateY          = ay + 50 -- 候选区Y坐标
+    local candidateHeight     = 50
     -- 中文候选带左右翻页:左右各占1格(30px),中间8格候选
-    local candidateBtnSize = 30 -- 每个候选按钮大小(30x30)
+    local candidateBtnSize    = 30 -- 每个候选按钮大小(30x30)
 
-    local dark = (current_theme == "dark")
-    local bg_color  = dark and COLOR_WIN11_DARK_BUTTON_BG or COLOR_WIN11_LIGHT_BUTTON_BG
-    local border_color = dark and COLOR_WIN11_DARK_BUTTON_BORDER or COLOR_WIN11_LIGHT_BUTTON_BORDER
-    local text_color = dark and COLOR_WHITE or COLOR_BLACK
-    local selecte_dbg_color  = COLOR_SKY_BLUE
+    local dark                = (current_theme == "dark")
+    local bg_color            = dark and COLOR_WIN11_DARK_BUTTON_BG or COLOR_WIN11_LIGHT_BUTTON_BG
+    local border_color        = dark and COLOR_WIN11_DARK_BUTTON_BORDER or COLOR_WIN11_LIGHT_BUTTON_BORDER
+    local text_color          = dark and COLOR_WHITE or COLOR_BLACK
+    local selecte_dbg_color   = COLOR_SKY_BLUE
     local selected_text_color = COLOR_WHITE
-    local presse_dbg_color  = COLOR_GRAY
+    local presse_dbg_color    = COLOR_GRAY
 
     -- 左侧分页按键(←)
-    local arrowW = candidateBtnSize
-    local leftArrowX = ax
-    local leftArrowY = candidateY + (candidateHeight - candidateBtnSize) // 2
-    ctx:fill_rect(leftArrowX, leftArrowY, arrowW, candidateBtnSize, bg_color )
+    local arrowW              = candidateBtnSize
+    local leftArrowX          = ax
+    local leftArrowY          = candidateY + (candidateHeight - candidateBtnSize) // 2
+    ctx:fill_rect(leftArrowX, leftArrowY, arrowW, candidateBtnSize, bg_color)
     ctx:stroke_rect(leftArrowX, leftArrowY, arrowW, candidateBtnSize, border_color)
     draw_arrow_icon(leftArrowX, leftArrowY, arrowW, candidateBtnSize, "left", text_color)
 
     -- 右侧分页按键(→)
     local rightArrowX = ax + self.w - arrowW
     local rightArrowY = leftArrowY
-    ctx:fill_rect(rightArrowX, rightArrowY, arrowW, candidateBtnSize, bg_color )
+    ctx:fill_rect(rightArrowX, rightArrowY, arrowW, candidateBtnSize, bg_color)
     ctx:stroke_rect(rightArrowX, rightArrowY, arrowW, candidateBtnSize, border_color)
     draw_arrow_icon(rightArrowX, rightArrowY, arrowW, candidateBtnSize, "right", text_color)
 
@@ -3109,18 +3109,18 @@ function keyboard:draw_pinyin_candidates(ctx, ax, ay)
             local char = self.pinyinCandidates[idx] -- 直接使用UTF-8字符串
             local isSelected = (idx == self.selectedCandidateIndex)
             local isPressed = (self._pressedCandidateIndex == idx)
-            local btnbg_color 
+            local btnbg_color
             if self.enable_click_effect and isPressed then
-                btnbg_color  = presse_dbg_color 
+                btnbg_color = presse_dbg_color
             elseif isSelected then
-                btnbg_color  = selecte_dbg_color 
+                btnbg_color = selecte_dbg_color
             else
-                btnbg_color  = bg_color 
+                btnbg_color = bg_color
             end
             local btntext_color = (isSelected or (self.enable_click_effect and isPressed)) and selected_text_color or
-            text_color
+                text_color
 
-            ctx:fill_rect(btnX, btnY, candidateBtnSize, candidateBtnSize, btnbg_color )
+            ctx:fill_rect(btnX, btnY, candidateBtnSize, candidateBtnSize, btnbg_color)
             ctx:stroke_rect(btnX, btnY, candidateBtnSize, candidateBtnSize, border_color)
 
             -- 使用字体渲染候选字(优先使用hzfont,如果不可用则降级到其他字体后端)
@@ -3132,7 +3132,7 @@ function keyboard:draw_pinyin_candidates(ctx, ax, ay)
             })
         else
             -- 空按钮
-            ctx:fill_rect(btnX, btnY, candidateBtnSize, candidateBtnSize, bg_color )
+            ctx:fill_rect(btnX, btnY, candidateBtnSize, candidateBtnSize, bg_color)
             ctx:stroke_rect(btnX, btnY, candidateBtnSize, candidateBtnSize, border_color)
         end
     end
@@ -3917,4 +3917,4 @@ function ui.refresh()
     return ui.render.present()
 end
 
-return ui
+return ui

+ 789 - 0
script/libs/exmtn.lua

@@ -0,0 +1,789 @@
+--[[
+@module  exmtn
+@summary 运维日志扩展库,负责日志的持久化存储
+@version 1.0
+@date    2025.12.9
+@author  zengeshuai
+@usage
+exmtn.init(1, 0)  -- 初始化,1个块,缓存写入
+exmtn.log("info", "tag", "message", 123)  -- 输出运维日志
+]]
+
+local exmtn = {}
+
+-- 常量定义
+local LOG_MTN_CACHE_SIZE = 4096
+local LOG_MTN_FILE_COUNT = 4
+local LOG_MTN_CONFIG_FILE = "/exmtn.trc"
+local LOG_MTN_DEFAULT_BLOCKS_DIVISOR = 40
+local LOG_MTN_ADD_WRITE_THRESHOLD = 256
+local LOG_MTN_CONFIG_VERSION = 1
+
+-- 写入方式常量
+exmtn.CACHE_WRITE = 0
+exmtn.ADD_WRITE = 1
+
+-- 内部状态
+local ctx = {
+    inited = false,
+    enabled = false,
+    cur_index = 1,           -- 1-4
+    block_size = 4096,       -- 默认块大小
+    blocks_per_file = 1,     -- 每文件块数
+    file_limit = 4096,       -- 每文件大小限制
+    write_way = 0,           -- 0=缓存写入, 1=直接追加
+    cache = "",              -- 缓存缓冲区
+    cache_used = 0,          -- 缓存已使用字节数
+}
+
+-- 重置缓存
+local function reset_cache()
+    ctx.cache = ""
+    ctx.cache_used = 0
+end
+
+-- 获取当前文件路径
+local function get_file_path(index)
+    return string.format("/hzmtn%d.trc", index or ctx.cur_index)
+end
+
+-- 获取当前文件大小
+local function get_current_file_size()
+    local path = get_file_path()
+    local file = io.open(path, "rb")
+    if not file then
+        return 0
+    end
+    local size = file:seek("end")
+    file:close()
+    -- file:seek("end") 返回文件大小,如果失败返回 nil
+    if size and size > 0 then
+        return size
+    end
+    return 0
+end
+
+-- 检查文件是否存在
+local function file_exists(path)
+    local file = io.open(path, "rb")
+    if file then
+        file:close()
+        return true
+    end
+    return false
+end
+
+-- 检查所有日志文件是否存在
+local function files_exist()
+    for i = 1, LOG_MTN_FILE_COUNT do
+        local path = get_file_path(i)
+        if file_exists(path) then
+            return true
+        end
+    end
+    return false
+end
+
+-- 删除所有日志文件
+local function remove_files()
+    for i = 1, LOG_MTN_FILE_COUNT do
+        local path = get_file_path(i)
+        os.remove(path)
+    end
+end
+
+-- 创建空文件
+local function create_files()
+    for i = 1, LOG_MTN_FILE_COUNT do
+        local path = get_file_path(i)
+        local file = io.open(path, "wb")
+        if file then
+            file:close()
+        end
+    end
+end
+
+-- 读取配置文件
+local function load_config()
+    local file = io.open(LOG_MTN_CONFIG_FILE, "rb")
+    if not file then
+        return nil  -- 文件不存在,返回 nil
+    end
+    
+    local content = file:read("*a")
+    file:close()
+    
+    if not content or #content == 0 then
+        return nil  -- 文件为空
+    end
+    
+    -- 解析配置:格式为 "VERSION=1\nINDEX=2\nBLOCKS=10\nWRITE_WAY=0\n"
+    local config = {}
+    for line in content:gmatch("[^\r\n]+") do
+        -- 移除首尾空白字符
+        line = line:match("^%s*(.-)%s*$") or line
+        local key, value = line:match("([^=]+)=(.+)")
+        if key and value then
+            -- 移除 key 和 value 的首尾空白字符
+            key = key:match("^%s*(.-)%s*$") or key
+            value = value:match("^%s*(.-)%s*$") or value
+            local num_value = tonumber(value)
+            if num_value then
+                config[key] = num_value
+            else
+                config[key] = value
+            end
+        end
+    end
+    
+    -- 验证版本号
+    if config.VERSION ~= LOG_MTN_CONFIG_VERSION then
+        return nil  -- 版本不匹配
+    end
+    
+    return config
+end
+
+-- 保存配置文件
+local function save_config(index, blocks, write_way)
+    local content = string.format("VERSION=%d\nINDEX=%d\nBLOCKS=%d\nWRITE_WAY=%d\n", 
+        LOG_MTN_CONFIG_VERSION, index, blocks, write_way)
+    
+    local file = io.open(LOG_MTN_CONFIG_FILE, "wb")
+    if not file then
+        log.warn("exmtn", "无法打开配置文件: " .. LOG_MTN_CONFIG_FILE)
+        return false
+    end
+    
+    local ok = file:write(content)
+    file:close()
+    
+    if not ok then
+        log.warn("exmtn", "写入配置文件失败: " .. LOG_MTN_CONFIG_FILE)
+        return false
+    end
+    
+    return true
+end
+
+-- 更新索引(同时保存完整配置)
+local function update_index(index)
+    return save_config(index, ctx.blocks_per_file, ctx.write_way)
+end
+
+-- 格式化时间戳
+-- 返回格式: [2025-11-05 15:06:49.947][00000027.994]
+local function format_timestamp()
+    -- 获取系统运行时间(毫秒)
+    local ticks_ms = 0
+    if mcu and mcu.ticks then
+        local ticks = mcu.ticks()
+        if ticks then
+            ticks_ms = ticks
+        end
+    end
+    
+    -- 获取当前日期时间
+    local date_time_str = ""
+    local ms = 0
+    
+    if os and os.date then
+        -- 获取当前日期时间字符串: 2025-11-05 15:06:49
+        local dt = os.date("%Y-%m-%d %H:%M:%S")
+        if dt then
+            -- 计算毫秒:使用系统运行时间的毫秒部分
+            -- 如果 RTC 已设置,时间会更准确
+            ms = ticks_ms % 1000
+            date_time_str = string.format("%s.%03d", dt, ms)
+        end
+    end
+    
+    -- 如果无法获取日期时间,使用默认格式
+    if date_time_str == "" then
+        date_time_str = "1970-01-01 00:00:00.000"
+    end
+    
+    -- 计算系统运行时间(秒.毫秒)
+    local uptime_sec = math.floor(ticks_ms / 1000)
+    local uptime_ms = ticks_ms % 1000
+    
+    -- 格式化运行时间部分: 00000027.994(固定宽度,9位整数+3位小数)
+    local uptime_str = string.format("%09d.%03d", uptime_sec, uptime_ms)
+    
+    -- 返回完整时间戳
+    return string.format("[%s][%s]", date_time_str, uptime_str)
+end
+
+-- 格式化调试信息
+local function format_debug_info(level, include_level)
+    local info = debug.getinfo(2, "Sl")
+    if not info or not info.source then
+        return nil
+    end
+    
+    local src = info.source
+    -- 跳过第一个字符(@ 或 =)
+    if src:sub(1, 1) == "@" or src:sub(1, 1) == "=" then
+        src = src:sub(2)
+    end
+    
+    local line = info.currentline or 0
+    if line > 64 * 1024 then
+        line = 0
+    end
+    
+    if include_level and level then
+        return string.format("%s/%s:%d", level, src, line)
+    else
+        return string.format("%s:%d", src, line)
+    end
+end
+
+-- 格式化消息(与 log.info/warn/error 格式一致,但添加时间戳前缀)
+local function format_message(level, tag, ...)
+    local argc = select("#", ...)
+    
+    -- 获取 log.style 配置
+    local log_style = 0
+    if log and log.style then
+        log_style = log.style() or 0
+    end
+    
+    -- 根据级别确定日志标识
+    local level_char = "I"  -- 默认 info
+    if level == "warn" then
+        level_char = "W"
+    elseif level == "error" then
+        level_char = "E"
+    end
+    
+    local msg = ""
+    local dbg_info_with_level = format_debug_info(level_char, true)
+    local dbg_info_only = format_debug_info(nil, false)
+    
+    if log_style == 0 then
+        -- LOG_STYLE_NORMAL: "I/user.tag arg1 arg2 ...\n"
+        msg = string.format("%s/user.%s", level_char, tag)
+        for i = 1, argc do
+            local arg = select(i, ...)
+            msg = msg .. " " .. tostring(arg)
+        end
+    elseif log_style == 1 then
+        -- LOG_STYLE_DEBUG_INFO: "I/file.lua:123 tag arg1 arg2 ...\n"
+        if dbg_info_with_level then
+            msg = dbg_info_with_level
+        else
+            msg = level_char
+        end
+        msg = msg .. " " .. tag
+        for i = 1, argc do
+            local arg = select(i, ...)
+            msg = msg .. " " .. tostring(arg)
+        end
+    else
+        -- LOG_STYLE_FULL: "I/user.tag file.lua:123 arg1 arg2 ...\n"
+        msg = string.format("%s/user.%s", level_char, tag)
+        if dbg_info_only then
+            msg = msg .. " " .. dbg_info_only
+        end
+        for i = 1, argc do
+            local arg = select(i, ...)
+            msg = msg .. " " .. tostring(arg)
+        end
+    end
+    
+    msg = msg .. "\n"
+    
+    -- 添加时间戳前缀
+    local timestamp = format_timestamp()
+    return timestamp .. " " .. msg
+end
+
+-- 刷新缓存到文件
+local function flush_cache()
+    if ctx.cache_used == 0 then
+        return true
+    end
+    
+    local path = get_file_path()
+    local file = io.open(path, "ab")
+    if not file then
+        log.warn("exmtn", "无法打开文件: " .. path)
+        return false
+    end
+    
+    -- file:write 返回 true/false 或 nil,不返回字节数
+    local ok = file:write(ctx.cache)
+    file:close()
+    
+    if not ok then
+        log.warn("exmtn", "写入文件失败: " .. path)
+        return false
+    end
+    
+    reset_cache()
+    return true
+end
+
+-- 直接写入文件(ADD_WRITE 模式)
+local function direct_write(data)
+    local path = get_file_path()
+    local file = io.open(path, "ab")
+    if not file then
+        log.warn("exmtn", "无法打开文件: " .. path)
+        return false
+    end
+    
+    -- file:write 返回 true/false 或 nil,不返回字节数
+    local ok = file:write(data)
+    file:close()
+    
+    if not ok then
+        log.warn("exmtn", "写入文件失败: " .. path)
+        return false
+    end
+    
+    return true
+end
+
+-- 将数据追加到缓存或直接写入
+local function buffer_append(data)
+    if not data or #data == 0 then
+        return true
+    end
+    
+    local len = #data
+    
+    -- ADD_WRITE 模式:直接写入文件
+    if ctx.write_way == exmtn.ADD_WRITE then
+        -- 小数据先缓存,累积到阈值再写入
+        if len < LOG_MTN_ADD_WRITE_THRESHOLD then
+            if ctx.cache_used + len > LOG_MTN_CACHE_SIZE then
+                if not flush_cache() then
+                    return false
+                end
+            end
+            ctx.cache = ctx.cache .. data
+            ctx.cache_used = ctx.cache_used + len
+            -- 如果累积到阈值,立即写入
+            if ctx.cache_used >= LOG_MTN_ADD_WRITE_THRESHOLD then
+                return flush_cache()
+            end
+            return true
+        end
+        -- 大数据直接写入
+        return direct_write(data)
+    end
+    
+    -- CACHE_WRITE 模式:原有逻辑
+    if len > LOG_MTN_CACHE_SIZE then
+        -- 先刷新缓存
+        if not flush_cache() then
+            return false
+        end
+        -- 大数据直接写入
+        return direct_write(data)
+    end
+    
+    -- 检查缓存是否足够
+    if ctx.cache_used + len > LOG_MTN_CACHE_SIZE then
+        if not flush_cache() then
+            return false
+        end
+    end
+    
+    ctx.cache = ctx.cache .. data
+    ctx.cache_used = ctx.cache_used + len
+    return true
+end
+
+-- 写入日志到文件
+local function write_to_file(msg)
+    if not ctx.enabled then
+        return true  -- 未启用时返回成功,不写入
+    end
+    
+    local len = #msg
+    
+    -- CACHE_WRITE 模式
+    if ctx.write_way == exmtn.CACHE_WRITE then
+        -- 检查文件大小 + 缓存大小 + 当前数据是否会超过限制
+        -- 如果会超过,先刷新缓存
+        if ctx.cache_used > 0 then
+            local file_sz = get_current_file_size()
+            if file_sz + ctx.cache_used + len > ctx.file_limit then
+                -- 先刷新缓存
+                if not flush_cache() then
+                    return false
+                end
+                -- 重新获取文件大小
+                file_sz = get_current_file_size()
+                -- 检查文件是否已满
+                if file_sz >= ctx.file_limit then
+                    -- 文件已满,切换到下一个文件
+                    ctx.cur_index = (ctx.cur_index % LOG_MTN_FILE_COUNT) + 1
+                    local path = get_file_path()
+                    local file = io.open(path, "wb")
+                    if file then
+                        file:close()
+                    end
+                    if not update_index(ctx.cur_index) then
+                        log.warn("exmtn", "更新索引失败")
+                        return false
+                    end
+                    reset_cache()
+                end
+            end
+        else
+            -- 缓存为空,检查文件大小
+            local file_sz = get_current_file_size()
+            if file_sz + len > ctx.file_limit then
+                -- 文件已满,切换到下一个文件
+                ctx.cur_index = (ctx.cur_index % LOG_MTN_FILE_COUNT) + 1
+                local path = get_file_path()
+                local file = io.open(path, "wb")
+                if file then
+                    file:close()
+                end
+                if not update_index(ctx.cur_index) then
+                    log.warn("exmtn", "更新索引失败")
+                    return false
+                end
+                reset_cache()
+            end
+        end
+        
+        -- 如果加入这条数据后缓存会满,先刷新缓存
+        if ctx.cache_used + len > LOG_MTN_CACHE_SIZE then
+            if not flush_cache() then
+                return false
+            end
+            
+            -- 刷新后再次检查文件大小
+            local file_sz = get_current_file_size()
+            if file_sz >= ctx.file_limit then
+                -- 文件已满,切换到下一个文件
+                ctx.cur_index = (ctx.cur_index % LOG_MTN_FILE_COUNT) + 1
+                local path = get_file_path()
+                local file = io.open(path, "wb")
+                if file then
+                    file:close()
+                end
+                if not update_index(ctx.cur_index) then
+                    log.warn("exmtn", "更新索引失败")
+                    return false
+                end
+                reset_cache()
+            end
+        end
+        
+        -- 加入缓存
+        return buffer_append(msg)
+    else
+        -- ADD_WRITE 模式:先刷新缓存,确保文件大小准确
+        if ctx.cache_used > 0 then
+            if not flush_cache() then
+                return false
+            end
+        end
+        
+        -- 获取当前文件大小
+        local file_sz = get_current_file_size()
+        
+        -- 检查当前文件是否已写满
+        if file_sz >= ctx.file_limit then
+            -- 文件已满,切换到下一个文件
+            ctx.cur_index = (ctx.cur_index % LOG_MTN_FILE_COUNT) + 1
+            local path = get_file_path()
+            local file = io.open(path, "wb")
+            if file then
+                file:close()
+            end
+            if not update_index(ctx.cur_index) then
+                log.warn("exmtn", "更新索引失败")
+                return false
+            end
+            reset_cache()
+        end
+        
+        -- 检查当前数据是否能放入当前文件
+        if file_sz + len > ctx.file_limit then
+            -- 当前数据放不下,切换到下一个文件
+            ctx.cur_index = (ctx.cur_index % LOG_MTN_FILE_COUNT) + 1
+            local path = get_file_path()
+            local file = io.open(path, "wb")
+            if file then
+                file:close()
+            end
+            if not update_index(ctx.cur_index) then
+                log.warn("exmtn", "更新索引失败")
+                return false
+            end
+            reset_cache()
+        end
+        
+        -- 加入缓存或直接写入(buffer_append 会根据大小决定)
+        return buffer_append(msg)
+    end
+end
+
+--[[
+初始化运维日志
+@api exmtn.init(blocks, write_way)
+@int blocks 每个文件的块数,0表示禁用,正整数表示块数量
+@int write_way 写入方式,可选参数。exmtn.CACHE_WRITE(0)表示缓存写入,exmtn.ADD_WRITE(1)表示直接追加写入,默认为exmtn.CACHE_WRITE
+@return boolean 成功返回true,失败返回false
+@usage
+exmtn.init(1, exmtn.CACHE_WRITE)  -- 初始化,1个块,缓存写入
+]]
+function exmtn.init(blocks, write_way)
+    -- 参数校验
+    if blocks == nil then
+        blocks = 0
+    end
+    blocks = math.floor(blocks)
+    if blocks < 0 then
+        log.warn("exmtn", "无效的块数")
+        return false
+    end
+    
+    write_way = write_way or exmtn.CACHE_WRITE
+    if write_way ~= exmtn.CACHE_WRITE and write_way ~= exmtn.ADD_WRITE then
+        write_way = exmtn.CACHE_WRITE
+    end
+    
+    -- 如果禁用
+    if blocks == 0 then
+        reset_cache()
+        remove_files()
+        ctx.enabled = false
+        ctx.cur_index = 1
+        -- 删除配置文件
+        os.remove(LOG_MTN_CONFIG_FILE)
+        ctx.inited = true
+        return true
+    end
+    
+    -- 读取文件系统信息
+    if not ctx.inited then
+        ctx.block_size = 4096
+        ctx.blocks_per_file = 1
+        
+        -- 尝试获取文件系统信息(需要 fs 模块支持)
+        -- fs.fsstat 返回: success, total_blocks, used_blocks, block_size, fs_type
+        if fs and fs.fsstat then
+            local success, total_blocks, used_blocks, block_size, fs_type = fs.fsstat("/")
+            if success and block_size and block_size > 0 then
+                ctx.block_size = block_size
+                if total_blocks and total_blocks > 0 then
+                    local def_blocks = math.floor(total_blocks / LOG_MTN_DEFAULT_BLOCKS_DIVISOR)
+                    if def_blocks > 0 then
+                        ctx.blocks_per_file = def_blocks
+                    end
+                end
+            end
+        end
+    end
+    
+    -- 读取配置文件(仅在首次初始化时读取)
+    if not ctx.inited then
+        local config = load_config()
+        if config then
+            -- 读取索引
+            if config.INDEX and config.INDEX >= 1 and config.INDEX <= LOG_MTN_FILE_COUNT then
+                ctx.cur_index = config.INDEX
+            end
+            
+            -- 读取块数配置
+            if config.BLOCKS and config.BLOCKS > 0 then
+                ctx.blocks_per_file = config.BLOCKS
+            end
+            
+            -- 读取写入方式配置
+            if config.WRITE_WAY == 0 or config.WRITE_WAY == 1 then
+                ctx.write_way = config.WRITE_WAY
+            end
+
+            log.info("exmtn", "读取索引", ctx.cur_index)
+            log.info("exmtn", "读取块数配置", ctx.blocks_per_file)
+            log.info("exmtn", "读取写入方式配置", ctx.write_way)
+        end
+    end
+    
+    -- 检查配置是否变化
+    -- 如果已初始化,比较当前配置和新配置;如果未初始化,不需要判断(首次初始化总是"变化"的)
+    local config_changed = false
+    if ctx.inited then
+        -- 已初始化:比较当前配置和新传入的配置
+        config_changed = (ctx.blocks_per_file ~= blocks) or (ctx.write_way ~= write_way)
+    end
+    -- 未初始化:config_changed 保持为 false,因为首次初始化不算"变化"
+
+    log.info("exmtn", "配置变化", config_changed)
+    -- 更新配置
+    ctx.blocks_per_file = blocks
+    ctx.write_way = write_way
+    ctx.file_limit = ctx.block_size * ctx.blocks_per_file
+    if ctx.file_limit == 0 then
+        ctx.file_limit = LOG_MTN_CACHE_SIZE
+    end
+    
+    -- 处理文件的三种情况
+    if config_changed then
+        -- 情况1:配置变化,清空文件
+        log.info("exmtn", "配置变化,清空文件")
+        reset_cache()
+        remove_files()
+        create_files()
+        ctx.cur_index = 1
+    elseif files_exist() then
+        -- 情况2:配置没有变化,文件存在,根据配置文件中保存的文件指针继续写
+        log.info("exmtn", "配置未变化,文件存在,继续写入")
+        -- ctx.cur_index 已经从配置文件读取(如果是首次初始化)或保持当前值(如果已初始化),不需要重置
+    else
+        -- 情况3:配置没有变化,文件不存在,创建文件
+        log.info("exmtn", "配置未变化,文件不存在,创建文件")
+        create_files()
+        -- ctx.cur_index 已经从配置文件读取(如果是首次初始化)或保持当前值(如果已初始化),不需要重置
+    end
+    
+    -- 保存配置到文件
+    if not save_config(ctx.cur_index, blocks, write_way) then
+        log.warn("exmtn", "保存配置失败")
+        return false
+    end
+    
+    ctx.enabled = true
+    ctx.inited = true
+    
+    -- 打印初始化信息
+    if blocks > 0 then
+        local total_size = ctx.file_limit * LOG_MTN_FILE_COUNT
+        local file_size_mb = ctx.file_limit / (1024 * 1024)
+        local total_size_mb = total_size / (1024 * 1024)
+        local file_size_kb = ctx.file_limit / 1024
+        local total_size_kb = total_size / 1024
+        
+        if ctx.file_limit >= 1024 * 1024 then
+            log.info("exmtn", string.format("初始化成功: 每个文件 %.2f MB (%d 块 × %d 字节), 总空间 %.2f MB (%d 个文件)", 
+                file_size_mb, ctx.blocks_per_file, ctx.block_size, total_size_mb, LOG_MTN_FILE_COUNT))
+        elseif ctx.file_limit >= 1024 then
+            log.info("exmtn", string.format("初始化成功: 每个文件 %.2f KB (%d 块 × %d 字节), 总空间 %.2f KB (%d 个文件)", 
+                file_size_kb, ctx.blocks_per_file, ctx.block_size, total_size_kb, LOG_MTN_FILE_COUNT))
+        else
+            log.info("exmtn", string.format("初始化成功: 每个文件 %d 字节 (%d 块 × %d 字节), 总空间 %d 字节 (%d 个文件)", 
+                ctx.file_limit, ctx.blocks_per_file, ctx.block_size, total_size, LOG_MTN_FILE_COUNT))
+        end
+    end
+    
+    return true
+end
+
+--[[
+输出运维日志并写入文件
+@api exmtn.log(level, tag, ...)
+@string level 日志级别,必须是 "info", "warn", 或 "error"
+@string tag 日志标识,必须是字符串
+@... 需打印的参数
+@return boolean 成功返回true,失败返回false
+@usage
+exmtn.log("info", "message", 123)
+exmtn.log("warn", "message", 456)
+exmtn.log("error", "message", 789)
+]]
+function exmtn.log(level, tag, ...)
+    if not level or type(level) ~= "string" then
+        log.warn("exmtn", "level 必须是字符串")
+        return false
+    end
+    
+    if not tag or type(tag) ~= "string" then
+        log.warn("exmtn", "tag 必须是字符串")
+        return false
+    end
+    
+    -- 根据级别调用对应的底层函数(会被日志级别过滤)
+    if level == "info" then
+        log.info(tag, ...)
+    elseif level == "warn" then
+        log.warn(tag, ...)
+    elseif level == "error" then
+        log.error(tag, ...)
+    else
+        log.warn("exmtn", "level 必须是 'info', 'warn' 或 'error'")
+        return false
+    end
+    
+    -- 格式化消息(用于文件写入)
+    local msg = format_message(level, tag, ...)
+    if not msg then
+        log.warn("exmtn", "格式化消息失败")
+        return false
+    end
+    
+    -- 写入文件(不受日志级别影响)
+    return write_to_file(msg)
+end
+
+--[[
+获取当前配置
+@api exmtn.get_config()
+@return table|nil 配置信息,失败返回nil
+@usage
+local config = exmtn.get_config()
+if config then
+    log.info("exmtn", "blocks:", config.blocks, "write_way:", config.write_way)
+end
+]]
+function exmtn.get_config()
+    if not ctx.inited then
+        return nil
+    end
+    return {
+        enabled = ctx.enabled,
+        cur_index = ctx.cur_index,
+        block_size = ctx.block_size,
+        blocks_per_file = ctx.blocks_per_file,
+        file_limit = ctx.file_limit,
+        write_way = ctx.write_way,
+    }
+end
+
+--[[
+清除所有运维日志文件
+@api exmtn.clear()
+@return boolean 成功返回true,失败返回false
+@usage
+local ok = exmtn.clear()
+if ok then
+    log.info("exmtn", "日志文件已清除")
+end
+]]
+function exmtn.clear()
+    -- 如果已初始化,先刷新缓存(确保数据不丢失)
+    if ctx.inited and ctx.cache_used > 0 then
+        if not flush_cache() then
+            return false
+        end
+    end
+    
+    -- 删除所有日志文件
+    remove_files()
+    
+    -- 重新创建空文件
+    create_files()
+    
+    -- 重置索引为1
+    ctx.cur_index = 1
+    
+    -- 更新配置文件
+    if not save_config(1, ctx.blocks_per_file, ctx.write_way) then
+        return false
+    end
+    
+    log.info("exmtn", "运维日志文件已清除")
+    return true
+end
+
+return exmtn
+

+ 11 - 8
script/libs/httpplus.lua

@@ -403,7 +403,7 @@ end
 
 -- socket 回调函数
 local function http_socket_cb(opts, event)
-    opts.log(TAG, "tcp.event", event)
+    opts.log(TAG, "tcp.event", string.format("%08X", event))
     if event == socket.ON_LINE then
         -- TCP链接已建立, 那就可以上行了
         -- opts.state = "ON_LINE"
@@ -447,8 +447,8 @@ local function http_exec(opts)
     opts.rx_buff = zbuff.create(1024)
     opts.topic = tostring(netc)
     socket.config(netc, nil,nil, opts.is_ssl)
-    if opts.debug or httpplus.debug then
-        socket.debug(netc)
+    if opts.debug_socket then
+        socket.debug(netc, true)
     end
     if not socket.connect(netc, opts.host, opts.port, opts.try_ipv6) then
         log.warn(TAG, "调用socket.connect返回错误了")
@@ -508,6 +508,7 @@ local function http_exec(opts)
                 local fd = io.open(v[1], "rb")
                 -- log.info("写入文件数据", v[1])
                 if fd then
+                    local total = 0
                     while not opts.is_closed do
                         fbuf:seek(0)
                         local ok, flen = fd:fill(fbuf)
@@ -515,7 +516,7 @@ local function http_exec(opts)
                             break
                         end
                         fbuf:seek(flen)
-                        -- log.info("写入文件数据", "长度", #fdata)
+                        opts.log(TAG, "写入文件数据", "长度", flen, "总计", total)
                         if socket.tx(netc, fbuf) == false then
                             log.warn(TAG, "socket.tx返回错误了, 传送失败!!!!")
                             fail_check = false
@@ -523,7 +524,7 @@ local function http_exec(opts)
                         end
                         write_counter = write_counter + flen
                         -- 注意, 这里要等待TX_OK事件
-                        sys.waitUntil(opts.topic, 300)
+                        sys.waitUntil(opts.topic, 1000)
                     end
                     fd:close()
                 end
@@ -543,6 +544,7 @@ local function http_exec(opts)
         local fd = io.open(opts.bodyfile, "rb")
         -- log.info("写入文件数据", v[1])
         if fd then
+            local total = 0
             while not opts.is_closed do
                 fbuf:seek(0)
                 local ok, flen = fd:fill(fbuf)
@@ -550,7 +552,8 @@ local function http_exec(opts)
                     break
                 end
                 fbuf:seek(flen)
-                -- log.info("写入文件数据", "长度", #fdata)
+                total = total + flen
+                opts.log(TAG, "写入文件数据", "长度", flen, "总计", total)
                 if socket.tx(netc, fbuf) == false then
                     log.warn(TAG, "socket.tx返回错误了, 传送失败!!!!")
                     fail_check = false
@@ -558,7 +561,7 @@ local function http_exec(opts)
                 end
                 write_counter = write_counter + flen
                 -- 注意, 这里要等待TX_OK事件
-                sys.waitUntil(opts.topic, 300)
+                sys.waitUntil(opts.topic, 1000)
             end
             fd:close()
         end
@@ -588,7 +591,7 @@ local function http_exec(opts)
                             break
                         end
                         offset = offset + fbuf:len()
-                        sys.waitUntil(opts.topic, 300)
+                        sys.waitUntil(opts.topic, 1000)
                     else
                         fbuf:copy(0, tmpbuff, offset, tsize - offset)
                         fbuf:seek(tsize - offset)