Преглед изворни кода

add:Air8000下新增httpv2的demo,等2011版本的内核固件发布之后,才能正常使用

zhutianhua пре 7 месеци
родитељ
комит
001694aee7

+ 512 - 0
module/Air8000/demo/httpv2/http_app.lua

@@ -0,0 +1,512 @@
+--[[
+@module  http_app
+@summary http应用功能模块 
+@version 1.0
+@date    2025.08.01
+@author  朱天华
+@usage
+本文件为http应用功能模块,核心业务逻辑为:基于不同的应用场景,演示http核心库的使用方式;
+http核心库和httpplus扩展库的区别如下:
+|   区别项                          |   http核心库                                                               |  httpplus扩展库               |
+| --------------------------------- | ------------------------------------------------------------------------- | ---------------------------- |
+|    文件上传                        |    文件最大64KB                                                           |    只要内存够用,文件大小不限   |
+|    文件下载                        |    支持,只要文件系统空间够用,文件大小不限                                  |    不支持                     |
+|    http header的key: value的限制   |    所有header的value总长度不能超过4KB,单个header的value长度不能超过1KB      |    只要内存够用,header长度不限 |
+|    鉴权URL自动识别                 |    不支持                                                                 |    支持                        |
+|    接收到的body数据存储支持zbuff      |    不支持                                                               |    支持,可以直接传输给uart等库 |
+|    接收到的body数据存储到内存中     |    最大支持32KB                                                            |    只要内存够用,大小不限       |
+|    chunk编码                       |    支持                                                                   |    不支持                       |
+
+
+本文件没有对外接口,直接在main.lua中require "http_app"就可以加载运行;
+]]
+
+
+--[[
+此处先详细解释下http.request接口的使用方法
+
+接口定义:
+    http.request(method, url, headers, body, opts, server_ca_cert, client_cert, client_key, client_password)
+
+使用方法:
+    local code, headers, body = http.request(method, url, headers, body, opts, server_ca_cert, client_cert, client_key, client_password).wait()
+    只能在task中使用
+    发送http请求到服务器,等待服务器的http应答,此处会阻塞当前task,等待整个过程成功结束或者出现错误异常结束或者超时结束
+
+参数定义:
+    method,stirng类型,必须包含此参数,表示HTTP请求方法,支持"GET"、"POST"、"HEAD"等所有HTTP请求方法
+    url,string类型,必须包含此参数,表示HTTP请求URL地址,支持HTTP、HTTPS,支持域名、IP地址,支持自定义端口,标准的HTTP URL格式都支持
+    headers,table或者nil类型,可选包含此参数,表示HTTP请求头,例如 {["Content-Type"] = "application/x-www-form-urlencoded", ["self_defined_key"] = "self_defined_value"} 
+    body,string或者zbuff或者nil类型,可选包含此参数,表示HTTP请求体,如果请求体是一个文件中的内容,要把文件内容读出来,赋值给body使用
+    opts,table或者nil类型,可选包含此参数,表示HTTP请求的一些额外配置,包含以下内容
+    {
+        timeout    -- -- number或者nil类型,单位毫秒,可选包含此参数,表示从发送请求到读取到服务器响应整个过程的超时时间,如果传入0,表示永久等待;如果没有传入此参数或者传入nil,则使用默认值10分钟
+        dst        -- 下载路径,string类型,当HTTP请求的数据需要保存到文件中时,此处填写完整的文件路径
+        adapter    -- 使用的网卡ID,number类型,例如4G网卡,SPI外挂以太网卡,WIFI网卡等;如果没有传入此参数,内核固件会自动选择当前时间点其他功能模块设置的默认网卡
+                    -- 除非你HTTP请求时,一定要使用某一种网卡,才设置此参数;如果没什么特别要求,不要使用此参数,使用系统中设置的默认网卡即可
+                    -- 这个参数和本demo中的netdrv_device.lua关系比较大,netdrv_device会设置默认网卡,此处http不要设置adapter参数,直接使用netdrv_device设置的默认网卡就行
+        debug      -- 调试开关,bool类型,true表示打开debug调试信息日志,false表示关闭debug调试信息日志,默认为关闭状态
+        ipv6       -- 是否为ipv6,bool类型,true表示使用ipv6,false表示不使用ipv6,默认为false
+        userdata   -- 下载回调函数使用的用户自定义回调参数,做为callback回调函数的第三个参数使用
+        callback   -- 下载回调函数,function类型,当下载数据时,无论是保存到内存中,还是保存到文件系统中,如果设置了callback,内核固件中每收到一包body数据,都会自动执行一次callback回调函数
+                    -- 回调函数的调用形式为callback(content_len, body_len, userdata)
+                    --     content_len:number类型,数据总长度
+                    --     body_len:number类型,已经下载的数据长度
+                    --     userdata:下载回调函数使用的用户自定义回调参数
+    }
+    server_ca_cert,string类型,服务器ca证书数据,可选包含此参数,当客户端需要验证服务器证书时,需要此参数,如果证书数据在一个文件中,要把文件内容读出来,赋值给server_ca_cert
+    client_cert,string类型,客户端证书数据,可选包含此参数,当服务器需要验证客户端证书时,需要此参数,如果证书数据在一个文件中,要把文件内容读出来,赋值给client_cert
+    client_key, string类型,客户端加密后的私钥数据,可选包含此参数,当服务器需要验证客户端证书时,需要此参数,如果加密后的私钥数据在一个文件中,要把文件内容读出来,赋值给client_key
+    client_password,string类型,客户端私钥口令数据,可选包含此参数,当服务器需要验证客户端证书时,需要此参数,如果私钥口令数据在一个文件中,要把文件内容读出来,赋值给client_password
+
+返回值定义:
+
+    http.request().wait()有三个返回值code,headers,body
+    code表示执行结果,number类型,有以下两种含义:
+        1、code大于等于100时,表示服务器返回的HTTP状态码,例如200表示成功,详细说明可以通过搜索引擎搜索“HTTP状态码”自行了解
+        2、code小于0时,表示内核固件中检测到通信异常,有如下几种:
+            -1 HTTP_ERROR_STATE 错误的状态, 一般是底层异常,请报issue
+            -2 HTTP_ERROR_HEADER 错误的响应头部, 通常是服务器问题
+            -3 HTTP_ERROR_BODY 错误的响应体,通常是服务器问题
+            -4 HTTP_ERROR_CONNECT 连接服务器失败, 未联网,地址错误,域名错误
+            -5 HTTP_ERROR_CLOSE 提前断开了连接, 网络或服务器问题
+            -6 HTTP_ERROR_RX 接收数据报错, 网络问题
+            -7 HTTP_ERROR_DOWNLOAD 下载文件过程报错, 网络问题或下载路径问题
+            -8 HTTP_ERROR_TIMEOUT 超时, 包括连接超时,读取数据超时
+            -9 HTTP_ERROR_FOTA fota功能报错,通常是更新包不合法  
+    headers有以下两种含义:
+        1、当code的返回值大于等于100时,headers表示服务器返回的应答头,table类型
+        2、当code的返回值小于0时,headers为nil        
+    body有以下三种含义
+        1、当code的返回值大于等于100时,如果请求的body数据不需要保存到文件中,而是直接保存到内存中,则body表示请求到的数据内容,string类型
+        2、当code的返回值大于等于100时,如果请求的body数据需要保存到文件中,则body表示保存请求数据后的文件的大小,number类型
+        3、当code的返回值小于0时,body为nil
+]]
+
+
+-- http下载数据回调函数
+-- content_len:number类型,数据总长度
+-- body_len:number类型,已经下载的数据长度
+-- userdata:下载回调函数使用的用户自定义回调参数
+-- 每收到一包body数据,就会调用一次http_cbfunc回调函数
+local function http_cbfunc(content_len, body_len, userdata)
+    log.info("http_cbfunc", content_len, body_len, userdata)
+end
+
+
+-- 普通的http get请求功能演示
+-- 请求的body数据保存到内存变量中,在内存够用的情况下,最大支持32KB的数据存储到内存中
+-- timeout可以设置超时时间
+-- callback可以设置回调函数,可用于实时检测body数据的下载进度
+local function http_app_get()
+    -- https get请求https://www.air32.cn/网页内容
+    -- 如果请求成功,请求的数据保存到body中
+    local code, headers, body = http.request("GET", "https://www.air32.cn/").wait()
+    log.info("http_app_get1", 
+        code==200 and "success" or "error", 
+        code, 
+        json.encode(headers or {}), 
+        body and (body:len()>512 and body:len() or body) or "nil")
+
+    -- https get请求https://www.luatos.com/网页内容
+    -- 请求超时时间为3秒,用户自己写代码时,不要照抄10秒,根据自己业务逻辑的需要设置合适的超时时间
+    -- 回调函数为http_cbfunc,回调函数使用的第三个回调参数为"http_app_get2"
+    -- 如果请求成功,请求的数据保存到body中
+    code, headers, body = http.request("GET", "https://www.luatos.com/", nil, nil, {timeout=10000, userdata="http_app_get2", callback=http_cbfunc}).wait()
+    log.info("http_app_get2", 
+        code==200 and "success" or "error", 
+        code, 
+        json.encode(headers or {}), 
+        body and (body:len()>512 and body:len() or body) or "nil")
+
+    -- http get请求http://httpbin.air32.cn/get网页内容,超时时间为3秒
+    -- 请求超时时间为3秒,用户自己写代码时,不要照抄3秒,根据自己业务逻辑的需要设置合适的超时时间
+    -- 回调函数为http_cbfunc,回调函数使用的第三个回调参数为"http_app_get3"
+    -- 如果请求成功,请求的数据保存到body中
+    code, headers, body = http.request("GET", "http://httpbin.air32.cn/get", nil, nil, {timeout=3000, userdata="http_app_get3", callback=http_cbfunc}).wait()
+    log.info("http_app_get3", 
+        code==200 and "success" or "error", 
+        code, 
+        json.encode(headers or {}), 
+        body and (body:len()>512 and body:len() or body) or "nil")
+end
+
+
+-- http get下载压缩数据的功能演示
+local function http_app_get_gzip()
+    -- https get请求https://devapi.qweather.com/v7/weather/now?location=101010100&key=0e8c72015e2b4a1dbff1688ad54053de网页内容,超时时间为3秒
+    -- 如果请求成功,请求的数据保存到body中
+    local code, headers, body = http.request("GET", "https://devapi.qweather.com/v7/weather/now?location=101010100&key=0e8c72015e2b4a1dbff1688ad54053de").wait()
+    log.info("http_app_get_gzip", 
+        code==200 and "success" or "error", 
+        code, 
+        json.encode(headers or {}), 
+        body and (body:len()>512 and body:len() or body) or "nil")
+
+    -- 如果请求成功
+    if code == 200 then
+        -- 从body的第11个字节开始解压缩
+        local uncompress_data = miniz.uncompress(body:sub(11,-1), 0)
+        if not uncompress_data then
+            log.error("http_app_get_gzip uncompress error")
+            return
+        end
+
+        local json_data = json.decode(uncompress_data)
+        if not json_data then
+            log.error("http_app_get_gzip json.decode error")
+            return
+        end
+
+        log.info("http_app_get_gzip json_data", json_data)
+        log.info("http_app_get_gzip", "和风天气", json_data.code)
+        if json_data.now then
+            log.info("http_app_get_gzip", "和风天气", "天气", json_data.now.text)
+            log.info("http_app_get_gzip", "和风天气", "温度", json_data.now.temp)
+        end
+    end
+end
+
+
+-- http get下载数据保存到文件中的功能演示
+-- 请求的body数据保存到文件中,在文件系统够用的情况下,文件大小不限
+-- timeout可以设置超时时间
+-- callback可以设置回调函数,可用于实时检测文件下载进度
+local function http_app_get_file()
+
+    -- 创建/http_download目录,用来存放通过http下载的文件
+    -- 重复创建目录会返回失败
+    -- 在创建目录之前可以使用api判断下目录是否存在
+    -- 不过只有最新版本的内核固件才支持判断目录是否存在的api
+    -- 在编写本demo时还没有这个api
+    -- 如果Luatools烧录软件时,没有勾选 清除FS分区,此处日志有可能输出error
+    -- 如果输出error,不用理会,不会影响后续逻辑的执行
+    -- 等后续的新版本内核固件支持 判断目录是否存在 的api之后,再加上api判断
+    local download_dir = "/http_download/"
+    local result, reason = io.mkdir(download_dir)
+    if not result then
+        log.error("http_app_get_file io.mkdir error", reason)
+    end
+
+    
+    local file_path = download_dir.."get_file1.html"
+    -- https get请求https://www.air32.cn/网页内容
+    -- 如果请求成功,请求的数据保存到文件file_path中
+    local code, headers, body_size = http.request("GET", "https://www.air32.cn/", nil, nil, {dst=file_path}).wait()
+    log.info("http_app_get_file1", 
+        code==200 and "success" or "error", 
+        code, 
+        json.encode(headers or {}), 
+        body_size)
+
+    -- 如果下载成功
+    if code==200 then
+        -- 读取文件大小
+        local size = io.fileSize(file_path)
+        log.info("http_app_get_file1", "io.fileSize="..size)
+
+        if size~=body_size then
+            log.error("io.fileSize doesn't equal with body_size, error", size, body_size)
+        end
+
+        --文件使用完之后,如果以后不再用到,根据需要可以自行删除
+        os.remove(file_path)
+    end
+ 
+
+
+
+    file_path = download_dir.."get_file2.html"
+    -- https get请求https://www.luatos.com/网页内容
+    -- 请求超时时间为3秒,用户自己写代码时,不要照抄10秒,根据自己业务逻辑的需要设置合适的超时时间
+    -- 回调函数为http_cbfunc,回调函数使用的第三个回调参数为"http_app_get_file2"
+    -- 如果请求成功,请求的数据保存到文件file_path中
+    code, headers, body_size = http.request("GET", "https://www.luatos.com/", nil, nil, {dst=file_path, timeout=10000, userdata="http_app_get_file2", callback=http_cbfunc}).wait()
+    log.info("http_app_get_file2", 
+        code==200 and "success" or "error", 
+        code, 
+        json.encode(headers or {}), 
+        body_size)
+
+    -- 如果下载成功
+    if code==200 then
+        -- 读取文件大小
+        local size = io.fileSize(file_path)
+        log.info("http_app_get_file2", "io.fileSize="..size)
+
+        if size~=body_size then
+            log.error("io.fileSize doesn't equal with body_size, error", size, body_size)
+        end
+
+        --文件使用完之后,如果以后不再用到,根据需要可以自行删除
+        os.remove(file_path)
+    end
+
+
+
+
+    file_path = download_dir.."get_file3.html"
+    -- http get请求http://httpbin.air32.cn/get网页内容,超时时间为3秒
+    -- 请求超时时间为3秒,用户自己写代码时,不要照抄3秒,根据自己业务逻辑的需要设置合适的超时时间
+    -- 回调函数为http_cbfunc,回调函数使用的第三个回调参数为"http_app_get_file3"
+    -- 如果请求成功,请求的数据保存到文件file_path中
+    code, headers, body_size = http.request("GET", "http://httpbin.air32.cn/get", nil, nil, {dst=file_path, timeout=3000, userdata="http_app_get_file3", callback=http_cbfunc}).wait()
+    log.info("http_app_get_file3", 
+        code==200 and "success" or "error", 
+        code, 
+        json.encode(headers or {}), 
+        body_size)
+
+    -- 如果下载成功
+    if code==200 then
+        -- 读取文件大小
+        local size = io.fileSize(file_path)
+        log.info("http_app_get_file3", "io.fileSize="..size)
+
+        if size~=body_size then
+            log.error("io.fileSize doesn't equal with body_size, error", size, body_size)
+        end
+
+        --文件使用完之后,如果以后不再用到,根据需要可以自行删除
+        os.remove(file_path)
+    end
+end
+
+
+-- http post提交表单数据功能演示
+local function http_app_post_form()
+    local params = {
+        username = "LuatOS",
+        password = "123456"
+    }
+    local body = ""
+    -- 拼接成url编码的键值对的形式
+    for k, v in pairs(params) do
+        body = body .. k .. "=" .. tostring(v):urlEncode() .. "&"
+    end
+    -- 删除最后一位的&字符,最终为string类型的username=LuatOS&password=123456
+    body = body:sub(1,-2)
+
+    -- http post提交表单数据
+    -- http://httpbin.air32.cn/post为回环测试服务器,服务器收到post提交的表单数据后,还会下发同样的表单数据给设备
+    -- ["Content-Type"] = "application/x-www-form-urlencoded" 表示post提交的body数据格式为url编码的键值对形式的表单数据
+    -- 如果请求成功,服务器应答的数据会保存到resp_body中
+    local code, headers, resp_body = http.request("POST", "http://httpbin.air32.cn/post", {["Content-Type"] = "application/x-www-form-urlencoded"}, body).wait()
+    log.info("http_app_post_form", 
+        code==200 and "success" or "error", 
+        code, 
+        json.encode(headers or {}), 
+        resp_body and (resp_body:len()>512 and resp_body:len() or resp_body) or "nil")
+end
+
+
+-- http post提交json数据功能演示
+local function http_app_post_json()
+    local params = {
+        username = "LuatOS",
+        password = "123456"
+    }
+    local body = json.encode(params)
+
+    -- http post提交json数据
+    -- http://httpbin.air32.cn/post为回环测试服务器,服务器收到post提交的json数据后,还会下发同样的json数据给设备
+    -- ["Content-Type"] = "application/json" 表示post提交的body数据格式为json格式的数据
+    -- 如果请求成功,服务器应答的数据会保存到resp_body中
+    local code, headers, resp_body = http.request("POST", "http://httpbin.air32.cn/post", {["Content-Type"] = "application/json"}, body).wait()
+    log.info("http_app_post_json", 
+        code==200 and "success" or "error", 
+        code, 
+        json.encode(headers or {}), 
+        resp_body and (resp_body:len()>512 and resp_body:len() or resp_body) or "nil")
+end
+
+
+-- http post提交纯文本数据功能演示
+local function http_app_post_text()
+    -- http post提交纯文本数据
+    -- http://httpbin.air32.cn/post为回环测试服务器,服务器收到post提交的纯文本数据后,还会下发同样的纯文本数据给设备
+    -- ["Content-Type"] = "text/plain" 表示post提交的body数据格式为纯文本格式的数据
+    -- 如果请求成功,服务器应答的数据会保存到resp_body中
+    local code, headers, resp_body = http.request("POST", "http://httpbin.air32.cn/post", {["Content-Type"] = "text/plain"}, "This is a raw text message from LuatOS device").wait()
+    log.info("http_app_post_text", 
+        code==200 and "success" or "error", 
+        code, 
+        json.encode(headers or {}), 
+        resp_body and (resp_body:len()>512 and resp_body:len() or resp_body) or "nil")
+end
+
+
+-- http post提交xml数据功能演示
+local function http_app_post_xml()
+    -- [=[ 和 ]=] 之间是一个多行字符串
+    local body = [=[
+        <?xml version="1.0" encoding="UTF-8"?>
+        <user>
+            <name>LuatOS</name>
+            <password>123456</password>
+        </user>
+    ]=]
+
+    -- http post提交xml数据
+    -- http://httpbin.air32.cn/post为回环测试服务器,服务器收到post提交的xml数据后,还会下发同样的xml数据给设备
+    -- ["Content-Type"] = "text/xml" 表示post提交的body数据格式为xml格式的数据
+    -- 如果请求成功,服务器应答的数据会保存到resp_body中
+    local code, headers, resp_body = http.request("POST", "http://httpbin.air32.cn/post", {["Content-Type"] = "text/xml"}, body).wait()
+    log.info("http_app_post_xml", 
+        code==200 and "success" or "error", 
+        code, 
+        json.encode(headers or {}), 
+        resp_body and (resp_body:len()>512 and resp_body:len() or resp_body) or "nil")
+end
+
+
+-- http post提交原始二进制数据功能演示
+local function http_app_post_binary()
+    local body = io.readFile("/luadb/logo.jpg")
+
+    -- http post提交原始二进制数据
+    -- http://upload.air32.cn/api/upload/jpg为jpg图片上传测试服务器
+    -- 此处将logo.jpg的原始二进制数据做为body上传到服务器
+    -- 上传成功后,电脑上浏览器打开https://www.air32.cn/upload/data/jpg/,打开对应的测试日期目录,点击具体的测试时间照片,可以查看上传的照片
+    -- ["Content-Type"] = "application/octet-stream" 表示post提交的body数据格式为原始二进制格式的数据
+    -- 如果请求成功,服务器应答的数据会保存到resp_body中
+    local code, headers, resp_body = http.request("POST", "http://upload.air32.cn/api/upload/jpg", {["Content-Type"] = "application/octet-stream"}, body).wait()
+    log.info("http_app_post_binary", 
+        code==200 and "success" or "error", 
+        code, 
+        json.encode(headers or {}), 
+        resp_body and (resp_body:len()>512 and resp_body:len() or resp_body) or "nil")
+end
+
+
+local function post_multipart_form_data(url, params)
+    local boundary = "----WebKitFormBoundary"..os.time()
+    local req_headers = {
+        ["Content-Type"] = "multipart/form-data; boundary="..boundary,
+    }
+    local body = {}
+
+    -- 解析拼接 body
+    for k,v in pairs(params) do
+        if k=="texts" then
+            local bodyText = ""
+            for kk,vv in pairs(v) do
+                print(kk,vv)
+                bodyText = bodyText.."--"..boundary.."\r\nContent-Disposition: form-data; name=\""..kk.."\"\r\n\r\n"..vv.."\r\n"
+            end
+            table.insert(body, bodyText)
+        elseif k=="files" then
+            local contentType =
+            {
+                txt = "text/plain",             -- 文本
+                jpg = "image/jpeg",             -- JPG 格式图片
+                jpeg = "image/jpeg",            -- JPEG 格式图片
+                png = "image/png",              -- PNG 格式图片   
+                gif = "image/gif",              -- GIF 格式图片
+                html = "image/html",            -- HTML
+                json = "application/json",      -- JSON
+            }
+            
+            for kk,vv in pairs(v) do
+                if type(vv) == "table" then
+                    for i=1, #vv do
+                        print(kk,vv[i])
+                        table.insert(body, "--"..boundary.."\r\nContent-Disposition: form-data; name=\""..kk.."\"; filename=\""..vv[i]:match("[^%/]+%w$").."\"\r\nContent-Type: "..contentType[vv[i]:match("%.(%w+)$")].."\r\n\r\n")
+                        table.insert(body, io.readFile(vv[i]))
+                        table.insert(body, "\r\n")
+                    end
+                else
+                    print(kk,vv)
+                    table.insert(body, "--"..boundary.."\r\nContent-Disposition: form-data; name=\""..kk.."\"; filename=\""..vv:match("[^%/]+%w$").."\"\r\nContent-Type: "..contentType[vv:match("%.(%w+)$")].."\r\n\r\n")
+                    table.insert(body, io.readFile(vv))
+                    table.insert(body, "\r\n")
+                end
+            end
+        end
+    end 
+    table.insert(body, "--"..boundary.."--\r\n")
+    body = table.concat(body)
+    log.info("headers: ", "\r\n" .. json.encode(req_headers), type(body))
+    log.info("body: " .. body:len() .. "\r\n" .. body)
+
+    local code, headers, resp_body = http.request("POST", url, req_headers, body).wait()  
+    log.info("post_multipart_form_data", 
+        code==200 and "success" or "error", 
+        code, 
+        json.encode(headers or {}), 
+        resp_body and (resp_body:len()>512 and resp_body:len() or resp_body) or "nil")
+    
+end
+
+
+-- http post文件上传功能演示
+local function http_app_post_file()
+    -- 此接口post_multipart_form_data支持单文件上传、多文件上传、单文本上传、多文本上传、单/多文本+单/多文件上传
+    -- http://airtest.openluat.com:2900/uploadFileToStatic 仅支持单文件上传,并且上传的文件name必须使用"uploadFile"
+    -- 所以此处仅演示了单文件上传功能,并且"uploadFile"不能改成其他名字,否则会出现上传失败的应答
+    -- 如果你自己的http服务支持更多类型的文本/文件混合上传,可以打开注释自行验证
+    post_multipart_form_data(
+        "http://airtest.openluat.com:2900/uploadFileToStatic",
+        {
+            -- texts = 
+            -- {
+            --     ["username"] = "LuatOS",
+            --     ["password"] = "123456"
+            -- },
+            
+            files =
+            {
+                ["uploadFile"] = "/luadb/logo.jpg",
+                -- ["logo1.jpg"] = "/luadb/logo.jpg",
+            }
+        }
+    )
+end
+
+
+
+-- http app task 的任务处理函数
+local function http_app_task_func() 
+    while true do
+        -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+        while not socket.adapter(socket.dft()) do
+            log.warn("http_app_task_func", "wait IP_READY", socket.dft())
+            -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+            -- 或者等待1秒超时退出阻塞等待状态;
+            -- 注意:此处的1000毫秒超时不要修改的更长;
+            -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+            -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+            -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+            sys.waitUntil("IP_READY", 1000)
+        end
+
+        -- 检测到了IP_READY消息
+        log.info("http_app_task_func", "recv IP_READY", socket.dft())
+
+        -- 普通的http get请求功能演示
+        http_app_get()
+        -- http get下载压缩数据的功能演示
+        http_app_get_gzip()
+        -- http get下载数据保存到文件中的功能演示
+        http_app_get_file()
+        -- http post提交表单数据功能演示
+        http_app_post_form()
+        -- http post提交json数据功能演示
+        http_app_post_json()
+        -- http post提交纯文本数据功能演示
+        http_app_post_text()
+        -- http post提交xml数据功能演示
+        http_app_post_xml()
+        -- http post提交原始二进制数据功能演示
+        http_app_post_binary()
+        -- http post文件上传功能演示
+        http_app_post_file()
+
+        -- 60秒之后,循环测试
+        sys.wait(60000)
+    end
+end
+
+--创建并且启动一个task
+--运行这个task的处理函数http_app_task_func
+sys.taskInit(http_app_task_func)

+ 360 - 0
module/Air8000/demo/httpv2/httpplus_app.lua

@@ -0,0 +1,360 @@
+--[[
+@module  httpplus
+@summary httpplus应用功能模块 
+@version 1.0
+@date    2025.08.06
+@author  朱天华
+@usage
+本文件为httpplus应用功能模块,核心业务逻辑为:基于不同的应用场景,演示httpplus扩展库的使用方式;
+http核心库和httpplus扩展库的区别如下:
+|   区别项                          |   http核心库                                                               |  httpplus扩展库               |
+| --------------------------------- | ------------------------------------------------------------------------- | ---------------------------- |
+|    文件上传                        |    文件最大64KB                                                           |    只要内存够用,文件大小不限   |
+|    文件下载                        |    支持,只要文件系统空间够用,文件大小不限                                  |    不支持                     |
+|    http header的key: value的限制   |    所有header的value总长度不能超过4KB,单个header的value长度不能超过1KB      |    只要内存够用,header长度不限 |
+|    鉴权URL自动识别                 |    不支持                                                                 |    支持                        |
+|    接收到的body数据存储支持zbuff      |    不支持                                                               |    支持,可以直接传输给uart等库 |
+|    接收到的body数据存储到内存中     |    最大支持32KB                                                            |    只要内存够用,大小不限       |
+|    chunk编码                       |    支持                                                                   |    不支持                       |
+
+
+本文件没有对外接口,直接在main.lua中require "httpplus_app"就可以加载运行;
+]]
+
+
+--[[
+此处先详细解释下httpplus.request接口的使用方法
+
+接口定义:
+    httpplus.request(opts)
+
+使用方法:
+    local code, response = httpplus.request(opts)
+    只能在task中使用
+    发送http请求到服务器,等待服务器的http应答,此处会阻塞当前task,等待整个过程成功结束或者出现错误异常结束或者超时结束
+
+参数定义:
+    opts,table类型,表示HTTP请求参数,包含以下内容
+    {
+        url        -- string类型,必须包含此参数,表示HTTP请求URL地址,支持HTTP、HTTPS,支持域名、IP地址,支持自定义端口,标准的HTTP URL格式都支持
+        method     -- stirng或者nil类型,可选包含此参数,表示HTTP请求方法,支持"GET"、"POST"、"HEAD"等所有HTTP请求方法,如果没有传入此参数或者传入了nil类型,则使用默认值,默认值分为以下两种情况:
+                   -- 如果没有设置files,forms,body,bodyfile参数,则默认为"GET"
+                   -- 如果至少设置了files,forms,body,bodyfile中的一种参数,则默认为"POST"
+        headers    -- table或者nil类型,可选包含此参数,表示自定义的一个或者多个HTTP请求头,例如 {["self_defined_key1"] = "self_defined_value1", ["self_defined_key2"] = "self_defined_value2"}
+        timeout    -- number或者nil类型,单位秒,可选包含此参数,表示从发送请求到读取到服务器响应整个过程的超时时间,如果传入0,表示永久等待;如果没有传入此参数或者传入nil,则使用默认值30秒
+        files      -- table或者nil类型,可选包含此参数,表示POST上传的一个或者多个文件列表,键值对的形式,若存在本参数,会自动强制以multipart/form-data形式上传;例如
+                   -- {
+                   --     ["uploadFile"] = "/luadb/logo.jpg",
+                   --     ["logo1.jpg"] = "/luadb/logo.jpg",
+                   -- }
+
+        forms      -- table或者nil类型,可选包含此参数,表示POST上传的一个或者多个表单参数列表,键值对的形式
+                   -- 若存在本参数并且不存在files参数,会自动强制以application/x-www-form-urlencoded形式上传
+                   -- 若存在本参数并且存在files参数,会自动强制以multipart/form-data形式上传,也就是说支持同时上传文件和表单参数
+                   -- 例如:
+                   -- {
+                   --     ["username"] = "LuatOS",
+                   --     ["password"] = "123456",
+                   -- } 
+        body       -- string,zbuff,table或者nil类型,可选包含此参数,表示自定义的body内容, 不能与files或者forms同时存在
+        bodyfile   -- string或者nil类型,可选包含此参数,表示要上传的一个文件的路径,会自动读取文件中的内容进行上传
+                   -- 不能与files或者forms同时存在
+                   -- 可以与body同时存在,与body同时存在时, 优先级高于body参数,也就是说,bodyfile对应的文件路径中的内容在body参数对应的内容之前
+        debug      -- bool或者nil类型,可选包含此参数,表示调试开关,true表示打开debug调试信息日志,false表示关闭debug调试信息日志,如果没有传入此参数或者传入了nil类型,则使用默认值false
+        try_ipv6   -- bool或者nil类型,可选包含此参数,表示是否优先尝试ipv6地址,true表示优先尝试使用ipv6,false表示不尝试使用ipv6,如果没有传入此参数或者传入了nil类型,则使用默认值false
+        adapter    -- number或者nil类型,可选包含此参数,表示使用的网卡ID,例如4G网卡,SPI外挂以太网卡,WIFI网卡等;如果没有传入此参数,内核固件会自动选择当前时间点其他功能模块设置的默认网卡
+                   -- 除非你HTTP请求时,一定要使用某一种网卡,才设置此参数;如果没什么特别要求,不要使用此参数,使用系统中设置的默认网卡即可
+                   -- 这个参数和本demo中的netdrv_device.lua关系比较大,netdrv_device会设置默认网卡,此处http不要设置adapter参数,直接使用netdrv_device设置的默认网卡就行
+    }
+
+返回值定义:
+    httpplus.request(opts)有两个返回值code,response
+    code表示执行结果,number类型,有以下两种含义:
+        1、code大于等于100时,表示服务器返回的HTTP状态码,例如200表示成功,详细说明可以通过搜索引擎搜索“HTTP状态码”自行了解
+        2、code小于0时,表示内核固件中检测到通信异常,有如下几种:
+            -1 HTTP_ERROR_STATE 错误的状态, 一般是底层异常,请报issue
+            -2 HTTP_ERROR_HEADER 错误的响应头部, 通常是服务器问题
+            -3 HTTP_ERROR_BODY 错误的响应体,通常是服务器问题
+            -4 HTTP_ERROR_CONNECT 连接服务器失败, 未联网,地址错误,域名错误
+            -5 HTTP_ERROR_CLOSE 提前断开了连接, 网络或服务器问题
+            -6 HTTP_ERROR_RX 接收数据报错, 网络问题
+            -7 HTTP_ERROR_DOWNLOAD 下载文件过程报错, 网络问题或下载路径问题
+            -8 HTTP_ERROR_TIMEOUT 超时, 包括连接超时,读取数据超时
+            -9 HTTP_ERROR_FOTA fota功能报错,通常是更新包不合法     
+    response有以下两种含义
+        1、当code的返回值大于等于100时,response为table类型,包含以下两项内容
+           {
+               headers = {},    -- table类型,一个或者多个应答头,键值对的形式,可以使用json.encode(response.headers)在日志中打印
+               body = ,         -- zbuff类型,应答体数据;通过zbuff的query函数,可以转化为string类型:response.body:query();也可以通过uart.tx等支持zbuff的函数直接使用,例如uart.tx(1, response.body)
+           }
+        2、当code的返回值小于0时,response为nil
+]]
+
+local httpplus = require "httpplus"
+
+
+-- 普通的http get请求功能演示
+-- 请求的body数据保存到内存变量中,在内存够用的情况下,长度不限
+-- timeout可以设置超时时间
+local function httpplus_app_get()
+    local body
+    -- https get请求https://www.air32.cn/网页内容
+    -- 如果请求成功,请求的数据保存到response.body中
+    local code, response = httpplus.request({url="https://www.air32.cn/"})
+    log.info("httpplus_app_get1", code==200 and "success" or "error", code)
+    if code==200 then
+        log.info("httpplus_app_get1 headers", json.encode(response.headers or {}))
+        body = response.body:query()
+        log.info("httpplus_app_get1 body", body and (body:len()>512 and body:len() or body) or "nil")
+    end
+
+
+    -- http get请求http://httpbin.air32.cn/get网页内容,超时时间为3秒
+    -- 请求超时时间为3秒,用户自己写代码时,不要照抄3秒,根据自己业务逻辑的需要设置合适的超时时间
+    -- 如果请求成功,请求的数据保存到body中
+    code, response = httpplus.request({url="http://httpbin.air32.cn/get", timeout=3})
+    log.info("httpplus_app_get2", code==200 and "success" or "error", code)
+    if code==200 then
+        log.info("httpplus_app_get2 headers", json.encode(response.headers or {}))
+        body = response.body:query()
+        log.info("httpplus_app_get2 body", body and (body:len()>512 and body:len() or body) or "nil")
+    end
+end
+
+
+-- http get下载压缩数据的功能演示
+local function httpplus_app_get_gzip()
+    local body
+    -- https get请求https://devapi.qweather.com/v7/weather/now?location=101010100&key=0e8c72015e2b4a1dbff1688ad54053de网页内容,超时时间为3秒
+    -- 如果请求成功,请求的数据保存到response.body中
+    local code, response = httpplus.request({url="https://devapi.qweather.com/v7/weather/now?location=101010100&key=0e8c72015e2b4a1dbff1688ad54053de"})
+    log.info("httpplus_app_get_gzip", code==200 and "success" or "error", code)
+    if code==200 then
+        log.info("httpplus_app_get_gzip headers", json.encode(response.headers or {}))
+        body = response.body:query()
+        log.info("httpplus_app_get_gzip body", body and (body:len()>512 and body:len() or body) or "nil")
+    end
+
+    -- 如果请求成功
+    if code == 200 then
+        -- 从body的第11个字节开始解压缩
+        local uncompress_data = miniz.uncompress(body:sub(11,-1), 0)
+        if not uncompress_data then
+            log.error("httpplus_app_get_gzip uncompress error")
+            return
+        end
+
+        local json_data = json.decode(uncompress_data)
+        if not json_data then
+            log.error("httpplus_app_get_gzip json.decode error")
+            return
+        end
+
+        log.info("httpplus_app_get_gzip json_data", json_data)
+        log.info("httpplus_app_get_gzip", "和风天气", json_data.code)
+        if json_data.now then
+            log.info("httpplus_app_get_gzip", "和风天气", "天气", json_data.now.text)
+            log.info("httpplus_app_get_gzip", "和风天气", "温度", json_data.now.temp)
+        end
+    end
+end
+
+
+-- http post提交表单数据功能演示
+local function httpplus_app_post_form()
+    -- http post提交表单数据
+    -- http://httpbin.air32.cn/post为回环测试服务器,服务器收到post提交的表单数据后,还会下发同样的表单数据给设备
+    -- 如果请求成功,服务器应答的数据会保存到response.body中
+    local code, response = httpplus.request(
+    {
+        url = "http://httpbin.air32.cn/post",
+        forms = {username="LuatOS", password="123456"}
+    })
+    log.info("httpplus_app_post_form", code==200 and "success" or "error", code)
+    if code==200 then
+        log.info("httpplus_app_post_form headers", json.encode(response.headers or {}))
+        local body = response.body:query()
+        log.info("httpplus_app_post_form body", body and (body:len()>512 and body:len() or body) or "nil")
+    end
+end
+
+
+-- http post提交json数据功能演示
+local function httpplus_app_post_json()
+    local params = {
+        username = "LuatOS",
+        password = "123456"
+    }
+
+    -- http post提交json数据
+    -- http://httpbin.air32.cn/post为回环测试服务器,服务器收到post提交的json数据后,还会下发同样的json数据给设备
+    -- ["Content-Type"] = "application/json" 表示post提交的body数据格式为json格式的数据
+    -- 如果请求成功,服务器应答的数据会保存到response.body中
+    local code, response = httpplus.request(
+    {
+        method = "POST",
+        url = "http://httpbin.air32.cn/post",
+        headers = {["Content-Type"] = "application/json"},
+        body = json.encode(params)
+    })
+    log.info("httpplus_app_post_json", code==200 and "success" or "error", code)
+    if code==200 then
+        log.info("httpplus_app_post_json headers", json.encode(response.headers or {}))
+        local body = response.body:query()
+        log.info("httpplus_app_post_json body", body and (body:len()>512 and body:len() or body) or "nil")
+    end
+end
+
+
+-- http post提交纯文本数据功能演示
+local function httpplus_app_post_text()
+    -- http post提交纯文本数据
+    -- http://httpbin.air32.cn/post为回环测试服务器,服务器收到post提交的纯文本数据后,还会下发同样的纯文本数据给设备
+    -- ["Content-Type"] = "text/plain" 表示post提交的body数据格式为纯文本格式的数据
+    -- 如果请求成功,服务器应答的数据会保存到response.body中
+    local code, response = httpplus.request(
+    {
+        method = "POST",
+        url = "http://httpbin.air32.cn/post",
+        headers = {["Content-Type"] = "text/plain"},
+        body = "This is a raw text message from LuatOS device"
+    })
+    log.info("httpplus_app_post_text", code==200 and "success" or "error", code)
+    if code==200 then
+        log.info("httpplus_app_post_text headers", json.encode(response.headers or {}))
+        local body = response.body:query()
+        log.info("httpplus_app_post_text body", body and (body:len()>512 and body:len() or body) or "nil")
+    end
+end
+
+
+-- http post提交xml数据功能演示
+local function httpplus_app_post_xml()
+    -- [=[ 和 ]=] 之间是一个多行字符串
+    local body = [=[
+        <?xml version="1.0" encoding="UTF-8"?>
+        <user>
+            <name>LuatOS</name>
+            <password>123456</password>
+        </user>
+    ]=]
+
+    -- http post提交xml数据
+    -- http://httpbin.air32.cn/post为回环测试服务器,服务器收到post提交的xml数据后,还会下发同样的xml数据给设备
+    -- ["Content-Type"] = "text/xml" 表示post提交的body数据格式为xml格式的数据
+    -- 如果请求成功,服务器应答的数据会保存到response.body中
+    local code, response = httpplus.request(
+    {
+        method = "POST",
+        url = "http://httpbin.air32.cn/post",
+        headers = {["Content-Type"] = "text/xml"},
+        body = body
+    })
+    log.info("httpplus_app_post_xml", code==200 and "success" or "error", code)
+    if code==200 then
+        log.info("httpplus_app_post_xml headers", json.encode(response.headers or {}))
+        body = response.body:query()
+        log.info("httpplus_app_post_xml body", body and (body:len()>512 and body:len() or body) or "nil")
+    end
+end
+
+
+-- http post提交原始二进制数据功能演示
+local function httpplus_app_post_binary()
+    local body = io.readFile("/luadb/logo.jpg")
+
+    -- http post提交原始二进制数据
+    -- http://upload.air32.cn/api/upload/jpg为jpg图片上传测试服务器
+    -- 此处将logo.jpg的原始二进制数据做为body上传到服务器
+    -- 上传成功后,电脑上浏览器打开https://www.air32.cn/upload/data/jpg/,打开对应的测试日期目录,点击具体的测试时间照片,可以查看上传的照片
+    -- ["Content-Type"] = "application/octet-stream" 表示post提交的body数据格式为原始二进制格式的数据
+    -- 如果请求成功,服务器应答的数据会保存到response.body中
+    local code, response = httpplus.request(
+    {
+        method = "POST",
+        url = "http://upload.air32.cn/api/upload/jpg",
+        headers = {["Content-Type"] = "application/octet-stream"},
+        body = body
+    })
+    log.info("httpplus_app_post_binary", code==200 and "success" or "error", code)
+    if code==200 then
+        log.info("httpplus_app_post_binary headers", json.encode(response.headers or {}))
+        body = response.body:query()
+        log.info("httpplus_app_post_binary body", body and (body:len()>512 and body:len() or body) or "nil")
+    end
+end
+
+
+-- http post文件上传功能演示
+local function httpplus_app_post_file()
+    -- hhtplus.request接口支持单文件上传、多文件上传、单文本上传、多文本上传、单/多文本+单/多文件上传
+    -- http://airtest.openluat.com:2900/uploadFileToStatic 仅支持单文件上传,并且上传的文件name必须使用"uploadFile"
+    -- 所以此处仅演示了单文件上传功能,并且"uploadFile"不能改成其他名字,否则会出现上传失败的应答
+    -- 如果你自己的http服务支持更多类型的文本/文件混合上传,可以打开注释自行验证
+    local code, response = httpplus.request(
+    {
+        url = "http://airtest.openluat.com:2900/uploadFileToStatic",
+        files =
+        {
+            ["uploadFile"] = "/luadb/logo.jpg",
+            -- ["logo1.jpg"] = "/luadb/logo.jpg",
+        },
+        -- forms =
+        -- {
+        --     ["username"] = "LuatOS",
+        --     ["password"] = "123456",
+        -- },
+    })
+    log.info("httpplus_app_post_file", code==200 and "success" or "error", code)
+    if code==200 then
+        log.info("httpplus_app_post_file headers", json.encode(response.headers or {}))
+        local body = response.body:query()
+        log.info("httpplus_app_post_file body", body and (body:len()>512 and body:len() or body) or "nil")
+    end
+end
+
+
+
+-- http app task 的任务处理函数
+local function httpplus_app_task_func() 
+    while true do
+        -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+        while not socket.adapter(socket.dft()) do
+            log.warn("httpplus_app_task_func", "wait IP_READY", socket.dft())
+            -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+            -- 或者等待1秒超时退出阻塞等待状态;
+            -- 注意:此处的1000毫秒超时不要修改的更长;
+            -- 因为当使用libnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+            -- 当libnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+            -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+            sys.waitUntil("IP_READY", 1000)
+        end
+
+        -- 检测到了IP_READY消息
+        log.info("httpplus_app_task_func", "recv IP_READY", socket.dft())
+
+        -- 普通的http get请求功能演示
+        httpplus_app_get()
+        -- http get下载压缩数据的功能演示
+        httpplus_app_get_gzip()
+        -- http post提交表单数据功能演示
+        httpplus_app_post_form()
+        -- -- http post提交json数据功能演示
+        httpplus_app_post_json()
+        -- http post提交纯文本数据功能演示
+        httpplus_app_post_text()
+        -- http post提交xml数据功能演示
+        httpplus_app_post_xml()
+        -- http post提交原始二进制数据功能演示
+        httpplus_app_post_binary()
+        -- http post文件上传功能演示
+        httpplus_app_post_file()
+
+        -- 60秒之后,循环测试
+        sys.wait(60000)
+    end
+end
+
+--创建并且启动一个task
+--运行这个task的处理函数httpplus_app_task_func
+sys.taskInit(httpplus_app_task_func)

BIN
module/Air8000/demo/httpv2/logo.jpg


+ 91 - 0
module/Air8000/demo/httpv2/main.lua

@@ -0,0 +1,91 @@
+--[[
+@module  main
+@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑 
+@version 1.0
+@date    2025.07.28
+@author  朱天华
+@usage
+本demo演示的核心功能为:
+1、分别使用http核心库和httpplus扩展库,演示以下几种应用场景的使用方式
+   (1) 普通的http get请求功能演示;
+   (2) http get下载压缩数据的功能演示;
+   (3) http get下载数据保存到文件中的功能演示;(仅http核心库支持,httpplus扩展库不支持)
+   (4) http post提交表单数据功能演示;
+   (5) http post提交json数据功能演示;
+   (6) http post提交纯文本数据功能演示;
+   (7) http post提交xml数据功能演示;
+   (8) http post提交原始二进制数据功能演示;
+   (9) http post文件上传功能演示;
+2、netdrv_device:配置连接外网使用的网卡,目前支持以下四种选择(四选一)
+   (1) netdrv_4g:4G网卡
+   (2) netdrv_wifi:WIFI STA网卡
+   (3) netdrv_eth_spi:通过SPI外挂CH390H芯片的以太网卡
+   (4) netdrv_multiple:支持以上三种网卡,可以配置三种网卡的优先级
+
+更多说明参考本目录下的readme.md文件
+]]
+
+
+--[[
+必须定义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 = "HTTP"
+VERSION = "001.000.000"
+
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+
+-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
+-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
+if wdt then
+    --配置喂狗超时时间为9秒钟
+    wdt.init(9000)
+    --启动一个循环定时器,每隔3秒钟喂一次狗
+    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)
+
+-- 加载网络驱动设备功能模块
+require "netdrv_device"
+
+-- 加载http应用功能模块
+require "http_app"
+-- 加载httpplus应用功能模块
+require "httpplus_app"
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行

+ 33 - 0
module/Air8000/demo/httpv2/netdrv/netdrv_4g.lua

@@ -0,0 +1,33 @@
+--[[
+@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()
+    log.info("netdrv_4g.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_GP))
+end
+
+local function ip_lose_func()
+    log.warn("netdrv_4g.ip_lose_func", "IP_LOSE")
+end
+
+
+
+--此处订阅"IP_READY"和"IP_LOSE"两种消息
+--在消息的处理函数中,仅仅打印了一些信息,便于实时观察4G网络的连接状态
+--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+-- 设置默认网卡为socket.LWIP_GP
+-- 在Air8000上,内核固件运行起来之后,默认网卡就是socket.LWIP_GP
+-- 在单4G网卡使用场景下,下面这一行代码加不加都没有影响,为了和其他网卡驱动模块的代码风格保持一致,所以加上了
+socket.dft(socket.LWIP_GP)

+ 85 - 0
module/Air8000/demo/httpv2/netdrv/netdrv_eth_spi.lua

@@ -0,0 +1,85 @@
+--[[
+@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 function ip_ready_func()
+    log.info("netdrv_eth_spi.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_ETH))
+end
+
+local function ip_lose_func()
+    log.warn("netdrv_eth_spi.ip_lose_func", "IP_LOSE")
+end
+
+
+
+--此处订阅"IP_READY"和"IP_LOSE"两种消息
+--在消息的处理函数中,仅仅打印了一些信息,便于实时观察“通过SPI外挂CH390H芯片的以太网卡”的连接状态
+--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+
+-- 设置默认网卡为socket.LWIP_ETH
+socket.dft(socket.LWIP_ETH)
+
+
+--本demo测试使用的是Air8000开发板
+--GPIO140为CH390H以太网芯片的供电使能控制引脚
+gpio.setup(140, 1, gpio.PULLUP)
+
+--这个task的核心业务逻辑是:初始化SPI,初始化以太网卡,并在以太网卡上开启动态主机配置协议
+local function netdrv_eth_spi_task_func()
+    -- 初始化SPI1
+    local result = spi.setup(
+        1,--spi_id
+        nil,
+        0,--CPHA
+        0,--CPOL
+        8,--数据宽度
+        25600000--,--频率
+        -- spi.MSB,--高低位顺序    可选,默认高位在前
+        -- spi.master,--主模式     可选,默认主
+        -- spi.full--全双工       可选,默认全双工
+    )
+    log.info("netdrv_eth_spi", "spi open result", result)
+    --返回值为0,表示打开成功
+    if result ~= 0 then
+        log.error("netdrv_eth_spi", "spi open error",result)
+        return
+    end
+
+    --初始化以太网卡
+
+    --以太网联网成功(成功连接路由器,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
+    --各个功能模块可以订阅"IP_READY"消息实时处理以太网联网成功的事件
+    --也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
+
+    --以太网断网后,内核固件会产生一个"IP_LOSE"消息
+    --各个功能模块可以订阅"IP_LOSE"消息实时处理以太网断网的事件
+    --也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
+
+    -- socket.LWIP_ETH 指定网络适配器编号
+    -- netdrv.CH390外挂CH390
+    -- SPI ID 1, 片选 GPIO12
+    netdrv.setup(socket.LWIP_ETH, netdrv.CH390, {spi=1, cs=12})
+
+    --在以太上开启动态主机配置协议
+    netdrv.dhcp(socket.LWIP_ETH, true)
+end
+
+--创建并且启动一个task
+--task的处理函数为netdrv_eth_spi_task_func
+sys.taskInit(netdrv_eth_spi_task_func)

+ 95 - 0
module/Air8000/demo/httpv2/netdrv/netdrv_multiple.lua

@@ -0,0 +1,95 @@
+--[[
+@module  netdrv_multiple
+@summary 多网卡(4G网卡、WIFI STA网卡、通过SPI外挂CH390H芯片的以太网卡)驱动模块 
+@version 1.0
+@date    2025.07.24
+@author  朱天华
+@usage
+本文件为多网卡驱动模块 ,核心业务逻辑为:
+1、调用libnetif.set_priority_order配置多网卡的控制参数以及优先级;
+
+直接使用Air8000开发板硬件测试即可;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_multiple"就可以加载运行;
+]]
+
+
+local libnetif = require "libnetif"
+
+-- 网卡状态变化通知回调函数
+-- 当libnetif中检测到网卡切换或者所有网卡都断网时,会触发调用此回调函数
+-- 当网卡切换切换时:
+--     net_type:string类型,表示当前使用的网卡字符串
+--     adapter:number类型,表示当前使用的网卡id
+-- 当所有网卡断网时:
+--     net_type:为nil
+--     adapter:number类型,为-1
+local function netdrv_multiple_notify_cbfunc(net_type,adapter)
+    if type(net_type)=="string" then
+        log.info("netdrv_multiple_notify_cbfunc", "use new adapter", net_type, adapter)
+    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)
+    end
+end
+
+local function netdrv_multiple_task_func()
+    --设置网卡优先级
+    libnetif.set_priority_order(
+        {
+            -- “通过SPI外挂CH390H芯片”的以太网卡,使用Air8000开发板验证
+            {
+                ETHERNET = {
+                    -- 供电使能GPIO
+                    pwrpin = 140,
+                    -- 设置的多个“已经IP READY,但是还没有ping通”网卡,循环执行ping动作的间隔(单位毫秒,可选)
+                    -- 如果没有传入此参数,libnetif会使用默认值10秒
+                    ping_time = 3000,
+
+                    -- 连通性检测ip(选填参数);
+                    -- 如果没有传入ip地址,libnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
+                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
+                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",     
+                    
+                    -- 网卡芯片型号(选填参数),仅spi方式外挂以太网时需要填写。
+                    tp = netdrv.CH390, 
+                    opts = {spi=1, cs=12}
+                }
+            },
+
+            -- WIFI STA网卡
+            {
+                WIFI = {
+                    -- 要连接的WIFI路由器名称
+                    ssid = "茶室-降功耗,找合宙!",
+                    -- 要连接的WIFI路由器密码
+                    password = "Air123456", 
+
+                    -- 连通性检测ip(选填参数);
+                    -- 如果没有传入ip地址,libnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
+                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
+                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",
+                }
+            },
+
+            -- 4G网卡
+            {
+                LWIP_GP = true
+            }
+        }
+    )    
+end
+
+-- 设置网卡状态变化通知回调函数netdrv_multiple_notify_cbfunc
+libnetif.notify_status(netdrv_multiple_notify_cbfunc)
+
+-- 如果存在udp网络应用,并且udp网络应用中,根据应用层的心跳能够判断出来udp数据通信出现了异常;
+-- 可以在判断出现异常的位置,调用一次libnetif.check_network_status()接口,强制对当前正式使用的网卡进行一次连通性检测;
+-- 如果存在tcp网络应用,不需要用户调用libnetif.check_network_status()接口去控制,libnetif会在tcp网络应用通信异常时自动对当前使用的网卡进行连通性检测。
+
+
+-- 启动一个task,task的处理函数为netdrv_multiple_task_func
+-- 在处理函数中调用libnetif.set_priority_order设置网卡优先级
+-- 因为libnetif.set_priority_order要求必须在task中被调用,所以此处启动一个task
+sys.taskInit(netdrv_multiple_task_func)

+ 50 - 0
module/Air8000/demo/httpv2/netdrv/netdrv_wifi.lua

@@ -0,0 +1,50 @@
+--[[
+@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 function ip_ready_func()
+    log.info("netdrv_wifi.ip_ready_func", "IP_READY", json.encode(wlan.getInfo()))
+end
+
+local function ip_lose_func()
+    log.warn("netdrv_wifi.ip_lose_func", "IP_LOSE")
+end
+
+
+
+--此处订阅"IP_READY"和"IP_LOSE"两种消息
+--在消息的处理函数中,仅仅打印了一些信息,便于实时观察WIFI的连接状态
+--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+
+-- 设置默认网卡为socket.LWIP_STA
+socket.dft(socket.LWIP_STA)
+
+
+wlan.init()
+--连接WIFI热点,连接结果会通过"IP_READY"或者"IP_LOSE"消息通知
+--Air8000仅支持2.4G的WIFI,不支持5G的WIFI
+--此处前两个参数表示WIFI热点名称以及密码,更换为自己测试时的真实参数即可
+--第三个参数1表示WIFI连接异常时,内核固件会自动重连
+wlan.connect("茶室-降功耗,找合宙!", "Air123456", 1)
+
+--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网络是否连接成功

+ 33 - 0
module/Air8000/demo/httpv2/netdrv_device.lua

@@ -0,0 +1,33 @@
+--[[
+@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_USER1,通过SPI外挂CH390H芯片的以太网卡;
+4、netdrv_multiple:可以配置多种网卡的优先级,按照优先级配置,使用其中一种网卡连接外网;
+
+根据自己的项目需求,只需要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"

+ 136 - 0
module/Air8000/demo/httpv2/readme.md

@@ -0,0 +1,136 @@
+## 功能模块介绍
+
+1、main.lua:主程序入口;
+
+2、netdrv_device.lua:网卡驱动设备,可以配置使用netdrv文件夹内的四种网卡(单4g网卡,单wifi网卡,单spi以太网卡,多网卡)中的任何一种网卡;
+
+3、http_app.lua:基于不同的应用场景,演示http核心库的使用方式;
+
+4、httpplus_app.lua:基于不同的应用场景,演示httpplus扩展库的使用方式;
+
+
+
+## 演示功能概述
+
+1、http_app:使用http核心库,演示以下几种应用场景的使用方式
+
+- 普通的http get请求功能演示;
+
+- http get下载压缩数据的功能演示;
+
+- http get下载数据保存到文件中的功能演示;
+
+- http post提交表单数据功能演示;
+
+- http post提交json数据功能演示;
+
+- http post提交纯文本数据功能演示;
+
+- http post提交xml数据功能演示;
+
+- http post提交原始二进制数据功能演示;
+
+- http post文件上传功能演示;
+
+2、httpplus_app:使用httpplus扩展库,演示以下几种应用场景的使用方式
+
+- 普通的http get请求功能演示;
+
+- http get下载压缩数据的功能演示;
+
+- http post提交表单数据功能演示;
+
+- http post提交json数据功能演示;
+
+- http post提交纯文本数据功能演示;
+
+- http post提交xml数据功能演示;
+
+- http post提交原始二进制数据功能演示;
+
+- http post文件上传功能演示;
+
+3、netdrv_device:配置连接外网使用的网卡,目前支持以下四种选择(四选一)
+
+   (1) netdrv_4g:4G网卡
+
+   (2) netdrv_wifi:WIFI STA网卡
+
+   (3) netdrv_eth_spi:通过SPI外挂CH390H芯片的以太网卡
+
+   (4) netdrv_multiple:支持以上三种网卡,可以配置三种网卡的优先级
+
+
+
+## 演示硬件环境
+
+![](https://docs.openluat.com/air8000/luatos/app/image/netdrv_multi.jpg)
+
+1、Air8000开发板一块+可上网的sim卡一张+4g天线一根+wifi天线一根+网线一根:
+
+- sim卡插入开发板的sim卡槽
+
+- 天线装到开发板上
+
+- 网线一端插入开发板网口,另外一端连接可以上外网的路由器网口
+
+2、TYPE-C USB数据线一根 + USB转串口数据线一根,Air8000开发板和数据线的硬件接线方式为:
+
+- Air8000开发板通过TYPE-C USB口供电;(外部供电/USB供电 拨动开关 拨到 USB供电一端)
+
+- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
+
+- USB转串口数据线,一般来说,白线连接开发板的UART1_TX,绿线连接开发板的UART1_RX,黑线连接核心板的GND,另外一端连接电脑USB口;
+
+
+## 演示软件环境
+
+1、Luatools下载调试工具
+
+2、[Air8000 V2011版本固件)](https://docs.openluat.com/air8000/luatos/firmware/)(理论上,2025年7月26日之后发布的固件都可以)
+
+
+
+## 演示核心步骤
+
+1、搭建好硬件环境
+
+2、demo脚本代码netdrv_device.lua中,按照自己的网卡需求启用对应的Lua文件
+
+- 如果需要单4G网卡,打开require "netdrv_4g",其余注释掉
+
+- 如果需要单WIFI STA网卡,打开require "netdrv_wifi",其余注释掉;同时netdrv_wifi.lua中的wlan.connect("茶室-降功耗,找合宙!", "Air123456", 1),前两个参数,修改为自己测试时wifi热点的名称和密码;注意:仅支持2.4G的wifi,不支持5G的wifi
+
+- 如果需要以太网卡,打开require "netdrv_eth_spi",其余注释掉
+
+- 如果需要多网卡,打开require "netdrv_multiple",其余注释掉;同时netdrv_multiple.lua中的ssid = "茶室-降功耗,找合宙!", password = "Air123456", 修改为自己测试时wifi热点的名称和密码;注意:仅支持2.4G的wifi,不支持5G的wifi
+
+3、Luatools烧录内核固件和修改后的demo脚本代码
+
+4、烧录成功后,自动开机运行,在日志中搜索success 200,每隔1分钟测试一轮,如果每轮出现22次success 200,如以下日志所示,就表示成功;详细日志所表示的含义,可以结合代码自行分析
+
+``` lua
+[2025-08-06 15:34:56.201][000000007.113] I/user.http_app_get1 success 200 {"Transfer-
+[2025-08-06 15:34:56.354][000000007.271] I/user.httpplus_app_get1 success 200
+[2025-08-06 15:34:56.622][000000007.537] I/user.httpplus_app_get2 success 200
+[2025-08-06 15:34:57.896][000000008.796] I/user.httpplus_app_get_gzip success 200
+[2025-08-06 15:34:58.287][000000009.070] I/user.http_app_get2 success 200 {"Vary":"Ac
+[2025-08-06 15:34:58.369][000000009.112] I/user.httpplus_app_post_form success 200
+[2025-08-06 15:34:58.592][000000009.248] I/user.http_app_get3 success 200 {"Connectio
+[2025-08-06 15:34:58.765][000000009.412] I/user.httpplus_app_post_json success 200
+[2025-08-06 15:34:59.043][000000009.951] I/user.http_app_get_gzip success 200 {"Conte
+[2025-08-06 15:34:59.291][000000010.065] I/user.httpplus_app_post_text success 200
+[2025-08-06 15:34:59.820][000000010.736] I/user.httpplus_app_post_xml success 200
+[2025-08-06 15:34:59.903][000000010.744] I/user.http_app_get_file1 success 200 {"Tran
+[2025-08-06 15:35:00.706][000000011.503] I/user.httpplus_app_post_binary success 200
+[2025-08-06 15:35:01.008][000000011.862] I/user.http_app_get_file2 success 200 {"Vary
+[2025-08-06 15:35:01.094][000000011.917] I/user.httpplus_app_post_file success 200
+[2025-08-06 15:35:01.215][000000012.079] I/user.http_app_get_file3 success 200 {"Conn
+[2025-08-06 15:35:01.356][000000012.270] I/user.http_app_post_form success 200 {"Conn
+[2025-08-06 15:35:01.569][000000012.479] I/user.http_app_post_json success 200 {"Conn
+[2025-08-06 15:35:01.769][000000012.681] I/user.http_app_post_text success 200 {"Conn
+[2025-08-06 15:35:01.949][000000012.858] I/user.http_app_post_xml success 200 {"Conne
+[2025-08-06 15:35:02.236][000000013.145] I/user.http_app_post_binary success 200 {"Da
+[2025-08-06 15:35:02.437][000000013.348] I/user.post_multipart_form_data success 200 
+```
+