Просмотр исходного кода

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

13917187172 4 месяцев назад
Родитель
Сommit
2976279820
50 измененных файлов с 7185 добавлено и 8501 удалено
  1. 7 0
      components/airlink/include/luat_airlink_fota.h
  2. 55 0
      components/airlink/src/luat_airlink_fota.c
  3. 3 3
      components/fatfs/diskio.h
  4. 241 183
      components/fatfs/ff.c
  5. 91 73
      components/fatfs/ff.h
  6. 27 10
      components/fatfs/ffconf.h
  7. 3600 6381
      components/fatfs/ffunicode.c
  8. 12 0
      components/lcd/luat_lcd.c
  9. 0 1
      components/network/httpsrv/src/luat_httpsrv_lwip.c
  10. 0 541
      module/Air780EHM_Air780EHV_Air780EGH/demo/audio/exaudio.lua
  11. 282 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fs_io/flash_fs_io.lua
  12. 74 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fs_io/http_download_flash.lua
  13. 78 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fs_io/main.lua
  14. 143 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/fs_io/readme.md
  15. 0 311
      module/Air780EPM/demo/780EPM_1.3版本开发板 ch390h联网测试/lan/dhcpsrv.lua
  16. 0 113
      module/Air780EPM/demo/780EPM_1.3版本开发板 ch390h联网测试/lan/dnsproxy.lua
  17. 1 1
      module/Air780EPM/demo/accessory_board/AirSHT30_1000/main.lua
  18. 8 8
      module/Air780EPM/demo/accessory_board/AirSHT30_1000/readme.md
  19. 0 6
      module/Air780EPM/demo/accessory_board/AirSHT30_1000/sht30_app.lua
  20. 1 1
      module/Air780EPM/demo/accessory_board/AirVOC_1000/main.lua
  21. 8 8
      module/Air780EPM/demo/accessory_board/AirVOC_1000/readme.md
  22. 0 8
      module/Air780EPM/demo/accessory_board/AirVOC_1000/voc_app.lua
  23. 282 0
      module/Air780EPM/demo/fs_io/flash_fs_io.lua
  24. 74 0
      module/Air780EPM/demo/fs_io/http_download_flash.lua
  25. 78 0
      module/Air780EPM/demo/fs_io/main.lua
  26. 141 0
      module/Air780EPM/demo/fs_io/readme.md
  27. 0 3
      module/Air8000/demo/accessory_board/AirETH_1000/http/http_app.lua
  28. 1 1
      module/Air8000/demo/accessory_board/AirETH_1000/network_routing/4g_out_ethernet_in_wifi_in/readme.md
  29. 0 541
      module/Air8000/demo/audio/exaudio.lua
  30. 282 0
      module/Air8000/demo/fs_io/flash_fs_io.lua
  31. 74 0
      module/Air8000/demo/fs_io/http_download_flash.lua
  32. 78 0
      module/Air8000/demo/fs_io/main.lua
  33. 139 0
      module/Air8000/demo/fs_io/readme.md
  34. 14 7
      module/Air8000/demo/pins/pins_test.lua
  35. 122 0
      module/Air8000/demo/pins/readme.md
  36. 27 12
      module/Air8000/project/sms_call_forward/cc_forward.lua
  37. 8 3
      module/Air8000/project/sms_call_forward/main.lua
  38. 44 0
      module/Air8000/project/sms_call_forward/netdrv/netdrv_4g.lua
  39. 70 0
      module/Air8000/project/sms_call_forward/netdrv/netdrv_eth_spi.lua
  40. 39 8
      module/Air8000/project/sms_call_forward/netdrv/netdrv_multiple.lua
  41. 45 0
      module/Air8000/project/sms_call_forward/netdrv/netdrv_pc.lua
  42. 68 0
      module/Air8000/project/sms_call_forward/netdrv/netdrv_wifi.lua
  43. 37 0
      module/Air8000/project/sms_call_forward/netdrv_device.lua
  44. 317 0
      module/Air8000/project/sms_call_forward/readme.md
  45. 26 20
      module/Air8000/project/sms_call_forward/sms_forward.lua
  46. 0 258
      module/Air8000/project/sms_forward/readme.md
  47. 282 0
      module/Air8101/demo/fs_io/flash_fs_io.lua
  48. 85 0
      module/Air8101/demo/fs_io/http_download_flash.lua
  49. 78 0
      module/Air8101/demo/fs_io/main.lua
  50. 143 0
      module/Air8101/demo/fs_io/readme.md

+ 7 - 0
components/airlink/include/luat_airlink_fota.h

@@ -3,6 +3,13 @@
 #include "luat_base.h"
 #include "luat_fs.h"
 
+#define AIRLINK_FOTA_SUCCESS            (0)
+#define AIRLINK_FOTA_NO_MEM             (1)
+#define AIRLINK_FOTA_OPEN_FILE_FAIL     (2)
+
+
+
+
 typedef struct luat_airlink_fota {
     uint32_t state;
     size_t total_size;

+ 55 - 0
components/airlink/src/luat_airlink_fota.c

@@ -8,6 +8,46 @@
 #define LUAT_LOG_TAG "airlink.fota"
 #include "luat_log.h"
 
+#ifdef __LUATOS__
+#include "luat_msgbus.h"
+/*
+@sys_pub airlink
+AIRLINK升级结束消息 2025/10/24启用
+AIRLINK_SFOTA_DONE
+@bool result, 升级成功为true,否则为false
+@string reason, 失败原因,当前取值有"no_memory" 内存不足, "file_error" 文件打开异常, "upgrading" 升级中
+@usage
+-- 订阅式
+sys.subscribe("AIRLINK_SFOTA_DONE", function(result, reason)
+    log.info("airlink fota", result, reason)
+end)
+*/
+static int airlink_sfota_lua_cb(lua_State *L, void *ptr)
+{
+    rtos_msg_t *msg = (rtos_msg_t *)lua_topointer(L, -1);
+    lua_getglobal(L, "sys_pub");
+    lua_pushstring(L, "AIRLINK_SFOTA_DONE");
+    lua_pushboolean(L, msg->arg1 == AIRLINK_FOTA_SUCCESS ? 1 : 0);
+    switch (msg->arg1)
+    {
+    case AIRLINK_FOTA_SUCCESS:
+        lua_call(L, 2, 0);
+        break;
+    case AIRLINK_FOTA_NO_MEM:
+        lua_pushstring(L, "no_memory");
+        lua_call(L, 3, 0);
+        break;
+    case AIRLINK_FOTA_OPEN_FILE_FAIL:
+        lua_pushstring(L, "file_error");
+        lua_call(L, 3, 0);
+        break;
+    default:
+        break;
+    }
+    return 0;
+}
+#endif
+
 luat_airlink_fota_t *g_airlink_fota;
 
 int luat_airlink_fota_init(luat_airlink_fota_t *ctx)
@@ -69,6 +109,7 @@ void airlink_sfota_exec(void)
 {
     size_t wait_timeout = 0;
     int ret = 0;
+    uint8_t fota_ret = AIRLINK_FOTA_SUCCESS;
     g_airlink_fota->total_size = luat_fs_fsize(g_airlink_fota->path);
     LLOGI("开始执行sFOTA file size %ld", g_airlink_fota->total_size);
     uint32_t tmpv = 0;
@@ -77,6 +118,7 @@ void airlink_sfota_exec(void)
     if (fd == NULL)
     {
         LLOGE("打开sFOTA文件失败 %s", g_airlink_fota->path);
+        fota_ret = AIRLINK_FOTA_OPEN_FILE_FAIL;
         goto clean;
     }
     if (s_airlink_fota_txbuff == NULL)
@@ -87,6 +129,7 @@ void airlink_sfota_exec(void)
         if (s_airlink_fota_txbuff == NULL || s_airlink_fota_rxbuff == NULL || s_airlink_fota_cmdbuff == NULL)
         {
             LLOGE("申请sFOTA内存失败");
+            fota_ret = AIRLINK_FOTA_NO_MEM;
             goto clean;
         }
         memset(s_airlink_fota_txbuff, 0, AIRLINK_SFOTA_BUFF_SIZE);
@@ -187,6 +230,8 @@ void airlink_sfota_exec(void)
     luat_rtos_task_sleep(wait_timeout);
     LLOGI("FOTA执行完毕");
 
+
+
 clean:
     g_airlink_fota->state = 0;
     if (fd)
@@ -208,5 +253,15 @@ clean:
         luat_heap_opt_free(AIRLINK_MEM_TYPE, s_airlink_fota_cmdbuff);
         s_airlink_fota_cmdbuff = NULL;
     }
+
+#ifdef __LUATOS__
+    rtos_msg_t msg = {
+        .handler = airlink_sfota_lua_cb,
+        .arg1 = fota_ret,
+        .arg2 = 0,
+        .ptr = NULL
+    };
+    luat_msgbus_put(&msg, 0);
+#endif
     return;
 }

+ 3 - 3
components/fatfs/diskio.h

@@ -1,5 +1,5 @@
 /*-----------------------------------------------------------------------/
-/  Low level disk interface modlue include file   (C)ChaN, 2019          /
+/  Low level disk interface modlue include file   (C)ChaN, 2025          /
 /-----------------------------------------------------------------------*/
 
 #ifndef _DISKIO_DEFINED
@@ -77,7 +77,7 @@ DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void* buff);
 #define CTRL_EJECT			7	/* Eject media */
 #define CTRL_FORMAT			8	/* Create physical format on the media */
 
-/* MMC/SDC specific ioctl command */
+/* MMC/SDC specific ioctl command (Not used by FatFs) */
 #define MMC_GET_TYPE		10	/* Get card type */
 #define MMC_GET_CSD			11	/* Get CSD */
 #define MMC_GET_CID			12	/* Get CID */
@@ -87,7 +87,7 @@ DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void* buff);
 #define ISDIO_WRITE			56	/* Write data to SD iSDIO register */
 #define ISDIO_MRITE			57	/* Masked write data to SD iSDIO register */
 
-/* ATA/CF specific ioctl command */
+/* ATA/CF specific ioctl command (Not used by FatFs) */
 #define ATA_GET_REV			20	/* Get F/W revision */
 #define ATA_GET_MODEL		21	/* Get model name */
 #define ATA_GET_SN			22	/* Get serial number */

Разница между файлами не показана из-за своего большого размера
+ 241 - 183
components/fatfs/ff.c


+ 91 - 73
components/fatfs/ff.h

@@ -1,8 +1,8 @@
 /*----------------------------------------------------------------------------/
-/  FatFs - Generic FAT Filesystem module  R0.15a                              /
+/  FatFs - Generic FAT Filesystem module  R0.16                               /
 /-----------------------------------------------------------------------------/
 /
-/ Copyright (C) 2024, ChaN, all right reserved.
+/ Copyright (C) 2025, ChaN, all right reserved.
 /
 / FatFs module is an open source software. Redistribution and use of FatFs in
 / source and binary forms, with or without modification, are permitted provided
@@ -20,7 +20,7 @@
 
 
 #ifndef FF_DEFINED
-#define FF_DEFINED	5380	/* Revision ID */
+#define FF_DEFINED	80386	/* Revision ID */
 
 #ifdef __cplusplus
 extern "C" {
@@ -127,51 +127,65 @@ extern const char* VolumeStr[FF_VOLUMES];	/* User defined volume ID table */
 #endif
 
 
+/* Current working directory structure (FFXCWDS) */
+
+#if FF_FS_EXFAT && FF_FS_RPATH
+#if FF_PATH_DEPTH < 1
+#error FF_PATH_DEPTH must not be zero
+#endif
+typedef struct {
+	DWORD	d_scl;		/* Directory start cluster (0:root dir) */
+	DWORD	d_size;		/* Size of directory (b7-b0: cluster chain status) (invalid if d_scl == 0) */
+	DWORD	nxt_ofs;	/* Offset of entry of next dir in this directory (invalid if last link) */
+} FFXCWDL;
+typedef struct {
+	UINT	depth;		/* Current directory depth (0:root dir) */
+	FFXCWDL	tbl[FF_PATH_DEPTH + 1];	/* Directory chain of current working directory path */
+} FFXCWDS;
+#endif
+
 
 /* Filesystem object structure (FATFS) */
 
 typedef struct {
-	BYTE	fs_type;		/* Filesystem type (0:blank filesystem object) */
-	BYTE	pdrv;			/* Volume hosting physical drive */
-	BYTE	ldrv;			/* Logical drive number (used only when FF_FS_REENTRANT) */
-	BYTE	n_fats;			/* Number of FATs (1 or 2) */
-	BYTE	wflag;			/* win[] status (1:dirty) */
-	BYTE	fsi_flag;		/* Allocation information control (b7:disabled, b0:dirty) */
-	WORD	id;				/* Volume mount ID */
-	WORD	n_rootdir;		/* Number of root directory entries (FAT12/16) */
-	WORD	csize;			/* Cluster size [sectors] */
+	BYTE	fs_type;	/* Filesystem type (0:not mounted) */
+	BYTE	pdrv;		/* Physical drive that holds this volume */
+	BYTE	ldrv;		/* Logical drive number (used only when FF_FS_REENTRANT) */
+	BYTE	n_fats;		/* Number of FATs (1 or 2) */
+	BYTE	wflag;		/* win[] status (b0:dirty) */
+	BYTE	fsi_flag;	/* Allocation information control (b7:disabled, b0:dirty) */
+	WORD	id;			/* Volume mount ID */
+	WORD	n_rootdir;	/* Number of root directory entries (FAT12/16) */
+	WORD	csize;		/* Cluster size [sectors] */
 #if FF_MAX_SS != FF_MIN_SS
-	WORD	ssize;			/* Sector size (512, 1024, 2048 or 4096) */
+	WORD	ssize;		/* Sector size (512, 1024, 2048 or 4096) */
 #endif
 #if FF_USE_LFN
-	WCHAR*	lfnbuf;			/* LFN working buffer */
-#endif
-#if FF_FS_EXFAT
-	BYTE*	dirbuf;			/* Directory entry block scratch pad buffer for exFAT */
+	WCHAR*	lfnbuf;		/* Pointer to LFN working buffer */
 #endif
 #if !FF_FS_READONLY
-	DWORD	last_clst;		/* Last allocated cluster (Unknown if >= n_fatent) */
-	DWORD	free_clst;		/* Number of free clusters (Unknown if >= n_fatent-2) */
+	DWORD	last_clst;	/* Last allocated cluster (invalid if >=n_fatent) */
+	DWORD	free_clst;	/* Number of free clusters (invalid if >=fs->n_fatent-2) */
 #endif
 #if FF_FS_RPATH
-	DWORD	cdir;			/* Current directory start cluster (0:root) */
+	DWORD	cdir;		/* Current directory start cluster (0:root) */
+#endif
+	DWORD	n_fatent;	/* Number of FAT entries (number of clusters + 2) */
+	DWORD	fsize;		/* Number of sectors per FAT */
+	LBA_t	winsect;	/* Current sector appearing in the win[] */
+	LBA_t	volbase;	/* Volume base sector */
+	LBA_t	fatbase;	/* FAT base sector */
+	LBA_t	dirbase;	/* Root directory base sector (FAT12/16) or cluster (FAT32/exFAT) */
+	LBA_t	database;	/* Data base sector */
 #if FF_FS_EXFAT
-	DWORD	cdc_scl;		/* Containing directory start cluster (invalid when cdir is 0) */
-	DWORD	cdc_size;		/* b31-b8:Size of containing directory, b7-b0: Chain status */
-	DWORD	cdc_ofs;		/* Offset in the containing directory (invalid when cdir is 0) */
-#endif
+	LBA_t	bitbase;	/* Allocation bitmap base sector */
+	BYTE*	dirbuf;		/* Pointer to directory entry block buffer */
+#if FF_FS_RPATH
+	FFXCWDS	xcwds;		/* Crrent working directory structure */
+	FFXCWDS	xcwds2;		/* Working buffer to follow the path */
 #endif
-	DWORD	n_fatent;		/* Number of FAT entries (number of clusters + 2) */
-	DWORD	fsize;			/* Number of sectors per FAT */
-	LBA_t	volbase;		/* Volume base sector */
-	LBA_t	fatbase;		/* FAT base sector */
-	LBA_t	dirbase;		/* Root directory base sector (FAT12/16) or cluster (FAT32/exFAT) */
-	LBA_t	database;		/* Data base sector */
-#if FF_FS_EXFAT
-	LBA_t	bitbase;		/* Allocation bitmap base sector */
 #endif
-	LBA_t	winsect;		/* Current sector appearing in the win[] */
-	BYTE	win[FF_MAX_SS];	/* Disk access window for Directory, FAT (and file data at tiny cfg) */
+	BYTE	win[FF_MAX_SS];	/* Disk access window for directory, FAT (and file data in tiny cfg) */
 } FATFS;
 
 
@@ -179,21 +193,21 @@ typedef struct {
 /* Object ID and allocation information (FFOBJID) */
 
 typedef struct {
-	FATFS*	fs;				/* Pointer to the hosting volume of this object */
-	WORD	id;				/* Hosting volume's mount ID */
-	BYTE	attr;			/* Object attribute */
-	BYTE	stat;			/* Object chain status (b1-0: =0:not contiguous, =2:contiguous, =3:fragmented in this session, b2:sub-directory stretched) */
-	DWORD	sclust;			/* Object data start cluster (0:no cluster or root directory) */
-	FSIZE_t	objsize;		/* Object size (valid when sclust != 0) */
+	FATFS*	fs;			/* Pointer to the volume holding this object */
+	WORD	id;			/* Volume mount ID when this object was opened */
+	BYTE	attr;		/* Object attribute */
+	BYTE	stat;		/* Object chain status (exFAT: b1-0: =0:not contiguous, =2:contiguous, =3:fragmented in this session, b2:sub-directory stretched) */
+	DWORD	sclust;		/* Object data cluster (0:no data or root directory) */
+	FSIZE_t	objsize;	/* Object size (valid when sclust != 0) */
 #if FF_FS_EXFAT
-	DWORD	n_cont;			/* Size of first fragment - 1 (valid when stat == 3) */
-	DWORD	n_frag;			/* Size of last fragment needs to be written to FAT (valid when not zero) */
-	DWORD	c_scl;			/* Containing directory start cluster (valid when sclust != 0) */
-	DWORD	c_size;			/* b31-b8:Size of containing directory, b7-b0: Chain status (valid when c_scl != 0) */
-	DWORD	c_ofs;			/* Offset in the containing directory (valid when file object and sclust != 0) */
+	DWORD	n_cont;		/* Size of first fragment - 1 (valid when stat == 3) */
+	DWORD	n_frag;		/* Size of last fragment needs to be written to FAT (valid when not zero) */
+	DWORD	c_scl;		/* Cluster of directory holding this object (valid when sclust != 0) */
+	DWORD	c_size;		/* Size of directory holding this object (b7-b0: allocation status, valid when c_scl != 0) */
+	DWORD	c_ofs;		/* Offset of entry in the holding directory */
 #endif
 #if FF_FS_LOCK
-	UINT	lockid;			/* File lock ID origin from 1 (index of file semaphore table Files[]) */
+	UINT	lockid;		/* File lock ID origin from 1 (index of file semaphore table Files[]) */
 #endif
 } FFOBJID;
 
@@ -202,18 +216,18 @@ typedef struct {
 /* File object structure (FIL) */
 
 typedef struct {
-	FFOBJID	obj;			/* Object identifier (must be the 1st member to detect invalid object pointer) */
-	BYTE	flag;			/* File status flags */
-	BYTE	err;			/* Abort flag (error code) */
-	FSIZE_t	fptr;			/* File read/write pointer (Zeroed on file open) */
-	DWORD	clust;			/* Current cluster of fpter (invalid when fptr is 0) */
-	LBA_t	sect;			/* Sector number appearing in buf[] (0:invalid) */
+	FFOBJID	obj;		/* Object identifier (must be the 1st member to detect invalid object pointer) */
+	BYTE	flag;		/* File status flags */
+	BYTE	err;		/* Abort flag (error code) */
+	FSIZE_t	fptr;		/* File read/write pointer (0 on open) */
+	DWORD	clust;		/* Current cluster of fptr (invalid when fptr is 0) */
+	LBA_t	sect;		/* Sector number appearing in buf[] (0:invalid) */
 #if !FF_FS_READONLY
-	LBA_t	dir_sect;		/* Sector number containing the directory entry (not used at exFAT) */
-	BYTE*	dir_ptr;		/* Pointer to the directory entry in the win[] (not used at exFAT) */
+	LBA_t	dir_sect;	/* Sector number containing the directory entry (not used in exFAT) */
+	BYTE*	dir_ptr;	/* Pointer to the directory entry in the win[] (not used in exFAT) */
 #endif
 #if FF_USE_FASTSEEK
-	DWORD*	cltbl;			/* Pointer to the cluster link map table (nulled on open, set by application) */
+	DWORD*	cltbl;		/* Pointer to the cluster link map table (nulled on open; set by application) */
 #endif
 #if !FF_FS_TINY
 	BYTE	buf[FF_MAX_SS];	/* File private data read/write window */
@@ -225,40 +239,44 @@ typedef struct {
 /* Directory object structure (DIR) */
 
 typedef struct {
-	FFOBJID	obj;			/* Object identifier */
-	DWORD	dptr;			/* Current read/write offset */
-	DWORD	clust;			/* Current cluster */
-	LBA_t	sect;			/* Current sector (0:Read operation has terminated) */
-	BYTE*	dir;			/* Pointer to the directory item in the win[] */
-	BYTE	fn[12];			/* SFN (in/out) {body[8],ext[3],status[1]} */
+	FFOBJID	obj;		/* Object identifier (must be the 1st member to detect invalid object pointer) */
+	DWORD	dptr;		/* Current read/write offset */
+	DWORD	clust;		/* Current cluster */
+	LBA_t	sect;		/* Current sector (0:no more item to read) */
+	BYTE*	dir;		/* Pointer to the directory item in the win[] in filesystem object */
+	BYTE	fn[12];		/* SFN (in/out) {body[0-7],ext[8-10],status[11]} */
 #if FF_USE_LFN
-	DWORD	blk_ofs;		/* Offset of current entry block being processed (0xFFFFFFFF:Invalid) */
+	DWORD	blk_ofs;	/* Offset of current entry block being processed (0xFFFFFFFF:invalid) */
 #endif
 #if FF_USE_FIND
-	const TCHAR* pat;		/* Pointer to the name matching pattern */
+	const TCHAR *pat;	/* Pointer to the name matching pattern */
 #endif
 } DIR;
 
 
 
-/* File information structure (FILINFO) */
+/* File/directory information structure (FILINFO) */
 
 typedef struct {
-	FSIZE_t	fsize;			/* File size */
-	WORD	fdate;			/* Modified date */
-	WORD	ftime;			/* Modified time */
-	BYTE	fattrib;		/* File attribute */
+	FSIZE_t	fsize;			/* File size (invalid for directory) */
+	WORD	fdate;			/* Date of file modification or directory creation */
+	WORD	ftime;			/* Time of file modification or directory creation */
+#if FF_FS_CRTIME
+	WORD	crdate;			/* Date of object createion */
+	WORD	crtime;			/* Time of object createion */
+#endif
+	BYTE	fattrib;		/* Object attribute */
 #if FF_USE_LFN
-	TCHAR	altname[FF_SFN_BUF + 1];/* Alternative file name */
-	TCHAR	fname[FF_LFN_BUF + 1];	/* Primary file name */
+	TCHAR	altname[FF_SFN_BUF + 1];/* Alternative object name */
+	TCHAR	fname[FF_LFN_BUF + 1];	/* Primary object name */
 #else
-	TCHAR	fname[12 + 1];	/* File name */
+	TCHAR	fname[12 + 1];	/* Object name */
 #endif
 } FILINFO;
 
 
 
-/* Format parameter structure (MKFS_PARM) */
+/* Format parameter structure (MKFS_PARM) used for f_mkfs() */
 
 typedef struct {
 	BYTE fmt;			/* Format option (FM_FAT, FM_FAT32, FM_EXFAT and FM_SFD) */
@@ -290,7 +308,7 @@ typedef enum {
 	FR_MKFS_ABORTED,		/* (14) The f_mkfs function aborted due to some problem */
 	FR_TIMEOUT,				/* (15) Could not take control of the volume within defined period */
 	FR_LOCKED,				/* (16) The operation is rejected according to the file sharing policy */
-	FR_NOT_ENOUGH_CORE,		/* (17) LFN working buffer could not be allocated or given buffer is insufficient in size */
+	FR_NOT_ENOUGH_CORE,		/* (17) LFN working buffer could not be allocated, given buffer size is insufficient or too deep path */
 	FR_TOO_MANY_OPEN_FILES,	/* (18) Number of open files > FF_FS_LOCK */
 	FR_INVALID_PARAMETER	/* (19) Given parameter is invalid */
 } FRESULT;

+ 27 - 10
components/fatfs/ffconf.h

@@ -5,7 +5,7 @@
 /  Configurations of FatFs Module
 /---------------------------------------------------------------------------*/
 
-#define FFCONF_DEF	5380	/* Revision ID */
+#define FFCONF_DEF	80386	/* Revision ID */
 
 /*---------------------------------------------------------------------------/
 / Function Configurations
@@ -63,12 +63,12 @@
 #define FF_PRINT_LLI	0
 #define FF_PRINT_FLOAT	0
 #define FF_STRF_ENCODE	0
-/* FF_USE_STRFUNC switches the string API functions, f_gets(), f_putc(), f_puts()
-/  and f_printf().
+/* FF_USE_STRFUNC switches string API functions, f_gets(), f_putc(), f_puts() and
+/  f_printf().
 /
 /   0: Disable. FF_PRINT_LLI, FF_PRINT_FLOAT and FF_STRF_ENCODE have no effect.
-/   1: Enable without LF - CRLF conversion.
-/   2: Enable with LF - CRLF conversion.
+/   1: Enable without LF-CRLF conversion.
+/   2: Enable with LF-CRLF conversion.
 /
 /  FF_PRINT_LLI = 1 makes f_printf() support long long argument and FF_PRINT_FLOAT = 1/2
 /  makes f_printf() support floating point argument. These features want C99 or later.
@@ -165,14 +165,26 @@
 
 
 #define FF_FS_RPATH		0
-/* This option configures support for relative path.
+/* This option configures support for relative path feature.
 /
 /   0: Disable relative path and remove related API functions.
-/   1: Enable relative path. f_chdir() and f_chdrive() are available.
+/   1: Enable relative path and dot names. f_chdir() and f_chdrive() are available.
 /   2: f_getcwd() is available in addition to 1.
 */
 
 
+#define FF_PATH_DEPTH	10
+/*  This option defines maximum depth of directory in the exFAT volume. It is NOT
+/   relevant to FAT/FAT32 volume.
+/   For example, FF_PATH_DEPTH = 3 will able to follow a path "/dir1/dir2/dir3/file"
+/   but a sub-directory in the dir3 will not able to be followed and set current
+/   directory.
+/   The size of filesystem object (FATFS) increases FF_PATH_DEPTH * 24 bytes.
+/   When FF_FS_EXFAT == 0 or FF_FS_RPATH == 0, this option has no effect.
+*/
+
+
+
 /*---------------------------------------------------------------------------/
 / Drive/Volume Configurations
 /---------------------------------------------------------------------------*/
@@ -237,7 +249,7 @@
 
 #define FF_FS_TINY		0
 /* This option switches tiny buffer configuration. (0:Normal or 1:Tiny)
-/  At the tiny configuration, size of file object (FIL) is shrinked FF_MAX_SS bytes.
+/  At the tiny configuration, size of file object (FIL) is reduced FF_MAX_SS bytes.
 /  Instead of private sector buffer eliminated from the file object, common sector
 /  buffer in the filesystem object (FATFS) is used for the file data transfer. */
 
@@ -262,9 +274,14 @@
 /  These options have no effect in read-only configuration (FF_FS_READONLY = 1). */
 
 
+#define FF_FS_CRTIME	0
+/* This option enables(1)/disables(0) the timestamp of the file created. When
+/  set 1, the file created time is available in FILINFO structure. */
+
+
 #define FF_FS_NOFSINFO	0
-/* If you need to know correct free space on the FAT32 volume, set bit 0 of this
-/  option, and f_getfree() at the first time after volume mount will force
+/* If you need to know the correct free space on the FAT32 volume, set bit 0 of
+/  this option, and f_getfree() on the first time after volume mount will force
 /  a full FAT scan. Bit 1 controls the use of last allocated cluster number.
 /
 /  bit0=0: Use free cluster count in the FSINFO if available.

Разница между файлами не показана из-за своего большого размера
+ 3600 - 6381
components/fatfs/ffunicode.c


+ 12 - 0
components/lcd/luat_lcd.c

@@ -566,6 +566,18 @@ int luat_lcd_draw_line(luat_lcd_conf_t* conf,int16_t x1, int16_t y1, int16_t x2,
     int incx, incy, row, col;
     if (x1 == x2 || y1 == y2) // 直线
     {
+        if (x1 > x2)
+        {
+            int16_t tmp = x1;
+            x1 = x2;
+            x2 = tmp;
+        }
+        if (y1 > y2)
+        {
+            int16_t tmp = y1;
+            y1 = y2;
+            y2 = tmp;
+        }
         size_t dots = (x2 - x1 + 1) * (y2 - y1 + 1);//点数量
         luat_color_t* line_buf = (luat_color_t*) luat_heap_malloc(dots * sizeof(luat_color_t));
         if (conf->endianness_swap)

+ 0 - 1
components/network/httpsrv/src/luat_httpsrv_lwip.c

@@ -96,7 +96,6 @@ static const ct_reg_t ct_regs[] = {
     {"css",     "text/css"},
     {"wav",     "audio/wave"},
     {"ogg",     "audio/ogg"},
-    {"wav",     "audio/wave"},
     {"webm",    "video/webm"},
     {"mp4",     "video/mpeg4"},
     {"bin",     "application/octet-stream"},

+ 0 - 541
module/Air780EHM_Air780EHV_Air780EGH/demo/audio/exaudio.lua

@@ -1,541 +0,0 @@
---[[
-@module exaudio
-@summary exaudio扩展库
-@version 1.1
-@date    2025.09.01
-@author  梁健
-@usage
-]]
-local exaudio = {}
-
--- 常量定义
-local I2S_ID = 0
-local I2S_MODE = 0          -- 0:主机 1:从机
-local I2S_SAMPLE_RATE = 16000
-local I2S_CHANNEL_FORMAT = i2s.MONO_R  
-local I2S_COMM_FORMAT = i2s.MODE_LSB   -- 可选MODE_I2S, MODE_LSB, MODE_MSB
-local I2S_CHANNEL_BITS = 16
-local MULTIMEDIA_ID = 0
-local EX_MSG_PLAY_DONE = "playDone"
-local ES8311_ADDR = 0x18    -- 7位地址
-local CHIP_ID_REG = 0x00    -- 芯片ID寄存器地址
-
--- 模块常量
-exaudio.PLAY_DONE = 1         --   音频播放完毕的事件之一
-exaudio.RECORD_DONE = 1       --   音频录音完毕的事件之一  
-exaudio.AMR_NB = 0
-exaudio.AMR_WB = 1
-exaudio.PCM_8000 = 2
-exaudio.PCM_16000 = 3 
-exaudio.PCM_24000 = 4
-exaudio.PCM_32000 = 5
-
-
--- 默认配置参数
-local audio_setup_param = {
-    model = "es8311",         -- dac类型: "es8311","es8211"
-    i2c_id = 0,               -- i2c_id: 0,1
-    pa_ctrl = 0,              -- 音频放大器电源控制管脚
-    dac_ctrl = 0,             -- 音频编解码芯片电源控制管脚
-    dac_delay = 3,            -- DAC启动前冗余时间(100ms)
-    pa_delay = 100,           -- DAC启动后延迟打开PA的时间(ms)
-    dac_time_delay = 600,     -- 播放完毕后PA与DAC关闭间隔(ms)
-    bits_per_sample = 16,     -- 采样位数
-    pa_on_level = 1           -- PA打开电平 1:高 0:低        
-}
-
-local audio_play_param = {
-    type = 0,                 -- 0:文件 1:TTS 2:流式
-    content = nil,            -- 播放内容
-    cbfnc = nil,              -- 播放完毕回调
-    priority = 0,             -- 优先级(数值越大越高)
-    sampling_rate = 16000,    -- 采样率(仅流式)
-    sampling_depth = 16,      -- 采样位深(仅流式)
-    signed_or_unsigned = true -- PCM是否有符号(仅流式)
-}
-
-local audio_record_param = {
-    format = 0,               -- 录制格式,支持exaudio.AMR_NB,exaudio.AMR_WB,exaudio.PCM_8000,exaudio.PCM_16000,exaudio.PCM_24000,exaudio.PCM_32000
-    time = 5,                 -- 录制时间(秒)
-    path = nil,               -- 文件路径或流式回调
-    cbfnc = nil               -- 录音完毕回调
-}
-
--- 内部变量
-local pcm_buff0 = nil
-local pcm_buff1 = nil
-local voice_vol = 55
-local mic_vol = 80
-
--- 定义全局队列表
-local audio_play_queue = {
-    data = {},       -- 存储字符串的数组
-    sequenceIndex = 1  -- 用于跟踪插入顺序的索引
-}
-
--- 向队列中添加字符串(按调用顺序插入)
-local function audio_play_queue_push(str)
-    if type(str) == "string" then
-        -- 存储格式: {index = 顺序索引, value = 字符串值}
-        table.insert(audio_play_queue.data, {
-            index = audio_play_queue.sequenceIndex,
-            value = str
-        })
-        audio_play_queue.sequenceIndex = audio_play_queue.sequenceIndex + 1
-        return true
-    end
-    return false
-end
-
--- 从队列中取出最早插入的字符串(按顺序取出)
-local function audio_play_queue_pop()
-    if #audio_play_queue.data > 0 then
-        -- 取出并移除第一个元素
-        local item = table.remove(audio_play_queue.data, 1)
-        return item.value  -- 返回值
-    end
-    return nil
-end
--- 清空队列中所有数据
-function audio_queue_clear()
-    -- 清空数组
-    audio_play_queue.data = {}
-    -- 重置顺序索引
-    audio_play_queue.sequenceIndex = 1
-    return true
-end
-
--- 工具函数:参数检查
-local function check_param(param, expected_type, name)
-    if type(param) ~= expected_type then
-        log.error(string.format("参数错误: %s 应为 %s 类型", name, expected_type))
-        return false
-    end
-    return true 
-end
-
--- 音频回调处理
-local function audio_callback(id, event, point)
-    -- log.info("audio_callback", "event:", event, 
-    --         "MORE_DATA:", audio.MORE_DATA, 
-    --         "DONE:", audio.DONE,
-    --         "RECORD_DATA:", audio.RECORD_DATA,
-    --         "RECORD_DONE:", audio.RECORD_DONE)
-
-    if event == audio.MORE_DATA then
-        audio.write(MULTIMEDIA_ID,audio_play_queue_pop())
-    elseif event == audio.DONE then
-        if type(audio_play_param.cbfnc) == "function" then
-            audio_play_param.cbfnc(exaudio.PLAY_DONE)
-        end
-        audio_queue_clear()  -- 清空流式播放数据队列
-        sys.publish(EX_MSG_PLAY_DONE)
-        
-    elseif event == audio.RECORD_DATA then
-        if type(audio_record_param.path) == "function" then
-            local buff, len = point == 0 and pcm_buff0 or pcm_buff1,
-                             point == 0 and pcm_buff0:used() or pcm_buff1:used()
-            audio_record_param.path(buff, len)
-        end
-        
-    elseif event == audio.RECORD_DONE then
-        if type(audio_record_param.cbfnc) == "function" then
-            audio_record_param.cbfnc(exaudio.RECORD_DONE)
-        end
-    end
-end
-
--- 读取ES8311芯片ID
-local function read_es8311_id()
-
-
-    -- 发送读取请求
-    local send_ok = i2c.send(audio_setup_param.i2c_id, ES8311_ADDR, CHIP_ID_REG)
-    if not send_ok then
-        log.error("发送芯片ID读取请求失败")
-        return false
-    end
-
-    -- 读取数据
-    local data = i2c.recv(audio_setup_param.i2c_id, ES8311_ADDR, 1)
-    if data and #data == 1 then
-        return true
-    end
-
-    log.error("读取ES8311芯片ID失败")
-    return false
-end
-
--- 音频硬件初始化
-local function audio_setup()
-    -- I2C配置
-    if not i2c.setup(audio_setup_param.i2c_id, i2c.FAST) then
-        log.error("I2C初始化失败")
-        return false
-    end
-    -- 初始化I2S
-    local result, data = i2s.setup(
-        I2S_ID, 
-        I2S_MODE, 
-        I2S_SAMPLE_RATE, 
-        audio_setup_param.bits_per_sample, 
-        I2S_CHANNEL_FORMAT, 
-        I2S_COMM_FORMAT,
-        I2S_CHANNEL_BITS
-    )
-
-    if not result then
-        log.error("I2S设置失败")
-        return false
-    end
-    -- 配置音频通道
-    audio.config(
-        MULTIMEDIA_ID, 
-        audio_setup_param.pa_ctrl, 
-        audio_setup_param.pa_on_level, 
-        audio_setup_param.dac_delay, 
-        audio_setup_param.pa_delay, 
-        audio_setup_param.dac_ctrl, 
-        1,  -- power_on_level
-        audio_setup_param.dac_time_delay
-    )
-    -- 设置总线
-    audio.setBus(
-        MULTIMEDIA_ID, 
-        audio.BUS_I2S,
-        {
-            chip = audio_setup_param.model,
-            i2cid = audio_setup_param.i2c_id,
-            i2sid = I2S_ID,
-            voltage = audio.VOLTAGE_1800
-        }
-    )
-
-  
-    -- 设置音量
-    audio.vol(MULTIMEDIA_ID, voice_vol)
-    audio.micVol(MULTIMEDIA_ID, mic_vol)
-    audio.pm(MULTIMEDIA_ID, audio.RESUME)
-    
-    -- 检查芯片连接
-    if audio_setup_param.model == "es8311" and not read_es8311_id() then
-        log.error("ES8311通讯失败,请检查硬件")
-        return false
-    end
-
-    -- 注册回调
-    audio.on(MULTIMEDIA_ID, audio_callback)
-    return true
-end
-
--- 模块接口:初始化
-function exaudio.setup(audioConfigs)
-    -- 检查必要参数
-    if not  audio  then
-        log.error("不支持audio 库,请选择支持audio 的core")
-        return false
-    end
-    if not audioConfigs or type(audioConfigs) ~= "table" then
-        log.error("配置参数必须为table类型")
-        return false
-    end
-    -- 检查codec型号
-    if not audioConfigs.model or 
-       (audioConfigs.model ~= "es8311" and audioConfigs.model ~= "es8211") then
-        log.error("请指定正确的codec型号(es8311或es8211)")
-        return false
-    end
-    audio_setup_param.model = audioConfigs.model
-    -- 针对ES8311的特殊检查
-    if audioConfigs.model == "es8311" then
-        if not check_param(audioConfigs.i2c_id, "number", "i2c_id") then
-            return false
-        end
-        audio_setup_param.i2c_id = audioConfigs.i2c_id
-    end
-
-    -- 检查功率放大器控制管脚
-    if audioConfigs.pa_ctrl == nil then
-        log.warn("pa_ctrl(功率放大器控制管脚)是控制pop 音的重要管脚,建议硬件设计加上")
-    end
-    audio_setup_param.pa_ctrl = audioConfigs.pa_ctrl
-
-    -- 检查功率放大器控制管脚
-    if audioConfigs.dac_ctrl == nil then
-        log.warn("dac_ctrl(音频编解码控制管脚)是控制pop 音的重要管脚,建议硬件设计加上")
-    end
-    audio_setup_param.dac_ctrl = audioConfigs.dac_ctrl
-
-
-    -- 处理可选参数
-    local optional_params = {
-        {name = "dac_delay", type = "number"},
-        {name = "pa_delay", type = "number"},
-        {name = "dac_time_delay", type = "number"},
-        {name = "bits_per_sample", type = "number"},
-        {name = "pa_on_level", type = "number"}
-    }
-
-    for _, param in ipairs(optional_params) do
-        if audioConfigs[param.name] ~= nil then
-            if check_param(audioConfigs[param.name], param.type, param.name) then
-                audio_setup_param[param.name] = audioConfigs[param.name]
-            else
-                return false
-            end
-        end
-    end
-
-    -- 确保采样位数有默认值
-    audio_setup_param.bits_per_sample = audio_setup_param.bits_per_sample or 16
-    return audio_setup()
-end
-
--- 模块接口:开始播放
-function exaudio.play_start(playConfigs)
-    if not playConfigs or type(playConfigs) ~= "table" then
-        log.error("播放配置必须为table类型")
-        return false
-    end
-
-    -- 检查播放类型
-    if not check_param(playConfigs.type, "number", "type") then
-        log.error("type必须为数值(0:文件,1:TTS,2:流式)")
-        return false
-    end
-    audio_play_param.type = playConfigs.type
-
-    -- 处理优先级
-    if playConfigs.priority ~= nil then
-        if check_param(playConfigs.priority, "number", "priority") then
-            if playConfigs.priority > audio_play_param.priority then
-                log.error("是否完成播放",audio.isEnd(MULTIMEDIA_ID))
-                if not audio.isEnd(MULTIMEDIA_ID) then
-                    if audio.play(MULTIMEDIA_ID) ~= true then
-                        return false
-                    end
-                    sys.waitUntil(EX_MSG_PLAY_DONE)
-                end
-                audio_play_param.priority = playConfigs.priority
-            end
-        else
-            return false
-        end
-    end
-
-    -- 处理不同播放类型
-    local play_type = audio_play_param.type
-    if play_type == 0 then  -- 文件播放
-        if not playConfigs.content then
-            log.error("文件播放需要指定content(文件路径或路径表)")
-            return false
-        end
-
-        local content_type = type(playConfigs.content)
-        if content_type == "table" then
-            for _, path in ipairs(playConfigs.content) do
-                if type(path) ~= "string" then
-                    log.error("播放列表元素必须为字符串路径")
-                    return false
-                end
-            end
-        elseif content_type ~= "string" then
-            log.error("文件播放content必须为字符串或路径表")
-            return false
-        end
-
-        audio_play_param.content = playConfigs.content
-        if audio.play(MULTIMEDIA_ID, audio_play_param.content) ~= true then
-            return false
-        end
-
-    elseif play_type == 1 then  -- TTS播放
-        if not audio.tts then
-            log.error("本固件不支持TTS,请更换支持TTS 的固件")
-            return false
-        end
-        if not check_param(playConfigs.content, "string", "content") then
-            log.error("TTS播放content必须为字符串")
-            return false
-        end
-        audio_play_param.content = playConfigs.content
-        if audio.tts(MULTIMEDIA_ID, audio_play_param.content)  ~= true  then
-            return false
-        end
-
-    elseif play_type == 2 then  -- 流式播放
-        if not check_param(playConfigs.sampling_rate, "number", "sampling_rate") then
-            return false
-        end
-        if not check_param(playConfigs.sampling_depth, "number", "sampling_depth") then
-            return false
-        end
-
-        audio_play_param.content = playConfigs.content
-        audio_play_param.sampling_rate = playConfigs.sampling_rate
-        audio_play_param.sampling_depth = playConfigs.sampling_depth
-        
-        if playConfigs.signed_or_unsigned ~= nil then
-            audio_play_param.signed_or_unsigned = playConfigs.signed_or_unsigned
-        end
-
-        audio.start(
-            MULTIMEDIA_ID, 
-            audio.PCM, 
-            1, 
-            playConfigs.sampling_rate, 
-            playConfigs.sampling_depth, 
-            audio_play_param.signed_or_unsigned
-        )
-        -- 发送初始数据
-        if audio.write(MULTIMEDIA_ID, string.rep("\0", 512)) ~= true then
-            return false
-        end
-    end
-
-    -- 处理回调函数
-    if playConfigs.cbfnc ~= nil then
-        if check_param(playConfigs.cbfnc, "function", "cbfnc") then
-            audio_play_param.cbfnc = playConfigs.cbfnc
-        else
-            return false
-        end
-    else
-        audio_play_param.cbfnc = nil
-    end
-    return true
-end
-
--- 模块接口:流式播放数据写入
-function exaudio.play_stream_write(data)
-    audio_play_queue_push(data)
-    return true
-end
-
--- 模块接口:停止播放
-function exaudio.play_stop()
-    return audio.play(MULTIMEDIA_ID)
-end
-
--- 模块接口:检查播放是否结束
-function exaudio.is_end()
-    return audio.isEnd(MULTIMEDIA_ID)
-end
-
--- 模块接口:获取错误信息
-function exaudio.get_error()
-    return audio.getError(MULTIMEDIA_ID)
-end
-
--- 模块接口:开始录音
-function exaudio.record_start(recodConfigs)
-    if not recodConfigs or type(recodConfigs) ~= "table" then
-        log.error("录音配置必须为table类型")
-        return false
-    end
-    -- 检查录音格式
-    if recodConfigs.format == nil or type(recodConfigs.format) ~= "number" or recodConfigs.format > 5 then
-        log.error("请指定正确的录音格式")
-        return false
-    end
-    audio_record_param.format = recodConfigs.format
-
-    -- 处理录音时间
-    if recodConfigs.time ~= nil then
-        if check_param(recodConfigs.time, "number", "time") then
-            audio_record_param.time = recodConfigs.time
-        else
-            return false
-        end
-    else
-        audio_record_param.time = 0
-    end
-
-    -- 处理存储路径/回调
-    if not recodConfigs.path then
-        log.error("必须指定录音路径或流式回调函数")
-        return false
-    end
-    audio_record_param.path = recodConfigs.path
-
-    -- 转换录音格式
-    local recod_format, amr_quailty
-    if audio_record_param.format == exaudio.AMR_NB then
-        recod_format = audio.AMR_NB
-        amr_quailty = 7
-    elseif audio_record_param.format == exaudio.AMR_WB then
-        recod_format = audio.AMR_WB
-        amr_quailty = 8
-    elseif audio_record_param.format == exaudio.PCM_8000 then
-        recod_format = 8000
-    elseif audio_record_param.format == exaudio.PCM_16000 then
-        recod_format = 16000
-    elseif audio_record_param.format == exaudio.PCM_24000 then
-        recod_format = 24000
-    elseif audio_record_param.format == exaudio.PCM_32000 then
-        recod_format = 32000
-    end
-
-    -- 处理回调函数
-    if recodConfigs.cbfnc ~= nil then
-        if check_param(recodConfigs.cbfnc, "function", "cbfnc") then
-            audio_record_param.cbfnc = recodConfigs.cbfnc
-        else
-            return false
-        end
-    else
-        audio_record_param.cbfnc = nil
-    end
-    -- 开始录音
-    local path_type = type(audio_record_param.path)
-    if path_type == "string" then
-        return audio.record(
-            MULTIMEDIA_ID, 
-            recod_format, 
-            audio_record_param.time, 
-            amr_quailty, 
-            audio_record_param.path
-        )
-    elseif path_type == "function" then
-        -- 初始化缓冲区
-        if not pcm_buff0 or not pcm_buff1 then
-            pcm_buff0 = zbuff.create(16000)
-            pcm_buff1 = zbuff.create(16000)
-        end
-        return audio.record(
-            MULTIMEDIA_ID, 
-            recod_format, 
-            audio_record_param.time, 
-            amr_quailty, 
-            nil, 
-            3,
-            pcm_buff0,
-            pcm_buff1
-        )
-    end
-    log.error("录音路径必须为字符串或函数")
-    return false
-end
-
--- 模块接口:停止录音
-function exaudio.record_stop()
-    return audio.recordStop(MULTIMEDIA_ID)
-end
-
--- 模块接口:设置音量
-function exaudio.vol(play_volume)
-    if check_param(play_volume, "number", "音量值") then
-        return audio.vol(MULTIMEDIA_ID, play_volume)
-    end
-    return false
-end
-
--- 模块接口:设置麦克风音量
-function exaudio.mic_vol(record_volume)
-    if check_param(record_volume, "number", "麦克风音量值") then
-        return audio.micVol(MULTIMEDIA_ID, record_volume)  
-    end
-    return false
-end
-
-return exaudio

+ 282 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fs_io/flash_fs_io.lua

@@ -0,0 +1,282 @@
+--[[
+@module  flash_fs_io
+@summary 内置Flash文件系统操作测试模块
+@version 1.0.0
+@date    2025.09.23
+@author  王棚嶙
+@usage
+本文件为内置Flash文件系统的操作测试流程:
+1. 获取文件系统信息( io.fsstat)
+2. 创建目录
+3. 创建并写入文件
+4. 检查文件是否存在
+5. 获取文件大小(io.fileSize)
+6. 读取文件内容
+7. 启动计数文件操作
+8. 文件追加测试
+9. 按行读取测试
+10. 文件重命名
+11. 列举目录内容
+12. 删除文件
+13. 删除目录
+本文件没有对外接口,直接在main.lua中require "flash_fs_io"就可以加载运行
+]]
+
+function flash_fs_io_task()
+    -- 使用内置Flash文件系统,根目录为"/",
+    local base_path = "/"
+    -- 创建一个目录
+    local demo_dir = "flash_demo"
+    -- 文件名
+    local dir_path = base_path .. demo_dir
+
+    -- ########## 开始进行内置Flash文件系统操作 ##########
+    log.info("文件系统操作", "===== 开始文件系统操作 =====")
+
+    -- 1. 获取文件系统信息 (使用 io.fsstat接口)
+    local success, total_blocks, used_blocks, block_size, fs_type  =  io.fsstat(base_path)
+    if success then
+        log.info(" io.fsstat成功:", 
+            "总空间=" .. total_blocks .. "块", 
+            "已用=" .. used_blocks .. "块", 
+            "块大小=" .. block_size.."字节",
+            "类型=" .. fs_type)
+    else
+        log.error(" io.fsstat", "获取文件系统信息失败")
+        return
+    end
+
+    -- 2. 创建目录
+    -- 如果目录不存在,则创建目录
+    if not io.dexist(dir_path) then
+        -- 创建目录
+        if io.mkdir(dir_path) then
+            log.info("io.mkdir", "目录创建成功", "路径:" .. dir_path)
+        else
+            log.error("io.mkdir", "目录创建失败", "路径:" .. dir_path)
+            return
+        end
+    else
+        log.warn("io.mkdir", "目录已存在,跳过创建", "路径:" .. dir_path)
+    end
+
+    -- 3. 创建并写入文件
+    local file_path = dir_path .. "/boottime"
+    local file = io.open(file_path, "wb")
+    if file then
+        file:write("这是内置Flash文件系统API文档示例的测试内容")
+        file:close()
+        log.info("文件创建", "文件写入成功", "路径:" .. file_path)
+    else
+        log.error("文件创建", "文件创建失败", "路径:" .. file_path)
+        return
+    end
+
+    -- 4. 检查文件是否存在
+    if io.exists(file_path) then
+        log.info("io.exists", "文件存在", "路径:" .. file_path)
+    else
+        log.error("io.exists", "文件不存在", "路径:" .. file_path)
+        return
+    end
+
+    -- 5. 获取文件大小 (使用io.fileSize接口)
+    local file_size = io.fileSize(file_path)
+    if file_size then
+        log.info("io.fileSize", "文件大小:" .. file_size .. "字节", "路径:" .. file_path)
+    else
+        log.error("io.fileSize", "获取文件大小失败", "路径:" .. file_path)
+        return
+    end
+
+    -- 6. 读取文件内容
+    file = io.open(file_path, "rb")
+    if file then
+        local content = file:read("*a")
+        log.info("文件读取", "路径:" .. file_path, "内容:" .. content)
+        file:close()
+    else
+        log.error("文件操作", "无法打开文件读取内容", "路径:" .. file_path)
+        return
+    end
+
+    -- 7. 启动计数文件操作
+    local count = 0
+    file = io.open(file_path, "rb")
+    if file then
+        local data = file:read("*a")
+        log.info("启动计数", "文件内容:", data, "十六进制:", data:toHex())
+        count = tonumber(data) or 0
+        file:close()
+    else
+        log.warn("启动计数", "文件不存在或无法打开")
+    end
+
+    log.info("启动计数", "当前值:", count)
+    count = count + 1
+    log.info("启动计数", "更新值:", count)
+
+    file = io.open(file_path, "wb")
+    if file then
+        file:write(tostring(count))
+        file:close()
+        log.info("文件写入", "路径:" .. file_path, "内容:", count)
+    else
+        log.error("文件写入", "无法打开文件", "路径:" .. file_path)
+        return
+    end
+
+    -- 8. 文件追加测试
+    local append_file = dir_path .. "/test_a"
+    os.remove(append_file) -- 清理旧文件
+
+    file = io.open(append_file, "wb")
+    if file then
+        file:write("ABC")
+        file:close()
+        log.info("文件创建", "路径:" .. append_file, "初始内容:ABC")
+    else
+        log.error("文件创建", "无法创建文件", "路径:" .. append_file)
+        return
+    end
+
+    file = io.open(append_file, "a+")
+    if file then
+        file:write("def")
+        file:close()
+        log.info("文件追加", "路径:" .. append_file, "追加内容:def")
+    else
+        log.error("文件追加", "无法打开文件进行追加", "路径:" .. append_file)
+        return
+    end
+
+    -- 验证追加结果
+    file = io.open(append_file, "r")
+    if file then
+        local data = file:read("*a")
+        log.info("文件验证", "路径:" .. append_file, "内容:" .. data, "结果:",
+            data == "ABCdef" and "成功" or "失败")
+        file:close()
+    else
+        log.error("文件验证", "无法打开文件进行验证", "路径:" .. append_file)
+        return
+    end
+
+    -- 9. 按行读取测试
+    local line_file = dir_path .. "/testline"
+    file = io.open(line_file, "w")
+    if file then
+        file:write("abc\n")
+        file:write("123\n")
+        file:write("wendal\n")
+        file:close()
+        log.info("文件创建", "路径:" .. line_file, "写入3行文本")
+    else
+        log.error("文件创建", "无法创建文件", "路径:" .. line_file)
+        return
+    end
+
+    file = io.open(line_file, "r")
+    if file then
+        log.info("按行读取", "路径:" .. line_file, "第1行:", file:read("*l"))
+        log.info("按行读取", "路径:" .. line_file, "第2行:", file:read("*l"))
+        log.info("按行读取", "路径:" .. line_file, "第3行:", file:read("*l"))
+        file:close()
+    else
+        log.error("按行读取", "无法打开文件", "路径:" .. line_file)
+        return
+    end
+
+    -- 10. 文件重命名
+    local old_path = append_file
+    local new_path = dir_path .. "/renamed_file.txt"
+    local success, err = os.rename(old_path, new_path)
+    if success then
+        log.info("os.rename", "文件重命名成功", "原路径:" .. old_path, "新路径:" .. new_path)
+
+        -- 验证重命名结果
+        if io.exists(new_path) and not io.exists(old_path) then
+            log.info("验证结果", "重命名验证成功", "新文件存在", "原文件不存在")
+        else
+            log.error("验证结果", "重命名验证失败")
+        end
+    else
+        log.error("os.rename", "重命名失败", "错误:" .. tostring(err), "原路径:" .. old_path)
+        return
+    end
+
+    -- 11. 列举目录内容
+    log.info("目录操作", "===== 开始目录列举 =====")
+
+    local ret, data = io.lsdir(dir_path, 50, 0)
+    if ret then
+        log.info("fs", "lsdir", json.encode(data))
+    else
+        log.info("fs", "lsdir", "fail", ret, data)
+        return
+    end
+
+    -- 12. 删除文件测试
+    if os.remove(new_path) then
+        log.info("os.remove", "文件删除成功", "路径:" .. new_path)
+        if not io.exists(new_path) then
+            log.info("验证结果", "renamed_file.txt文件删除验证成功")
+        else
+            log.error("验证结果", "renamed_file.txt文件删除验证失败")
+        end
+    else
+        log.error("os.remove", "renamed_file.txt文件删除失败", "路径:" .. new_path)
+        return
+    end
+
+    if os.remove(line_file) then
+        log.info("os.remove", "testline文件删除成功", "路径:" .. line_file)
+        if not io.exists(line_file) then
+            log.info("验证结果", "testline文件删除验证成功")
+        else
+            log.error("验证结果", "testline文件删除验证失败")
+        end
+    else
+        log.error("os.remove", "testline文件删除失败", "路径:" .. line_file)
+        return
+    end
+
+    if os.remove(file_path) then
+        log.info("os.remove", "文件删除成功", "路径:" .. file_path)
+        if not io.exists(file_path) then
+            log.info("验证结果", "boottime文件删除验证成功")
+        else
+            log.error("验证结果", "boottime文件删除验证失败")
+        end
+    else
+        log.error("os.remove", "boottime文件删除失败", "路径:" .. file_path)
+        return
+    end
+
+    -- 13. 删除目录
+    if io.rmdir(dir_path) then
+        log.info("io.rmdir", "目录删除成功", "路径:" .. dir_path)
+        if not io.dexist(dir_path) then
+            log.info("验证结果", "目录删除验证成功")
+        else
+            log.error("验证结果", "目录删除验证失败")
+        end
+    else
+        log.error("io.rmdir", "目录删除失败", "路径:" .. dir_path)
+        return
+    end
+
+    -- 再次获取文件系统信息,查看空间变化
+    local final_success, final_total_blocks, final_used_blocks, final_block_size, final_fs_type =  io.fsstat(base_path)
+    if final_success then
+        log.info(" io.fsstat", "操作后文件系统信息:", 
+                 "总空间=" .. final_total_blocks .. "块", 
+                 "已用=" .. final_used_blocks .. "块", 
+                 "块大小=" .. final_block_size.."字节",
+                 "类型=" .. final_fs_type)
+    end
+
+    log.info("文件系统操作", "===== 文件系统操作完成 =====")
+end
+
+sys.taskInit(flash_fs_io_task)

+ 74 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fs_io/http_download_flash.lua

@@ -0,0 +1,74 @@
+--[[
+@module http_download_flash
+@summary HTTP下载文件到内置Flash模块
+@version 1.0.0
+@date    2025.09.23
+@author  王棚嶙
+@usage
+本文件演示的功能为通过HTTP下载文件到内置Flash中:
+1. 网络就绪检测
+2. 创建HTTP下载任务并等待完成
+3. 记录下载结果
+4. 获取并记录文件大小(使用io.fileSize)
+本文件没有对外接口,直接在main.lua中require "http_download_flash"即可
+]]
+
+local function http_download_flash_task()
+    -- 阶段1: 网络就绪检测
+    while not socket.adapter(socket.dft()) do
+        log.warn("HTTP下载", "等待网络连接", socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+
+    log.info("HTTP下载", "网络已就绪", socket.dft())
+
+    -- 阶段2: 执行下载任务
+    log.info("HTTP下载", "开始下载任务")
+
+    -- 创建下载目录
+    local download_dir = "/downloads"
+    if not io.dexist(download_dir) then
+        io.mkdir(download_dir)
+    end
+
+    -- 核心下载操作开始
+    local code, headers, body_size = http.request("GET",
+                                    "https://gitee.com/openLuat/LuatOS/raw/master/module/Air780EHM_Air780EHV_Air780EGH/demo/audio/sample-6s.mp3",
+                                    nil, nil, {dst = download_dir .. "/sample-6s.mp3"}).wait()
+
+    -- 阶段3: 记录下载结果
+    log.info("HTTP下载", "下载完成", 
+        code == 200 and "success" or "error", 
+        code, 
+        json.encode(headers or {}), 
+        body_size) 
+        
+    if code == 200 then
+        -- 获取实际文件大小 (使用io.fileSize接口)
+        local actual_size = io.fileSize(download_dir .. "/sample-6s.mp3")
+        if not actual_size then
+            -- 备用方案
+            actual_size = io.fileSize(download_dir .. "/sample-6s.mp3")
+        end
+        
+        log.info("HTTP下载", "文件大小验证", "预期:", body_size, "实际:", actual_size)
+        
+        if actual_size ~= body_size then
+            log.error("HTTP下载", "文件大小不一致", "预期:", body_size, "实际:", actual_size)
+        end
+        
+        -- 展示下载后的文件系统状态
+        local success, total_blocks, used_blocks, block_size, fs_type =  io.fsstat("/")
+        if success then
+            log.info("HTTP下载", "下载后文件系统信息:", 
+                     "总空间=" .. total_blocks .. "块", 
+                     "已用=" .. used_blocks .. "块", 
+                     "块大小=" .. block_size.."字节",
+                     "类型=" .. fs_type)
+        end
+    end
+
+end
+
+-- 创建下载任务
+sys.taskInit(http_download_flash_task)

+ 78 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fs_io/main.lua

@@ -0,0 +1,78 @@
+--[[
+@module  main
+@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
+@version 001.000.000
+@date    2025.09.23
+@author  王棚嶙
+@usage
+本 Demo 演示了在Air780EHM/780EGH/780EHV内置Flash文件系统中的完整操作流程:
+1. 基础操作:看门狗守护机制
+2. 文件系统操作:
+   - 文件系统信息查询( io.fsstat)
+   - 文件大小获取(io.fileSize)
+   - 文件创建/读写/追加
+   - 目录创建/删除
+   - 文件重命名/删除
+   - 文件存在性检查
+3. 下载功能:
+   - 网络检测与HTTP文件下载到内置Flash
+]]
+
+--[[
+必须定义PROJECT和VERSION变量,Luatools工具会用到这两个变量,远程升级功能也会用到这两个变量
+PROJECT:项目名,ascii string类型
+        可以随便定义,只要不使用,就行
+VERSION:项目版本号,ascii string类型
+        如果使用合宙iot.openluat.com进行远程升级,必须按照"XXX.YYY.ZZZ"三段格式定义:
+            X、Y、Z各表示1位数字,三个X表示的数字可以相同,也可以不同,同理三个Y和三个Z表示的数字也是可以相同,可以不同
+            因为历史原因,YYY这三位数字必须存在,但是没有任何用处,可以一直写为000
+        如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
+]]
+
+PROJECT = "flash_fs_io_demo"
+VERSION = "001.000.000"
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+-- 添加硬狗防止程序卡死
+if wdt then
+    -- 初始化watchdog设置为9s
+    wdt.init(9000)
+    -- 3s喂一次狗 
+    sys.timerLoopStart(wdt.feed, 3000) 
+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)
+
+--[[在加载以下两个功能时,建议分别打开进行测试,因为文件操作和http下载功能是异步操作。放到一个项目中,如果加载的时间点是随机的,就会出现哪个任务先抢到CPU时间片,哪个就先执行,不符合正常的业务逻辑,用户在参考编程的时候也要注意。]]
+
+-- 加载内置Flash文件系统操作演示模块
+require "flash_fs_io"
+-- 加载HTTP下载存入内置Flash功能演示模块
+-- require "http_download_flash"
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+-- sys.run()之后后面不要加任何语句!!!!!
+sys.run()

+ 143 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/fs_io/readme.md

@@ -0,0 +1,143 @@
+## **功能模块介绍**
+
+本 Demo 演示了在Air780EHM/780EGH/780EHV内置Flash文件系统中的完整操作流程,覆盖了从文件系统读写到高级文件操作的完整功能链。项目分为两个核心模块:
+
+1、main.lua:主程序入口 <br> 
+2、flash_fs_io.lua:内置Flash文件系统的操作测试流程模块,实现文件系统管理、文件操作和目录管理功能。<br> 
+3、http_download_flash.lua:HTTP下载模块,演示HTTP下载文件到内置Flash中的功能
+
+## **演示功能概述**
+
+### 1、主程序入口模块(main.lua)
+
+- 初始化项目信息和版本号
+- 初始化看门狗,并定时喂狗
+- 启动一个循环定时器,每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况方便分析内存使用是否有异常
+- 加载flash_fs_io模块(通过require "flash_fs_io")
+- 加载http_download_flash模块(通过require "http_download_flash")
+- 最后运行sys.run()。
+
+### 2、内置Flash文件系统演示模块(flash_fs_io.lua)
+
+#### 文件操作
+- 获取文件系统信息( io.fsstat)
+- 创建目录:io.mkdir("/flash_demo")
+- 创建/写入文件: io.open("/flash_demo/boottime", "wb")
+- 检查文件存在: io.exists(file_path)
+- 获取文件大小:io.fileSize(file_path)
+- 读取文件内容: io.open(file_path, "rb"):read("*a")
+- 启动计数文件: 记录设备启动次数
+- 文件追加: io.open(append_file, "a+")
+- 按行读取: file:read("*l")
+- 文件关闭: file:close()
+- 文件重命名: os.rename(old_path, new_path)
+- 列举目录: io.lsdir(dir_path)
+- 删除文件: os.remove(file_path)
+- 删除目录: io.rmdir(dir_path)
+
+### 3、HTTP下载功能 (http_download_flash.lua)
+
+
+#### 网络就绪检测
+
+- 1秒循环等待IP就绪
+- 网络故障处理机制
+
+#### 安全下载
+
+- HTTP下载
+
+#### 结果处理
+
+- 下载状态码解析
+- 自动文件大小验证
+- 获取文件系统信息(fs.fsstat)
+
+## **演示硬件环境**
+
+1、Air780EHM核心板一块(Air780EHM/780EGH/780EHV三种模块的核心板接线方式相同,这里以Air780EHM为例)
+
+2、TYPE-C USB数据线一根
+
+3、SIM卡一张
+
+4、Air780EHM/780EGH/780EHV核心板和数据线的硬件接线方式为
+
+- Air780EHM核心板通过TYPE-C USB口供电;(核心板USB旁边的开关拨到on一端)
+
+- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
+
+## **演示软件环境**
+
+1、Luatools下载调试工具: https://docs.openluat.com/air780epm/common/Luatools/
+
+2、内核固件版本:
+Air780EHM:https://docs.openluat.com/air780epm/luatos/firmware/version/
+Air780EGH:https://docs.openluat.com/air780egh/luatos/firmware/version/
+Air780EHV:https://docs.openluat.com/air780ehv/luatos/firmware/version/
+
+## **演示核心步骤**
+
+1、搭建好硬件环境
+
+2、通过Luatools将demo与固件烧录到开发板中
+
+3、烧录好后,板子开机将会在Luatools上看到如下打印
+
+```lua
+
+(1)文件操作演示
+[2025-10-22 15:23:25.096][000000000.595] I/user.文件系统操作 ===== 开始文件系统操作 =====
+[2025-10-22 15:23:25.104][000000000.601] I/user. io.fsstat成功: 总空间=192块 已用=20块 块大小=4096字节 类型=lfs
+[2025-10-22 15:23:25.118][000000000.644] I/user.io.mkdir 目录创建成功 路径:/flash_demo
+[2025-10-22 15:23:25.127][000000000.648] I/user.文件创建 文件写入成功 路径:/flash_demo/boottime
+[2025-10-22 15:23:25.145][000000000.651] I/user.io.exists 文件存在 路径:/flash_demo/boottime
+[2025-10-22 15:23:25.157][000000000.654] I/user.io.fileSize 文件大小:59字节 路径:/flash_demo/boottime
+[2025-10-22 15:23:25.165][000000000.657] I/user.文件读取 路径:/flash_demo/boottime 内容:这是内置Flash文件系统API文档示例的测试内容
+[2025-10-22 15:23:25.181][000000000.660] I/user.启动计数 文件内容: 这是内置Flash文件系统API文档示例的测试内容 十六进制: E8BF99E698AFE58685E7BDAE466C617368E69687E4BBB6E7B3BBE7BB9F415049E69687E6A1A3E7A4BAE4BE8BE79A84E6B58BE8AF95E58685E5AEB9 118
+[2025-10-22 15:23:25.189][000000000.660] I/user.启动计数 当前值: 0
+[2025-10-22 15:23:25.211][000000000.660] I/user.启动计数 更新值: 1
+[2025-10-22 15:23:25.217][000000000.663] I/user.文件写入 路径:/flash_demo/boottime 内容: 1
+[2025-10-22 15:23:25.224][000000000.669] I/user.文件创建 路径:/flash_demo/test_a 初始内容:ABC
+[2025-10-22 15:23:25.245][000000000.672] I/user.文件追加 路径:/flash_demo/test_a 追加内容:def
+[2025-10-22 15:23:25.251][000000000.675] I/user.文件验证 路径:/flash_demo/test_a 内容:ABCdef 结果: 成功
+[2025-10-22 15:23:25.267][000000000.678] I/user.文件创建 路径:/flash_demo/testline 写入3行文本
+[2025-10-22 15:23:25.277][000000000.681] I/user.按行读取 路径:/flash_demo/testline 第1行: abc
+[2025-10-22 15:23:25.285][000000000.682] I/user.按行读取 路径:/flash_demo/testline 第2行: 123
+[2025-10-22 15:23:25.295][000000000.682] I/user.按行读取 路径:/flash_demo/testline 第3行: wendal
+[2025-10-22 15:23:25.301][000000000.689] I/user.os.rename 文件重命名成功 原路径:/flash_demo/test_a 新路径:/flash_demo/renamed_file.txt
+[2025-10-22 15:23:25.312][000000000.694] D/vfs fopen /flash_demo/test_a r not found
+[2025-10-22 15:23:25.321][000000000.694] I/user.验证结果 重命名验证成功 新文件存在 原文件不存在
+[2025-10-22 15:23:25.340][000000000.695] I/user.目录操作 ===== 开始目录列举 =====
+[2025-10-22 15:23:25.348][000000000.706] I/user.fs lsdir [{"name":"boottime","size":1,"type":0},{"name":"renamed_file.txt","size":6,"type":0},{"name":"testline","size":15,"type":0}]
+[2025-10-22 15:23:25.359][000000000.710] I/user.os.remove 文件删除成功 路径:/flash_demo/renamed_file.txt
+[2025-10-22 15:23:25.366][000000000.713] D/vfs fopen /flash_demo/renamed_file.txt r not found
+[2025-10-22 15:23:25.373][000000000.713] I/user.验证结果 renamed_file.txt文件删除验证成功
+[2025-10-22 15:23:25.378][000000000.716] I/user.os.remove testline文件删除成功 路径:/flash_demo/testline
+[2025-10-22 15:23:25.390][000000000.719] D/vfs fopen /flash_demo/testline r not found
+[2025-10-22 15:23:25.395][000000000.719] I/user.验证结果 testline文件删除验证成功
+[2025-10-22 15:23:25.415][000000000.723] I/user.os.remove 文件删除成功 路径:/flash_demo/boottime
+[2025-10-22 15:23:25.423][000000000.726] D/vfs fopen /flash_demo/boottime r not found
+[2025-10-22 15:23:25.444][000000000.726] I/user.验证结果 boottime文件删除验证成功
+[2025-10-22 15:23:25.453][000000000.732] I/user.io.rmdir 目录删除成功 路径:/flash_demo
+[2025-10-22 15:23:25.464][000000000.734] I/user.验证结果 目录删除验证成功
+[2025-10-22 15:23:25.477][000000000.740] I/user. io.fsstat 操作后文件系统信息: 总空间=192块 已用=20块 块大小=4096字节 类型=lfs
+[2025-10-22 15:23:25.484][000000000.740] I/user.文件系统操作 ===== 文件系统操作完成 =====
+
+
+
+
+(2)网络连接与HTTP下载
+[2025-10-22 15:34:04.507][000000007.471] I/user.HTTP下载 网络已就绪 1 3
+[2025-10-22 15:34:04.550][000000007.471] I/user.HTTP下载 开始下载任务
+[2025-10-22 15:34:04.579][000000007.478] dns_run 676:gitee.com state 0 id 1 ipv6 0 use dns server2, try 0
+[2025-10-22 15:34:04.604][000000007.508] D/mobile TIME_SYNC 0
+[2025-10-22 15:34:04.734][000000007.517] dns_run 693:dns all done ,now stop
+[2025-10-22 15:34:06.390][000000009.741] I/user.HTTP下载 下载完成 success 200 
+[2025-10-22 15:34:06.422][000000009.741] {"Age":"0","Cache-Control":"public, max-age=60","Via":"1.1 varnish","Transfer-Encoding":"chunked","Date":"Wed, 22 Oct 2025 07:34:04 GMT","Access-Control-Allow-Credentials":"true","Vary":"Accept-Encoding","X-Served-By":"cache-ffe9","X-Gitee-Server":"http-pilot 1.9.21","Connection":"keep-alive","Server":"ADAS\/1.0.214","Access-Control-Allow-Headers":"Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With,X-CustomHeader,Content-Range,Range,Set-Language","Content-Security-Policy":"default-src 'none'; style-src 'unsafe-inline'; sandbox","X-Request-Id":"fa536af1-51bd-400f-8d4b-7322355a9db2","Accept-Ranges":"bytes","Etag":"W\/\"2aaa2788d394a924e258d6f26ad78b8c948950f5\"","Content-Type":"text\/plain; charset=utf-8","Access-Control-Allow-Methods":"GET, POST, PUT, PATCH, DELETE, OPTIONS","X-Frame-Options":"DENY","X-Cache":"MISS","Set-Cookie":"BEC=1f1759df3ccd099821dcf0da6feb0357;Path=\/;Max-Age=126000"}
+[2025-10-22 15:34:06.477][000000009.742]  103070
+[2025-10-22 15:34:06.492][000000009.745] I/user.HTTP下载 文件大小验证 预期: 103070 实际: 103070
+[2025-10-22 15:34:06.525][000000009.751] I/user.HTTP下载 下载后文件系统信息: 总空间=192块 已用=46块 块大小=4096字节 类型=lfs
+
+
+```

+ 0 - 311
module/Air780EPM/demo/780EPM_1.3版本开发板 ch390h联网测试/lan/dhcpsrv.lua

@@ -1,311 +0,0 @@
-
-local dhcpsrv = {}
-
-local udpsrv = require("udpsrv")
-
-local TAG = "dhcpsrv"
-
-----
--- 参考地址
--- https://en.wikipedia.org/wiki/Dynamic_Host_Configuration_Protocol
-
-local function dhcp_decode(buff)
-    -- buff:seek(0)
-    local dst = {}
-    -- 开始解析dhcp
-    dst.op = buff[0]
-    dst.htype = buff[1]
-    dst.hlen = buff[2]
-    dst.hops = buff[3]
-    buff:seek(4)
-    dst.xid = buff:read(4)
-
-    _, dst.secs = buff:unpack(">H")
-    _, dst.flags = buff:unpack(">H")
-    dst.ciaddr = buff:read(4)
-    dst.yiaddr = buff:read(4)
-    dst.siaddr = buff:read(4)
-    dst.giaddr = buff:read(4)
-    dst.chaddr = buff:read(16)
-
-    -- 跳过192字节
-    buff:seek(192, zbuff.SEEK_CUR)
-
-    -- 解析magic
-    _, dst.magic = buff:unpack(">I")
-
-    -- 解析option
-    local opt = {}
-    while buff:len() > buff:used() do
-        local tag = buff:read(1):byte()
-        if tag ~= 0 then
-            local len = buff:read(1):byte()
-            if tag == 0xFF or len == 0 then
-                break
-            end
-            local data = buff:read(len)
-            if tag == 53 then
-                -- 53: DHCP Message Type
-                dst.msgtype = data:byte()
-            end
-            table.insert(opt, {tag, data})
-            -- log.info(TAG, "tag", tag, "data", data:toHex())
-        end
-    end
-    if dst.msgtype == nil then
-        return -- 没有解析到msgtype,直接返回
-    end
-    dst.opts = opt
-    return dst
-end
-
-local function dhcp_buff2ip(buff)
-    return string.format("%d.%d.%d.%d", buff:byte(1), buff:byte(2), buff:byte(3), buff:byte(4))
-end
-
-local function dhcp_print_pkg(pkg)
-    log.info(TAG, "XID",  pkg.xid:toHex())
-    log.info(TAG, "secs", pkg.secs)
-    log.info(TAG, "flags", pkg.flags)
-    log.info(TAG, "chaddr", pkg.chaddr:sub(1, pkg.hlen):toHex())
-    log.info(TAG, "yiaddr", dhcp_buff2ip(pkg.yiaddr))
-    log.info(TAG, "siaddr", dhcp_buff2ip(pkg.siaddr))
-    log.info(TAG, "giaddr", dhcp_buff2ip(pkg.giaddr))
-    log.info(TAG, "ciaddr", dhcp_buff2ip(pkg.ciaddr))
-    log.info(TAG, "magic", string.format("%08X", pkg.magic))
-    for _, opt in pairs(pkg.opts) do
-        if opt[1] == 53 then
-            log.info(TAG, "msgtype", opt[2]:byte())
-        elseif opt[1] == 60 then
-            log.info(TAG, "auth", opt[2])
-        elseif opt[1] == 57 then
-            log.info(TAG, "Maximum DHCP message size", opt[2]:byte() * 256 + opt[2]:byte(2))
-        elseif opt[1] == 61 then
-            log.info(TAG, "Client-identifier", opt[2]:toHex())
-        elseif opt[1] == 55 then
-            log.info(TAG, "Parameter request list", opt[2]:toHex())
-        elseif opt[1] == 12 then
-            log.info(TAG, "Host name", opt[2])
-        -- elseif opt[1] == 58 then
-        --     log.info(TAG, "Renewal (T1) time value", opt[2]:unpack(">I"))
-        end
-    end
-end
-
-local function dhcp_encode(pkg, buff)
-    -- 合成DHCP包
-    buff:seek(0)
-    buff[0] = pkg.op
-    buff[1] = pkg.htype
-    buff[2] = pkg.hlen
-    buff[3] = pkg.hops
-    buff:seek(4)
-    -- 写入XID
-    buff:write(pkg.xid)
-    -- 几个重要的参数
-    buff:pack(">H", pkg.secs)
-    buff:pack(">H", pkg.flags)
-    buff:write(pkg.ciaddr)
-    buff:write(pkg.yiaddr)
-    buff:write(pkg.siaddr)
-    buff:write(pkg.giaddr)
-    -- 写入MAC地址
-    buff:write(pkg.chaddr)
-    -- 跳过192字节
-    buff:seek(192, zbuff.SEEK_CUR)
-    -- 写入magic
-    buff:pack(">I", pkg.magic)
-    -- 写入option
-    for _, opt in pairs(pkg.opts) do
-        buff:write(opt[1])
-        buff:write(#opt[2])
-        buff:write(opt[2])
-    end
-    buff:write(0xFF, 0x00)
-end
-
-----
-
-local function dhcp_send_x(srv, pkg, client, msgtype)
-    local buff = zbuff.create(300)
-    pkg.op = 2
-    pkg.ciaddr = "\0\0\0\0"
-    pkg.yiaddr = string.char(srv.opts.gw[1], srv.opts.gw[2], srv.opts.gw[3], client.ip)
-    pkg.siaddr = string.char(srv.opts.gw[1], srv.opts.gw[2], srv.opts.gw[3], srv.opts.gw[4])
-    pkg.giaddr = "\0\0\0\0"
-    pkg.secs = 0
-
-    pkg.opts = {} -- 复位option
-    table.insert(pkg.opts, {53, string.char(msgtype)})
-    table.insert(pkg.opts, {1, string.char(srv.opts.mark[1], srv.opts.mark[2], srv.opts.mark[3], srv.opts.mark[4])})
-    table.insert(pkg.opts, {3, string.char(srv.opts.gw[1], srv.opts.gw[2], srv.opts.gw[3], srv.opts.gw[4])})
-    table.insert(pkg.opts, {51, "\x00\x00\x1E\x00"}) -- 7200秒, 大概
-    table.insert(pkg.opts, {54, string.char(srv.opts.gw[1], srv.opts.gw[2], srv.opts.gw[3], srv.opts.gw[4])})
-    table.insert(pkg.opts, {6, string.char(223, 5, 5, 5)})
-    table.insert(pkg.opts, {6, string.char(119, 29, 29, 29)})
-    table.insert(pkg.opts, {6, string.char(srv.opts.gw[1], srv.opts.gw[2], srv.opts.gw[3], srv.opts.gw[4])})
-
-    dhcp_encode(pkg, buff)
-
-    local dst = "255.255.255.255"
-    if 4 == msgtype then
-        dst = string.format("%d.%d.%d.%d", srv.opts.gw[1], srv.opts.gw[2], srv.opts.gw[3], client.ip)
-    end
-    -- log.info(TAG, "发送", msgtype, dst, buff:query():toHex())
-    srv.udp:send(buff, dst, 68)
-end
-
-local function dhcp_send_offer(srv, pkg, client)
-    dhcp_send_x(srv, pkg, client, 2)
-end
-
-local function dhcp_send_ack(srv, pkg, client)
-    dhcp_send_x(srv, pkg, client, 5)
-end
-
-local function dhcp_send_nack(srv, pkg, client)
-    dhcp_send_x(srv, pkg, client, 6)
-end
-
-local function dhcp_handle_discover(srv, pkg)
-    local mac = pkg.chaddr:sub(1, pkg.hlen)
-    -- 看看是不是已经分配了ip
-    for _, client in pairs(srv.clients) do
-        if client.mac == mac then
-            log.info(TAG, "发现已经分配的mac地址, send offer")
-            dhcp_send_offer(srv, pkg, client)
-            return
-        end
-    end
-    -- TODO 清理已经过期的IP分配记录
-    -- 分配一个新的ip
-    if #srv.clients >= (srv.opts.ip_end - srv.opts.ip_start) then
-        log.info(TAG, "没有可分配的ip了")
-        return
-    end
-    local ip = nil
-    for i = srv.opts.ip_start, srv.opts.ip_end, 1 do
-        if srv.clients[i] == nil then
-            ip = i
-            break
-        end
-    end
-    if ip == nil then
-        log.info(TAG, "没有可分配的ip了")
-        return
-    end
-    log.info(TAG, "分配ip", mac:toHex(), string.format("%d.%d.%d.%d", srv.opts.gw[1], srv.opts.gw[2], srv.opts.gw[3], ip))
-    local client = {
-        mac = mac,
-        ip = ip,
-        tm = mcu.ticks() // mcu.hz(),
-        stat = 1
-    }
-    srv.clients[ip] = client
-    log.info(TAG, "send offer")
-    dhcp_send_offer(srv, pkg, client)
-end
-
-local function dhcp_handle_request(srv, pkg)
-    local mac = pkg.chaddr:sub(1, pkg.hlen)
-    -- 看看是不是已经分配了ip
-    for _, client in pairs(srv.clients) do
-        if client.mac == mac then
-            log.info(TAG, "request,发现已经分配的mac地址, send ack")
-            client.tm = mcu.ticks() // mcu.hz()
-            stat = 3
-            dhcp_send_ack(srv, pkg, client)
-            return
-        end
-    end
-    -- 没有找到, 那应该返回NACK
-    log.info(TAG, "request,没有分配的mac地址, send nack")
-    dhcp_send_nack(srv, pkg, {ip=pkg.yiaddr:byte(1)})
-end
-
-local function dhcp_pkg_handle(srv, pkg)
-    -- 进行基本的检查
-    if pkg.magic ~= 0x63825363 then
-        log.warn(TAG, "dhcp数据包的magic不对劲,忽略该数据包", pkg.magic)
-        return
-    end
-    if pkg.op ~= 1 then
-        log.info(TAG, "op不对,忽略该数据包", pkg.op)
-        return
-    end
-    if pkg.htype ~= 1 or pkg.hlen ~= 6 then
-        log.warn(TAG, "htype/hlen 不认识, 忽略该数据包")
-        return
-    end
-    -- 看看是不是能处理的类型, 当前只处理discover/request
-    if pkg.msgtype == 1 or pkg.msgtype == 3 then
-    else
-        log.warn(TAG, "msgtype不是discover/request, 忽略该数据包", pkg.msgtype)
-        return
-    end
-    -- 检查一下mac地址是否合法
-    local mac = pkg.chaddr:sub(1, pkg.hlen)
-    if mac == "\0\0\0\0\0\0" or mac == "\xFF\xFF\xFF\xFF\xFF\xFF" then
-        log.warn(TAG, "mac地址为空, 忽略该数据包")
-        return
-    end
-
-    -- 处理discover包
-    if pkg.msgtype == 1 then
-        log.info(TAG, "是discover包")
-        dhcp_handle_discover(srv, pkg)
-    elseif pkg.msgtype == 3 then
-        log.info(TAG, "是request包")
-        dhcp_handle_request(srv, pkg)
-    end
-    -- TODO 处理结束, 打印一下客户的列表?
-end
-
-local function dhcp_task(srv)
-    while 1 do
-        -- log.info("ulwip", "等待DHCP数据")
-        local result, data = sys.waitUntil(srv.udp_topic, 1000)
-        if result then
-            -- log.info("ulwip", "收到dhcp数据包", data:toHex())
-            -- 解析DHCP数据包
-            local pkg = dhcp_decode(zbuff.create(#data, data))
-            if pkg then
-                -- dhcp_print_pkg(pkg)
-                dhcp_pkg_handle(srv, pkg)
-            end
-        end
-    end
-end
-function dhcpsrv.create(opts)
-    local srv = {}
-    if not opts then
-        opts = {}
-    end
-    srv.udp_topic = "dhcpd_inc"
-    -- 补充参数
-    if not opts.mark then
-        opts.mark = {255, 255, 255, 0}
-    end
-    if not opts.gw then
-        opts.gw = {192, 168, 4, 1}
-    end
-    if not opts.dns then
-        opts.dns = opts.gw
-    end
-    if not opts.ip_start then
-        opts.ip_start = 100
-    end
-    if not opts.ip_end then
-        opts.ip_end = 200
-    end
-
-    srv.clients = {}
-    srv.opts = opts
-
-    srv.udp = udpsrv.create(67, srv.udp_topic, opts.adapter)
-    srv.task = sys.taskInit(dhcp_task, srv)
-    return srv
-end
-
-
-return dhcpsrv

+ 0 - 113
module/Air780EPM/demo/780EPM_1.3版本开发板 ch390h联网测试/lan/dnsproxy.lua

@@ -1,113 +0,0 @@
---[[
-@module dnsproxy
-@summary DNS代理转发
-@version 1.0
-@date    2024.4.20
-@author  wendal
-@demo    socket
-@tag LUAT_USE_NETWORK
-@usage
--- 具体用法请查阅demo
-]]
-
-local sys = require "sys"
-
-local dnsproxy = {}
-dnsproxy.map = {}
-dnsproxy.txid = 0x123
-dnsproxy.rxbuff = zbuff.create(1500)
-
-function dnsproxy.on_request(sc, event)
-    if event == socket.EVENT then
-        local rxbuff = dnsproxy.rxbuff
-        while 1 do
-            rxbuff:seek(0)
-            local succ, data_len, remote_ip, remote_port = socket.rx(sc, rxbuff)
-            if succ and data_len and data_len > 0 then
-                -- log.info("dnsproxy", "收到DNS查询数据", rxbuff:query():toHex())
-                if remote_ip and #remote_ip == 5 then
-                    local ip1,ip2,ip3,ip4 = remote_ip:byte(2),remote_ip:byte(3),remote_ip:byte(4),remote_ip:byte(5)
-                    remote_ip = string.format("%d.%d.%d.%d", ip1, ip2, ip3, ip4)
-                    local txid_request = rxbuff[0] + rxbuff[1] * 256
-                    local txid_map = dnsproxy.txid
-                    dnsproxy.txid = dnsproxy.txid + 1
-                    if dnsproxy.txid > 65000 then
-                        dnsproxy.txid = 0x123
-                    end
-                    table.insert(dnsproxy.map, {txid_request, txid_map, remote_ip, remote_port})
-                    rxbuff[0] = txid_map % 256
-                    rxbuff[1] = txid_map // 256
-                    socket.tx(dnsproxy.main_sc, rxbuff, "223.5.5.5", 53)
-                end
-            else
-                break
-            end
-        end
-    end
-end
-
-function dnsproxy.on_response(sc, event)
-    if event == socket.EVENT then
-        local rxbuff = dnsproxy.rxbuff
-        while 1 do
-            rxbuff:seek(0)
-            local succ, data_len = socket.rx(sc, rxbuff)
-            if succ and data_len and data_len > 0 then
-                if true then
-                    -- local ip1,ip2,ip3,ip4 = remote_ip:byte(2),remote_ip:byte(3),remote_ip:byte(4),remote_ip:byte(5)
-                    -- remote_ip = string.format("%d.%d.%d.%d", ip1, ip2, ip3, ip4)
-                    local txid_resp = rxbuff[0] + rxbuff[1] * 256
-                    local index = -1
-                    for i, mapit in pairs(dnsproxy.map) do
-                        if mapit[2] == txid_resp then
-                            local txid_request = mapit[1]
-                            local remote_ip = mapit[3]
-                            local remote_port = mapit[4]
-                            rxbuff[0] = txid_request % 256
-                            rxbuff[1] = txid_request // 256
-                            socket.tx(dnsproxy.sc, rxbuff, remote_ip, remote_port)
-                            index = i
-                            break
-                        end
-                    end
-                    if index > 0 then
-                        table.remove(dnsproxy.map, index)
-                    end
-                end
-            else
-                break
-            end
-        end
-    end
-end
-
---[[
-创建UDP服务器
-@api dnsproxy.create(adapter, main_adapter)
-@int 监听的网络适配器id
-@int 网络适配编号, 默认为nil,可选
-@return table UDP服务的实体, 若创建失败会返回nil
-]]
-function dnsproxy.setup(adapter, main_adapter)
-    log.info("dnsproxy", adapter, main_adapter)
-    dnsproxy.adapter = adapter
-    dnsproxy.main_adapter = main_adapter
-    dnsproxy.sc = socket.create(dnsproxy.adapter, dnsproxy.on_request)
-    dnsproxy.main_sc = socket.create(dnsproxy.main_adapter, dnsproxy.on_response)
-    socket.config(dnsproxy.sc, 53, true)
-    socket.config(dnsproxy.main_sc, 1053, true)
-    dnsproxy.on_ip_ready()
-    return true
-end
-
-function dnsproxy.on_ip_ready()
-    socket.close(dnsproxy.sc)
-    socket.close(dnsproxy.main_sc)
-    log.info("dnsproxy", "开启DNS代理")
-    socket.connect(dnsproxy.sc, "255.255.255.255", 0)
-    socket.connect(dnsproxy.main_sc, "223.5.5.5", 53)
-end
-
-sys.subscribe("IP_READY", dnsproxy.on_ip_ready)
-
-return dnsproxy

+ 1 - 1
module/Air780EPM/demo/accessory_board/AirSHT30_1000/main.lua

@@ -7,7 +7,7 @@
 @usage
 AirSHT30_1000是合宙设计生产的一款I2C接口的SHT30温湿度传感器配件板;
 本demo演示的核心功能为:
-Air780EPM开发板+AirSHT30_1000配件板,每隔1秒读取1次温湿度数据;
+Air780EPM核心板+AirSHT30_1000配件板,每隔1秒读取1次温湿度数据;
 更多说明参考本目录下的readme.md文件
 ]]
 

+ 8 - 8
module/Air780EPM/demo/accessory_board/AirSHT30_1000/readme.md

@@ -12,12 +12,12 @@ AirSHT30_1000是合宙设计生产的一款I2C接口的SHT30温湿度传感器
 
 本demo演示的核心功能为:
 
-Air780EPM开发板+AirSHT30_1000配件板,每隔1秒读取1次温湿度数据;
+Air780EPM核心板+AirSHT30_1000配件板,每隔1秒读取1次温湿度数据;
 
 
-## 开发板+配件板资料
+## 核心板+配件板资料
 
-[Air780EPM开发板](https://docs.openluat.com/air780epm/product/shouce/)
+[Air780EPM核心板](https://docs.openluat.com/air780epm/product/shouce/)
 
 [AirSHT30_1000配件板相关资料](https://docs.openluat.com/accessory/AirSHT30_1000/)
 
@@ -26,18 +26,18 @@ Air780EPM开发板+AirSHT30_1000配件板,每隔1秒读取1次温湿度数据
 
 ![](https://docs.openluat.com/accessory/AirSHT30_1000/image/connect_780epm.png)
 
-1、Air780EPM开发
+1、Air780EPM核心
 
 2、AirSHT30_1000配件板
 
 3、母对母的杜邦线4根
 
-| Air780EPM开发板 | AirSHT30_1000配件板|
+| Air780EPM核心板 | AirSHT30_1000配件板|
 | ------------ | ------------------ |
-|     3V3(VDD_EXT)     |         3V3        |
+|     3V3     |         3V3        |
 |     GND   |         GND        |
-|  I2C1_SDA(CAMERA_SDA)  |         SDA        |
-| I2C1_SCL(CAMERA_SCL) |         SCL        |
+|  66/I2C1SDA  |         SDA        |
+| 67/I2C1SCL |         SCL        |
 
 
 ## 演示软件环境

+ 0 - 6
module/Air780EPM/demo/accessory_board/AirSHT30_1000/sht30_app.lua

@@ -15,12 +15,6 @@
 --加载AirSHT30_1000驱动文件
 local air_sht30 = require "AirSHT30_1000"
 
---如果使用的是Air780EPM核心板,则需要注释掉下面这行代码;
---如果使用的是Air780EPM开发板,则需要打开下面这行代码;
---这行代码的作用是:
---因为Air780EPM开发板上I2C内部没上拉,需要外部加上拉
---所以设置gpio2输出高电平,给camera_sda、camera_scl引脚提供上拉
-gpio.setup(2, 1)
 
 --每隔1秒读取一次温湿度数据
 local function read_sht30_task_func()

+ 1 - 1
module/Air780EPM/demo/accessory_board/AirVOC_1000/main.lua

@@ -8,7 +8,7 @@
 AirVOC_1000是合宙设计生产的一款I2C接口的VOC(挥发性有机化合物)气体传感器配件板;
 主要用于检测甲醛、一氧化碳、可燃气体、酒精、氨气、硫化物、苯系蒸汽、烟雾、其它有害气体的监测;
 本demo演示的核心功能为:
-Air780EPM开发板+AirVOC_1000配件板,每隔1秒读取1次TVOC空气质量数据;
+Air780EPM核心板+AirVOC_1000配件板,每隔1秒读取1次TVOC空气质量数据;
 更多说明参考本目录下的readme.md文件
 ]]
 

+ 8 - 8
module/Air780EPM/demo/accessory_board/AirVOC_1000/readme.md

@@ -14,12 +14,12 @@ AirVOC_1000是合宙设计生产的一款I2C接口的VOC(挥发性有机化合
 
 本demo演示的核心功能为:
 
-Air780EPM开发板+AirVOC_1000配件板,每隔1秒读取1次TVOC空气质量数据;
+Air780EPM核心板+AirVOC_1000配件板,每隔1秒读取1次TVOC空气质量数据;
 
 
 ## 核心板+配件板资料
 
-[Air780EPM开发板](https://docs.openluat.com/air780epm/product/shouce/)
+[Air780EPM核心板](https://docs.openluat.com/air780epm/product/shouce/)
 
 [AirVOC_1000配件板相关资料](https://docs.openluat.com/accessory/AirVOC_1000/)
 
@@ -28,20 +28,20 @@ Air780EPM开发板+AirVOC_1000配件板,每隔1秒读取1次TVOC空气质量
 
 ![](https://docs.openluat.com/accessory/AirVOC_1000/image/connect_Air780EPM.png)
 
-1、Air780EPM开发
+1、Air780EPM核心
 
 2、AirVOC_1000配件板
 
 3、母对母的杜邦线4根
 
-4、Air780EPM开发板和AirVOC_1000配件板的硬件接线方式为
+4、Air780EPM核心板和AirVOC_1000配件板的硬件接线方式为
 
-| Air780EPM开发板 | AirVOC_1000配件板  |
+| Air780EPM核心板 | AirVOC_1000配件板  |
 | ------------ | ------------------ |
-|     3V3(VDD_EXT)     |         3V3        |
+|     3V3     |         3V3        |
 |     GND   |         GND        |
-| I2C1_SDA(CAMERA_SDA) |         SDA        |
-| I2C1_SCL(CAMERA_SCL) |         SCL        |
+| 66/I2C1SDA |         SDA        |
+| 67/I2C1SCL |         SCL        |
 
 
 ## 演示软件环境

+ 0 - 8
module/Air780EPM/demo/accessory_board/AirVOC_1000/voc_app.lua

@@ -16,14 +16,6 @@
 local air_voc = require "AirVOC_1000"
 
 
---如果使用的是Air780EPM核心板,则需要注释掉下面这行代码;
---如果使用的是Air780EPM开发板,则需要打开下面这行代码;
---这行代码的作用是:
---因为Air780EPM开发板上I2C内部没上拉,需要外部加上拉
---所以设置gpio2输出高电平,给camera_sda、camera_scl引脚提供上拉
-gpio.setup(2, 1)
-
-
 --每隔1秒读取一次TVOC数据
 local function read_voc_task_func()
     --打开voc硬件

+ 282 - 0
module/Air780EPM/demo/fs_io/flash_fs_io.lua

@@ -0,0 +1,282 @@
+--[[
+@module  flash_fs_io
+@summary 内置Flash文件系统操作测试模块
+@version 1.0.0
+@date    2025.09.23
+@author  王棚嶙
+@usage
+本文件为内置Flash文件系统的操作测试流程:
+1. 获取文件系统信息( io.fsstat)
+2. 创建目录
+3. 创建并写入文件
+4. 检查文件是否存在
+5. 获取文件大小(io.fileSize)
+6. 读取文件内容
+7. 启动计数文件操作
+8. 文件追加测试
+9. 按行读取测试
+10. 文件重命名
+11. 列举目录内容
+12. 删除文件
+13. 删除目录
+本文件没有对外接口,直接在main.lua中require "flash_fs_io"就可以加载运行
+]]
+
+function flash_fs_io_task()
+    -- 使用内置Flash文件系统,根目录为"/",
+    local base_path = "/"
+    -- 创建一个目录
+    local demo_dir = "flash_demo"
+    -- 文件名
+    local dir_path = base_path .. demo_dir
+
+    -- ########## 开始进行内置Flash文件系统操作 ##########
+    log.info("文件系统操作", "===== 开始文件系统操作 =====")
+
+    -- 1. 获取文件系统信息 (使用 io.fsstat接口)
+    local success, total_blocks, used_blocks, block_size, fs_type  =  io.fsstat(base_path)
+    if success then
+        log.info(" io.fsstat成功:", 
+            "总空间=" .. total_blocks .. "块", 
+            "已用=" .. used_blocks .. "块", 
+            "块大小=" .. block_size.."字节",
+            "类型=" .. fs_type)
+    else
+        log.error(" io.fsstat", "获取文件系统信息失败")
+        return
+    end
+
+    -- 2. 创建目录
+    -- 如果目录不存在,则创建目录
+    if not io.dexist(dir_path) then
+        -- 创建目录
+        if io.mkdir(dir_path) then
+            log.info("io.mkdir", "目录创建成功", "路径:" .. dir_path)
+        else
+            log.error("io.mkdir", "目录创建失败", "路径:" .. dir_path)
+            return
+        end
+    else
+        log.warn("io.mkdir", "目录已存在,跳过创建", "路径:" .. dir_path)
+    end
+
+    -- 3. 创建并写入文件
+    local file_path = dir_path .. "/boottime"
+    local file = io.open(file_path, "wb")
+    if file then
+        file:write("这是内置Flash文件系统API文档示例的测试内容")
+        file:close()
+        log.info("文件创建", "文件写入成功", "路径:" .. file_path)
+    else
+        log.error("文件创建", "文件创建失败", "路径:" .. file_path)
+        return
+    end
+
+    -- 4. 检查文件是否存在
+    if io.exists(file_path) then
+        log.info("io.exists", "文件存在", "路径:" .. file_path)
+    else
+        log.error("io.exists", "文件不存在", "路径:" .. file_path)
+        return
+    end
+
+    -- 5. 获取文件大小 (使用io.fileSize接口)
+    local file_size = io.fileSize(file_path)
+    if file_size then
+        log.info("io.fileSize", "文件大小:" .. file_size .. "字节", "路径:" .. file_path)
+    else
+        log.error("io.fileSize", "获取文件大小失败", "路径:" .. file_path)
+        return
+    end
+
+    -- 6. 读取文件内容
+    file = io.open(file_path, "rb")
+    if file then
+        local content = file:read("*a")
+        log.info("文件读取", "路径:" .. file_path, "内容:" .. content)
+        file:close()
+    else
+        log.error("文件操作", "无法打开文件读取内容", "路径:" .. file_path)
+        return
+    end
+
+    -- 7. 启动计数文件操作
+    local count = 0
+    file = io.open(file_path, "rb")
+    if file then
+        local data = file:read("*a")
+        log.info("启动计数", "文件内容:", data, "十六进制:", data:toHex())
+        count = tonumber(data) or 0
+        file:close()
+    else
+        log.warn("启动计数", "文件不存在或无法打开")
+    end
+
+    log.info("启动计数", "当前值:", count)
+    count = count + 1
+    log.info("启动计数", "更新值:", count)
+
+    file = io.open(file_path, "wb")
+    if file then
+        file:write(tostring(count))
+        file:close()
+        log.info("文件写入", "路径:" .. file_path, "内容:", count)
+    else
+        log.error("文件写入", "无法打开文件", "路径:" .. file_path)
+        return
+    end
+
+    -- 8. 文件追加测试
+    local append_file = dir_path .. "/test_a"
+    os.remove(append_file) -- 清理旧文件
+
+    file = io.open(append_file, "wb")
+    if file then
+        file:write("ABC")
+        file:close()
+        log.info("文件创建", "路径:" .. append_file, "初始内容:ABC")
+    else
+        log.error("文件创建", "无法创建文件", "路径:" .. append_file)
+        return
+    end
+
+    file = io.open(append_file, "a+")
+    if file then
+        file:write("def")
+        file:close()
+        log.info("文件追加", "路径:" .. append_file, "追加内容:def")
+    else
+        log.error("文件追加", "无法打开文件进行追加", "路径:" .. append_file)
+        return
+    end
+
+    -- 验证追加结果
+    file = io.open(append_file, "r")
+    if file then
+        local data = file:read("*a")
+        log.info("文件验证", "路径:" .. append_file, "内容:" .. data, "结果:",
+            data == "ABCdef" and "成功" or "失败")
+        file:close()
+    else
+        log.error("文件验证", "无法打开文件进行验证", "路径:" .. append_file)
+        return
+    end
+
+    -- 9. 按行读取测试
+    local line_file = dir_path .. "/testline"
+    file = io.open(line_file, "w")
+    if file then
+        file:write("abc\n")
+        file:write("123\n")
+        file:write("wendal\n")
+        file:close()
+        log.info("文件创建", "路径:" .. line_file, "写入3行文本")
+    else
+        log.error("文件创建", "无法创建文件", "路径:" .. line_file)
+        return
+    end
+
+    file = io.open(line_file, "r")
+    if file then
+        log.info("按行读取", "路径:" .. line_file, "第1行:", file:read("*l"))
+        log.info("按行读取", "路径:" .. line_file, "第2行:", file:read("*l"))
+        log.info("按行读取", "路径:" .. line_file, "第3行:", file:read("*l"))
+        file:close()
+    else
+        log.error("按行读取", "无法打开文件", "路径:" .. line_file)
+        return
+    end
+
+    -- 10. 文件重命名
+    local old_path = append_file
+    local new_path = dir_path .. "/renamed_file.txt"
+    local success, err = os.rename(old_path, new_path)
+    if success then
+        log.info("os.rename", "文件重命名成功", "原路径:" .. old_path, "新路径:" .. new_path)
+
+        -- 验证重命名结果
+        if io.exists(new_path) and not io.exists(old_path) then
+            log.info("验证结果", "重命名验证成功", "新文件存在", "原文件不存在")
+        else
+            log.error("验证结果", "重命名验证失败")
+        end
+    else
+        log.error("os.rename", "重命名失败", "错误:" .. tostring(err), "原路径:" .. old_path)
+        return
+    end
+
+    -- 11. 列举目录内容
+    log.info("目录操作", "===== 开始目录列举 =====")
+
+    local ret, data = io.lsdir(dir_path, 50, 0)
+    if ret then
+        log.info("fs", "lsdir", json.encode(data))
+    else
+        log.info("fs", "lsdir", "fail", ret, data)
+        return
+    end
+
+    -- 12. 删除文件测试
+    if os.remove(new_path) then
+        log.info("os.remove", "文件删除成功", "路径:" .. new_path)
+        if not io.exists(new_path) then
+            log.info("验证结果", "renamed_file.txt文件删除验证成功")
+        else
+            log.error("验证结果", "renamed_file.txt文件删除验证失败")
+        end
+    else
+        log.error("os.remove", "renamed_file.txt文件删除失败", "路径:" .. new_path)
+        return
+    end
+
+    if os.remove(line_file) then
+        log.info("os.remove", "testline文件删除成功", "路径:" .. line_file)
+        if not io.exists(line_file) then
+            log.info("验证结果", "testline文件删除验证成功")
+        else
+            log.error("验证结果", "testline文件删除验证失败")
+        end
+    else
+        log.error("os.remove", "testline文件删除失败", "路径:" .. line_file)
+        return
+    end
+
+    if os.remove(file_path) then
+        log.info("os.remove", "文件删除成功", "路径:" .. file_path)
+        if not io.exists(file_path) then
+            log.info("验证结果", "boottime文件删除验证成功")
+        else
+            log.error("验证结果", "boottime文件删除验证失败")
+        end
+    else
+        log.error("os.remove", "boottime文件删除失败", "路径:" .. file_path)
+        return
+    end
+
+    -- 13. 删除目录
+    if io.rmdir(dir_path) then
+        log.info("io.rmdir", "目录删除成功", "路径:" .. dir_path)
+        if not io.dexist(dir_path) then
+            log.info("验证结果", "目录删除验证成功")
+        else
+            log.error("验证结果", "目录删除验证失败")
+        end
+    else
+        log.error("io.rmdir", "目录删除失败", "路径:" .. dir_path)
+        return
+    end
+
+    -- 再次获取文件系统信息,查看空间变化
+    local final_success, final_total_blocks, final_used_blocks, final_block_size, final_fs_type =  io.fsstat(base_path)
+    if final_success then
+        log.info(" io.fsstat", "操作后文件系统信息:", 
+                 "总空间=" .. final_total_blocks .. "块", 
+                 "已用=" .. final_used_blocks .. "块", 
+                 "块大小=" .. final_block_size.."字节",
+                 "类型=" .. final_fs_type)
+    end
+
+    log.info("文件系统操作", "===== 文件系统操作完成 =====")
+end
+
+sys.taskInit(flash_fs_io_task)

+ 74 - 0
module/Air780EPM/demo/fs_io/http_download_flash.lua

@@ -0,0 +1,74 @@
+--[[
+@module http_download_flash
+@summary HTTP下载文件到内置Flash模块
+@version 1.0.0
+@date    2025.09.23
+@author  王棚嶙
+@usage
+本文件演示的功能为通过HTTP下载文件到内置Flash中:
+1. 网络就绪检测
+2. 创建HTTP下载任务并等待完成
+3. 记录下载结果
+4. 获取并记录文件大小(使用io.fileSize)
+本文件没有对外接口,直接在main.lua中require "http_download_flash"即可
+]]
+
+local function http_download_flash_task()
+    -- 阶段1: 网络就绪检测
+    while not socket.adapter(socket.dft()) do
+        log.warn("HTTP下载", "等待网络连接", socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+
+    log.info("HTTP下载", "网络已就绪", socket.dft())
+
+    -- 阶段2: 执行下载任务
+    log.info("HTTP下载", "开始下载任务")
+
+    -- 创建下载目录
+    local download_dir = "/downloads"
+    if not io.dexist(download_dir) then
+        io.mkdir(download_dir)
+    end
+
+    -- 核心下载操作开始
+    local code, headers, body_size = http.request("GET",
+                                    "https://gitee.com/openLuat/LuatOS/raw/master/module/Air780EHM_Air780EHV_Air780EGH/demo/audio/sample-6s.mp3",
+                                    nil, nil, {dst = download_dir .. "/sample-6s.mp3"}).wait()
+
+    -- 阶段3: 记录下载结果
+    log.info("HTTP下载", "下载完成", 
+        code == 200 and "success" or "error", 
+        code, 
+        json.encode(headers or {}), 
+        body_size) 
+        
+    if code == 200 then
+        -- 获取实际文件大小 (使用io.fileSize接口)
+        local actual_size = io.fileSize(download_dir .. "/sample-6s.mp3")
+        if not actual_size then
+            -- 备用方案
+            actual_size = io.fileSize(download_dir .. "/sample-6s.mp3")
+        end
+        
+        log.info("HTTP下载", "文件大小验证", "预期:", body_size, "实际:", actual_size)
+        
+        if actual_size ~= body_size then
+            log.error("HTTP下载", "文件大小不一致", "预期:", body_size, "实际:", actual_size)
+        end
+        
+        -- 展示下载后的文件系统状态
+        local success, total_blocks, used_blocks, block_size, fs_type =  io.fsstat("/")
+        if success then
+            log.info("HTTP下载", "下载后文件系统信息:", 
+                     "总空间=" .. total_blocks .. "块", 
+                     "已用=" .. used_blocks .. "块", 
+                     "块大小=" .. block_size.."字节",
+                     "类型=" .. fs_type)
+        end
+    end
+
+end
+
+-- 创建下载任务
+sys.taskInit(http_download_flash_task)

+ 78 - 0
module/Air780EPM/demo/fs_io/main.lua

@@ -0,0 +1,78 @@
+--[[
+@module  main
+@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
+@version 001.000.000
+@date    2025.09.23
+@author  王棚嶙
+@usage
+本 Demo 演示了在Air780EPM内置Flash文件系统中的完整操作流程:
+1. 基础操作:看门狗守护机制
+2. 文件系统操作:
+   - 文件系统信息查询( io.fsstat)
+   - 文件大小获取(io.fileSize)
+   - 文件创建/读写/追加
+   - 目录创建/删除
+   - 文件重命名/删除
+   - 文件存在性检查
+3. 下载功能:
+   - 网络检测与HTTP文件下载到内置Flash
+]]
+
+--[[
+必须定义PROJECT和VERSION变量,Luatools工具会用到这两个变量,远程升级功能也会用到这两个变量
+PROJECT:项目名,ascii string类型
+        可以随便定义,只要不使用,就行
+VERSION:项目版本号,ascii string类型
+        如果使用合宙iot.openluat.com进行远程升级,必须按照"XXX.YYY.ZZZ"三段格式定义:
+            X、Y、Z各表示1位数字,三个X表示的数字可以相同,也可以不同,同理三个Y和三个Z表示的数字也是可以相同,可以不同
+            因为历史原因,YYY这三位数字必须存在,但是没有任何用处,可以一直写为000
+        如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
+]]
+
+PROJECT = "flash_fs_io_demo"
+VERSION = "001.000.000"
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+-- 添加硬狗防止程序卡死
+if wdt then
+    -- 初始化watchdog设置为9s
+    wdt.init(9000)
+    -- 3s喂一次狗 
+    sys.timerLoopStart(wdt.feed, 3000) 
+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)
+
+--[[在加载以下两个功能时,建议分别打开进行测试,因为文件操作和http下载功能是异步操作。放到一个项目中,如果加载的时间点是随机的,就会出现哪个任务先抢到CPU时间片,哪个就先执行,不符合正常的业务逻辑,用户在参考编程的时候也要注意。]]
+
+-- 加载内置Flash文件系统操作演示模块
+require "flash_fs_io"
+-- 加载HTTP下载存入内置Flash功能演示模块
+-- require "http_download_flash"
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+-- sys.run()之后后面不要加任何语句!!!!!
+sys.run()

+ 141 - 0
module/Air780EPM/demo/fs_io/readme.md

@@ -0,0 +1,141 @@
+## **功能模块介绍**
+
+本 Demo 演示了在Air780EPM内置Flash文件系统中的完整操作流程,覆盖了从文件系统读写到高级文件操作的完整功能链。项目分为两个核心模块:
+
+1、main.lua:主程序入口 <br> 
+2、flash_fs_io.lua:内置Flash文件系统的操作测试流程模块,实现文件系统管理、文件操作和目录管理功能。<br> 
+3、http_download_flash.lua:HTTP下载模块,演示HTTP下载文件到内置Flash中的功能
+
+## **演示功能概述**
+
+### 1、主程序入口模块(main.lua)
+
+- 初始化项目信息和版本号
+- 初始化看门狗,并定时喂狗
+- 启动一个循环定时器,每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况方便分析内存使用是否有异常
+- 加载flash_fs_io模块(通过require "flash_fs_io")
+- 加载http_download_flash模块(通过require "http_download_flash")
+- 最后运行sys.run()。
+
+### 2、内置Flash文件系统演示模块(flash_fs_io.lua)
+
+#### 文件操作
+- 获取文件系统信息( io.fsstat)
+- 创建目录:io.mkdir("/flash_demo")
+- 创建/写入文件: io.open("/flash_demo/boottime", "wb")
+- 检查文件存在: io.exists(file_path)
+- 获取文件大小:io.fileSize(file_path)
+- 读取文件内容: io.open(file_path, "rb"):read("*a")
+- 启动计数文件: 记录设备启动次数
+- 文件追加: io.open(append_file, "a+")
+- 按行读取: file:read("*l")
+- 文件关闭: file:close()
+- 文件重命名: os.rename(old_path, new_path)
+- 列举目录: io.lsdir(dir_path)
+- 删除文件: os.remove(file_path)
+- 删除目录: io.rmdir(dir_path)
+
+### 3、HTTP下载功能 (http_download_flash.lua)
+
+
+#### 网络就绪检测
+
+- 1秒循环等待IP就绪
+- 网络故障处理机制
+
+#### 安全下载
+
+- HTTP下载
+
+#### 结果处理
+
+- 下载状态码解析
+- 自动文件大小验证
+- 获取文件系统信息( io.fsstat)
+
+## **演示硬件环境**
+
+1、Air780EPM核心板一块
+
+2、TYPE-C USB数据线一根
+
+3、SIM卡一张
+
+4、Air780EPM核心板和数据线的硬件接线方式为
+
+- Air780EPM核心板通过TYPE-C USB口供电;(核心板USB旁边的开关拨到on一端)
+
+- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
+
+## **演示软件环境**
+
+1、Luatools下载调试工具:https://docs.openluat.com/air780epm/common/Luatools/
+
+2、内核固件版本:https://docs.openluat.com/air780epm/luatos/firmware/version/
+
+## **演示核心步骤**
+
+1、搭建好硬件环境
+
+2、通过Luatools将demo与固件烧录到开发板中
+
+3、烧录好后,板子开机将会在Luatools上看到如下打印
+
+```lua
+
+(1)文件操作演示
+[2025-10-22 16:04:28.032][000000000.214] I/user.文件系统操作 ===== 开始文件系统操作 =====
+[2025-10-22 16:04:28.064][000000000.218] I/user. io.fsstat成功: 总空间=42块 已用=4块 块大小=4096字节 类型=lfs
+[2025-10-22 16:04:28.085][000000000.319] I/user.io.mkdir 目录创建成功 路径:/flash_demo
+[2025-10-22 16:04:28.105][000000000.328] I/user.文件创建 文件写入成功 路径:/flash_demo/boottime
+[2025-10-22 16:04:28.114][000000000.331] I/user.io.exists 文件存在 路径:/flash_demo/boottime
+[2025-10-22 16:04:28.130][000000000.334] I/user.io.fileSize 文件大小:59字节 路径:/flash_demo/boottime
+[2025-10-22 16:04:28.142][000000000.337] I/user.文件读取 路径:/flash_demo/boottime 内容:这是内置Flash文件系统API文档示例的测试内容
+[2025-10-22 16:04:28.167][000000000.340] I/user.启动计数 文件内容: 这是内置Flash文件系统API文档示例的测试内容 十六进制: E8BF99E698AFE58685E7BDAE466C617368E69687E4BBB6E7B3BBE7BB9F415049E69687E6A1A3E7A4BAE4BE8BE79A84E6B58BE8AF95E58685E5AEB9 118
+[2025-10-22 16:04:28.192][000000000.340] I/user.启动计数 当前值: 0
+[2025-10-22 16:04:28.217][000000000.340] I/user.启动计数 更新值: 1
+[2025-10-22 16:04:28.221][000000000.345] I/user.文件写入 路径:/flash_demo/boottime 内容: 1
+[2025-10-22 16:04:28.234][000000000.357] I/user.文件创建 路径:/flash_demo/test_a 初始内容:ABC
+[2025-10-22 16:04:28.242][000000000.360] I/user.文件追加 路径:/flash_demo/test_a 追加内容:def
+[2025-10-22 16:04:28.248][000000000.363] I/user.文件验证 路径:/flash_demo/test_a 内容:ABCdef 结果: 成功
+[2025-10-22 16:04:28.253][000000000.367] I/user.文件创建 路径:/flash_demo/testline 写入3行文本
+[2025-10-22 16:04:28.271][000000000.370] I/user.按行读取 路径:/flash_demo/testline 第1行: abc
+[2025-10-22 16:04:28.279][000000000.370] I/user.按行读取 路径:/flash_demo/testline 第2行: 123
+[2025-10-22 16:04:28.302][000000000.371] I/user.按行读取 路径:/flash_demo/testline 第3行: wendal
+[2025-10-22 16:04:28.331][000000000.377] I/user.os.rename 文件重命名成功 原路径:/flash_demo/test_a 新路径:/flash_demo/renamed_file.txt
+[2025-10-22 16:04:28.347][000000000.383] D/vfs fopen /flash_demo/test_a r not found
+[2025-10-22 16:04:28.361][000000000.383] I/user.验证结果 重命名验证成功 新文件存在 原文件不存在
+[2025-10-22 16:04:28.382][000000000.383] I/user.目录操作 ===== 开始目录列举 =====
+[2025-10-22 16:04:28.393][000000000.396] I/user.fs lsdir [{"name":"boottime","size":1,"type":0},{"name":"renamed_file.txt","size":6,"type":0},{"name":"testline","size":15,"type":0}]
+[2025-10-22 16:04:28.417][000000000.400] I/user.os.remove 文件删除成功 路径:/flash_demo/renamed_file.txt
+[2025-10-22 16:04:28.435][000000000.403] D/vfs fopen /flash_demo/renamed_file.txt r not found
+[2025-10-22 16:04:28.449][000000000.403] I/user.验证结果 renamed_file.txt文件删除验证成功
+[2025-10-22 16:04:28.469][000000000.407] I/user.os.remove testline文件删除成功 路径:/flash_demo/testline
+[2025-10-22 16:04:28.479][000000000.410] D/vfs fopen /flash_demo/testline r not found
+[2025-10-22 16:04:28.489][000000000.411] I/user.验证结果 testline文件删除验证成功
+[2025-10-22 16:04:28.507][000000000.417] I/user.os.remove 文件删除成功 路径:/flash_demo/boottime
+[2025-10-22 16:04:28.514][000000000.420] D/vfs fopen /flash_demo/boottime r not found
+[2025-10-22 16:04:28.519][000000000.421] I/user.验证结果 boottime文件删除验证成功
+[2025-10-22 16:04:28.532][000000000.427] I/user.io.rmdir 目录删除成功 路径:/flash_demo
+[2025-10-22 16:04:28.538][000000000.430] I/user.验证结果 目录删除验证成功
+[2025-10-22 16:04:28.547][000000000.435] I/user. io.fsstat 操作后文件系统信息: 总空间=42块 已用=4块 块大小=4096字节 类型=lfs
+[2025-10-22 16:04:28.554][000000000.435] I/user.文件系统操作 ===== 文件系统操作完成 =====
+
+
+
+
+
+(2)网络连接与HTTP下载
+[2025-10-22 15:34:04.507][000000007.471] I/user.HTTP下载 网络已就绪 1 3
+[2025-10-22 15:34:04.550][000000007.471] I/user.HTTP下载 开始下载任务
+[2025-10-22 15:34:04.579][000000007.478] dns_run 676:gitee.com state 0 id 1 ipv6 0 use dns server2, try 0
+[2025-10-22 15:34:04.604][000000007.508] D/mobile TIME_SYNC 0
+[2025-10-22 15:34:04.734][000000007.517] dns_run 693:dns all done ,now stop
+[2025-10-22 15:34:06.390][000000009.741] I/user.HTTP下载 下载完成 success 200 
+[2025-10-22 15:34:06.422][000000009.741] {"Age":"0","Cache-Control":"public, max-age=60","Via":"1.1 varnish","Transfer-Encoding":"chunked","Date":"Wed, 22 Oct 2025 07:34:04 GMT","Access-Control-Allow-Credentials":"true","Vary":"Accept-Encoding","X-Served-By":"cache-ffe9","X-Gitee-Server":"http-pilot 1.9.21","Connection":"keep-alive","Server":"ADAS\/1.0.214","Access-Control-Allow-Headers":"Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With,X-CustomHeader,Content-Range,Range,Set-Language","Content-Security-Policy":"default-src 'none'; style-src 'unsafe-inline'; sandbox","X-Request-Id":"fa536af1-51bd-400f-8d4b-7322355a9db2","Accept-Ranges":"bytes","Etag":"W\/\"2aaa2788d394a924e258d6f26ad78b8c948950f5\"","Content-Type":"text\/plain; charset=utf-8","Access-Control-Allow-Methods":"GET, POST, PUT, PATCH, DELETE, OPTIONS","X-Frame-Options":"DENY","X-Cache":"MISS","Set-Cookie":"BEC=1f1759df3ccd099821dcf0da6feb0357;Path=\/;Max-Age=126000"}
+[2025-10-22 15:34:06.477][000000009.742]  103070
+[2025-10-22 15:34:06.492][000000009.745] I/user.HTTP下载 文件大小验证 预期: 103070 实际: 103070
+[2025-10-22 15:34:06.525][000000009.751] I/user.HTTP下载 下载后文件系统信息: 总空间=42块 已用=32块 块大小=4096字节 类型=lfs
+
+
+```

+ 0 - 3
module/Air8000/demo/accessory_board/AirETH_1000/http/http_app.lua

@@ -9,9 +9,6 @@
 本文件没有对外接口,直接在main.lua中require "http_app"就可以加载运行;
 ]]
 
-
-
-
 -- 普通的http get请求功能演示
 -- 请求的body数据保存到内存变量中,在内存够用的情况下,最大支持32KB的数据存储到内存中
 -- timeout可以设置超时时间

+ 1 - 1
module/Air8000/demo/accessory_board/AirETH_1000/network_routing/4g_out_ethernet_in_wifi_in/readme.md

@@ -16,7 +16,7 @@
 
 [](https://docs.openLuat.com/cdn/image/AirETH_1000.jpg)
 
-![lan](E:\文档池\新建文件夹\luatos-doc-pool\docs\root\docs\air8000\luatos\app\image\lan.jpg)
+![](https://docs.openLuat.com/cdn/image/AirETH_1000.jpg)
 
 2、TYPE-C USB数据线一根 + 杜邦线若干;
 

+ 0 - 541
module/Air8000/demo/audio/exaudio.lua

@@ -1,541 +0,0 @@
---[[
-@module exaudio
-@summary exaudio扩展库
-@version 1.1
-@date    2025.09.01
-@author  梁健
-@usage
-]]
-local exaudio = {}
-
--- 常量定义
-local I2S_ID = 0
-local I2S_MODE = 0          -- 0:主机 1:从机
-local I2S_SAMPLE_RATE = 16000
-local I2S_CHANNEL_FORMAT = i2s.MONO_R  
-local I2S_COMM_FORMAT = i2s.MODE_LSB   -- 可选MODE_I2S, MODE_LSB, MODE_MSB
-local I2S_CHANNEL_BITS = 16
-local MULTIMEDIA_ID = 0
-local EX_MSG_PLAY_DONE = "playDone"
-local ES8311_ADDR = 0x18    -- 7位地址
-local CHIP_ID_REG = 0x00    -- 芯片ID寄存器地址
-
--- 模块常量
-exaudio.PLAY_DONE = 1         --   音频播放完毕的事件之一
-exaudio.RECORD_DONE = 1       --   音频录音完毕的事件之一  
-exaudio.AMR_NB = 0
-exaudio.AMR_WB = 1
-exaudio.PCM_8000 = 2
-exaudio.PCM_16000 = 3 
-exaudio.PCM_24000 = 4
-exaudio.PCM_32000 = 5
-
-
--- 默认配置参数
-local audio_setup_param = {
-    model = "es8311",         -- dac类型: "es8311","es8211"
-    i2c_id = 0,               -- i2c_id: 0,1
-    pa_ctrl = 0,              -- 音频放大器电源控制管脚
-    dac_ctrl = 0,             -- 音频编解码芯片电源控制管脚
-    dac_delay = 3,            -- DAC启动前冗余时间(100ms)
-    pa_delay = 100,           -- DAC启动后延迟打开PA的时间(ms)
-    dac_time_delay = 600,     -- 播放完毕后PA与DAC关闭间隔(ms)
-    bits_per_sample = 16,     -- 采样位数
-    pa_on_level = 1           -- PA打开电平 1:高 0:低        
-}
-
-local audio_play_param = {
-    type = 0,                 -- 0:文件 1:TTS 2:流式
-    content = nil,            -- 播放内容
-    cbfnc = nil,              -- 播放完毕回调
-    priority = 0,             -- 优先级(数值越大越高)
-    sampling_rate = 16000,    -- 采样率(仅流式)
-    sampling_depth = 16,      -- 采样位深(仅流式)
-    signed_or_unsigned = true -- PCM是否有符号(仅流式)
-}
-
-local audio_record_param = {
-    format = 0,               -- 录制格式,支持exaudio.AMR_NB,exaudio.AMR_WB,exaudio.PCM_8000,exaudio.PCM_16000,exaudio.PCM_24000,exaudio.PCM_32000
-    time = 5,                 -- 录制时间(秒)
-    path = nil,               -- 文件路径或流式回调
-    cbfnc = nil               -- 录音完毕回调
-}
-
--- 内部变量
-local pcm_buff0 = nil
-local pcm_buff1 = nil
-local voice_vol = 55
-local mic_vol = 80
-
--- 定义全局队列表
-local audio_play_queue = {
-    data = {},       -- 存储字符串的数组
-    sequenceIndex = 1  -- 用于跟踪插入顺序的索引
-}
-
--- 向队列中添加字符串(按调用顺序插入)
-local function audio_play_queue_push(str)
-    if type(str) == "string" then
-        -- 存储格式: {index = 顺序索引, value = 字符串值}
-        table.insert(audio_play_queue.data, {
-            index = audio_play_queue.sequenceIndex,
-            value = str
-        })
-        audio_play_queue.sequenceIndex = audio_play_queue.sequenceIndex + 1
-        return true
-    end
-    return false
-end
-
--- 从队列中取出最早插入的字符串(按顺序取出)
-local function audio_play_queue_pop()
-    if #audio_play_queue.data > 0 then
-        -- 取出并移除第一个元素
-        local item = table.remove(audio_play_queue.data, 1)
-        return item.value  -- 返回值
-    end
-    return nil
-end
--- 清空队列中所有数据
-function audio_queue_clear()
-    -- 清空数组
-    audio_play_queue.data = {}
-    -- 重置顺序索引
-    audio_play_queue.sequenceIndex = 1
-    return true
-end
-
--- 工具函数:参数检查
-local function check_param(param, expected_type, name)
-    if type(param) ~= expected_type then
-        log.error(string.format("参数错误: %s 应为 %s 类型", name, expected_type))
-        return false
-    end
-    return true 
-end
-
--- 音频回调处理
-local function audio_callback(id, event, point)
-    -- log.info("audio_callback", "event:", event, 
-    --         "MORE_DATA:", audio.MORE_DATA, 
-    --         "DONE:", audio.DONE,
-    --         "RECORD_DATA:", audio.RECORD_DATA,
-    --         "RECORD_DONE:", audio.RECORD_DONE)
-
-    if event == audio.MORE_DATA then
-        audio.write(MULTIMEDIA_ID,audio_play_queue_pop())
-    elseif event == audio.DONE then
-        if type(audio_play_param.cbfnc) == "function" then
-            audio_play_param.cbfnc(exaudio.PLAY_DONE)
-        end
-        audio_queue_clear()  -- 清空流式播放数据队列
-        sys.publish(EX_MSG_PLAY_DONE)
-        
-    elseif event == audio.RECORD_DATA then
-        if type(audio_record_param.path) == "function" then
-            local buff, len = point == 0 and pcm_buff0 or pcm_buff1,
-                             point == 0 and pcm_buff0:used() or pcm_buff1:used()
-            audio_record_param.path(buff, len)
-        end
-        
-    elseif event == audio.RECORD_DONE then
-        if type(audio_record_param.cbfnc) == "function" then
-            audio_record_param.cbfnc(exaudio.RECORD_DONE)
-        end
-    end
-end
-
--- 读取ES8311芯片ID
-local function read_es8311_id()
-
-
-    -- 发送读取请求
-    local send_ok = i2c.send(audio_setup_param.i2c_id, ES8311_ADDR, CHIP_ID_REG)
-    if not send_ok then
-        log.error("发送芯片ID读取请求失败")
-        return false
-    end
-
-    -- 读取数据
-    local data = i2c.recv(audio_setup_param.i2c_id, ES8311_ADDR, 1)
-    if data and #data == 1 then
-        return true
-    end
-
-    log.error("读取ES8311芯片ID失败")
-    return false
-end
-
--- 音频硬件初始化
-local function audio_setup()
-    -- I2C配置
-    if not i2c.setup(audio_setup_param.i2c_id, i2c.FAST) then
-        log.error("I2C初始化失败")
-        return false
-    end
-    -- 初始化I2S
-    local result, data = i2s.setup(
-        I2S_ID, 
-        I2S_MODE, 
-        I2S_SAMPLE_RATE, 
-        audio_setup_param.bits_per_sample, 
-        I2S_CHANNEL_FORMAT, 
-        I2S_COMM_FORMAT,
-        I2S_CHANNEL_BITS
-    )
-
-    if not result then
-        log.error("I2S设置失败")
-        return false
-    end
-    -- 配置音频通道
-    audio.config(
-        MULTIMEDIA_ID, 
-        audio_setup_param.pa_ctrl, 
-        audio_setup_param.pa_on_level, 
-        audio_setup_param.dac_delay, 
-        audio_setup_param.pa_delay, 
-        audio_setup_param.dac_ctrl, 
-        1,  -- power_on_level
-        audio_setup_param.dac_time_delay
-    )
-    -- 设置总线
-    audio.setBus(
-        MULTIMEDIA_ID, 
-        audio.BUS_I2S,
-        {
-            chip = audio_setup_param.model,
-            i2cid = audio_setup_param.i2c_id,
-            i2sid = I2S_ID,
-            voltage = audio.VOLTAGE_1800
-        }
-    )
-
-  
-    -- 设置音量
-    audio.vol(MULTIMEDIA_ID, voice_vol)
-    audio.micVol(MULTIMEDIA_ID, mic_vol)
-    audio.pm(MULTIMEDIA_ID, audio.RESUME)
-    
-    -- 检查芯片连接
-    if audio_setup_param.model == "es8311" and not read_es8311_id() then
-        log.error("ES8311通讯失败,请检查硬件")
-        return false
-    end
-
-    -- 注册回调
-    audio.on(MULTIMEDIA_ID, audio_callback)
-    return true
-end
-
--- 模块接口:初始化
-function exaudio.setup(audioConfigs)
-    -- 检查必要参数
-    if not  audio  then
-        log.error("不支持audio 库,请选择支持audio 的core")
-        return false
-    end
-    if not audioConfigs or type(audioConfigs) ~= "table" then
-        log.error("配置参数必须为table类型")
-        return false
-    end
-    -- 检查codec型号
-    if not audioConfigs.model or 
-       (audioConfigs.model ~= "es8311" and audioConfigs.model ~= "es8211") then
-        log.error("请指定正确的codec型号(es8311或es8211)")
-        return false
-    end
-    audio_setup_param.model = audioConfigs.model
-    -- 针对ES8311的特殊检查
-    if audioConfigs.model == "es8311" then
-        if not check_param(audioConfigs.i2c_id, "number", "i2c_id") then
-            return false
-        end
-        audio_setup_param.i2c_id = audioConfigs.i2c_id
-    end
-
-    -- 检查功率放大器控制管脚
-    if audioConfigs.pa_ctrl == nil then
-        log.warn("pa_ctrl(功率放大器控制管脚)是控制pop 音的重要管脚,建议硬件设计加上")
-    end
-    audio_setup_param.pa_ctrl = audioConfigs.pa_ctrl
-
-    -- 检查功率放大器控制管脚
-    if audioConfigs.dac_ctrl == nil then
-        log.warn("dac_ctrl(音频编解码控制管脚)是控制pop 音的重要管脚,建议硬件设计加上")
-    end
-    audio_setup_param.dac_ctrl = audioConfigs.dac_ctrl
-
-
-    -- 处理可选参数
-    local optional_params = {
-        {name = "dac_delay", type = "number"},
-        {name = "pa_delay", type = "number"},
-        {name = "dac_time_delay", type = "number"},
-        {name = "bits_per_sample", type = "number"},
-        {name = "pa_on_level", type = "number"}
-    }
-
-    for _, param in ipairs(optional_params) do
-        if audioConfigs[param.name] ~= nil then
-            if check_param(audioConfigs[param.name], param.type, param.name) then
-                audio_setup_param[param.name] = audioConfigs[param.name]
-            else
-                return false
-            end
-        end
-    end
-
-    -- 确保采样位数有默认值
-    audio_setup_param.bits_per_sample = audio_setup_param.bits_per_sample or 16
-    return audio_setup()
-end
-
--- 模块接口:开始播放
-function exaudio.play_start(playConfigs)
-    if not playConfigs or type(playConfigs) ~= "table" then
-        log.error("播放配置必须为table类型")
-        return false
-    end
-
-    -- 检查播放类型
-    if not check_param(playConfigs.type, "number", "type") then
-        log.error("type必须为数值(0:文件,1:TTS,2:流式)")
-        return false
-    end
-    audio_play_param.type = playConfigs.type
-
-    -- 处理优先级
-    if playConfigs.priority ~= nil then
-        if check_param(playConfigs.priority, "number", "priority") then
-            if playConfigs.priority > audio_play_param.priority then
-                log.error("是否完成播放",audio.isEnd(MULTIMEDIA_ID))
-                if not audio.isEnd(MULTIMEDIA_ID) then
-                    if audio.play(MULTIMEDIA_ID) ~= true then
-                        return false
-                    end
-                    sys.waitUntil(EX_MSG_PLAY_DONE)
-                end
-                audio_play_param.priority = playConfigs.priority
-            end
-        else
-            return false
-        end
-    end
-
-    -- 处理不同播放类型
-    local play_type = audio_play_param.type
-    if play_type == 0 then  -- 文件播放
-        if not playConfigs.content then
-            log.error("文件播放需要指定content(文件路径或路径表)")
-            return false
-        end
-
-        local content_type = type(playConfigs.content)
-        if content_type == "table" then
-            for _, path in ipairs(playConfigs.content) do
-                if type(path) ~= "string" then
-                    log.error("播放列表元素必须为字符串路径")
-                    return false
-                end
-            end
-        elseif content_type ~= "string" then
-            log.error("文件播放content必须为字符串或路径表")
-            return false
-        end
-
-        audio_play_param.content = playConfigs.content
-        if audio.play(MULTIMEDIA_ID, audio_play_param.content) ~= true then
-            return false
-        end
-
-    elseif play_type == 1 then  -- TTS播放
-        if not audio.tts then
-            log.error("本固件不支持TTS,请更换支持TTS 的固件")
-            return false
-        end
-        if not check_param(playConfigs.content, "string", "content") then
-            log.error("TTS播放content必须为字符串")
-            return false
-        end
-        audio_play_param.content = playConfigs.content
-        if audio.tts(MULTIMEDIA_ID, audio_play_param.content)  ~= true  then
-            return false
-        end
-
-    elseif play_type == 2 then  -- 流式播放
-        if not check_param(playConfigs.sampling_rate, "number", "sampling_rate") then
-            return false
-        end
-        if not check_param(playConfigs.sampling_depth, "number", "sampling_depth") then
-            return false
-        end
-
-        audio_play_param.content = playConfigs.content
-        audio_play_param.sampling_rate = playConfigs.sampling_rate
-        audio_play_param.sampling_depth = playConfigs.sampling_depth
-        
-        if playConfigs.signed_or_unsigned ~= nil then
-            audio_play_param.signed_or_unsigned = playConfigs.signed_or_unsigned
-        end
-
-        audio.start(
-            MULTIMEDIA_ID, 
-            audio.PCM, 
-            1, 
-            playConfigs.sampling_rate, 
-            playConfigs.sampling_depth, 
-            audio_play_param.signed_or_unsigned
-        )
-        -- 发送初始数据
-        if audio.write(MULTIMEDIA_ID, string.rep("\0", 512)) ~= true then
-            return false
-        end
-    end
-
-    -- 处理回调函数
-    if playConfigs.cbfnc ~= nil then
-        if check_param(playConfigs.cbfnc, "function", "cbfnc") then
-            audio_play_param.cbfnc = playConfigs.cbfnc
-        else
-            return false
-        end
-    else
-        audio_play_param.cbfnc = nil
-    end
-    return true
-end
-
--- 模块接口:流式播放数据写入
-function exaudio.play_stream_write(data)
-    audio_play_queue_push(data)
-    return true
-end
-
--- 模块接口:停止播放
-function exaudio.play_stop()
-    return audio.play(MULTIMEDIA_ID)
-end
-
--- 模块接口:检查播放是否结束
-function exaudio.is_end()
-    return audio.isEnd(MULTIMEDIA_ID)
-end
-
--- 模块接口:获取错误信息
-function exaudio.get_error()
-    return audio.getError(MULTIMEDIA_ID)
-end
-
--- 模块接口:开始录音
-function exaudio.record_start(recodConfigs)
-    if not recodConfigs or type(recodConfigs) ~= "table" then
-        log.error("录音配置必须为table类型")
-        return false
-    end
-    -- 检查录音格式
-    if recodConfigs.format == nil or type(recodConfigs.format) ~= "number" or recodConfigs.format > 5 then
-        log.error("请指定正确的录音格式")
-        return false
-    end
-    audio_record_param.format = recodConfigs.format
-
-    -- 处理录音时间
-    if recodConfigs.time ~= nil then
-        if check_param(recodConfigs.time, "number", "time") then
-            audio_record_param.time = recodConfigs.time
-        else
-            return false
-        end
-    else
-        audio_record_param.time = 0
-    end
-
-    -- 处理存储路径/回调
-    if not recodConfigs.path then
-        log.error("必须指定录音路径或流式回调函数")
-        return false
-    end
-    audio_record_param.path = recodConfigs.path
-
-    -- 转换录音格式
-    local recod_format, amr_quailty
-    if audio_record_param.format == exaudio.AMR_NB then
-        recod_format = audio.AMR_NB
-        amr_quailty = 7
-    elseif audio_record_param.format == exaudio.AMR_WB then
-        recod_format = audio.AMR_WB
-        amr_quailty = 8
-    elseif audio_record_param.format == exaudio.PCM_8000 then
-        recod_format = 8000
-    elseif audio_record_param.format == exaudio.PCM_16000 then
-        recod_format = 16000
-    elseif audio_record_param.format == exaudio.PCM_24000 then
-        recod_format = 24000
-    elseif audio_record_param.format == exaudio.PCM_32000 then
-        recod_format = 32000
-    end
-
-    -- 处理回调函数
-    if recodConfigs.cbfnc ~= nil then
-        if check_param(recodConfigs.cbfnc, "function", "cbfnc") then
-            audio_record_param.cbfnc = recodConfigs.cbfnc
-        else
-            return false
-        end
-    else
-        audio_record_param.cbfnc = nil
-    end
-    -- 开始录音
-    local path_type = type(audio_record_param.path)
-    if path_type == "string" then
-        return audio.record(
-            MULTIMEDIA_ID, 
-            recod_format, 
-            audio_record_param.time, 
-            amr_quailty, 
-            audio_record_param.path
-        )
-    elseif path_type == "function" then
-        -- 初始化缓冲区
-        if not pcm_buff0 or not pcm_buff1 then
-            pcm_buff0 = zbuff.create(16000)
-            pcm_buff1 = zbuff.create(16000)
-        end
-        return audio.record(
-            MULTIMEDIA_ID, 
-            recod_format, 
-            audio_record_param.time, 
-            amr_quailty, 
-            nil, 
-            3,
-            pcm_buff0,
-            pcm_buff1
-        )
-    end
-    log.error("录音路径必须为字符串或函数")
-    return false
-end
-
--- 模块接口:停止录音
-function exaudio.record_stop()
-    return audio.recordStop(MULTIMEDIA_ID)
-end
-
--- 模块接口:设置音量
-function exaudio.vol(play_volume)
-    if check_param(play_volume, "number", "音量值") then
-        return audio.vol(MULTIMEDIA_ID, play_volume)
-    end
-    return false
-end
-
--- 模块接口:设置麦克风音量
-function exaudio.mic_vol(record_volume)
-    if check_param(record_volume, "number", "麦克风音量值") then
-        return audio.micVol(MULTIMEDIA_ID, record_volume)  
-    end
-    return false
-end
-
-return exaudio

+ 282 - 0
module/Air8000/demo/fs_io/flash_fs_io.lua

@@ -0,0 +1,282 @@
+--[[
+@module  flash_fs_io
+@summary 内置Flash文件系统操作测试模块
+@version 1.0.0
+@date    2025.09.23
+@author  王棚嶙
+@usage
+本文件为内置Flash文件系统的操作测试流程:
+1. 获取文件系统信息( io.fsstat)
+2. 创建目录
+3. 创建并写入文件
+4. 检查文件是否存在
+5. 获取文件大小(io.fileSize)
+6. 读取文件内容
+7. 启动计数文件操作
+8. 文件追加测试
+9. 按行读取测试
+10. 文件重命名
+11. 列举目录内容
+12. 删除文件
+13. 删除目录
+本文件没有对外接口,直接在main.lua中require "flash_fs_io"就可以加载运行
+]]
+
+function flash_fs_io_task()
+    -- 使用内置Flash文件系统,根目录为"/",
+    local base_path = "/"
+    -- 创建一个目录
+    local demo_dir = "flash_demo"
+    -- 文件名
+    local dir_path = base_path .. demo_dir
+
+    -- ########## 开始进行内置Flash文件系统操作 ##########
+    log.info("文件系统操作", "===== 开始文件系统操作 =====")
+
+    -- 1. 获取文件系统信息 (使用 io.fsstat接口)
+    local success, total_blocks, used_blocks, block_size, fs_type  =  io.fsstat(base_path)
+    if success then
+        log.info(" io.fsstat成功:", 
+            "总空间=" .. total_blocks .. "块", 
+            "已用=" .. used_blocks .. "块", 
+            "块大小=" .. block_size.."字节",
+            "类型=" .. fs_type)
+    else
+        log.error(" io.fsstat", "获取文件系统信息失败")
+        return
+    end
+
+    -- 2. 创建目录
+    -- 如果目录不存在,则创建目录
+    if not io.dexist(dir_path) then
+        -- 创建目录
+        if io.mkdir(dir_path) then
+            log.info("io.mkdir", "目录创建成功", "路径:" .. dir_path)
+        else
+            log.error("io.mkdir", "目录创建失败", "路径:" .. dir_path)
+            return
+        end
+    else
+        log.warn("io.mkdir", "目录已存在,跳过创建", "路径:" .. dir_path)
+    end
+
+    -- 3. 创建并写入文件
+    local file_path = dir_path .. "/boottime"
+    local file = io.open(file_path, "wb")
+    if file then
+        file:write("这是内置Flash文件系统API文档示例的测试内容")
+        file:close()
+        log.info("文件创建", "文件写入成功", "路径:" .. file_path)
+    else
+        log.error("文件创建", "文件创建失败", "路径:" .. file_path)
+        return
+    end
+
+    -- 4. 检查文件是否存在
+    if io.exists(file_path) then
+        log.info("io.exists", "文件存在", "路径:" .. file_path)
+    else
+        log.error("io.exists", "文件不存在", "路径:" .. file_path)
+        return
+    end
+
+    -- 5. 获取文件大小 (使用io.fileSize接口)
+    local file_size = io.fileSize(file_path)
+    if file_size then
+        log.info("io.fileSize", "文件大小:" .. file_size .. "字节", "路径:" .. file_path)
+    else
+        log.error("io.fileSize", "获取文件大小失败", "路径:" .. file_path)
+        return
+    end
+
+    -- 6. 读取文件内容
+    file = io.open(file_path, "rb")
+    if file then
+        local content = file:read("*a")
+        log.info("文件读取", "路径:" .. file_path, "内容:" .. content)
+        file:close()
+    else
+        log.error("文件操作", "无法打开文件读取内容", "路径:" .. file_path)
+        return
+    end
+
+    -- 7. 启动计数文件操作
+    local count = 0
+    file = io.open(file_path, "rb")
+    if file then
+        local data = file:read("*a")
+        log.info("启动计数", "文件内容:", data, "十六进制:", data:toHex())
+        count = tonumber(data) or 0
+        file:close()
+    else
+        log.warn("启动计数", "文件不存在或无法打开")
+    end
+
+    log.info("启动计数", "当前值:", count)
+    count = count + 1
+    log.info("启动计数", "更新值:", count)
+
+    file = io.open(file_path, "wb")
+    if file then
+        file:write(tostring(count))
+        file:close()
+        log.info("文件写入", "路径:" .. file_path, "内容:", count)
+    else
+        log.error("文件写入", "无法打开文件", "路径:" .. file_path)
+        return
+    end
+
+    -- 8. 文件追加测试
+    local append_file = dir_path .. "/test_a"
+    os.remove(append_file) -- 清理旧文件
+
+    file = io.open(append_file, "wb")
+    if file then
+        file:write("ABC")
+        file:close()
+        log.info("文件创建", "路径:" .. append_file, "初始内容:ABC")
+    else
+        log.error("文件创建", "无法创建文件", "路径:" .. append_file)
+        return
+    end
+
+    file = io.open(append_file, "a+")
+    if file then
+        file:write("def")
+        file:close()
+        log.info("文件追加", "路径:" .. append_file, "追加内容:def")
+    else
+        log.error("文件追加", "无法打开文件进行追加", "路径:" .. append_file)
+        return
+    end
+
+    -- 验证追加结果
+    file = io.open(append_file, "r")
+    if file then
+        local data = file:read("*a")
+        log.info("文件验证", "路径:" .. append_file, "内容:" .. data, "结果:",
+            data == "ABCdef" and "成功" or "失败")
+        file:close()
+    else
+        log.error("文件验证", "无法打开文件进行验证", "路径:" .. append_file)
+        return
+    end
+
+    -- 9. 按行读取测试
+    local line_file = dir_path .. "/testline"
+    file = io.open(line_file, "w")
+    if file then
+        file:write("abc\n")
+        file:write("123\n")
+        file:write("wendal\n")
+        file:close()
+        log.info("文件创建", "路径:" .. line_file, "写入3行文本")
+    else
+        log.error("文件创建", "无法创建文件", "路径:" .. line_file)
+        return
+    end
+
+    file = io.open(line_file, "r")
+    if file then
+        log.info("按行读取", "路径:" .. line_file, "第1行:", file:read("*l"))
+        log.info("按行读取", "路径:" .. line_file, "第2行:", file:read("*l"))
+        log.info("按行读取", "路径:" .. line_file, "第3行:", file:read("*l"))
+        file:close()
+    else
+        log.error("按行读取", "无法打开文件", "路径:" .. line_file)
+        return
+    end
+
+    -- 10. 文件重命名
+    local old_path = append_file
+    local new_path = dir_path .. "/renamed_file.txt"
+    local success, err = os.rename(old_path, new_path)
+    if success then
+        log.info("os.rename", "文件重命名成功", "原路径:" .. old_path, "新路径:" .. new_path)
+
+        -- 验证重命名结果
+        if io.exists(new_path) and not io.exists(old_path) then
+            log.info("验证结果", "重命名验证成功", "新文件存在", "原文件不存在")
+        else
+            log.error("验证结果", "重命名验证失败")
+        end
+    else
+        log.error("os.rename", "重命名失败", "错误:" .. tostring(err), "原路径:" .. old_path)
+        return
+    end
+
+    -- 11. 列举目录内容
+    log.info("目录操作", "===== 开始目录列举 =====")
+
+    local ret, data = io.lsdir(dir_path, 50, 0)
+    if ret then
+        log.info("fs", "lsdir", json.encode(data))
+    else
+        log.info("fs", "lsdir", "fail", ret, data)
+        return
+    end
+
+    -- 12. 删除文件测试
+    if os.remove(new_path) then
+        log.info("os.remove", "文件删除成功", "路径:" .. new_path)
+        if not io.exists(new_path) then
+            log.info("验证结果", "renamed_file.txt文件删除验证成功")
+        else
+            log.error("验证结果", "renamed_file.txt文件删除验证失败")
+        end
+    else
+        log.error("os.remove", "renamed_file.txt文件删除失败", "路径:" .. new_path)
+        return
+    end
+
+    if os.remove(line_file) then
+        log.info("os.remove", "testline文件删除成功", "路径:" .. line_file)
+        if not io.exists(line_file) then
+            log.info("验证结果", "testline文件删除验证成功")
+        else
+            log.error("验证结果", "testline文件删除验证失败")
+        end
+    else
+        log.error("os.remove", "testline文件删除失败", "路径:" .. line_file)
+        return
+    end
+
+    if os.remove(file_path) then
+        log.info("os.remove", "文件删除成功", "路径:" .. file_path)
+        if not io.exists(file_path) then
+            log.info("验证结果", "boottime文件删除验证成功")
+        else
+            log.error("验证结果", "boottime文件删除验证失败")
+        end
+    else
+        log.error("os.remove", "boottime文件删除失败", "路径:" .. file_path)
+        return
+    end
+
+    -- 13. 删除目录
+    if io.rmdir(dir_path) then
+        log.info("io.rmdir", "目录删除成功", "路径:" .. dir_path)
+        if not io.dexist(dir_path) then
+            log.info("验证结果", "目录删除验证成功")
+        else
+            log.error("验证结果", "目录删除验证失败")
+        end
+    else
+        log.error("io.rmdir", "目录删除失败", "路径:" .. dir_path)
+        return
+    end
+
+    -- 再次获取文件系统信息,查看空间变化
+    local final_success, final_total_blocks, final_used_blocks, final_block_size, final_fs_type =  io.fsstat(base_path)
+    if final_success then
+        log.info(" io.fsstat", "操作后文件系统信息:", 
+                 "总空间=" .. final_total_blocks .. "块", 
+                 "已用=" .. final_used_blocks .. "块", 
+                 "块大小=" .. final_block_size.."字节",
+                 "类型=" .. final_fs_type)
+    end
+
+    log.info("文件系统操作", "===== 文件系统操作完成 =====")
+end
+
+sys.taskInit(flash_fs_io_task)

+ 74 - 0
module/Air8000/demo/fs_io/http_download_flash.lua

@@ -0,0 +1,74 @@
+--[[
+@module http_download_flash
+@summary HTTP下载文件到内置Flash模块
+@version 1.0.0
+@date    2025.09.23
+@author  王棚嶙
+@usage
+本文件演示的功能为通过HTTP下载文件到内置Flash中:
+1. 网络就绪检测
+2. 创建HTTP下载任务并等待完成
+3. 记录下载结果
+4. 获取并记录文件大小(使用io.fileSize)
+本文件没有对外接口,直接在main.lua中require "http_download_flash"即可
+]]
+
+local function http_download_flash_task()
+    -- 阶段1: 网络就绪检测
+    while not socket.adapter(socket.dft()) do
+        log.warn("HTTP下载", "等待网络连接", socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+
+    log.info("HTTP下载", "网络已就绪", socket.dft())
+
+    -- 阶段2: 执行下载任务
+    log.info("HTTP下载", "开始下载任务")
+
+    -- 创建下载目录
+    local download_dir = "/downloads"
+    if not io.dexist(download_dir) then
+        io.mkdir(download_dir)
+    end
+
+    -- 核心下载操作开始
+    local code, headers, body_size = http.request("GET",
+                                    "https://gitee.com/openLuat/LuatOS/raw/master/module/Air780EHM_Air780EHV_Air780EGH/demo/audio/sample-6s.mp3",
+                                    nil, nil, {dst = download_dir .. "/sample-6s.mp3"}).wait()
+
+    -- 阶段3: 记录下载结果
+    log.info("HTTP下载", "下载完成", 
+        code == 200 and "success" or "error", 
+        code, 
+        json.encode(headers or {}), 
+        body_size) 
+        
+    if code == 200 then
+        -- 获取实际文件大小 (使用io.fileSize接口)
+        local actual_size = io.fileSize(download_dir .. "/sample-6s.mp3")
+        if not actual_size then
+            -- 备用方案
+            actual_size = io.fileSize(download_dir .. "/sample-6s.mp3")
+        end
+        
+        log.info("HTTP下载", "文件大小验证", "预期:", body_size, "实际:", actual_size)
+        
+        if actual_size ~= body_size then
+            log.error("HTTP下载", "文件大小不一致", "预期:", body_size, "实际:", actual_size)
+        end
+        
+        -- 展示下载后的文件系统状态
+        local success, total_blocks, used_blocks, block_size, fs_type =  io.fsstat("/")
+        if success then
+            log.info("HTTP下载", "下载后文件系统信息:", 
+                     "总空间=" .. total_blocks .. "块", 
+                     "已用=" .. used_blocks .. "块", 
+                     "块大小=" .. block_size.."字节",
+                     "类型=" .. fs_type)
+        end
+    end
+
+end
+
+-- 创建下载任务
+sys.taskInit(http_download_flash_task)

+ 78 - 0
module/Air8000/demo/fs_io/main.lua

@@ -0,0 +1,78 @@
+--[[
+@module  main
+@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
+@version 001.000.000
+@date    2025.09.23
+@author  王棚嶙
+@usage
+本 Demo 演示了在Air8000内置Flash文件系统中的完整操作流程:
+1. 基础操作:看门狗守护机制
+2. 文件系统操作:
+   - 文件系统信息查询( io.fsstat)
+   - 文件大小获取(io.fileSize)
+   - 文件创建/读写/追加
+   - 目录创建/删除
+   - 文件重命名/删除
+   - 文件存在性检查
+3. 下载功能:
+   - 网络检测与HTTP文件下载到内置Flash
+]]
+
+--[[
+必须定义PROJECT和VERSION变量,Luatools工具会用到这两个变量,远程升级功能也会用到这两个变量
+PROJECT:项目名,ascii string类型
+        可以随便定义,只要不使用,就行
+VERSION:项目版本号,ascii string类型
+        如果使用合宙iot.openluat.com进行远程升级,必须按照"XXX.YYY.ZZZ"三段格式定义:
+            X、Y、Z各表示1位数字,三个X表示的数字可以相同,也可以不同,同理三个Y和三个Z表示的数字也是可以相同,可以不同
+            因为历史原因,YYY这三位数字必须存在,但是没有任何用处,可以一直写为000
+        如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
+]]
+
+PROJECT = "flash_fs_io_demo"
+VERSION = "001.000.000"
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+-- 添加硬狗防止程序卡死
+if wdt then
+    -- 初始化watchdog设置为9s
+    wdt.init(9000)
+    -- 3s喂一次狗 
+    sys.timerLoopStart(wdt.feed, 3000) 
+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)
+
+--[[在加载以下两个功能时,建议分别打开进行测试,因为文件操作和http下载功能是异步操作。放到一个项目中,如果加载的时间点是随机的,就会出现哪个任务先抢到CPU时间片,哪个就先执行,不符合正常的业务逻辑,用户在参考编程的时候也要注意。]]
+
+-- 加载内置Flash文件系统操作演示模块
+require "flash_fs_io"
+-- 加载HTTP下载存入内置Flash功能演示模块
+-- require "http_download_flash"
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+-- sys.run()之后后面不要加任何语句!!!!!
+sys.run()

+ 139 - 0
module/Air8000/demo/fs_io/readme.md

@@ -0,0 +1,139 @@
+## **功能模块介绍**
+
+本 Demo 演示了在Air8000内置Flash文件系统中的完整操作流程,覆盖了从文件系统读写到高级文件操作的完整功能链。项目分为两个核心模块:
+
+1、main.lua:主程序入口 <br> 
+2、flash_fs_io.lua:内置Flash文件系统的操作测试流程模块,实现文件系统管理、文件操作和目录管理功能。<br> 
+3、http_download_flash.lua:HTTP下载模块,演示HTTP下载文件到内置Flash中的功能
+
+## **演示功能概述**
+
+### 1、主程序入口模块(main.lua)
+
+- 初始化项目信息和版本号
+- 初始化看门狗,并定时喂狗
+- 启动一个循环定时器,每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况方便分析内存使用是否有异常
+- 加载flash_fs_io模块(通过require "flash_fs_io")
+- 加载http_download_flash模块(通过require "http_download_flash")
+- 最后运行sys.run()。
+
+### 2、内置Flash文件系统演示模块(flash_fs_io.lua)
+
+#### 文件操作
+- 获取文件系统信息( io.fsstat)
+- 创建目录:io.mkdir("/flash_demo")
+- 创建/写入文件: io.open("/flash_demo/boottime", "wb")
+- 检查文件存在: io.exists(file_path)
+- 获取文件大小:io.fileSize(file_path)
+- 读取文件内容: io.open(file_path, "rb"):read("*a")
+- 启动计数文件: 记录设备启动次数
+- 文件追加: io.open(append_file, "a+")
+- 按行读取: file:read("*l")
+- 文件关闭: file:close()
+- 文件重命名: os.rename(old_path, new_path)
+- 列举目录: io.lsdir(dir_path)
+- 删除文件: os.remove(file_path)
+- 删除目录: io.rmdir(dir_path)
+
+### 3、HTTP下载功能 (http_download_flash.lua)
+
+
+#### 网络就绪检测
+
+- 1秒循环等待IP就绪
+- 网络故障处理机制
+
+#### 安全下载
+
+- HTTP下载
+
+#### 结果处理
+
+- 下载状态码解析
+- 自动文件大小验证
+- 获取文件系统信息( io.fsstat)
+
+## **演示硬件环境**
+
+1、Air8000整机开发板一块
+
+2、sim卡一张
+
+3、TYPE-C USB数据线一根
+
+4、Air8000整机开发板和数据线的硬件接线方式为
+
+- Air8000整机开发板通过TYPE-C USB口供电;(USB旁边的开关拨到USB供电)
+- TYPE-C USB数据线直接插到Air8000整机开发板的TYPE-C USB座子,另外一端连接电脑USB口;
+
+## **演示软件环境**
+
+1、Luatools下载调试工具:https://docs.openluat.com/air780epm/common/Luatools/
+
+2、内核固件版本:https://docs.openluat.com/air8000/luatos/firmware/
+
+## **演示核心步骤**
+
+1、搭建好硬件环境
+
+2、通过Luatools将demo与固件烧录到开发板中
+
+3、烧录好后,板子开机将会在Luatools上看到如下打印
+
+```lua
+
+(1)文件操作演示
+[2025-10-22 15:23:25.096][000000000.595] I/user.文件系统操作 ===== 开始文件系统操作 =====
+[2025-10-22 15:23:25.104][000000000.601] I/user. io.fsstat成功: 总空间=192块 已用=20块 块大小=4096字节 类型=lfs
+[2025-10-22 15:23:25.118][000000000.644] I/user.io.mkdir 目录创建成功 路径:/flash_demo
+[2025-10-22 15:23:25.127][000000000.648] I/user.文件创建 文件写入成功 路径:/flash_demo/boottime
+[2025-10-22 15:23:25.145][000000000.651] I/user.io.exists 文件存在 路径:/flash_demo/boottime
+[2025-10-22 15:23:25.157][000000000.654] I/user.io.fileSize 文件大小:59字节 路径:/flash_demo/boottime
+[2025-10-22 15:23:25.165][000000000.657] I/user.文件读取 路径:/flash_demo/boottime 内容:这是内置Flash文件系统API文档示例的测试内容
+[2025-10-22 15:23:25.181][000000000.660] I/user.启动计数 文件内容: 这是内置Flash文件系统API文档示例的测试内容 十六进制: E8BF99E698AFE58685E7BDAE466C617368E69687E4BBB6E7B3BBE7BB9F415049E69687E6A1A3E7A4BAE4BE8BE79A84E6B58BE8AF95E58685E5AEB9 118
+[2025-10-22 15:23:25.189][000000000.660] I/user.启动计数 当前值: 0
+[2025-10-22 15:23:25.211][000000000.660] I/user.启动计数 更新值: 1
+[2025-10-22 15:23:25.217][000000000.663] I/user.文件写入 路径:/flash_demo/boottime 内容: 1
+[2025-10-22 15:23:25.224][000000000.669] I/user.文件创建 路径:/flash_demo/test_a 初始内容:ABC
+[2025-10-22 15:23:25.245][000000000.672] I/user.文件追加 路径:/flash_demo/test_a 追加内容:def
+[2025-10-22 15:23:25.251][000000000.675] I/user.文件验证 路径:/flash_demo/test_a 内容:ABCdef 结果: 成功
+[2025-10-22 15:23:25.267][000000000.678] I/user.文件创建 路径:/flash_demo/testline 写入3行文本
+[2025-10-22 15:23:25.277][000000000.681] I/user.按行读取 路径:/flash_demo/testline 第1行: abc
+[2025-10-22 15:23:25.285][000000000.682] I/user.按行读取 路径:/flash_demo/testline 第2行: 123
+[2025-10-22 15:23:25.295][000000000.682] I/user.按行读取 路径:/flash_demo/testline 第3行: wendal
+[2025-10-22 15:23:25.301][000000000.689] I/user.os.rename 文件重命名成功 原路径:/flash_demo/test_a 新路径:/flash_demo/renamed_file.txt
+[2025-10-22 15:23:25.312][000000000.694] D/vfs fopen /flash_demo/test_a r not found
+[2025-10-22 15:23:25.321][000000000.694] I/user.验证结果 重命名验证成功 新文件存在 原文件不存在
+[2025-10-22 15:23:25.340][000000000.695] I/user.目录操作 ===== 开始目录列举 =====
+[2025-10-22 15:23:25.348][000000000.706] I/user.fs lsdir [{"name":"boottime","size":1,"type":0},{"name":"renamed_file.txt","size":6,"type":0},{"name":"testline","size":15,"type":0}]
+[2025-10-22 15:23:25.359][000000000.710] I/user.os.remove 文件删除成功 路径:/flash_demo/renamed_file.txt
+[2025-10-22 15:23:25.366][000000000.713] D/vfs fopen /flash_demo/renamed_file.txt r not found
+[2025-10-22 15:23:25.373][000000000.713] I/user.验证结果 renamed_file.txt文件删除验证成功
+[2025-10-22 15:23:25.378][000000000.716] I/user.os.remove testline文件删除成功 路径:/flash_demo/testline
+[2025-10-22 15:23:25.390][000000000.719] D/vfs fopen /flash_demo/testline r not found
+[2025-10-22 15:23:25.395][000000000.719] I/user.验证结果 testline文件删除验证成功
+[2025-10-22 15:23:25.415][000000000.723] I/user.os.remove 文件删除成功 路径:/flash_demo/boottime
+[2025-10-22 15:23:25.423][000000000.726] D/vfs fopen /flash_demo/boottime r not found
+[2025-10-22 15:23:25.444][000000000.726] I/user.验证结果 boottime文件删除验证成功
+[2025-10-22 15:23:25.453][000000000.732] I/user.io.rmdir 目录删除成功 路径:/flash_demo
+[2025-10-22 15:23:25.464][000000000.734] I/user.验证结果 目录删除验证成功
+[2025-10-22 15:23:25.477][000000000.740] I/user. io.fsstat 操作后文件系统信息: 总空间=192块 已用=20块 块大小=4096字节 类型=lfs
+[2025-10-22 15:23:25.484][000000000.740] I/user.文件系统操作 ===== 文件系统操作完成 =====
+
+
+
+
+(2)网络连接与HTTP下载
+[2025-10-22 15:34:04.507][000000007.471] I/user.HTTP下载 网络已就绪 1 3
+[2025-10-22 15:34:04.550][000000007.471] I/user.HTTP下载 开始下载任务
+[2025-10-22 15:34:04.579][000000007.478] dns_run 676:gitee.com state 0 id 1 ipv6 0 use dns server2, try 0
+[2025-10-22 15:34:04.604][000000007.508] D/mobile TIME_SYNC 0
+[2025-10-22 15:34:04.734][000000007.517] dns_run 693:dns all done ,now stop
+[2025-10-22 15:34:06.390][000000009.741] I/user.HTTP下载 下载完成 success 200 
+[2025-10-22 15:34:06.422][000000009.741] {"Age":"0","Cache-Control":"public, max-age=60","Via":"1.1 varnish","Transfer-Encoding":"chunked","Date":"Wed, 22 Oct 2025 07:34:04 GMT","Access-Control-Allow-Credentials":"true","Vary":"Accept-Encoding","X-Served-By":"cache-ffe9","X-Gitee-Server":"http-pilot 1.9.21","Connection":"keep-alive","Server":"ADAS\/1.0.214","Access-Control-Allow-Headers":"Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With,X-CustomHeader,Content-Range,Range,Set-Language","Content-Security-Policy":"default-src 'none'; style-src 'unsafe-inline'; sandbox","X-Request-Id":"fa536af1-51bd-400f-8d4b-7322355a9db2","Accept-Ranges":"bytes","Etag":"W\/\"2aaa2788d394a924e258d6f26ad78b8c948950f5\"","Content-Type":"text\/plain; charset=utf-8","Access-Control-Allow-Methods":"GET, POST, PUT, PATCH, DELETE, OPTIONS","X-Frame-Options":"DENY","X-Cache":"MISS","Set-Cookie":"BEC=1f1759df3ccd099821dcf0da6feb0357;Path=\/;Max-Age=126000"}
+[2025-10-22 15:34:06.477][000000009.742]  103070
+[2025-10-22 15:34:06.492][000000009.745] I/user.HTTP下载 文件大小验证 预期: 103070 实际: 103070
+[2025-10-22 15:34:06.525][000000009.751] I/user.HTTP下载 下载后文件系统信息: 总空间=192块 已用=46块 块大小=4096字节 类型=lfs
+
+
+```

+ 14 - 7
module/Air8000/demo/pins/pins_test.lua

@@ -5,10 +5,10 @@
 @date    2025.10.15
 @author  马亚丹
 @usage
-本demo演示的功能为:使用Air8000核心板,演示动态修改管脚复用
+本demo演示的功能为:使用Air8000核心板,演示动态修改管脚复用功能
 核心逻辑:
 1.加载自定义的管脚配置文件
-2.修改管脚复用功能,这里演示SPI管脚复用为串口
+2.动态修改管脚复用功能,这里演示SPI管脚pin41脚即SPI1_CS复用为UART2_RX,pin40脚即SPI1_MOSI复用为UART2_TX
 3.演示复用的串口管脚的功能,通过串口工具收发数据。
 
 ]]
@@ -20,11 +20,16 @@
 --如果打开debug后需要关闭debug,在任何需要的地方添加这一行
 --log.info ("打开debug",pins.debug(false))
 
--- 也可以打开下面这行加载配置文件,如果烧录了pins_$model.json文件,就会自动加载,不需要pins.loadjson再设置加载
+--方式1 :打开下面这行加载配置文件,如果烧录了pins_$model.json文件,就会自动加载,不需要pins.loadjson再设置加载
 -- 其中 $model是模组型号, 例如 Air8000, 默认加载的是 luadb/pins_Air8000.json,其他格式的不会自动加载
 --log.info ("加载luatIO生成的配置文件",pins.loadjson("/luadb/pins_Air8000.json"))
 
---自定义配置文件要通过pins.loadjson加载
+
+
+--方式2 :自定义配置文件要通过pins.loadjson加载
+--如果烧录了pins_Air8000.json,在内核固件运行时,已经自动加载pins_Air8000.json,并且按照pins_Air8000.json的配置初始化所有io引脚功能,
+--此处再加载my.json文件,会覆盖pins_Air8000.json中配置的所有io引脚功能,按照my.json的配置再次初始化所有io引脚功能
+--烧录多个.json文件时以最后一个文件的配置初始化所有io引脚功能
 log.info ("加载自定义的配置文件",pins.loadjson("/luadb/my.json"))
 
 --=======配置管脚复用=========--
@@ -46,8 +51,7 @@ uart.setup(
     1--停止位
 )
 
--- 收取数据会触发回调, 这里的 "receive" 是固定值不要修改。
-uart.on(uartid, "receive", function(id, len)
+local function ur_rec(id, len)
     local s = ""
     repeat
         s = uart.read(id, 128)
@@ -59,7 +63,10 @@ uart.on(uartid, "receive", function(id, len)
             log.info("uart", "receive(hex)", id, #s, s:toHex())  
         end
     until s == ""
-end)
+end
+-- 收取数据会触发回调, 这里的 "receive" 是固定值不要修改。
+uart.on(uartid, "receive", ur_rec)
+
 
 --向串口发送数据
 local function uart_test()    

+ 122 - 0
module/Air8000/demo/pins/readme.md

@@ -0,0 +1,122 @@
+## 功能模块介绍:
+
+1、main.lua:主程序入口;
+
+2、pins_test.lua:   功能演示核心脚本,动态修改管脚复用功能,演示复用后的管脚的功能等,在main.lua中加载运行;
+
+3、my.json:自定义管脚配置文件,手动编写容易出错,建议使用合宙LuatIO可视化工具 [LuatIO初始化配置工具 - common@air780epm - 合宙模组资料中心](https://docs.openluat.com/air780epm/common/luatio/)自动生成;
+
+4、pins_Air8000.json:管脚配置文件,使用合宙LuatIO可视化工具自动生成。
+
+## 演示功能概述:
+
+1.加载管脚配置文件,初始化所有io引脚功能;
+
+2.动态修改管脚复用功能,这里演示SPI管脚pin41脚即SPI1_CS复用为UART2_RX,pin40脚即SPI1_MOSI复用为UART2_TX;
+
+3.演示复用的串口管脚的功能,通过串口工具收发数据。
+
+
+
+## 演示硬件环境
+
+![netdrv_multi](https://docs.openluat.com/air8000/product/image/8000核心板.jpg)
+
+
+
+1、Air8000核心板一块+可上网的sim卡一张+4g天线一根+wifi天线一根+网线一根:
+
+* sim卡插入核心板的sim卡槽
+
+* 天线装到开发板上
+
+* 网线一端插入核心板网口,另外一端连接可以上外网的路由器网口
+
+2、TYPE-C USB数据线一根 ,Air8000核心板和数据线的硬件接线方式为:
+
+* Air8000核心板通过TYPE-C USB口供电;(外部供电/USB供电 拨动开关 拨到 USB供电一端)
+
+* TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口。 
+
+3、USB转TTL串口线一根,串口线usb口连接电脑USB口,Air8000核心板和串口线,按以下方式接线:
+
+| Air8000核心板 | 串口线     |
+| ---------- | ------- |
+| SPI1_CS    | uart_tx |
+| SPI1_MOSI  | uart_rx |
+| GND        | GND     |
+
+
+
+## 演示软件环境
+
+1、 Luatools下载调试工具
+
+2、 固件版本:LuatOS-SoC_V2016_Air8000_1,固件地址,如有最新固件请用最新 [https://docs.openluat.com/air8000/luatos/firmware/](https://docs.openluat.com/air8000/luatos/firmware/)
+
+3、 脚本文件:
+    main.lua
+
+
+
+   pins_test.lua
+
+
+
+   my.json
+
+
+
+   pins_Air8000.json
+
+4、 pc 系统 win11(win10 及以上)
+
+5、sscom串口工具
+
+
+
+## 演示核心步骤
+
+1、搭建好硬件环境
+
+2、demo脚本文件pins_test.lua中,设置了方式1和方式2两种加载管脚配置文件的方式,按照自己的需求选择其一,脚本中默认是方式2:加载my.json自定义管脚配置文件。
+
+3、Luatools烧录内核固件和修改后的demo脚本代码
+
+4、烧录成功后,代码会自动运行,查看打印日志,如果正常运行,会打印加载配置文件,配置管脚,以及配置完成后串口交互等信息,如下log显示:其中 I/user.uart receive日志是串口工具向模组发数据,模组收到数据触发打印。
+
+```
+[2025-10-24 15:31:09.491][000000000.385] I/user.main Air8000_pins 001.000.000
+[2025-10-24 15:31:09.496][000000000.407] E/pins _STB不是可配置的外设功能
+[2025-10-24 15:31:09.500][000000000.407] W/pins pins 35 CAN_STB setup failed
+[2025-10-24 15:31:09.503][000000000.410] I/user.加载自定义的配置文件 true 0
+[2025-10-24 15:31:09.508][000000000.411] I/user.配置pin41脚即SPI1_CS为UART2_RX true
+[2025-10-24 15:31:09.514][000000000.411] I/user.配置pin40脚即SPI1_MOSI为UART2_TX true
+[2025-10-24 15:31:09.518][000000000.411] Uart_ChangeBR 1338:uart2, 115200 115203 26000000 3611
+[2025-10-24 15:31:11.167][000000002.412] I/user.这是第0次向串口发数据
+[2025-10-24 15:31:11.736][000000002.955] D/mobile cid1, state0
+[2025-10-24 15:31:11.740][000000002.956] D/mobile bearer act 0, result 0
+[2025-10-24 15:31:11.746][000000002.956] D/mobile NETIF_LINK_ON -> IP_READY
+[2025-10-24 15:31:11.750][000000002.974] D/mobile TIME_SYNC 0
+[2025-10-24 15:31:15.178][000000006.413] I/user.这是第1次向串口发数据
+[2025-10-24 15:31:19.172][000000010.414] I/user.这是第2次向串口发数据
+[2025-10-24 15:31:23.166][000000014.415] I/user.这是第3次向串口发数据
+[2025-10-24 15:31:24.212][000000015.459] I/user.uart receive 2 11 123456789
+
+[2025-10-24 15:31:24.221][000000015.460] I/user.uart receive(hex) 2 11 3132333435363738390D0A 22
+[2025-10-24 15:31:25.945][000000017.183] I/user.uart receive 2 11 123456789
+
+[2025-10-24 15:31:25.954][000000017.183] I/user.uart receive(hex) 2 11 3132333435363738390D0A 22
+[2025-10-24 15:31:27.171][000000018.415] I/user.这是第4次向串口发数据
+[2025-10-24 15:31:31.173][000000022.416] I/user.这是第5次向串口发数据
+[2025-10-24 15:31:35.179][000000026.417] I/user.这是第6次向串口发数据
+[2025-10-24 15:31:39.176][000000030.418] I/user.这是第7次向串口发数据
+[2025-10-24 15:31:43.171][000000034.419] I/user.这是第8次向串口发数据
+[2025-10-24 15:31:47.176][000000038.420] I/user.这是第9次向串口发数据
+
+
+```
+
+
+
+

+ 27 - 12
module/Air8000/project/sms_forward/cc_forward.lua → module/Air8000/project/sms_call_forward/cc_forward.lua

@@ -11,7 +11,7 @@
 3、cc_state(state),电话状态判断并获取来电号码,来电或者挂断等不同情况做不同处理。
 4、cc_forward(),来电号码信息转发到指定机器人
 
-直接使用Air8000核心板板硬件测试即可;
+直接使用Air8000开发板硬件测试即可;
 
 本文件没有对外接口,直接在main.lua中require "cc_forward"就可以加载运行;
 ]]
@@ -27,8 +27,8 @@ local webhook_dingding =
 "https://oapi.dingtalk.com/robot/send?access_token=03f4753ec6aa6f0524fb85907c94b17f3fa0fed3107d4e8f4eee1d4a97855f4d"
 local secret_dingding = "SECac5b455d6b567f64073a456e91feec6ad26c0f8f7dcca85dd2ce6c23ea466c52"
 
---local webhook_weixin = "https://work.weixin.qq.com/wework_admin/common/openBotProfile/24caa08b3a985454055047454d883fc98f"
-local webhook_weixin = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=36648707-4eba-4d21-9d3a-2244e1e9bc3b"
+
+local webhook_weixin = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=71017f82-e027-4c5d-a618-eb4ee01750e9"
 -- 飞书关于机器人的文档 https://open.feishu.cn/document/ukTMukTMukTM/ucTM5YjL3ETO24yNxkjN?lang=zh-CN
 
 
@@ -87,7 +87,7 @@ local function feishu_post_cc(num)
     local code, headers, body = http.request("POST", url, rheaders, rbody).wait()
     -- 正常会返回 200, {"errcode":0,"errmsg":"ok"}
     -- 其他错误, 一般是密钥错了, 仔细检查吧
-    log.info("feishu", code, body, socket.adapter(socket.LWIP_STA), socket.adapter())
+    log.info("feishu", code, body)
 end
 
 --2.功能函数:来电转发到钉钉
@@ -112,7 +112,7 @@ local function dingding_post_cc(num)
     local code, headers, body = http.request("POST", url, rheaders, (json.encode(data))).wait()
     -- 正常会返回 200, {"errcode":0,"errmsg":"ok"}
     -- 其他错误, 一般是密钥错了, 仔细检查吧
-    log.info("dingding", code, body, socket.adapter(socket.LWIP_STA), socket.adapter())
+    log.info("dingding", code, body)
 end
 
 --3.功能函数:来电转发到企业微信
@@ -134,7 +134,7 @@ local function weixin_post_cc(num)
     local code, headers, body = http.request("POST", url, rheaders, (json.encode(data))).wait()
     -- 正常会返回 200, {"errcode":0,"errmsg":"ok"}
     -- 其他错误, 一般是密钥错了, 仔细检查吧
-    log.info("weixin", code, body, socket.adapter(socket.LWIP_STA), socket.adapter())
+    log.info("weixin", code, body)
 end
 
 
@@ -142,7 +142,21 @@ end
 
 --4.初始化cc
 local function cc_setup()
-    sys.waitUntil("IP_READY")
+    --查看网卡适配器的联网状态是否IP_READY,true表示已经准备好可以联网了,false暂时不可以联网
+    while not socket.adapter(socket.dft()) do
+        log.warn("cc", "wait IP_READY", socket.dft())
+        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+        -- 或者等待1秒超时退出阻塞等待状态;
+        -- 注意:此处的1000毫秒超时不要修改的更长;
+        -- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+        -- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+
+    -- 检测到了IP_READY消息,设置默认网络适配器编号
+    log.info("cc", "recv IP_READY", socket.dft())
+
     --初始化电话功能
     local cc_int = cc.init(multimedia_id)
     if cc_int then
@@ -157,19 +171,19 @@ local function cc_forward()
     log.info(" 来电号码转发到飞书")
     feishu_post_cc(phone_num)
 
-    sys.wait(1000)
+   
     log.info("来电号码转发到钉钉")
     dingding_post_cc(phone_num)
 
-    sys.wait(1000)
+    
     log.info("来电号码转发到微信")
     weixin_post_cc(phone_num)
 end
 
 --6.来电判断
 local function cc_state(state)
-    if state == "READY" then
-        sys.publish("CC_READY")
+    if state == "READY" then        
+        log.info("通话准备完成,可以拨打电话或者呼入电话了")
         --有电话呼入
     elseif state == "INCOMINGCALL" then
         if cnt == 0 then
@@ -186,8 +200,9 @@ local function cc_state(state)
             --自动接听
             --cc.accept(0)
 
-            --响3声以后自动自动挂断
+            --响4声以后自动自动挂断
             cc.hangUp()
+            cnt = 0
         end
     end
 end

+ 8 - 3
module/Air8000/project/sms_forward/main.lua → module/Air8000/project/sms_call_forward/main.lua

@@ -6,7 +6,12 @@
 @author  马亚丹
 @usage
 1. 详细逻辑请看cc_forward文件和sms_forward文件
-2. netdrv_multiple:设置WIFI STA网卡和4G网卡优先级
+2. netdrv_device:配置连接外网使用的网卡,目前支持以下四种选择(四选一)
+   (1) netdrv_4g:4G网卡
+   (2) netdrv_wifi:WIFI STA网卡
+   (3) netdrv_eth_spi:通过SPI外挂CH390H芯片的以太网卡
+   (4) netdrv_multiple:支持以上三种网卡,可以配置三种网卡的优先级
+   (5) netdrv_pc:pc模拟器上的网卡
 
 ]]
 
@@ -68,8 +73,8 @@ require "cc_forward"
 --加载sms_forward功能模块
 require "sms_forward"
 
---加载网络驱动设备功能模块netdrv_multiple
-require "netdrv_multiple"
+-- 加载网络驱动设备功能模块,在该文件中修改自己使用的联网方式
+require"netdrv_device"
 
 
 

+ 44 - 0
module/Air8000/project/sms_call_forward/netdrv/netdrv_4g.lua

@@ -0,0 +1,44 @@
+--[[
+@module  netdrv_4g
+@summary “4G网卡”驱动模块 
+@version 1.0
+@date    2025.07.01
+@author  朱天华
+@usage
+本文件为4G网卡驱动模块,核心业务逻辑为:
+1、监听"IP_READY"和"IP_LOSE",在日志中进行打印;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_4g"就可以加载运行;
+]]
+
+local function ip_ready_func(ip, adapter)    
+    if adapter == socket.LWIP_GP then
+        -- 在位置1和2设置自定义的DNS服务器ip地址:
+        -- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
+        -- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
+        -- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
+        -- 如果使用专网卡,不要使用这两行代码;
+        -- 如果使用国外的网络,不要使用这两行代码;
+        socket.setDNS(adapter, 1, "223.5.5.5")
+        socket.setDNS(adapter, 2, "114.114.114.114")
+        
+        log.info("netdrv_4g.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_GP))
+    end
+end
+
+local function ip_lose_func(adapter)    
+    if adapter == socket.LWIP_GP then
+        log.warn("netdrv_4g.ip_lose_func", "IP_LOSE")
+    end
+end
+
+
+
+--此处订阅"IP_READY"和"IP_LOSE"两种消息
+--在消息的处理函数中,仅仅打印了一些信息,便于实时观察4G网络的连接状态
+--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+-- 在Air8000上,内核固件运行起来之后,默认网卡就是socket.LWIP_GP
+

+ 70 - 0
module/Air8000/project/sms_call_forward/netdrv/netdrv_eth_spi.lua

@@ -0,0 +1,70 @@
+--[[
+@module  netdrv_eth_spi
+@summary “通过SPI外挂CH390H芯片的以太网卡”驱动模块 
+@version 1.0
+@date    2025.07.24
+@author  朱天华
+@usage
+本文件为“通过SPI外挂CH390H芯片的以太网卡”驱动模块 ,核心业务逻辑为:
+1、打开CH390H芯片供电开关;
+2、初始化spi1,初始化以太网卡,并且在以太网卡上开启DHCP(动态主机配置协议);
+3、以太网卡的连接状态发生变化时,在日志中进行打印;
+
+直接使用Air8000开发板硬件测试即可;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_eth_spi"就可以加载运行;
+]]
+
+local exnetif = require "exnetif"
+
+local function ip_ready_func(ip, adapter)    
+    if adapter == socket.LWIP_ETH then
+        -- 在位置1和2设置自定义的DNS服务器ip地址:
+        -- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
+        -- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
+        -- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
+        -- 如果使用专网卡,不要使用这两行代码;
+        -- 如果使用国外的网络,不要使用这两行代码;
+        socket.setDNS(adapter, 1, "223.5.5.5")
+        socket.setDNS(adapter, 2, "114.114.114.114")
+
+        log.info("netdrv_eth_spi.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_ETH))
+    end
+end
+
+local function ip_lose_func(adapter)    
+    if adapter == socket.LWIP_ETH then
+        log.warn("netdrv_eth_spi.ip_lose_func", "IP_LOSE")
+    end
+end
+
+
+-- 以太网联网成功(成功连接路由器,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
+-- 各个功能模块可以订阅"IP_READY"消息实时处理以太网联网成功的事件
+-- 也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
+
+-- 以太网断网后,内核固件会产生一个"IP_LOSE"消息
+-- 各个功能模块可以订阅"IP_LOSE"消息实时处理以太网断网的事件
+-- 也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
+
+--此处订阅"IP_READY"和"IP_LOSE"两种消息
+--在消息的处理函数中,仅仅打印了一些信息,便于实时观察“通过SPI外挂CH390H芯片的以太网卡”的连接状态
+--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+
+-- 配置SPI外接以太网芯片CH390H的单网卡,exnetif.set_priority_order使用的网卡编号为socket.LWIP_ETH
+-- 本demo使用Air8000开发板测试,开发板上的硬件配置为:
+-- GPIO140为CH390H以太网芯片的供电使能控制引脚
+-- 使用spi1,片选引脚使用GPIO12
+-- 如果使用的硬件不是Air8000开发板,根据自己的硬件配置修改以下参数
+exnetif.set_priority_order({
+    {
+        ETHERNET = {
+            pwrpin = 140, 
+            tp = netdrv.CH390,
+            opts = {spi = 1, cs = 12}
+        }
+    }
+})

+ 39 - 8
module/Air8000/project/sms_forward/netdrv_multiple.lua → module/Air8000/project/sms_call_forward/netdrv/netdrv_multiple.lua

@@ -1,18 +1,19 @@
 --[[
 @module  netdrv_multiple
-@summary 多网卡(4G网卡、WIFI STA网卡、通过SPI外挂CH390H芯片的以太网卡)驱动模块
+@summary 多网卡(4G网卡、WIFI STA网卡、通过SPI外挂CH390H芯片的以太网卡)驱动模块 
 @version 1.0
 @date    2025.07.24
 @author  朱天华
 @usage
-本文件为多网卡驱动模块,核心业务逻辑为:
+本文件为多网卡驱动模块 ,核心业务逻辑为:
 1、调用exnetif.set_priority_order配置多网卡的控制参数以及优先级;
 
-直接使用Air8000核心板板硬件测试即可;
+直接使用Air8000开发板硬件测试即可;
 
 本文件没有对外接口,直接在其他功能模块中require "netdrv_multiple"就可以加载运行;
 ]]
 
+
 local exnetif = require "exnetif"
 
 -- 网卡状态变化通知回调函数
@@ -23,10 +24,19 @@ local exnetif = require "exnetif"
 -- 当所有网卡断网时:
 --     net_type:为nil
 --     adapter:number类型,为-1
-local function netdrv_multiple_notify_cbfunc(net_type, adapter)
-    if type(net_type) == "string" then
+local function netdrv_multiple_notify_cbfunc(net_type,adapter)
+    -- 在位置1和2设置自定义的DNS服务器ip地址:
+    -- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
+    -- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
+    -- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
+    -- 如果使用专网卡,不要使用这两行代码;
+    -- 如果使用国外的网络,不要使用这两行代码;
+    socket.setDNS(adapter, 1, "223.5.5.5")
+    socket.setDNS(adapter, 2, "114.114.114.114")
+    
+    if type(net_type)=="string" then
         log.info("netdrv_multiple_notify_cbfunc", "use new adapter", net_type, adapter)
-    elseif type(net_type) == "nil" then
+    elseif type(net_type)=="nil" then
         log.warn("netdrv_multiple_notify_cbfunc", "no available adapter", net_type, adapter)
     else
         log.warn("netdrv_multiple_notify_cbfunc", "unknown status", net_type, adapter)
@@ -37,13 +47,34 @@ local function netdrv_multiple_task_func()
     --设置网卡优先级
     exnetif.set_priority_order(
         {
+            -- “通过SPI外挂CH390H芯片”的以太网卡,使用Air8000开发板验证
+            {
+                ETHERNET = {
+                    -- 供电使能GPIO
+                    pwrpin = 140,
+                    -- 设置的多个“已经IP READY,但是还没有ping通”网卡,循环执行ping动作的间隔(单位毫秒,可选)
+                    -- 如果没有传入此参数,exnetif会使用默认值10秒
+                    ping_time = 3000,
+
+                    -- 连通性检测ip(选填参数);
+                    -- 如果没有传入ip地址,exnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
+                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
+                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",     
+                    
+                    -- 网卡芯片型号(选填参数),仅spi方式外挂以太网时需要填写。
+                    tp = netdrv.CH390, 
+                    opts = {spi=1, cs=12}
+                }
+            },
+
             -- WIFI STA网卡
             {
                 WIFI = {
                     -- 要连接的WIFI路由器名称
+                     -- 要连接的WIFI路由器名称
                     ssid = "Mayadan",
                     -- 要连接的WIFI路由器密码
-                    password = "12345678",
+                    password = "12345678", 
 
                     -- 连通性检测ip(选填参数);
                     -- 如果没有传入ip地址,exnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
@@ -57,7 +88,7 @@ local function netdrv_multiple_task_func()
                 LWIP_GP = true
             }
         }
-    )
+    )    
 end
 
 -- 设置网卡状态变化通知回调函数netdrv_multiple_notify_cbfunc

+ 45 - 0
module/Air8000/project/sms_call_forward/netdrv/netdrv_pc.lua

@@ -0,0 +1,45 @@
+--[[
+@module  netdrv_pc
+@summary “pc模拟器网卡”驱动模块 
+@version 1.0
+@date    2025.07.01
+@author  朱天华
+@usage
+本文件为pc模拟器网卡驱动模块,核心业务逻辑为:
+1、监听"IP_READY"和"IP_LOSE",在日志中进行打印;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_pc"就可以加载运行;
+]]
+
+local function ip_ready_func(ip, adapter)    
+    if adapter == socket.ETH0 then
+        -- 在位置1和2设置自定义的DNS服务器ip地址:
+        -- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
+        -- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
+        -- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
+        -- 如果使用专网卡,不要使用这两行代码;
+        -- 如果使用国外的网络,不要使用这两行代码;
+        socket.setDNS(adapter, 1, "223.5.5.5")
+        socket.setDNS(adapter, 2, "114.114.114.114")
+
+        log.info("netdrv_pc.ip_ready_func", "IP_READY", socket.localIP(socket.ETH0))
+    end
+end
+
+local function ip_lose_func(adapter)    
+    if adapter == socket.ETH0 then
+        log.warn("netdrv_pc.ip_lose_func", "IP_LOSE")
+    end
+end
+
+
+
+--此处订阅"IP_READY"和"IP_LOSE"两种消息
+--在消息的处理函数中,仅仅打印了一些信息,便于实时观察pc模拟器网络的连接状态
+--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+-- 设置默认网卡为socket.ETH0
+-- pc模拟器上的默认网卡仍然需要使用接口(socket.ETH0)来设置,因为exnetif扩展库当前还不支持模拟器
+socket.dft(socket.ETH0)

+ 68 - 0
module/Air8000/project/sms_call_forward/netdrv/netdrv_wifi.lua

@@ -0,0 +1,68 @@
+--[[
+@module  netdrv_wifi
+@summary “WIFI STA网卡”驱动模块 
+@version 1.0
+@date    2025.07.01
+@author  朱天华
+@usage
+本文件为WIFI STA网卡驱动模块,核心业务逻辑为:
+1、初始化WIFI网络;
+2、连接WIFI路由器;
+3、和WIFI路由器之间的连接状态发生变化时,在日志中进行打印;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_wifi"就可以加载运行;
+]]
+
+local exnetif = require "exnetif"
+
+local function ip_ready_func(ip, adapter)
+    if adapter == socket.LWIP_STA then
+        -- 在位置1和2设置自定义的DNS服务器ip地址:
+        -- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
+        -- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
+        -- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
+        -- 如果使用专网卡,不要使用这两行代码;
+        -- 如果使用国外的网络,不要使用这两行代码;
+        socket.setDNS(adapter, 1, "223.5.5.5")
+        socket.setDNS(adapter, 2, "114.114.114.114")
+
+        log.info("netdrv_wifi.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_STA))
+    end
+end
+
+local function ip_lose_func(adapter)    
+    if adapter == socket.LWIP_STA then
+        log.warn("netdrv_wifi.ip_lose_func", "IP_LOSE")
+    end
+end
+
+
+--WIFI联网成功(做为STATION成功连接AP,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
+--各个功能模块可以订阅"IP_READY"消息实时处理WIFI联网成功的事件
+--也可以在任何时刻调用socket.adapter(socket.LWIP_STA)来获取WIFI网络是否连接成功
+
+--WIFI断网后,内核固件会产生一个"IP_LOSE"消息
+--各个功能模块可以订阅"IP_LOSE"消息实时处理WIFI断网的事件
+--也可以在任何时刻调用socket.adapter(socket.LWIP_STA)来获取WIFI网络是否连接成功
+
+--此处订阅"IP_READY"和"IP_LOSE"两种消息
+--在消息的处理函数中,仅仅打印了一些信息,便于实时观察WIFI的连接状态
+--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+
+-- 配置WiFi设备模式的单网卡,exnetif.set_priority_order使用的网卡编号为socket.LWIP_STA
+-- ssid为要连接的WiFi路由器名称;
+-- password为要连接的WiFi路由器密码;
+-- 注意:仅支持2.4G的WiFi,不支持5G的WiFi;
+-- 实际测试时,根据自己要连接的WiFi热点信息修改以下参数
+exnetif.set_priority_order({
+    {
+        WIFI = {
+            ssid = "茶室-降功耗,找合宙!", 
+            password = "Air123456"
+        }
+    }
+})
+

+ 37 - 0
module/Air8000/project/sms_call_forward/netdrv_device.lua

@@ -0,0 +1,37 @@
+--[[
+@module  netdrv_device
+@summary 网络驱动设备功能模块 
+@version 1.0
+@date    2025.07.24
+@author  朱天华
+@usage
+本文件为网络驱动设备功能模块,核心业务逻辑为:根据项目需求,选择并且配置合适的网卡(网络适配器)
+1、netdrv_4g:socket.LWIP_GP,4G网卡;
+2、netdrv_wifi:socket.LWIP_STA,WIFI STA网卡;
+3、netdrv_ethernet_spi:socket.LWIP_ETH,通过SPI外挂CH390H芯片的以太网卡;
+4、netdrv_multiple:可以配置多种网卡的优先级,按照优先级配置,使用其中一种网卡连接外网;
+5、netdrv_pc:pc模拟器上的网卡
+
+根据自己的项目需求,只需要require以上四种中的一种即可;
+
+
+本文件没有对外接口,直接在main.lua中require "netdrv_device"就可以加载运行;
+]]
+
+
+-- 根据自己的项目需求,只需要require以下四种中的一种即可;
+
+-- 加载“4G网卡”驱动模块
+-- require "netdrv_4g"
+
+-- 加载“WIFI STA网卡”驱动模块
+-- require "netdrv_wifi"
+
+-- 加载“通过SPI外挂CH390H芯片的以太网卡”驱动模块
+-- require "netdrv_eth_spi"
+
+-- 加载“可以配置优先级的多种网卡”驱动模块
+require "netdrv_multiple"
+
+-- 加载“pc模拟器网卡”驱动模块
+-- require "netdrv_pc"

+ 317 - 0
module/Air8000/project/sms_call_forward/readme.md

@@ -0,0 +1,317 @@
+# 8000-SMS
+
+## 功能模块介绍:
+
+1、main.lua:主程序入口文件,加载以下 3 个文件运行。
+
+2、netdrv_multiple.lua:网卡驱动配置文件,可以配置以太网卡,wifi 网卡,单 4g 网卡三种网卡的使用优先级
+
+3、sms_forward.lua: 短信转发功能模块文件
+
+4、cc_forward.lua:来电转发功能模块文件
+
+5、netdrv_pc:pc模拟器上的网卡
+
+## 演示功能概述:
+
+**sms_forward.lua:**
+
+1、配置飞书,钉钉,企业微信机器人的 webhook 和 secret(加签)。
+
+2、send_sms(),发送短信的功能函数,等待 CC_IND 消息后,手机卡可以进行收发短信。
+
+3、receive_sms(),接收短信处理的功能函数,收到短信后获取来信号码和短信内容,通过回调函数 sms_handler(num, txt)转发到指定的机器人。
+
+**cc_forward.lua:**
+
+1、配置飞书,钉钉,企业微信机器人的 webhook 和 secret(加签)。
+
+2、cc_setup(),初始化电话功能,做好接收来电的准备。
+
+3、cc_state(state),电话状态判断并获取来电号码,来电或者挂断等不同情况做不同处理。
+
+4、cc_forward(),来电号码信息转发到指定机器人
+
+## 演示硬件环境:
+
+![8000w](https://docs.openluat.com/accessory/AirSPINORFLASH_1000/image/8000w.jpg)
+
+1、Air8000W 开发板一块 + 正常手机卡一张(三大运营商的都可以)+4g 天线一根 +wifi 天线一根
+
+- sim 卡插入开发板的 sim 卡槽
+- 天线装到开发板上
+
+2、TYPE-C USB 数据线一根 ,Air8000W 开发板和数据线的硬件接线方式为:
+
+- Air8000 开发板通过 TYPE-C USB 口供电;(外部供电/USB 供电 拨动开关 拨到 USB 供电一端)
+- TYPE-C USB 数据线直接插到开发板的 TYPE-C USB 座子,另外一端连接电脑 USB 口;
+
+## 演示软件环境:
+
+1、 Luatools 下载调试工具
+
+2、 固件版本:LuatOS-SoC_V2014_Air8000_1,固件地址,如有最新固件请用最新 [https://docs.openluat.com/air8000/luatos/firmware/](https://docs.openluat.com/air8000/luatos/firmware/)
+
+3、 脚本文件:
+
+main.lua
+
+netdrv_device.lua:
+
+sms_forward.lua:
+
+cc_forward.lua:
+
+netdrv文件夹
+
+4、 pc 系统 win11(win10 及以上)
+
+5、飞书,钉钉,企业微信等自己需要的机器人。
+
+## 演示核心步骤:
+
+1、搭建好硬件环境
+
+2、demo 脚本代码 netdrv_multiple.lua 中,ssid = "茶室-降功耗,找合宙!", password = "Air123456", 修改为自己测试时 wifi 热点的名称和密码;注意:仅支持 2.4G 的 wifi,不支持 5G 的 wifi
+
+3、[https://open.feishu.cn/document/ukTMukTMukTM/ucTM5YjL3ETO24yNxkjN?lang=zh-CN](https://open.feishu.cn/document/ukTMukTMukTM/ucTM5YjL3ETO24yNxkjN?lang=zh-CN) 参考此教程,获取飞书,钉钉,企业微信的 webhook 和 secret(加签),在 cc_forward.lua 和 sms_forward.lua 脚本中找到 local webhook_feishu,secret_feishu,webhook_dingding,secret_dingding,webhook_weixin 的参数定义,修改为自己的参数。
+
+4、Luatools 烧录内核固件和修改后的 demo 脚本代码
+
+5、此处演示设置了优先使用 以太网,其次wifi 网络,最低优先级使用 4G 网络.
+
+烧录成功后,代码会自动运行,log 日志打印以太网信息, wif 网络信息、CC_READY 等消息,log 日志打印如下:
+
+```yaml
+[2025-10-24 18:54:07.534][000000000.657] I/user.main cc_sms_forward 001.000.000
+[2025-10-24 18:54:07.536][000000000.668] W/user.cc wait IP_READY 1 3
+[2025-10-24 18:54:07.539][000000000.770] I/user.notify_status function
+[2025-10-24 18:54:07.542][000000000.771] I/user.初始化以太网
+[2025-10-24 18:54:07.546][000000000.771] I/user.config.opts.spi 1 ,config.type 1
+[2025-10-24 18:54:07.551][000000000.772] SPI_HWInit 552:spi1 speed 25600000,25600000,12
+[2025-10-24 18:54:07.553][000000000.772] I/user.main open spi 0
+[2025-10-24 18:54:07.557][000000000.773] D/ch390h 注册CH390H设备(4) SPI id 1 cs 12 irq 255
+[2025-10-24 18:54:07.561][000000000.773] D/ch390h adapter 4 netif init ok
+[2025-10-24 18:54:07.564][000000000.774] D/netdrv.ch390x task started
+[2025-10-24 18:54:07.568][000000000.774] D/ch390h 注册完成 adapter 4 spi 1 cs 12 irq 255
+[2025-10-24 18:54:07.570][000000000.774] I/user.以太网初始化完成
+[2025-10-24 18:54:07.574][000000000.775] I/user.netdrv 订阅socket连接状态变化事件 Ethernet
+[2025-10-24 18:54:07.578][000000000.775] I/user.WiFi名称: Mayadan
+[2025-10-24 18:54:07.583][000000000.776] I/user.密码     : 12345678
+[2025-10-24 18:54:07.587][000000000.776] I/user.ping_ip  : nil
+[2025-10-24 18:54:07.590][000000000.776] I/user.WiFi STA初始化完成
+[2025-10-24 18:54:07.594][000000000.776] I/user.netdrv 订阅socket连接状态变化事件 WiFi
+[2025-10-24 18:54:07.604][000000000.777] change from 1 to 4
+[2025-10-24 18:54:07.610][000000000.827] D/netdrv.ch390x 初始化MAC 3CAB724406AF
+[2025-10-24 18:54:07.615][000000001.669] W/user.cc wait IP_READY 4 4
+[2025-10-24 18:54:07.620][000000001.770] I/user.4G网卡开始PING
+[2025-10-24 18:54:07.625][000000001.770] I/user.dns_request 4G true
+[2025-10-24 18:54:07.736][000000002.670] W/user.cc wait IP_READY 4 4
+[2025-10-24 18:54:07.763][000000002.675] I/netdrv.ch390x link is up 1 12 100M
+[2025-10-24 18:54:07.772][000000002.676] D/netdrv 网卡(4)设置为UP
+[2025-10-24 18:54:07.799][000000002.733] D/ulwip adapter 4 dhcp start netif c149084
+[2025-10-24 18:54:07.806][000000002.734] D/DHCP dhcp state 6 tnow 2734 p1 0 p2 0
+[2025-10-24 18:54:07.811][000000002.734] D/DHCP dhcp discover 3CAB724406AF
+[2025-10-24 18:54:07.819][000000002.734] I/ulwip adapter 4 dhcp payload len 308
+[2025-10-24 18:54:08.267][000000003.208] D/airlink wifi sta上线了
+[2025-10-24 18:54:08.275][000000003.209] D/netdrv 网卡(2)设置为UP
+[2025-10-24 18:54:08.314][000000003.259] D/ulwip adapter 2 dhcp start netif c10d04c
+[2025-10-24 18:54:08.321][000000003.259] D/DHCP dhcp state 6 tnow 3259 p1 0 p2 0
+[2025-10-24 18:54:08.325][000000003.259] D/DHCP dhcp discover C8C2C68CA00E
+[2025-10-24 18:54:08.329][000000003.259] I/ulwip adapter 2 dhcp payload len 308
+[2025-10-24 18:54:08.394][000000003.325] D/ulwip 收到DHCP数据包(len=548)
+[2025-10-24 18:54:08.400][000000003.325] D/DHCP dhcp state 7 tnow 3325 p1 0 p2 0
+[2025-10-24 18:54:08.403][000000003.325] D/DHCP find ip 6e00a8c0 192.168.0.110
+[2025-10-24 18:54:08.406][000000003.326] D/DHCP result 2
+[2025-10-24 18:54:08.412][000000003.326] D/DHCP select offer, wait ack
+[2025-10-24 18:54:08.415][000000003.326] I/ulwip adapter 4 dhcp payload len 338
+[2025-10-24 18:54:08.419][000000003.332] D/ulwip 收到DHCP数据包(len=548)
+[2025-10-24 18:54:08.421][000000003.332] D/DHCP dhcp state 9 tnow 3332 p1 0 p2 0
+[2025-10-24 18:54:08.426][000000003.332] D/DHCP find ip 6e00a8c0 192.168.0.110
+[2025-10-24 18:54:08.429][000000003.333] D/DHCP result 5
+[2025-10-24 18:54:08.432][000000003.333] D/DHCP DHCP get ip ready
+[2025-10-24 18:54:08.435][000000003.333] D/ulwip adapter 4 ip 192.168.0.110
+[2025-10-24 18:54:08.437][000000003.333] D/ulwip adapter 4 mask 255.255.255.0
+[2025-10-24 18:54:08.442][000000003.333] D/ulwip adapter 4 gateway 192.168.0.1
+[2025-10-24 18:54:08.445][000000003.333] D/ulwip adapter 4 lease_time 7200s
+[2025-10-24 18:54:08.449][000000003.333] D/ulwip adapter 4 DNS1:114.114.114.114
+[2025-10-24 18:54:08.451][000000003.333] D/ulwip adapter 4 DNS2:192.168.0.1
+[2025-10-24 18:54:08.455][000000003.334] D/net network ready 4, setup dns server
+[2025-10-24 18:54:08.461][000000003.334] D/netdrv IP_READY 4 192.168.0.110
+[2025-10-24 18:54:08.466][000000003.336] I/user.ip_ready_handle 192.168.0.110 Ethernet state 3 gw 192.168.0.1
+[2025-10-24 18:54:08.469][000000003.336] I/user.eth_ping_ip nil wifi_ping_ip nil
+[2025-10-24 18:54:08.472][000000003.336] I/user.dnsproxy 开始监听
+[2025-10-24 18:54:08.476][000000003.337] I/user.cc recv IP_READY 4 4
+[2025-10-24 18:54:09.326][000000004.258] D/DHCP dhcp state 7 tnow 4258 p1 0 p2 0
+[2025-10-24 18:54:09.331][000000004.259] D/DHCP long time no offer, resend
+[2025-10-24 18:54:09.336][000000004.259] I/ulwip adapter 2 dhcp payload len 308
+[2025-10-24 18:54:09.838][000000004.771] I/user.4G网卡httpdns域名解析失败
+[2025-10-24 18:54:09.849][000000004.772] I/user.httpdns baidu.com nil
+[2025-10-24 18:54:10.323][000000005.258] D/DHCP dhcp state 7 tnow 5258 p1 0 p2 0
+[2025-10-24 18:54:11.330][000000006.258] D/DHCP dhcp state 7 tnow 6258 p1 0 p2 0
+[2025-10-24 18:54:11.339][000000006.259] D/DHCP long time no offer, resend
+[2025-10-24 18:54:11.346][000000006.259] I/ulwip adapter 2 dhcp payload len 308
+[2025-10-24 18:54:11.376][000000006.314] D/ulwip 收到DHCP数据包(len=303)
+[2025-10-24 18:54:11.384][000000006.314] D/DHCP dhcp state 7 tnow 6314 p1 0 p2 0
+[2025-10-24 18:54:11.389][000000006.314] D/DHCP find ip 8e2ba8c0 192.168.43.142
+[2025-10-24 18:54:11.396][000000006.314] D/DHCP result 2
+[2025-10-24 18:54:11.401][000000006.314] D/DHCP select offer, wait ack
+[2025-10-24 18:54:11.405][000000006.314] I/ulwip adapter 2 dhcp payload len 338
+[2025-10-24 18:54:11.486][000000006.413] D/ulwip 收到DHCP数据包(len=333)
+[2025-10-24 18:54:11.492][000000006.414] D/DHCP dhcp state 9 tnow 6414 p1 0 p2 0
+[2025-10-24 18:54:11.497][000000006.414] D/DHCP find ip 8e2ba8c0 192.168.43.142
+[2025-10-24 18:54:11.503][000000006.414] D/DHCP result 5
+[2025-10-24 18:54:11.507][000000006.414] D/DHCP DHCP get ip ready
+[2025-10-24 18:54:11.510][000000006.414] D/ulwip adapter 2 ip 192.168.43.142
+[2025-10-24 18:54:11.513][000000006.414] D/ulwip adapter 2 mask 255.255.255.0
+[2025-10-24 18:54:11.519][000000006.414] D/ulwip adapter 2 gateway 192.168.43.1
+[2025-10-24 18:54:11.521][000000006.415] D/ulwip adapter 2 lease_time 3600s
+[2025-10-24 18:54:11.524][000000006.415] D/ulwip adapter 2 DNS1:192.168.43.1
+[2025-10-24 18:54:11.526][000000006.415] D/net network ready 2, setup dns server
+[2025-10-24 18:54:11.529][000000006.416] D/netdrv IP_READY 2 192.168.43.142
+[2025-10-24 18:54:11.534][000000006.418] I/user.ip_ready_handle 192.168.43.142 WiFi state 3 gw 192.168.43.1
+[2025-10-24 18:54:11.536][000000006.418] I/user.eth_ping_ip nil wifi_ping_ip nil
+[2025-10-24 18:54:11.540][000000006.418] I/user.dnsproxy 开始监听
+[2025-10-24 18:54:12.557][000000007.450] D/mobile cid1, state0
+[2025-10-24 18:54:12.562][000000007.451] D/mobile bearer act 0, result 0
+[2025-10-24 18:54:12.564][000000007.451] D/mobile NETIF_LINK_ON -> IP_READY
+[2025-10-24 18:54:12.567][000000007.453] I/user.ip_ready_handle 10.172.199.213 4G state 1 gw 0.0.0.0
+[2025-10-24 18:54:12.570][000000007.453] I/user.eth_ping_ip nil wifi_ping_ip nil
+[2025-10-24 18:54:12.574][000000007.453] I/user.dnsproxy 开始监听
+[2025-10-24 18:54:12.581][000000007.480] D/mobile TIME_SYNC 0
+[2025-10-24 18:54:12.681][000000007.624] soc_cms_proc 2219:cenc report 1,51,1,15
+[2025-10-24 18:54:12.778][000000007.718] D/mobile NETIF_LINK_ON -> IP_READY
+[2025-10-24 18:54:12.783][000000007.719] I/user.ip_ready_handle 10.172.199.213 4G state 1 gw 0.0.0.0
+[2025-10-24 18:54:12.787][000000007.719] I/user.eth_ping_ip nil wifi_ping_ip nil
+[2025-10-24 18:54:12.791][000000007.720] I/user.dnsproxy 开始监听
+[2025-10-24 18:54:13.831][000000008.772] I/user.Ethernet网卡开始PING
+[2025-10-24 18:54:13.839][000000008.773] I/user.dns_request Ethernet true
+[2025-10-24 18:54:13.844][000000008.774] D/net adapter 4 connect 223.5.5.5:80 TCP
+[2025-10-24 18:54:13.908][000000008.849] I/user.Ethernet网卡httpdns域名解析成功
+[2025-10-24 18:54:13.916][000000008.849] I/user.httpdns baidu.com 39.156.70.37
+[2025-10-24 18:54:13.921][000000008.850] I/user.设置网卡 Ethernet
+[2025-10-24 18:54:13.929][000000008.850] D/net 设置DNS服务器 id 4 index 0 ip 223.5.5.5
+[2025-10-24 18:54:13.932][000000008.850] D/net 设置DNS服务器 id 4 index 1 ip 114.114.114.114
+[2025-10-24 18:54:13.937][000000008.851] I/user.netdrv_multiple_notify_cbfunc use new adapter Ethernet 4
+[2025-10-24 18:54:16.674][000000011.612] D/mobile ims reg state 0
+[2025-10-24 18:54:16.681][000000011.613] D/mobile LUAT_MOBILE_EVENT_CC status 0
+[2025-10-24 18:54:16.686][000000011.613] D/mobile LUAT_MOBILE_CC_READY
+
+```
+
+此处短信演示使用了电信卡,发送"102"给"10001"查询余额,会收到电信回复的短信,并转发到飞书、钉钉和微信,log 打印如下:
+
+```bash
+[2025-10-24 18:54:16.756][000000011.691] I/user.现在可以收发短信
+[2025-10-24 18:54:16.760][000000011.691] I/user.mobile.number(id) =  nil
+[2025-10-24 18:54:16.764][000000011.691] I/user.mobile.iccid(id) =  89860325743780541565
+[2025-10-24 18:54:16.768][000000011.692] I/user.mobile.simid(id) =  0
+[2025-10-24 18:54:16.772][000000011.692] I/user.mobile.imsi(index) =  460115726670673
+[2025-10-24 18:54:16.777][000000011.692] D/sms pdu len 18
+[2025-10-24 18:54:16.907][000000011.852] I/user.WiFi网卡开始PING
+[2025-10-24 18:54:16.915][000000011.853] I/user.dns_request WiFi true
+[2025-10-24 18:54:16.920][000000011.854] D/net adapter 2 connect 223.5.5.5:80 TCP
+[2025-10-24 18:54:17.792][000000012.726] luat_sms_proc 1239:[DIO 1239]: CMI_SMS_SEND_MSG_CNF is in
+[2025-10-24 18:54:17.799][000000012.726] I/sms long sms callback seqNum = 1
+[2025-10-24 18:54:18.476][000000013.409] I/user.WiFi网卡httpdns域名解析成功
+[2025-10-24 18:54:18.486][000000013.410] I/user.httpdns baidu.com 220.181.7.203
+[2025-10-24 18:54:18.946][000000013.885] luat_sms_proc 1236:[DIO 1236]: CMI_SMS_NEW_MSG_IND is in
+[2025-10-24 18:54:18.951][000000013.886] D/sms dcs 2 | 0 | 0 | 0
+[2025-10-24 18:54:18.958][000000013.887] I/sms long-sms, wait more frags 1/2
+[2025-10-24 18:54:21.469][000000016.410] I/user.4G网卡开始PING
+[2025-10-24 18:54:21.480][000000016.410] I/user.dns_request 4G true
+[2025-10-24 18:54:21.640][000000016.578] I/user.4G网卡httpdns域名解析成功
+[2025-10-24 18:54:21.647][000000016.578] I/user.httpdns baidu.com 220.181.7.203
+[2025-10-24 18:54:22.309][000000017.246] luat_sms_proc 1236:[DIO 1236]: CMI_SMS_NEW_MSG_IND is in
+[2025-10-24 18:54:22.317][000000017.247] D/sms dcs 2 | 0 | 0 | 0
+[2025-10-24 18:54:22.320][000000017.247] I/sms long-sms is ok
+[2025-10-24 18:54:22.323][000000017.249] I/user.num是 10001
+[2025-10-24 18:54:22.327][000000017.249] I/user.收到来自10001的短信:截止到2025年10月24日18时,本机可用余额:50.63元,帐户余额:50.63,欠费:0.0元 查询结果仅供参考,实际费用以出账单为准。更多服务请微信关注【河南电信】公众号,或下载欢go客户端( http://a.189.cn/JJTh4u )。
+[2025-10-24 18:54:22.335][000000017.250] I/user.转发到飞书
+[2025-10-24 18:54:22.342][000000017.252] I/user.timestamp 1761303263
+[2025-10-24 18:54:22.345][000000017.252] I/user.sign awgZFT0rgM/LneXnL095BaG//GNHfLq+5/ISWt2Uoow=
+[2025-10-24 18:54:22.347][000000017.253] I/user.url https://open.feishu.cn/open-apis/bot/v2/hook/bb089165-4b73-4f80-9ed0-da0c908b44e5
+[2025-10-24 18:54:22.351][000000017.254] I/user.feishu {"content":{"text":"我的id是nil,Fri Oct 24 18:54:23 2025,Air8000,    10001发来短信,内容是:截止到2025年10月24日18时,本机可用余额:50.63元,帐户余额:50.63,欠费:0.0元 查询结果仅供参考,实际费用以出账单为准。更多服务请微信关注【河南电信】公众号,或下载欢go客户端( http:\/\/a.189.cn\/JJTh4u )。"},"sign":"awgZFT0rgM\/LneXnL095BaG\/\/GNHfLq+5\/ISWt2Uoow=","msg_type":"text","timestamp":"1761303263"}
+[2025-10-24 18:54:22.356][000000017.260] dns_run 676:open.feishu.cn state 0 id 2 ipv6 0 use dns server0, try 0
+[2025-10-24 18:54:22.359][000000017.260] D/net adatper 4 dns server 223.5.5.5
+[2025-10-24 18:54:22.362][000000017.260] D/net dns udp sendto 223.5.5.5:53 from 192.168.0.110
+[2025-10-24 18:54:22.364][000000017.293] dns_run 693:dns all done ,now stop
+[2025-10-24 18:54:22.372][000000017.294] D/net adapter 4 connect 1.194.220.72:443 TCP
+[2025-10-24 18:54:23.135][000000018.070] I/user.feishu 200 {"StatusCode":0,"StatusMessage":"success","code":0,"data":{},"msg":"success"}
+[2025-10-24 18:54:23.145][000000018.071] I/user.转发到钉钉
+[2025-10-24 18:54:23.153][000000018.073] I/user.timestamp 1761303264000
+[2025-10-24 18:54:23.157][000000018.073] I/user.sign oXkYEXnC6p9QLr4hR8Hw8Qykz%2F2vdF7L8BjXAOVbdKY%3D
+[2025-10-24 18:54:23.160][000000018.073] I/user.url https://oapi.dingtalk.com/robot/send?access_token=03f4753ec6aa6f0524fb85907c94b17f3fa0fed3107d4e8f4eee1d4a97855f4d&timestamp=1761303264000&sign=oXkYEXnC6p9QLr4hR8Hw8Qykz%2F2vdF7L8BjXAOVbdKY%3D
+[2025-10-24 18:54:23.163][000000018.074] I/user.dingding {"text":{"content":"我的id是nil,Fri Oct 24 18:54:24 2025,Air8000,    10001发来短信,内容是:截止到2025年10月24日18时,本机可用余额:50.63元,帐户余额:50.63,欠费:0.0元 查询结果仅供参考,实际费用以出账单为准。更多服务请微信关注【河南电信】公众号,或下载欢go客户端( http:\/\/a.189.cn\/JJTh4u )。"},"msgtype":"text"}
+[2025-10-24 18:54:23.170][000000018.076] dns_run 676:oapi.dingtalk.com state 0 id 3 ipv6 0 use dns server0, try 0
+[2025-10-24 18:54:23.172][000000018.076] D/net adatper 4 dns server 223.5.5.5
+[2025-10-24 18:54:23.175][000000018.076] D/net dns udp sendto 223.5.5.5:53 from 192.168.0.110
+[2025-10-24 18:54:23.179][000000018.105] dns_run 693:dns all done ,now stop
+[2025-10-24 18:54:23.185][000000018.106] D/net adapter 4 connect 106.11.43.136:443 TCP
+[2025-10-24 18:54:23.725][000000018.658] I/user.dingding 200 {"errcode":0,"errmsg":"ok"}
+[2025-10-24 18:54:23.742][000000018.659] I/user.转发到微信
+[2025-10-24 18:54:23.746][000000018.660] I/user.timestamp 1761303264000
+[2025-10-24 18:54:23.752][000000018.660] I/user.url https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=71017f82-e027-4c5d-a618-eb4ee01750e9&timestamp=1761303264000
+[2025-10-24 18:54:23.756][000000018.661] I/user.weixin {"text":{"content":"我的id是nil,Fri Oct 24 18:54:24 2025,Air8000,    10001发来短信,内容是:截止到2025年10月24日18时,本机可用余额:50.63元,帐户余额:50.63,欠费:0.0元 查询结果仅供参考,实际费用以出账单为准。更多服务请微信关注【河南电信】公众号,或下载欢go客户端( http:\/\/a.189.cn\/JJTh4u )。"},"msgtype":"text"}
+[2025-10-24 18:54:23.763][000000018.663] dns_run 676:qyapi.weixin.qq.com state 0 id 4 ipv6 0 use dns server0, try 0
+[2025-10-24 18:54:23.767][000000018.663] D/net adatper 4 dns server 223.5.5.5
+[2025-10-24 18:54:23.771][000000018.663] D/net dns udp sendto 223.5.5.5:53 from 192.168.0.110
+[2025-10-24 18:54:23.776][000000018.692] dns_run 693:dns all done ,now stop
+[2025-10-24 18:54:23.778][000000018.693] D/net adapter 4 connect 101.91.40.24:443 TCP
+[2025-10-24 18:54:24.509][000000019.450] I/user.weixin 200 {"errcode":0,"errmsg":"ok"}
+
+```
+
+此处来电转发演示使用了电信卡,用另外手机拨打模组上的电话号码,响铃 4声后自动挂断,log 打印如下:
+
+```lua
+[2025-10-24 18:55:03.753][000000058.690] D/mobile LUAT_MOBILE_EVENT_CC status 12
+[2025-10-24 18:55:03.759][000000058.691] D/mobile LUAT_MOBILE_EVENT_CC status 1
+[2025-10-24 18:55:03.765][000000058.692] I/user.获取最后一次通话的号码
+[2025-10-24 18:55:03.767][000000058.692] I/user.来电号码是: 18317857567
+[2025-10-24 18:55:03.774][000000058.693] I/user. 来电号码转发到飞书
+[2025-10-24 18:55:03.777][000000058.694] I/user.timestamp 1761303304
+[2025-10-24 18:55:03.780][000000058.695] I/user.sign ilEYzlFdmUkoV2j8A6E3s+rDt0F132O7Mr3wwNxm76M=
+[2025-10-24 18:55:03.783][000000058.696] I/user.url https://open.feishu.cn/open-apis/bot/v2/hook/bb089165-4b73-4f80-9ed0-da0c908b44e5
+[2025-10-24 18:55:03.790][000000058.697] I/user.feishu {"content":{"text":"我的id是nil,Fri Oct 24 18:55:04 2025,Air8000,    18317857567来电"},"sign":"ilEYzlFdmUkoV2j8A6E3s+rDt0F132O7Mr3wwNxm76M=","msg_type":"text","timestamp":"1761303304"}
+[2025-10-24 18:55:03.795][000000058.699] dns_run 676:open.feishu.cn state 0 id 5 ipv6 0 use dns server0, try 0
+[2025-10-24 18:55:03.797][000000058.699] D/net adatper 4 dns server 223.5.5.5
+[2025-10-24 18:55:03.801][000000058.700] D/net dns udp sendto 223.5.5.5:53 from 192.168.0.110
+[2025-10-24 18:55:03.806][000000058.701] D/mobile LUAT_MOBILE_EVENT_CC status 2
+[2025-10-24 18:55:03.810][000000058.723] dns_run 693:dns all done ,now stop
+[2025-10-24 18:55:03.815][000000058.724] D/net adapter 4 connect 1.194.220.72:443 TCP
+[2025-10-24 18:55:04.614][000000059.553] I/user.feishu 200 {"StatusCode":0,"StatusMessage":"success","code":0,"data":{},"msg":"success"}
+[2025-10-24 18:55:04.619][000000059.553] I/user.来电号码转发到钉钉
+[2025-10-24 18:55:04.624][000000059.555] I/user.timestamp 1761303305000
+[2025-10-24 18:55:04.626][000000059.555] I/user.sign pwTUpkuxUSnUiN5XmXLXXL5%2FPTYD22icTHW09YslWN4%3D
+[2025-10-24 18:55:04.630][000000059.556] I/user.url https://oapi.dingtalk.com/robot/send?access_token=03f4753ec6aa6f0524fb85907c94b17f3fa0fed3107d4e8f4eee1d4a97855f4d&timestamp=1761303305000&sign=pwTUpkuxUSnUiN5XmXLXXL5%2FPTYD22icTHW09YslWN4%3D
+[2025-10-24 18:55:04.634][000000059.557] I/user.dingding {"text":{"content":"我的id是nil,Fri Oct 24 18:55:05 2025,Air8000,    18317857567来电"},"msgtype":"text"}
+[2025-10-24 18:55:04.637][000000059.559] dns_run 676:oapi.dingtalk.com state 0 id 6 ipv6 0 use dns server0, try 0
+[2025-10-24 18:55:04.641][000000059.559] D/net adatper 4 dns server 223.5.5.5
+[2025-10-24 18:55:04.652][000000059.559] D/net dns udp sendto 223.5.5.5:53 from 192.168.0.110
+[2025-10-24 18:55:04.656][000000059.588] dns_run 693:dns all done ,now stop
+[2025-10-24 18:55:04.659][000000059.589] D/net adapter 4 connect 106.11.35.100:443 TCP
+[2025-10-24 18:55:05.255][000000060.198] I/user.dingding 200 {"errcode":0,"errmsg":"ok"}
+[2025-10-24 18:55:05.262][000000060.199] I/user.来电号码转发到微信
+[2025-10-24 18:55:05.267][000000060.199] I/user.timestamp 1761303306000
+[2025-10-24 18:55:05.271][000000060.200] I/user.url https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=71017f82-e027-4c5d-a618-eb4ee01750e9&timestamp=1761303306000
+[2025-10-24 18:55:05.276][000000060.201] I/user.weixin {"text":{"content":"我的id是nil,Fri Oct 24 18:55:06 2025,Air8000,    18317857567来电"},"msgtype":"text"}
+[2025-10-24 18:55:05.279][000000060.202] dns_run 676:qyapi.weixin.qq.com state 0 id 7 ipv6 0 use dns server0, try 0
+[2025-10-24 18:55:05.283][000000060.203] D/net adatper 4 dns server 223.5.5.5
+[2025-10-24 18:55:05.289][000000060.203] D/net dns udp sendto 223.5.5.5:53 from 192.168.0.110
+[2025-10-24 18:55:05.317][000000060.246] dns_run 693:dns all done ,now stop
+[2025-10-24 18:55:05.326][000000060.247] D/net adapter 4 connect 101.226.141.58:443 TCP
+[2025-10-24 18:55:06.065][000000061.005] I/user.weixin 200 {"errcode":0,"errmsg":"ok"}
+[2025-10-24 18:55:08.398][000000063.332] D/DHCP dhcp state 1 tnow 63332 p1 3603332 p2 6303332
+[2025-10-24 18:55:09.749][000000064.690] D/mobile LUAT_MOBILE_EVENT_CC status 1
+[2025-10-24 18:55:11.477][000000066.413] D/DHCP dhcp state 1 tnow 66413 p1 1806414 p2 3156414
+[2025-10-24 18:55:15.759][000000070.689] D/mobile LUAT_MOBILE_EVENT_CC status 1
+[2025-10-24 18:55:21.758][000000076.689] D/mobile LUAT_MOBILE_EVENT_CC status 1
+[2025-10-24 18:55:21.766][000000076.698] luat_i2s_load_old_config 287:i2s0 old param not saved!
+[2025-10-24 18:55:21.772][000000076.699] D/cc VOLTE_EVENT_PLAY_STOP
+[2025-10-24 18:55:21.777][000000076.699] D/mobile LUAT_MOBILE_EVENT_CC status 12
+[2025-10-24 18:55:21.948][000000076.886] soc_cms_proc 2219:cenc report 1,38,1,7
+[2025-10-24 18:55:21.960][000000076.887] D/mobile cid7, state2
+[2025-10-24 18:55:23.764][000000078.694] D/mobile LUAT_MOBILE_EVENT_CC status 10
+
+```

+ 26 - 20
module/Air8000/project/sms_forward/sms_forward.lua → module/Air8000/project/sms_call_forward/sms_forward.lua

@@ -10,7 +10,7 @@
 2、send_sms(),发送短信的功能函数,等待CC_IND消息后,手机卡可以进行收发短信。
 3、receive_sms(),接收短信处理的功能函数,收到短信后获取来信号码和短信内容,通过回调函数sms_handler(num, txt)转发到指定的机器人。
 
-直接使用Air8000核心板板硬件测试即可;
+直接使用Air8000开发板板硬件测试即可;
 
 本文件没有对外接口,直接在main.lua中require "sms_forward"就可以加载运行;
 ]]
@@ -26,8 +26,8 @@ local webhook_dingding =
 "https://oapi.dingtalk.com/robot/send?access_token=03f4753ec6aa6f0524fb85907c94b17f3fa0fed3107d4e8f4eee1d4a97855f4d"
 local secret_dingding = "SECac5b455d6b567f64073a456e91feec6ad26c0f8f7dcca85dd2ce6c23ea466c52"
 
---local webhook_weixin = "https://work.weixin.qq.com/wework_admin/common/openBotProfile/24caa08b3a985454055047454d883fc98f"
-local webhook_weixin = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=36648707-4eba-4d21-9d3a-2244e1e9bc3b"
+
+local webhook_weixin = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=71017f82-e027-4c5d-a618-eb4ee01750e9"
 -- 飞书关于机器人的文档 https://open.feishu.cn/document/ukTMukTMukTM/ucTM5YjL3ETO24yNxkjN?lang=zh-CN
 
 
@@ -57,10 +57,10 @@ local function feishu_post_sms(num, rctxt)
     local code, headers, body = http.request("POST", url, rheaders, rbody).wait()
     -- 正常会返回 200, {"errcode":0,"errmsg":"ok"}
     -- 其他错误, 一般是密钥错了, 仔细检查吧
-    log.info("feishu", code, body, "当前网络:", socket.adapter(socket.LWIP_STA))
+    log.info("feishu", code, body)
 end
 
---2.功能函数:短信转发到叮叮
+--2.功能函数:短信转发到钉钉
 local function dingding_post(num, rctxt)
     local rheaders = {}
     rheaders["Content-Type"] = "application/json"
@@ -82,7 +82,7 @@ local function dingding_post(num, rctxt)
     local code, headers, body = http.request("POST", url, rheaders, (json.encode(data))).wait()
     -- 正常会返回 200, {"errcode":0,"errmsg":"ok"}
     -- 其他错误, 一般是密钥错了, 仔细检查吧
-    log.info("dingding", code, body, "当前网络:", socket.adapter(socket.LWIP_STA))
+    log.info("dingding", code, body)
 end
 
 --3.功能函数:短信转发到企业微信
@@ -104,7 +104,7 @@ local function weixin_post(num, rctxt)
     local code, headers, body = http.request("POST", url, rheaders, (json.encode(data))).wait()
     -- 正常会返回 200, {"errcode":0,"errmsg":"ok"}
     -- 其他错误, 一般是密钥错了, 仔细检查吧
-    log.info("weixin", code, body, "当前网络:", socket.adapter(socket.LWIP_STA), socket.adapter())
+    log.info("weixin", code, body)
 end
 
 
@@ -121,11 +121,11 @@ local function sms_handler(num, txt)
     feishu_post_sms(num, txt)
 
 
-    sys.wait(1000)
+    
     log.info("转发到钉钉")
     dingding_post(num, txt)
 
-    sys.wait(1000)
+    
     log.info("转发到微信")
     weixin_post(num, txt)
 end
@@ -145,10 +145,6 @@ local function receive_sms()
             log.info("num是", num)
             log.info("收到来自" .. num .. "的短信:" .. txt)
 
-            --local isReady1, index1 = socket.adapter()
-            log.info("当前网络", socket.adapter())
-            log.info("当前wifi网络情况", socket.adapter(socket.LWIP_STA))
-
             sms_handler(num, txt)
         end
     end
@@ -160,7 +156,7 @@ local function send_sms()
     --系统消息CC_IND到了才能收发短信
     sys.waitUntil("CC_IND")
     log.info("发送短信前wait CC_IND")
-    -- 如果是联网卡, 这里是需要sntp的, 否则时间不对
+    -- 时间同步,以免转发时携带的时间不对
     log.info("时间同步", socket.sntp())
     sys.waitUntil("NTP_UPDATE", 5000)
     local cont = 1
@@ -169,16 +165,26 @@ local function send_sms()
 
         --获取本机号码,如果卡商没写入会返回nil
         log.info("mobile.number(id) = ", mobile.number())
-
+        --获取本机iccid,失败返回nil
         log.info("mobile.iccid(id) = ", mobile.iccid())
-
+        --获取本机simid,失败返回-1
         log.info("mobile.simid(id) = ", mobile.simid())
-
+        --获取本机imsi,失败返回nil
         log.info("mobile.imsi(index) = ", mobile.imsi())
-        --电信卡查话费
-        log.info("给10001发送查询短信", "这是第" .. cont .. "次发送", sms.send("10001", "102"))
+
         --给自己发短信
-        --log.info("给159发送查询短信1", "这是第" .. cont .. "次发送", sms.send("1593868****", "test"))
+        --local result = sms.send("1593868****", "test")
+
+        --电信卡给10001发送“102”查话费
+        local result = sms.send("10001", "102")
+        if result then
+            --sms.send 的同步结果仅表示任务启动成功,
+            --最终发送状态需通过异步事件 "SMS_SEND_RESULT" 获取;
+            sys.waitUntil("SMS_SEND_RESULT")
+            log.info("给10001发送查询短信", "这是第" .. cont .. "次发送", " 发送结果:", result)
+        else
+            log.warn("sms", "短信发送失败")
+        end
         cont = cont + 1
         sys.wait(10 * 60 * 1000)
     end

+ 0 - 258
module/Air8000/project/sms_forward/readme.md

@@ -1,258 +0,0 @@
-# 8000-SMS
-
-## 功能模块介绍:
-
-1、main.lua:主程序入口文件,加载以下 3 个文件运行。
-
-2、netdrv_multiple.lua:网卡驱动配置文件,可以配置以太网卡,wifi 网卡,单 4g 网卡三种网卡的使用优先级
-
-3、sms_forward.lua: 短信转发功能模块文件
-
-4、cc_forward.lua:来电转发功能模块文件
-
-## 演示功能概述:
-
-**sms_forward.lua:**
-
-1、配置飞书,钉钉,企业微信机器人的 webhook 和 secret(加签)。
-
-2、send_sms(),发送短信的功能函数,等待 CC_IND 消息后,手机卡可以进行收发短信。
-
-3、receive_sms(),接收短信处理的功能函数,收到短信后获取来信号码和短信内容,通过回调函数 sms_handler(num, txt)转发到指定的机器人。
-
-**cc_forward.lua:**
-
-1、配置飞书,钉钉,企业微信机器人的 webhook 和 secret(加签)。
-
-2、cc_setup(),初始化电话功能,做好接收来电的准备。
-
-3、cc_state(state),电话状态判断并获取来电号码,来电或者挂断等不同情况做不同处理。
-
-4、cc_forward(),来电号码信息转发到指定机器人
-
-## 演示硬件环境:
-
-![8000w](https://docs.openluat.com/accessory/AirSPINORFLASH_1000/image/8000w.jpg)
-
-1、Air8000W 核心板一块 + 正常手机卡一张(三大运营商的都可以)+4g 天线一根 +wifi 天线一根
-
-- sim 卡插入核心板的 sim 卡槽
-- 天线装到核心板上
-
-2、TYPE-C USB 数据线一根 ,Air8000W 核心板和数据线的硬件接线方式为:
-
-- Air8000 核心板通过 TYPE-C USB 口供电;(外部供电/USB 供电 拨动开关 拨到 USB 供电一端)
-- TYPE-C USB 数据线直接插到核心板的 TYPE-C USB 座子,另外一端连接电脑 USB 口;
-
-## 演示软件环境:
-
-1、 Luatools 下载调试工具
-
-2、 固件版本:LuatOS-SoC_V2014_Air8000_1,固件地址,如有最新固件请用最新 [https://docs.openluat.com/air8000/luatos/firmware/](https://docs.openluat.com/air8000/luatos/firmware/)
-
-3、 脚本文件:
-
-main.lua
-
-netdrv_multiple.lua:
-
-sms_forward.lua:
-
-cc_forward.lua:
-
-4、 pc 系统 win11(win10 及以上)
-
-5、飞书,钉钉,企业微信等自己需要的机器人。
-
-## 演示核心步骤:
-
-1、搭建好硬件环境
-
-2、demo 脚本代码 netdrv_multiple.lua 中,ssid = "茶室-降功耗,找合宙!", password = "Air123456", 修改为自己测试时 wifi 热点的名称和密码;注意:仅支持 2.4G 的 wifi,不支持 5G 的 wifi
-
-3、[https://open.feishu.cn/document/ukTMukTMukTM/ucTM5YjL3ETO24yNxkjN?lang=zh-CN](https://open.feishu.cn/document/ukTMukTMukTM/ucTM5YjL3ETO24yNxkjN?lang=zh-CN) 参考此教程,获取飞书,钉钉,企业微信的 webhook 和 secret(加签),在 cc_forward.lua 和 sms_forward.lua 脚本中找到 local webhook_feishu,secret_feishu,webhook_dingding,secret_dingding,webhook_weixin 的参数定义,修改为自己的参数。
-
-4、Luatools 烧录内核固件和修改后的 demo 脚本代码
-
-5、此处演示设置了优先使用 wifi 网络,其次是 4G 网络.
-
-烧录成功后,代码会自动运行,log 日志打印 wif 网络信息、CC_READY 等消息,log 日志打印如下:
-
-```yaml
-[2025-09-22 10:51:57.930][000000000.358] I/user.main cc_sms_forward 001.000.000
-[2025-09-22 10:51:57.942][000000000.470] I/user.notify_status function
-[2025-09-22 10:51:57.956][000000000.471] I/user.WiFi名称: Mayadan
-[2025-09-22 10:51:57.965][000000000.472] I/user.密码     : 12345678
-[2025-09-22 10:51:57.973][000000000.472] I/user.ping_ip  : nil
-[2025-09-22 10:51:57.978][000000000.472] I/user.WiFi STA初始化完成
-[2025-09-22 10:51:57.983][000000000.473] change from 1 to 2
-[2025-09-22 10:51:58.448][000000001.474] I/user.4G网卡开始PING
-[2025-09-22 10:51:58.454][000000001.474] I/user.dns_request 4G true
-[2025-09-22 10:51:59.022][000000002.004] D/mobile cid1, state0
-[2025-09-22 10:51:59.028][000000002.005] D/mobile bearer act 0, result 0
-[2025-09-22 10:51:59.031][000000002.006] D/mobile NETIF_LINK_ON -> IP_READY
-[2025-09-22 10:51:59.035][000000002.007] I/user.ip_ready_handle 10.41.25.58 4G state 1 gw 0.0.0.0
-[2025-09-22 10:51:59.044][000000002.007] I/user.eth_ping_ip nil wifi_ping_ip nil
-[2025-09-22 10:51:59.047][000000002.007] I/user.dnsproxy 开始监听
-[2025-09-22 10:51:59.052][000000002.014] D/mobile TIME_SYNC 0
-[2025-09-22 10:51:59.055][000000002.073] I/user.4G网卡httpdns域名解析成功
-[2025-09-22 10:51:59.059][000000002.074] I/user.httpdns baidu.com 220.181.7.203
-[2025-09-22 10:51:59.062][000000002.075] I/user.设置网卡 4G
-[2025-09-22 10:51:59.066][000000002.075] I/user.netdrv_multiple_notify_cbfunc use new adapter 4G 1
-[2025-09-22 10:51:59.071][000000002.076] change from 2 to 1
-[2025-09-22 10:51:59.103][000000002.122] soc_cms_proc 2189:cenc report 1,51,1,15
-[2025-09-22 10:51:59.168][000000002.188] D/mobile NETIF_LINK_ON -> IP_READY
-[2025-09-22 10:51:59.173][000000002.189] I/user.ip_ready_handle 10.41.25.58 4G state 2 gw 0.0.0.0
-[2025-09-22 10:51:59.178][000000002.190] I/user.eth_ping_ip nil wifi_ping_ip nil
-[2025-09-22 10:51:59.183][000000002.190] I/user.dnsproxy 开始监听
-[2025-09-22 10:52:00.221][000000003.247] D/mobile ims reg state 0
-[2025-09-22 10:52:00.227][000000003.247] D/mobile LUAT_MOBILE_EVENT_CC status 0
-[2025-09-22 10:52:00.232][000000003.247] D/mobile LUAT_MOBILE_CC_READY
-```
-
-此处短信演示使用了电信卡,发送"102"给"10001"查询余额,会收到电信回复的短信,并转发到飞书、钉钉和微信,log 打印如下:
-
-```bash
-[2025-09-22 10:52:00.239][000000003.248] I/user.发送短信前wait CC_IND
-[2025-09-22 10:52:00.243][000000003.249] D/sntp query ntp.aliyun.com
-[2025-09-22 10:52:00.248][000000003.249] dns_run 676:ntp.aliyun.com state 0 id 1 ipv6 0 use dns server2, try 0
-[2025-09-22 10:52:00.256][000000003.251] I/user.时间同步
-[2025-09-22 10:52:00.261][000000003.270] dns_run 693:dns all done ,now stop
-[2025-09-22 10:52:00.314][000000003.332] D/sntp Unix timestamp: 1758509523
-[2025-09-22 10:52:00.322][000000003.334] I/user.现在可以收发短信
-[2025-09-22 10:52:00.326][000000003.334] I/user.mobile.number(id) =  nil
-[2025-09-22 10:52:00.330][000000003.334] I/user.mobile.iccid(id) =  89860325743780541565
-[2025-09-22 10:52:00.333][000000003.335] I/user.mobile.simid(id) =  0
-[2025-09-22 10:52:00.337][000000003.335] I/user.mobile.imsi(index) =  460115726670673
-[2025-09-22 10:52:00.340][000000003.335] W/sms pdu len 18
-[2025-09-22 10:52:00.346][000000003.337] I/user.给10001发送查询短信 这是第1次发送 true
-[2025-09-22 10:52:00.812][000000003.835] luat_sms_proc 1239:[DIO 1239]: CMI_SMS_SEND_MSG_CNF is in
-[2025-09-22 10:52:00.817][000000003.836] E/sms long sms callback seqNum = 1
-[2025-09-22 10:52:00.921][000000003.947] D/airlink wifi sta上线了
-[2025-09-22 10:52:00.952][000000003.956] D/DHCP dhcp state 6 3956 0 0
-[2025-09-22 10:52:00.959][000000003.957] D/DHCP dhcp discover C8C2C68CD816
-[2025-09-22 10:52:00.962][000000003.957] I/ulwip adapter 2 dhcp payload len 308
-[2025-09-22 10:52:00.966][000000003.958] D/netdrv.whale IP_LOSE 2
-[2025-09-22 10:52:00.971][000000003.959] I/user.ip_lose_handle WiFi
-[2025-09-22 10:52:00.980][000000003.981] D/ulwip 收到DHCP数据包(len=303)
-[2025-09-22 10:52:00.984][000000003.982] D/DHCP dhcp state 7 3982 0 0
-[2025-09-22 10:52:00.987][000000003.982] D/DHCP find ip d72ba8c0 192.168.43.215
-[2025-09-22 10:52:00.995][000000003.982] D/DHCP result 2
-[2025-09-22 10:52:01.001][000000003.982] D/DHCP select offer, wait ack
-[2025-09-22 10:52:01.005][000000003.982] I/ulwip adapter 2 dhcp payload len 338
-[2025-09-22 10:52:01.012][000000004.007] D/ulwip 收到DHCP数据包(len=333)
-[2025-09-22 10:52:01.016][000000004.008] D/DHCP dhcp state 9 4008 0 0
-[2025-09-22 10:52:01.019][000000004.008] D/DHCP find ip d72ba8c0 192.168.43.215
-[2025-09-22 10:52:01.027][000000004.008] D/DHCP result 5
-[2025-09-22 10:52:01.032][000000004.008] D/DHCP DHCP get ip ready
-[2025-09-22 10:52:01.035][000000004.008] D/ulwip adapter 2 ip 192.168.43.215
-[2025-09-22 10:52:01.042][000000004.008] D/ulwip adapter 2 mask 255.255.255.0
-[2025-09-22 10:52:01.046][000000004.009] D/ulwip adapter 2 gateway 192.168.43.1
-[2025-09-22 10:52:01.050][000000004.009] D/ulwip adapter 2 lease_time 3600s
-[2025-09-22 10:52:01.053][000000004.009] D/ulwip adapter 2 DNS1:192.168.43.1
-[2025-09-22 10:52:01.062][000000004.009] D/net network ready 2, setup dns server
-[2025-09-22 10:52:01.070][000000004.010] D/ulwip IP_READY 2 192.168.43.215
-[2025-09-22 10:52:01.073][000000004.012] I/user.ip_ready_handle 192.168.43.215 WiFi state 3 gw 192.168.43.1
-[2025-09-22 10:52:01.079][000000004.012] I/user.eth_ping_ip nil wifi_ping_ip nil
-[2025-09-22 10:52:01.083][000000004.012] I/user.dnsproxy 开始监听
-[2025-09-22 10:52:01.979][000000005.007] luat_sms_proc 1236:[DIO 1236]: CMI_SMS_NEW_MSG_IND is in
-[2025-09-22 10:52:01.991][000000005.008] D/sms dcs 2 | 0 | 0 | 0
-[2025-09-22 10:52:02.002][000000005.008] I/sms long-sms, wait more frags 2/2
-[2025-09-22 10:52:05.202][000000008.219] luat_sms_proc 1236:[DIO 1236]: CMI_SMS_NEW_MSG_IND is in
-[2025-09-22 10:52:05.210][000000008.219] D/sms dcs 2 | 0 | 0 | 0
-[2025-09-22 10:52:05.214][000000008.220] I/sms long-sms is ok
-[2025-09-22 10:52:05.220][000000008.222] I/user.num是 10001
-[2025-09-22 10:52:05.224][000000008.222] I/user.收到来自10001的短信:截止到2025年09月22日10时,本机可用余额:64.4元,帐户余额:64.4,欠费:0.0元 查询结果仅供参考,实际费用以出账单为准。更多服务请微信关注【河南电信】公众号,或下载欢go客户端( http://a.189.cn/JJTh4u )。
-[2025-09-22 10:52:05.228][000000008.222] I/user.当前网络 true 1
-[2025-09-22 10:52:05.231][000000008.223] I/user.当前wifi网络情况 true 2
-[2025-09-22 10:52:05.236][000000008.223] I/user.转发到飞书
-[2025-09-22 10:52:05.241][000000008.225] I/user.timestamp 1758509527
-[2025-09-22 10:52:05.243][000000008.225] I/user.sign Z17kK76tCSB9L+VrkLP8mKYvkpu0lVWEJWKZ7GbVDJ0=
-[2025-09-22 10:52:05.249][000000008.226] I/user.url https://open.feishu.cn/open-apis/bot/v2/hook/bb089165-4b73-4f80-9ed0-da0c908b44e5
-[2025-09-22 10:52:05.252][000000008.227] I/user.feishu {"content":{"text":"我的id是nil,Mon Sep 22 10:52:07 2025,Air8000,    10001发来短信,内容是:截止到2025年09月22日10时,本机可用余额:64.4元,帐户余额:64.4,欠费:0.0元 查询结果仅供参考,实际费用以出账单为准。更多服务请微信关注【河南电信】公众号,或下载欢go客户端( http:\/\/a.189.cn\/JJTh4u )。"},"sign":"Z17kK76tCSB9L+VrkLP8mKYvkpu0lVWEJWKZ7GbVDJ0=","msg_type":"text","timestamp":"1758509527"}
-[2025-09-22 10:52:05.258][000000008.232] dns_run 676:open.feishu.cn state 0 id 2 ipv6 0 use dns server2, try 0
-[2025-09-22 10:52:05.261][000000008.251] dns_run 693:dns all done ,now stop
-[2025-09-22 10:52:06.117][000000009.147] I/user.feishu 200 {"StatusCode":0,"StatusMessage":"success","code":0,"data":{},"msg":"success"} 当前网络: true 2
-[2025-09-22 10:52:07.126][000000010.148] I/user.转发到钉钉
-[2025-09-22 10:52:07.139][000000010.150] I/user.timestamp 1758509529000
-[2025-09-22 10:52:07.143][000000010.150] I/user.sign 2Q1L9DWjmdMoW1OMmSQMJ7rc3HyPZdt1ollEVQVmf58%3D
-[2025-09-22 10:52:07.146][000000010.151] I/user.url https://oapi.dingtalk.com/robot/send?access_token=03f4753ec6aa6f0524fb85907c94b17f3fa0fed3107d4e8f4eee1d4a97855f4d&timestamp=1758509529000&sign=2Q1L9DWjmdMoW1OMmSQMJ7rc3HyPZdt1ollEVQVmf58%3D
-[2025-09-22 10:52:07.150][000000010.152] I/user.dingding {"text":{"content":"我的id是nil,Mon Sep 22 10:52:09 2025,Air8000,    10001发来短信,内容是:截止到2025年09月22日10时,本机可用余额:64.4元,帐户余额:64.4,欠费:0.0元 查询结果仅供参考,实际费用以出账单为准。更多服务请微信关注【河南电信】公众号,或下载欢go客户端( http:\/\/a.189.cn\/JJTh4u )。"},"msgtype":"text"}
-[2025-09-22 10:52:07.155][000000010.154] dns_run 676:oapi.dingtalk.com state 0 id 3 ipv6 0 use dns server2, try 0
-[2025-09-22 10:52:08.135][000000011.153] dns_run 676:oapi.dingtalk.com state 0 id 3 ipv6 0 use dns server2, try 1
-[2025-09-22 10:52:10.062][000000013.076] I/user.WiFi网卡开始PING
-[2025-09-22 10:52:10.078][000000013.076] I/user.dns_request WiFi true
-[2025-09-22 10:52:10.083][000000013.078] D/net connect 223.5.5.5:80 TCP
-[2025-09-22 10:52:10.139][000000013.153] dns_run 676:oapi.dingtalk.com state 0 id 3 ipv6 0 use dns server2, try 2
-[2025-09-22 10:52:10.201][000000013.218] I/user.WiFi网卡httpdns域名解析成功
-[2025-09-22 10:52:10.211][000000013.218] I/user.httpdns baidu.com 220.181.7.203
-[2025-09-22 10:52:10.225][000000013.219] I/user.设置网卡 WiFi
-[2025-09-22 10:52:10.230][000000013.220] I/user.netdrv_multiple_notify_cbfunc use new adapter WiFi 2
-[2025-09-22 10:52:10.235][000000013.220] change from 1 to 2
-[2025-09-22 10:52:13.130][000000016.153] dns_run 676:oapi.dingtalk.com state 0 id 3 ipv6 0 use dns server3, try 0
-[2025-09-22 10:52:13.161][000000016.188] dns_run 693:dns all done ,now stop
-[2025-09-22 10:52:13.832][000000016.848] I/user.dingding 200 {"errcode":0,"errmsg":"ok"} 当前网络: true 2
-[2025-09-22 10:52:14.829][000000017.848] I/user.转发到微信
-[2025-09-22 10:52:14.836][000000017.850] I/user.timestamp 1758509537000
-[2025-09-22 10:52:14.841][000000017.851] I/user.sign foZ%2BwVSF%2BC6V7rwNRDvlUD5V6k1UU0wAMupeyGrHD3Y%3D
-[2025-09-22 10:52:14.848][000000017.851] I/user.url https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=36648707-4eba-4d21-9d3a-2244e1e9bc3b&timestamp=1758509537000&sign=
-[2025-09-22 10:52:14.853][000000017.852] I/user.weixin {"text":{"content":"我的id是nil,Mon Sep 22 10:52:17 2025,Air8000,    10001发来短信,内容是:截止到2025年09月22日10时,本机可用余额:64.4元,帐户余额:64.4,欠费:0.0元 查询结果仅供参考,实际费用以出账单为准。更多服务请微信关注【河南电信】公众号,或下载欢go客户端( http:\/\/a.189.cn\/JJTh4u )。"},"msgtype":"text"}
-[2025-09-22 10:52:14.860][000000017.854] dns_run 676:qyapi.weixin.qq.com state 0 id 1 ipv6 0 use dns server0, try 0
-[2025-09-22 10:52:14.863][000000017.854] D/net adatper 2 dns server 192.168.43.1
-[2025-09-22 10:52:14.867][000000017.854] D/net dns udp sendto 192.168.43.1:53 from 192.168.43.215
-[2025-09-22 10:52:14.890][000000017.908] dns_run 693:dns all done ,now stop
-[2025-09-22 10:52:14.895][000000017.909] D/net connect 101.226.141.58:443 TCP
-[2025-09-22 10:52:15.688][000000018.716] I/user.weixin 200 {"errcode":0,"errmsg":"ok"} 当前网络: true true 1
-```
-
-此处来电转发演示使用了电信卡,用另外手机拨打模组上的电话号码,响铃 3 声后自动挂断,log 打印如下:
-
-```lua
-[2025-09-22 10:59:04.001][000000427.015] D/mobile LUAT_MOBILE_EVENT_CC status 12
-[2025-09-22 10:59:04.013][000000427.016] D/mobile LUAT_MOBILE_EVENT_CC status 1
-[2025-09-22 10:59:04.026][000000427.017] I/user.获取最后一次通话的号码
-[2025-09-22 10:59:04.033][000000427.017] I/user.来电号码是 18317857567
-[2025-09-22 10:59:04.045][000000427.018] I/user. 来电号码转发到飞书
-[2025-09-22 10:59:04.052][000000427.020] I/user.timestamp 1758509946
-[2025-09-22 10:59:04.060][000000427.021] I/user.sign 0/ThEtxIC5eD7tpdAZMN8ZjTpviUvboF52agGrm7qso=
-[2025-09-22 10:59:04.068][000000427.021] I/user.url https://open.feishu.cn/open-apis/bot/v2/hook/bb089165-4b73-4f80-9ed0-da0c908b44e5
-[2025-09-22 10:59:04.079][000000427.022] I/user.feishu {"content":{"text":"我的id是nil,Mon Sep 22 10:59:06 2025,Air8000,    18317857567来电"},"sign":"0\/ThEtxIC5eD7tpdAZMN8ZjTpviUvboF52agGrm7qso=","msg_type":"text","timestamp":"1758509946"}
-[2025-09-22 10:59:04.090][000000427.023] dns_run 676:open.feishu.cn state 0 id 2 ipv6 0 use dns server0, try 0
-[2025-09-22 10:59:04.097][000000427.024] D/net adatper 2 dns server 192.168.43.1
-[2025-09-22 10:59:04.108][000000427.024] D/net dns udp sendto 192.168.43.1:53 from 192.168.43.215
-[2025-09-22 10:59:04.115][000000427.026] D/mobile LUAT_MOBILE_EVENT_CC status 2
-[2025-09-22 10:59:04.124][000000427.094] dns_run 693:dns all done ,now stop
-[2025-09-22 10:59:04.133][000000427.095] D/net connect 221.229.209.220:443 TCP
-[2025-09-22 10:59:05.166][000000428.177] I/user.feishu 200 {"StatusCode":0,"StatusMessage":"success","code":0,"data":{},"msg":"success"} true true 1
-[2025-09-22 10:59:06.163][000000429.177] I/user.来电号码转发到钉钉
-[2025-09-22 10:59:06.175][000000429.179] I/user.timestamp 1758509948000
-[2025-09-22 10:59:06.185][000000429.180] I/user.sign %2FVjHC0cAIWwJqTPRez%2FITF4AOa4wPQF4DuTlNn7Dos4%3D
-[2025-09-22 10:59:06.200][000000429.180] I/user.url https://oapi.dingtalk.com/robot/send?access_token=03f4753ec6aa6f0524fb85907c94b17f3fa0fed3107d4e8f4eee1d4a97855f4d&timestamp=1758509948000&sign=%2FVjHC0cAIWwJqTPRez%2FITF4AOa4wPQF4DuTlNn7Dos4%3D
-[2025-09-22 10:59:06.212][000000429.181] I/user.dingding {"text":{"content":"我的id是nil,Mon Sep 22 10:59:08 2025,Air8000,    18317857567来电"},"msgtype":"text"}
-[2025-09-22 10:59:06.222][000000429.183] dns_run 676:oapi.dingtalk.com state 0 id 3 ipv6 0 use dns server0, try 0
-[2025-09-22 10:59:06.233][000000429.183] D/net adatper 2 dns server 192.168.43.1
-[2025-09-22 10:59:06.244][000000429.183] D/net dns udp sendto 192.168.43.1:53 from 192.168.43.215
-[2025-09-22 10:59:06.253][000000429.242] dns_run 693:dns all done ,now stop
-[2025-09-22 10:59:06.264][000000429.243] D/net connect 106.11.35.100:443 TCP
-[2025-09-22 10:59:07.007][000000430.016] I/user.dingding 200 {"errcode":0,"errmsg":"ok"} true true 1
-[2025-09-22 10:59:08.005][000000431.017] I/user.来电号码转发到微信
-[2025-09-22 10:59:08.015][000000431.019] I/user.timestamp 1758509950000
-[2025-09-22 10:59:08.025][000000431.019] I/user.sign SK6tHgVsVn%2FUhPMaNsb3aRcP9iVAqVAQz1Kwx0GDy7c%3D
-[2025-09-22 10:59:08.033][000000431.020] I/user.url https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=36648707-4eba-4d21-9d3a-2244e1e9bc3b&timestamp=1758509950000&sign=
-[2025-09-22 10:59:08.045][000000431.021] I/user.weixin {"text":{"content":"我的id是nil,Mon Sep 22 10:59:10 2025,Air8000,    18317857567来电"},"msgtype":"text"}
-[2025-09-22 10:59:08.055][000000431.022] dns_run 676:qyapi.weixin.qq.com state 0 id 4 ipv6 0 use dns server0, try 0
-[2025-09-22 10:59:08.063][000000431.023] D/net adatper 2 dns server 192.168.43.1
-[2025-09-22 10:59:08.075][000000431.023] D/net dns udp sendto 192.168.43.1:53 from 192.168.43.215
-[2025-09-22 10:59:08.083][000000431.072] dns_run 693:dns all done ,now stop
-[2025-09-22 10:59:08.090][000000431.073] D/net connect 101.226.141.58:443 TCP
-[2025-09-22 10:59:08.985][000000431.998] I/user.weixin 200 {"errcode":0,"errmsg":"ok"} true true 1
-[2025-09-22 10:59:09.999][000000433.014] D/mobile LUAT_MOBILE_EVENT_CC status 1
-[2025-09-22 10:59:16.002][000000439.014] D/mobile LUAT_MOBILE_EVENT_CC status 1
-[2025-09-22 10:59:21.995][000000445.014] D/mobile LUAT_MOBILE_EVENT_CC status 1
-[2025-09-22 10:59:22.010][000000445.022] luat_i2s_load_old_config 287:i2s0 old param not saved!
-[2025-09-22 10:59:22.020][000000445.022] D/cc VOLTE_EVENT_PLAY_STOP
-[2025-09-22 10:59:22.028][000000445.023] D/mobile LUAT_MOBILE_EVENT_CC status 12
-[2025-09-22 10:59:22.120][000000445.134] soc_cms_proc 2189:cenc report 1,38,1,7
-[2025-09-22 10:59:22.134][000000445.135] D/mobile cid7, state2
-[2025-09-22 10:59:23.997][000000447.019] D/mobile LUAT_MOBILE_EVENT_CC status 10
-```

+ 282 - 0
module/Air8101/demo/fs_io/flash_fs_io.lua

@@ -0,0 +1,282 @@
+--[[
+@module  flash_fs_io
+@summary 内置Flash文件系统操作测试模块
+@version 1.0.0
+@date    2025.09.23
+@author  王棚嶙
+@usage
+本文件为内置Flash文件系统的操作测试流程:
+1. 获取文件系统信息( io.fsstat)
+2. 创建目录
+3. 创建并写入文件
+4. 检查文件是否存在
+5. 获取文件大小(io.fileSize)
+6. 读取文件内容
+7. 启动计数文件操作
+8. 文件追加测试
+9. 按行读取测试
+10. 文件重命名
+11. 列举目录内容
+12. 删除文件
+13. 删除目录
+本文件没有对外接口,直接在main.lua中require "flash_fs_io"就可以加载运行
+]]
+
+function flash_fs_io_task()
+    -- 使用内置Flash文件系统,根目录为"/",
+    local base_path = "/"
+    -- 创建一个目录
+    local demo_dir = "flash_demo"
+    -- 文件名
+    local dir_path = base_path .. demo_dir
+
+    -- ########## 开始进行内置Flash文件系统操作 ##########
+    log.info("文件系统操作", "===== 开始文件系统操作 =====")
+
+    -- 1. 获取文件系统信息 (使用 io.fsstat接口)
+    local success, total_blocks, used_blocks, block_size, fs_type  =  io.fsstat(base_path)
+    if success then
+        log.info(" io.fsstat成功:", 
+            "总空间=" .. total_blocks .. "块", 
+            "已用=" .. used_blocks .. "块", 
+            "块大小=" .. block_size.."字节",
+            "类型=" .. fs_type)
+    else
+        log.error(" io.fsstat", "获取文件系统信息失败")
+        return
+    end
+
+    -- 2. 创建目录
+    -- 如果目录不存在,则创建目录
+    if not io.dexist(dir_path) then
+        -- 创建目录
+        if io.mkdir(dir_path) then
+            log.info("io.mkdir", "目录创建成功", "路径:" .. dir_path)
+        else
+            log.error("io.mkdir", "目录创建失败", "路径:" .. dir_path)
+            return
+        end
+    else
+        log.warn("io.mkdir", "目录已存在,跳过创建", "路径:" .. dir_path)
+    end
+
+    -- 3. 创建并写入文件
+    local file_path = dir_path .. "/boottime"
+    local file = io.open(file_path, "wb")
+    if file then
+        file:write("这是内置Flash文件系统API文档示例的测试内容")
+        file:close()
+        log.info("文件创建", "文件写入成功", "路径:" .. file_path)
+    else
+        log.error("文件创建", "文件创建失败", "路径:" .. file_path)
+        return
+    end
+
+    -- 4. 检查文件是否存在
+    if io.exists(file_path) then
+        log.info("io.exists", "文件存在", "路径:" .. file_path)
+    else
+        log.error("io.exists", "文件不存在", "路径:" .. file_path)
+        return
+    end
+
+    -- 5. 获取文件大小 (使用io.fileSize接口)
+    local file_size = io.fileSize(file_path)
+    if file_size then
+        log.info("io.fileSize", "文件大小:" .. file_size .. "字节", "路径:" .. file_path)
+    else
+        log.error("io.fileSize", "获取文件大小失败", "路径:" .. file_path)
+        return
+    end
+
+    -- 6. 读取文件内容
+    file = io.open(file_path, "rb")
+    if file then
+        local content = file:read("*a")
+        log.info("文件读取", "路径:" .. file_path, "内容:" .. content)
+        file:close()
+    else
+        log.error("文件操作", "无法打开文件读取内容", "路径:" .. file_path)
+        return
+    end
+
+    -- 7. 启动计数文件操作
+    local count = 0
+    file = io.open(file_path, "rb")
+    if file then
+        local data = file:read("*a")
+        log.info("启动计数", "文件内容:", data, "十六进制:", data:toHex())
+        count = tonumber(data) or 0
+        file:close()
+    else
+        log.warn("启动计数", "文件不存在或无法打开")
+    end
+
+    log.info("启动计数", "当前值:", count)
+    count = count + 1
+    log.info("启动计数", "更新值:", count)
+
+    file = io.open(file_path, "wb")
+    if file then
+        file:write(tostring(count))
+        file:close()
+        log.info("文件写入", "路径:" .. file_path, "内容:", count)
+    else
+        log.error("文件写入", "无法打开文件", "路径:" .. file_path)
+        return
+    end
+
+    -- 8. 文件追加测试
+    local append_file = dir_path .. "/test_a"
+    os.remove(append_file) -- 清理旧文件
+
+    file = io.open(append_file, "wb")
+    if file then
+        file:write("ABC")
+        file:close()
+        log.info("文件创建", "路径:" .. append_file, "初始内容:ABC")
+    else
+        log.error("文件创建", "无法创建文件", "路径:" .. append_file)
+        return
+    end
+
+    file = io.open(append_file, "a+")
+    if file then
+        file:write("def")
+        file:close()
+        log.info("文件追加", "路径:" .. append_file, "追加内容:def")
+    else
+        log.error("文件追加", "无法打开文件进行追加", "路径:" .. append_file)
+        return
+    end
+
+    -- 验证追加结果
+    file = io.open(append_file, "r")
+    if file then
+        local data = file:read("*a")
+        log.info("文件验证", "路径:" .. append_file, "内容:" .. data, "结果:",
+            data == "ABCdef" and "成功" or "失败")
+        file:close()
+    else
+        log.error("文件验证", "无法打开文件进行验证", "路径:" .. append_file)
+        return
+    end
+
+    -- 9. 按行读取测试
+    local line_file = dir_path .. "/testline"
+    file = io.open(line_file, "w")
+    if file then
+        file:write("abc\n")
+        file:write("123\n")
+        file:write("wendal\n")
+        file:close()
+        log.info("文件创建", "路径:" .. line_file, "写入3行文本")
+    else
+        log.error("文件创建", "无法创建文件", "路径:" .. line_file)
+        return
+    end
+
+    file = io.open(line_file, "r")
+    if file then
+        log.info("按行读取", "路径:" .. line_file, "第1行:", file:read("*l"))
+        log.info("按行读取", "路径:" .. line_file, "第2行:", file:read("*l"))
+        log.info("按行读取", "路径:" .. line_file, "第3行:", file:read("*l"))
+        file:close()
+    else
+        log.error("按行读取", "无法打开文件", "路径:" .. line_file)
+        return
+    end
+
+    -- 10. 文件重命名
+    local old_path = append_file
+    local new_path = dir_path .. "/renamed_file.txt"
+    local success, err = os.rename(old_path, new_path)
+    if success then
+        log.info("os.rename", "文件重命名成功", "原路径:" .. old_path, "新路径:" .. new_path)
+
+        -- 验证重命名结果
+        if io.exists(new_path) and not io.exists(old_path) then
+            log.info("验证结果", "重命名验证成功", "新文件存在", "原文件不存在")
+        else
+            log.error("验证结果", "重命名验证失败")
+        end
+    else
+        log.error("os.rename", "重命名失败", "错误:" .. tostring(err), "原路径:" .. old_path)
+        return
+    end
+
+    -- 11. 列举目录内容
+    log.info("目录操作", "===== 开始目录列举 =====")
+
+    local ret, data = io.lsdir(dir_path, 50, 0)
+    if ret then
+        log.info("fs", "lsdir", json.encode(data))
+    else
+        log.info("fs", "lsdir", "fail", ret, data)
+        return
+    end
+
+    -- 12. 删除文件测试
+    if os.remove(new_path) then
+        log.info("os.remove", "文件删除成功", "路径:" .. new_path)
+        if not io.exists(new_path) then
+            log.info("验证结果", "renamed_file.txt文件删除验证成功")
+        else
+            log.error("验证结果", "renamed_file.txt文件删除验证失败")
+        end
+    else
+        log.error("os.remove", "renamed_file.txt文件删除失败", "路径:" .. new_path)
+        return
+    end
+
+    if os.remove(line_file) then
+        log.info("os.remove", "testline文件删除成功", "路径:" .. line_file)
+        if not io.exists(line_file) then
+            log.info("验证结果", "testline文件删除验证成功")
+        else
+            log.error("验证结果", "testline文件删除验证失败")
+        end
+    else
+        log.error("os.remove", "testline文件删除失败", "路径:" .. line_file)
+        return
+    end
+
+    if os.remove(file_path) then
+        log.info("os.remove", "文件删除成功", "路径:" .. file_path)
+        if not io.exists(file_path) then
+            log.info("验证结果", "boottime文件删除验证成功")
+        else
+            log.error("验证结果", "boottime文件删除验证失败")
+        end
+    else
+        log.error("os.remove", "boottime文件删除失败", "路径:" .. file_path)
+        return
+    end
+
+    -- 13. 删除目录
+    if io.rmdir(dir_path) then
+        log.info("io.rmdir", "目录删除成功", "路径:" .. dir_path)
+        if not io.dexist(dir_path) then
+            log.info("验证结果", "目录删除验证成功")
+        else
+            log.error("验证结果", "目录删除验证失败")
+        end
+    else
+        log.error("io.rmdir", "目录删除失败", "路径:" .. dir_path)
+        return
+    end
+
+    -- 再次获取文件系统信息,查看空间变化
+    local final_success, final_total_blocks, final_used_blocks, final_block_size, final_fs_type =  io.fsstat(base_path)
+    if final_success then
+        log.info(" io.fsstat", "操作后文件系统信息:", 
+                 "总空间=" .. final_total_blocks .. "块", 
+                 "已用=" .. final_used_blocks .. "块", 
+                 "块大小=" .. final_block_size.."字节",
+                 "类型=" .. final_fs_type)
+    end
+
+    log.info("文件系统操作", "===== 文件系统操作完成 =====")
+end
+
+sys.taskInit(flash_fs_io_task)

+ 85 - 0
module/Air8101/demo/fs_io/http_download_flash.lua

@@ -0,0 +1,85 @@
+--[[
+@module http_download_flash
+@summary HTTP下载文件到内置Flash模块
+@version 1.0.0
+@date    2025.09.23
+@author  王棚嶙
+@usage
+本文件演示的功能为通过HTTP下载文件到内置Flash中:
+1. 网络就绪检测
+2. 创建HTTP下载任务并等待完成
+3. 记录下载结果
+4. 获取并记录文件大小(使用io.fileSize)
+本文件没有对外接口,直接在main.lua中require "http_download_flash"即可
+]]
+
+local function http_download_flash_task()
+
+    
+    -- 要连接的WIFI路由器名称
+    local ssid ="A上海合宙通讯"
+    -- 要连接的WIFI路由器密码
+    local password = "HZ88888888"
+    log.info("wifi", ssid, password)
+    wlan.init()
+    -- 连接WIFI
+    wlan.connect(ssid, password, 1)
+
+    -- 阶段1: 网络就绪检测
+    while not socket.adapter(socket.dft()) do
+        log.warn("HTTP下载", "等待网络连接", socket.dft())
+        sys.waitUntil("IP_READY", 1000)
+    end
+
+    log.info("HTTP下载", "网络已就绪", socket.dft())
+
+    -- 阶段2: 执行下载任务
+    log.info("HTTP下载", "开始下载任务")
+
+    -- 创建下载目录
+    local download_dir = "/downloads"
+    if not io.dexist(download_dir) then
+        io.mkdir(download_dir)
+    end
+
+    -- 核心下载操作开始
+    local code, headers, body_size = http.request("GET",
+                                    "https://www.air32.cn/demo/sample-6s.mp3",
+                                    nil, nil, {dst = download_dir .. "/sample-6s.mp3"}).wait()
+
+    -- 阶段3: 记录下载结果
+    log.info("HTTP下载", "下载完成", 
+        code == 200 and "success" or "error", 
+        code, 
+        json.encode(headers or {}), 
+        body_size) 
+        
+    if code == 200 then
+        -- 获取实际文件大小 (使用io.fileSize接口)
+        local actual_size = io.fileSize(download_dir .. "/sample-6s.mp3")
+        if not actual_size then
+            -- 备用方案
+            actual_size = io.fileSize(download_dir .. "/sample-6s.mp3")
+        end
+        
+        log.info("HTTP下载", "文件大小验证", "预期:", body_size, "实际:", actual_size)
+        
+        if actual_size ~= body_size then
+            log.error("HTTP下载", "文件大小不一致", "预期:", body_size, "实际:", actual_size)
+        end
+        
+        -- 展示下载后的文件系统状态
+        local success, total_blocks, used_blocks, block_size, fs_type =  io.fsstat("/")
+        if success then
+            log.info("HTTP下载", "下载后文件系统信息:", 
+                     "总空间=" .. total_blocks .. "块", 
+                     "已用=" .. used_blocks .. "块", 
+                     "块大小=" .. block_size.."字节",
+                     "类型=" .. fs_type)
+        end
+    end
+
+end
+
+-- 创建下载任务
+sys.taskInit(http_download_flash_task)

+ 78 - 0
module/Air8101/demo/fs_io/main.lua

@@ -0,0 +1,78 @@
+--[[
+@module  main
+@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
+@version 001.000.000
+@date    2025.09.23
+@author  王棚嶙
+@usage
+本 Demo 演示了在Air8101内置Flash文件系统中的完整操作流程:
+1. 基础操作:看门狗守护机制
+2. 文件系统操作:
+   - 文件系统信息查询( io.fsstat)
+   - 文件大小获取(io.fileSize)
+   - 文件创建/读写/追加
+   - 目录创建/删除
+   - 文件重命名/删除
+   - 文件存在性检查
+3. 下载功能:
+   - 网络检测与HTTP文件下载到内置Flash
+]]
+
+--[[
+必须定义PROJECT和VERSION变量,Luatools工具会用到这两个变量,远程升级功能也会用到这两个变量
+PROJECT:项目名,ascii string类型
+        可以随便定义,只要不使用,就行
+VERSION:项目版本号,ascii string类型
+        如果使用合宙iot.openluat.com进行远程升级,必须按照"XXX.YYY.ZZZ"三段格式定义:
+            X、Y、Z各表示1位数字,三个X表示的数字可以相同,也可以不同,同理三个Y和三个Z表示的数字也是可以相同,可以不同
+            因为历史原因,YYY这三位数字必须存在,但是没有任何用处,可以一直写为000
+        如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
+]]
+
+PROJECT = "flash_fs_io_demo"
+VERSION = "001.000.000"
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+-- 添加硬狗防止程序卡死
+if wdt then
+    -- 初始化watchdog设置为9s
+    wdt.init(9000)
+    -- 3s喂一次狗 
+    sys.timerLoopStart(wdt.feed, 3000) 
+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)
+
+--[[在加载以下两个功能时,建议分别打开进行测试,因为文件操作和http下载功能是异步操作。放到一个项目中,如果加载的时间点是随机的,就会出现哪个任务先抢到CPU时间片,哪个就先执行,不符合正常的业务逻辑,用户在参考编程的时候也要注意。]]
+
+-- 加载内置Flash文件系统操作演示模块
+require "flash_fs_io"
+-- 加载HTTP下载存入内置Flash功能演示模块
+-- require "http_download_flash"
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+-- sys.run()之后后面不要加任何语句!!!!!
+sys.run()

+ 143 - 0
module/Air8101/demo/fs_io/readme.md

@@ -0,0 +1,143 @@
+## **功能模块介绍**
+
+本 Demo 演示了在Air8101内置Flash文件系统中的完整操作流程,覆盖了从文件系统读写到高级文件操作的完整功能链。项目分为两个核心模块:
+
+1、main.lua:主程序入口 <br> 
+2、flash_fs_io.lua:内置Flash文件系统的操作测试流程模块,实现文件系统管理、文件操作和目录管理功能。<br> 
+3、http_download_flash.lua:HTTP下载模块,演示HTTP下载文件到内置Flash中的功能
+
+## **演示功能概述**
+
+### 1、主程序入口模块(main.lua)
+
+- 初始化项目信息和版本号
+- 初始化看门狗,并定时喂狗
+- 启动一个循环定时器,每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况方便分析内存使用是否有异常
+- 加载flash_fs_io模块(通过require "flash_fs_io")
+- 加载http_download_flash模块(通过require "http_download_flash")
+- 最后运行sys.run()。
+
+### 2、内置Flash文件系统演示模块(flash_fs_io.lua)
+
+#### 文件操作
+- 获取文件系统信息( io.fsstat)
+- 创建目录:io.mkdir("/flash_demo")
+- 创建/写入文件: io.open("/flash_demo/boottime", "wb")
+- 检查文件存在: io.exists(file_path)
+- 获取文件大小:io.fileSize(file_path)
+- 读取文件内容: io.open(file_path, "rb"):read("*a")
+- 启动计数文件: 记录设备启动次数
+- 文件追加: io.open(append_file, "a+")
+- 按行读取: file:read("*l")
+- 文件关闭: file:close()
+- 文件重命名: os.rename(old_path, new_path)
+- 列举目录: io.lsdir(dir_path)
+- 删除文件: os.remove(file_path)
+- 删除目录: io.rmdir(dir_path)
+
+### 3、HTTP下载功能 (http_download_flash.lua)
+
+
+#### 网络就绪检测
+
+- 连接WiFi
+- 1秒循环等待IP就绪
+- 网络故障处理机制
+
+#### 安全下载
+
+- HTTP下载
+
+#### 结果处理
+
+- 下载状态码解析
+- 自动文件大小验证
+- 获取文件系统信息( io.fsstat)
+
+## **演示硬件环境**
+
+1、Air8101核心板一块
+
+2、TYPE-C USB数据线一根
+
+3、Air8101核心板和数据线的硬件接线方式为
+
+- Air8101核心板通过TYPE-C USB口供电;(正面的开关拨到3.3v,背面的开关拨到off)
+
+- TYPE-C USB数据线直接插到Air8101核心板的TYPE-C USB座子,另外一端连接电脑USB口
+
+## **演示软件环境**
+
+1、Luatools下载调试工具:https://docs.openluat.com/air780epm/common/Luatools/
+
+2、内核固件版本:https://docs.openluat.com/air8101/luatos/firmware/
+
+## **演示核心步骤**
+
+1、搭建好硬件环境
+
+2、通过Luatools将demo与固件烧录到开发板中
+
+3、烧录好后,板子开机将会在Luatools上看到如下打印
+
+```lua
+
+(1)文件操作演示
+[2025-10-22 16:32:47.705] luat:U(172):I/user.文件系统操作 ===== 开始文件系统操作 =====
+[2025-10-22 16:32:47.705] luat:U(174):I/user. io.fsstat成功: 总空间=64块 已用=2块 块大小=4096字节 类型=lfs
+[2025-10-22 16:32:47.767] luat:U(215):I/user.io.mkdir 目录创建成功 路径:/flash_demo
+[2025-10-22 16:32:47.767] luat:U(224):I/user.文件创建 文件写入成功 路径:/flash_demo/boottime
+[2025-10-22 16:32:47.767] luat:U(226):I/user.io.exists 文件存在 路径:/flash_demo/boottime
+[2025-10-22 16:32:47.767] luat:U(228):I/user.io.fileSize 文件大小:59字节 路径:/flash_demo/boottime
+[2025-10-22 16:32:47.767] luat:U(230):I/user.文件读取 路径:/flash_demo/boottime 内容:这是内置Flash文件系统API文档示例的测试内容
+[2025-10-22 16:32:47.767] luat:U(232):I/user.启动计数 文件内容: 这是内置Flash文件系统API文档示例的测试内容 十六进制: E8BF99E698AFE58685E7BDAE466C617368E69687E4BBB6E7B3BBE7BB9F415049E69687E6A1A3E7A4BAE4BE8BE79A84E6B58BE8AF95E58685E5AEB9 118
+[2025-10-22 16:32:47.767] luat:U(233):I/user.启动计数 当前值: 0
+[2025-10-22 16:32:47.767] luat:U(233):I/user.启动计数 更新值: 1
+[2025-10-22 16:32:47.767] luat:U(238):I/user.文件写入 路径:/flash_demo/boottime 内容: 1
+[2025-10-22 16:32:47.831] luat:U(249):I/user.文件创建 路径:/flash_demo/test_a 初始内容:ABC
+[2025-10-22 16:32:47.831] luat:U(255):I/user.文件追加 路径:/flash_demo/test_a 追加内容:def
+[2025-10-22 16:32:47.831] luat:U(257):I/user.文件验证 路径:/flash_demo/test_a 内容:ABCdef 结果: 成功
+[2025-10-22 16:32:47.831] luat:U(266):I/user.文件创建 路径:/flash_demo/testline 写入3行文本
+[2025-10-22 16:32:47.831] luat:U(268):I/user.按行读取 路径:/flash_demo/testline 第1行: abc
+[2025-10-22 16:32:47.831] luat:U(269):I/user.按行读取 路径:/flash_demo/testline 第2行: 123
+[2025-10-22 16:32:47.831] luat:U(269):I/user.按行读取 路径:/flash_demo/testline 第3行: wendal
+[2025-10-22 16:32:47.831] luat:U(277):I/user.os.rename 文件重命名成功 原路径:/flash_demo/test_a 新路径:/flash_demo/renamed_file.txt
+[2025-10-22 16:32:47.831] luat:D(281):vfs:fopen /flash_demo/test_a r not found
+[2025-10-22 16:32:47.831] luat:U(281):I/user.验证结果 重命名验证成功 新文件存在 原文件不存在
+[2025-10-22 16:32:47.831] luat:U(282):I/user.目录操作 ===== 开始目录列举 =====
+[2025-10-22 16:32:47.831] luat:U(291):I/user.fs lsdir [{"name":"boottime","size":1,"type":0},{"name":"renamed_file.txt","size":6,"type":0},{"name":"testline","size":15,"type":0}]
+[2025-10-22 16:32:47.831] luat:U(297):I/user.os.remove 文件删除成功 路径:/flash_demo/renamed_file.txt
+[2025-10-22 16:32:47.831] luat:D(299):vfs:fopen /flash_demo/renamed_file.txt r not found
+[2025-10-22 16:32:47.831] luat:U(299):I/user.验证结果 renamed_file.txt文件删除验证成功
+[2025-10-22 16:32:47.831] luat:U(305):I/user.os.remove testline文件删除成功 路径:/flash_demo/testline
+[2025-10-22 16:32:47.831] luat:D(307):vfs:fopen /flash_demo/testline r not found
+[2025-10-22 16:32:47.831] luat:U(307):I/user.验证结果 testline文件删除验证成功
+[2025-10-22 16:32:47.831] luat:U(313):I/user.os.remove 文件删除成功 路径:/flash_demo/boottime
+[2025-10-22 16:32:47.831] luat:D(315):vfs:fopen /flash_demo/boottime r not found
+[2025-10-22 16:32:47.831] luat:U(316):I/user.验证结果 boottime文件删除验证成功
+[2025-10-22 16:32:47.988] luat:U(326):I/user.io.rmdir 目录删除成功 路径:/flash_demo
+[2025-10-22 16:32:47.988] luat:U(327):I/user.验证结果 目录删除验证成功
+[2025-10-22 16:32:47.988] luat:U(329):I/user. io.fsstat 操作后文件系统信息: 总空间=64块 已用=2块 块大小=4096字节 类型=lfs
+[2025-10-22 16:32:47.988] luat:U(330):I/user.文件系统操作 ===== 文件系统操作完成 =====
+
+
+
+
+
+(2)网络连接与HTTP下载
+[2025-10-24 10:51:10.116] luat:U(2895):I/user.HTTP下载 网络已就绪 2 2
+[2025-10-24 10:51:10.116] luat:U(2896):I/user.HTTP下载 开始下载任务
+[2025-10-24 10:51:10.202] luat:D(2967):DNS:www.air32.cn state 0 id 1 ipv6 0 use dns server0, try 0
+[2025-10-24 10:51:10.202] luat:D(2967):net:adatper 2 dns server 192.168.1.1
+[2025-10-24 10:51:10.202] luat:D(2968):net:dns udp sendto 192.168.1.1:53 from 192.168.1.116
+[2025-10-24 10:51:10.202] luat:D(2971):wlan:sta ip 192.168.1.116
+[2025-10-24 10:51:10.202] luat:D(2971):wlan:设置STA网卡可用
+[2025-10-24 10:51:10.222] luat:I(2989):DNS:dns all done ,now stop
+[2025-10-24 10:51:10.222] luat:D(2990):net:adapter 2 connect 49.232.89.122:443 TCP
+[2025-10-24 10:51:14.599] luat:U(7368):I/user.HTTP下载 下载完成 success 200 {"Last-Modified":"Thu, 23 Oct 2025 03:14:41 GMT","Accept-Ranges":"bytes","ETag":"\"68f99da1-1929e\"","Date":"Fri, 24 Oct 2025 02:51:09 GMT","Connection":"keep-alive","Server":"openresty\/1.27.1.2","Content-Length":"103070","Content-Type":"audio\/mpeg"} 103070
+[2025-10-24 10:51:14.599] luat:U(7384):I/user.HTTP下载 文件大小验证 预期: 103070 实际: 103070
+[2025-10-24 10:51:14.630] luat:U(7409):I/user.HTTP下载 下载后文件系统信息: 总空间=64块 已用=30块 块大小=4096字节 类型=lfs
+
+
+
+```

Некоторые файлы не были показаны из-за большого количества измененных файлов