Browse Source

Merge branch '0wQ:main' into main

机丸 1 year ago
parent
commit
2de10ff319

+ 6 - 2
README.md

@@ -1,4 +1,4 @@
-# Air700E / Air780E / Air780EG 短信转发
+# Air700E / Air780E / Air780EP / Air780EPV 短信转发 来电通知
 
 ## 保姆级教程:https://kdocs.cn/l/coe1ozIlSX70
 
@@ -15,6 +15,7 @@
     - [x] [邮件 next-smtp-proxy](https://github.com/0wQ/next-smtp-proxy)
     - [x] [Gotify](https://gotify.net)
     - [x] [Inotify](https://github.com/xpnas/Inotify) / [合宙官方的推送服务](https://push.luatos.org)
+    - [x] 邮件 (SMTP协议)
 - [x] 通过短信控制设备
     - [x] 发短信, 格式: `SMS,10010,余额查询`
 - [x] 定时基站定位
@@ -24,14 +25,17 @@
 - [x] 低功耗模式 (使用 IoT Power 测量, 开发板待机 30min 平均电流 2.5mA)
 - [x] 使用消息队列, 经测试同时发送几百条通知, 不会卡死
 - [x] 通知发送失败, 自动重发
+- [x] 支持主从模式,一主对多从,从机通过串口转发消息,主机接受消息后转发到通知服务
 
 ## :hammer: Usage
 
+> 可以参考 [lageev](https://github.com/lageev/air780e-forwarder) 写的教程 https://kdocs.cn/l/coe1ozIlSX70
+
 ### 1. 按注释修改 `script/config.lua` 配置文件
 
 ### 2. 烧录脚本
 
-> 推荐使用 `core` 目录下的固件
+> 固件下载 https://gitee.com/openLuat/LuatOS/releases 选择带有 EC618 或 Air780 关键字的固件
 >
 > `core` 目录下文件名中带有 `RNDIS` 的, 支持 RNDIS 网卡功能, 如果 SIM 卡流量不多请勿选择
 

BIN
core/LuatOS-SoC_V0002_EC718PV.soc


BIN
core/LuatOS-SoC_V1106_EC618.soc


BIN
core/LuatOS-SoC_V1106_EC618_RNDIS.soc


BIN
core/LuatOS-SoC_V1108_EC618.soc


+ 28 - 5
script/config.lua

@@ -1,11 +1,19 @@
 return {
     -- 通知类型, 支持配置多个
-    -- NOTIFY_TYPE = {"telegram", "pushdeer", "bark", "dingtalk", "feishu", "wecom", "pushover", "inotify", "next-smtp-proxy", "gotify"},
-    NOTIFY_TYPE = "pushdeer",
+    -- NOTIFY_TYPE = {"custom_post", "telegram", "pushdeer", "bark", "dingtalk", "feishu", "wecom", "pushover", "inotify", "next-smtp-proxy", "gotify", "serial"},
+    NOTIFY_TYPE = "custom_post",
     --
-    -- telegram 通知配置, https://github.com/0wQ/telegram-notify
-    TELEGRAM_PROXY_API = "",
-    TELEGRAM_TOKEN = "",
+    -- 角色类型, 用于区分主从机, 仅当使用串口转发 NOTIFY_TYPE = "serial" 时才需要配置
+    -- MASTER: 主机, 可主动联网; SLAVE: 从机, 不可主动联网, 通过串口发送数据
+    ROLE = "MASTER",
+    --
+    -- custom_post 通知配置, 自定义 POST 请求, CUSTOM_POST_BODY_TABLE 中的 {msg} 会被替换为通知内容
+    CUSTOM_POST_URL = "https://sctapi.ftqq.com/<SENDKEY>.send",
+    CUSTOM_POST_CONTENT_TYPE = "application/json",
+    CUSTOM_POST_BODY_TABLE = {["title"] = "这里是标题", ["desp"] = "这里是内容, 会被替换掉:\n{msg}\n{msg}"},
+    --
+    -- telegram 通知配置, https://github.com/0wQ/telegram-notify 或者自行反代
+    TELEGRAM_API = "https://api.telegram.org/bot{token}/sendMessage",
     TELEGRAM_CHAT_ID = "",
     --
     -- pushdeer 通知配置, https://www.pushdeer.com/
@@ -17,7 +25,9 @@ return {
     BARK_KEY = "",
     --
     -- dingtalk 通知配置, https://open.dingtalk.com/document/robots/custom-robot-access
+    -- 如果是加签方式, 请填写 DINGTALK_SECRET, 否则留空为自定义关键词方式, https://open.dingtalk.com/document/robots/customize-robot-security-settings
     DINGTALK_WEBHOOK = "",
+    DINGTALK_SECRET = "",
     --
     -- feishu 通知配置, https://open.feishu.cn/document/ukTMukTMukTM/ucTM5YjL3ETO24yNxkjN
     FEISHU_WEBHOOK = "",
@@ -42,6 +52,16 @@ return {
     NEXT_SMTP_PROXY_TO_EMAIL = "",
     NEXT_SMTP_PROXY_SUBJECT = "来自 Air780E 的通知",
     --
+    -- smtp 通知配置(可能不支持加密协议)
+    SMTP_HOST = "smtp.qq.com",
+    SMTP_PORT = 25,
+    SMTP_USERNAME = "",
+    SMTP_PASSWORD = "",
+    SMTP_MAIL_FROM = "",
+    SMTP_MAIL_TO = "",
+    SMTP_MAIL_SUBJECT = "来自 Air780E 的通知",
+    SMTP_TLS_ENABLE = false,
+    --
     -- gotify 通知配置, https://gotify.net/
     GOTIFY_API = "",
     GOTIFY_TITLE = "Air780E",
@@ -65,4 +85,7 @@ return {
     --
     -- 开启低功耗模式, USB 断开连接无法查看日志, RNDIS 网卡会断开
     LOW_POWER_MODE = false,
+    --
+    -- 本机号码, 优先使用 mobile.number() 接口获取, 如果获取不到则使用此号码
+    FALLBACK_LOCAL_NUMBER = "+8618888888888",
 }

+ 189 - 0
script/lib_smtp.lua

@@ -0,0 +1,189 @@
+local lib_smtp = {}
+
+lib_smtp.socket_debug_enable = false
+lib_smtp.packet_size = 512
+lib_smtp.timeout = 1000 * 30
+
+--- 日志格式化函数
+-- @param content string, 日志内容
+-- @return string, 处理后的日志内容
+local function logFormat(content)
+    -- 隐藏 AUTH 用户信息
+    content = content:gsub("AUTH PLAIN (.-)\r\n", "AUTH PLAIN ***\r\n")
+    -- 替换换行符
+    content = content:gsub("\r", "\\r"):gsub("\n", "\\n")
+    -- 截取
+    content = content:sub(1, 200) .. (#content > 200 and " ..." or "")
+    return content
+end
+
+--- 转义句号函数
+-- @param content string, 需要转义的内容
+-- @return string, 转义后的内容
+local function escapeDot(content)
+    return content:gsub("(.-\r\n)", function(line)
+        if line:sub(1, 1) == "." then line = "." .. line end
+        return line
+    end)
+end
+
+--- 接收到数据时的处理函数
+-- @param netc userdata, socket.create 返回的 netc
+-- @param rxbuf userdata, 接收到的数据
+-- @param socket_id string, socket id
+-- @param current_command string, 当前要发送的命令
+local function recvHandler(netc, rxbuf, socket_id, current_command)
+    local rx_str = rxbuf:toStr(0, rxbuf:used())
+    log.info("lib_smtp", socket_id, "<-", logFormat(rx_str))
+
+    -- 如果返回非 2xx 或 3xx 状态码, 则断开连接
+    if not rx_str:match("^[23]%d%d") then
+        log.error("lib_smtp", socket_id, "服务器返回错误状态码, 断开连接, 请检查日志")
+        sys.publish(socket_id .. "_disconnect", { success = false, message = "服务器返回错误状态码", is_retry = false })
+        return
+    end
+
+    if current_command == nil then
+        log.info("lib_smtp", socket_id, "全部发送完成")
+        sys.publish(socket_id .. "_disconnect", { success = true, message = "发送成功", is_retry = false })
+        return
+    end
+
+    -- 分包发送
+    local index = 1
+    sys.taskInit(function()
+        while index <= #current_command do
+            local packet = current_command:sub(index, index + lib_smtp.packet_size - 1)
+            socket.tx(netc, packet)
+            log.info("lib_smtp", socket_id, "->", logFormat(packet))
+            index = index + lib_smtp.packet_size
+            sys.wait(100)
+        end
+    end)
+end
+
+local function validateParameters(smtp_config)
+    -- 配置参数验证规则
+    local validation_rules = {
+        { field = "host", type = "string", required = true },
+        { field = "port", type = "number", required = true },
+        { field = "username", type = "string", required = true },
+        { field = "password", type = "string", required = true },
+        { field = "mail_from", type = "string", required = true },
+        { field = "mail_to", type = "string", required = true },
+        { field = "tls_enable", type = "boolean", required = false },
+    }
+    local result = true
+    for _, rule in ipairs(validation_rules) do
+        local value = smtp_config[rule.field]
+        if rule.type == "string" and (value == nil or value == "") then
+            log.error("lib_smtp", string.format("`smtp_config.%s` 应为非空字符串", rule.field))
+            result = false
+        elseif rule.required and type(value) ~= rule.type then
+            log.error("lib_smtp", string.format("`smtp_config.%s` 应为 %s 类型", rule.field, rule.type))
+            result = false
+        end
+    end
+    return result
+end
+
+--- 发送邮件
+-- @param body string 邮件正文
+-- @param subject string 邮件主题
+-- @param smtp_config table 配置参数
+--   - smtp_config.host string SMTP 服务器地址
+--   - smtp_config.username string SMTP 账号用户名
+--   - smtp_config.password string SMTP 账号密码
+--   - smtp_config.mail_from string 发件人邮箱地址
+--   - smtp_config.mail_to string 收件人邮箱地址
+--   - smtp_config.port number SMTP 服务器端口号
+--   - smtp_config.tls_enable boolean 是否启用 TLS(可选,默认为 false)
+-- @return result table 发送结果
+--   - result.success boolean 是否发送成功
+--   - result.message string 发送结果描述
+--   - result.is_retry boolean 是否需要重试
+function lib_smtp.send(body, subject, smtp_config)
+    -- 参数验证
+    if type(smtp_config) ~= "table" then
+        log.error("lib_smtp", "`smtp_config` 应为 table 类型")
+        return { success = false, message = "参数错误", is_retry = false }
+    end
+    local valid = validateParameters(smtp_config)
+    if not valid then return { success = false, message = "参数错误", is_retry = false } end
+
+    subject = type(subject) == "string" and subject or ""
+    body = type(body) == "string" and escapeDot(body) or ""
+
+    lib_smtp.send_count = (lib_smtp.send_count or 0) + 1
+    local socket_id = "socket_" .. lib_smtp.send_count
+    local rxbuf = zbuff.create(256)
+
+    local commands = {
+        "HELO " .. smtp_config.host .. "\r\n",
+        "AUTH PLAIN " .. string.toBase64("\0" .. smtp_config.username .. "\0" .. smtp_config.password) .. "\r\n",
+        "MAIL FROM: <" .. smtp_config.mail_from .. ">\r\n",
+        "RCPT TO: <" .. smtp_config.mail_to .. ">\r\n",
+        "DATA\r\n",
+        table.concat({
+            "From: " .. smtp_config.mail_from,
+            "To: " .. smtp_config.mail_to,
+            "Subject: " .. subject,
+            "Content-Type: text/plain; charset=UTF-8",
+            "",
+            body,
+            ".",
+            "",
+        }, "\r\n"),
+    }
+    local current_command_index = 1
+    local function getNextCommand()
+        local command = commands[current_command_index]
+        current_command_index = current_command_index + 1
+        return command
+    end
+
+    -- socket 回调
+    local function netCB(netc, event, param)
+        if param ~= 0 then
+            sys.publish(socket_id .. "_disconnect", { success = false, message = "param~=0", is_retry = true })
+            return
+        end
+        if event == socket.LINK then
+            log.info("lib_smtp", socket_id, "LINK")
+        elseif event == socket.ON_LINE then
+            log.info("lib_smtp", socket_id, "ON_LINE")
+        elseif event == socket.EVENT then
+            socket.rx(netc, rxbuf)
+            socket.wait(netc)
+            if rxbuf:used() > 0 then recvHandler(netc, rxbuf, socket_id, getNextCommand()) end
+            rxbuf:del()
+        elseif event == socket.TX_OK then
+            socket.wait(netc)
+        elseif event == socket.CLOSE then
+            log.info("lib_smtp", socket_id, "CLOSED")
+            sys.publish(socket_id .. "_disconnect", { success = false, message = "服务器断开连接", is_retry = true })
+        end
+    end
+
+    -- 初始化 socket
+    local netc = socket.create(nil, netCB)
+    socket.debug(netc, lib_smtp.socket_debug_enable)
+    socket.config(netc, nil, nil, smtp_config.tls_enable)
+    -- 连接 smtp 服务器
+    local is_connect_success = socket.connect(netc, smtp_config.host, smtp_config.port)
+    if not is_connect_success then
+        socket.close(netc)
+        return { success = false, message = "未知错误", is_retry = true }
+    end
+    -- 等待发送结果
+    local is_send_success, send_result = sys.waitUntil(socket_id .. "_disconnect", lib_smtp.timeout)
+    socket.close(netc)
+    if is_send_success then
+        return send_result
+    else
+        log.error("lib_smtp", socket_id, "发送超时")
+        return { success = false, message = "发送超时", is_retry = true }
+    end
+end
+
+return lib_smtp

+ 130 - 94
script/main.lua

@@ -7,12 +7,14 @@ log.info("main", "开机原因", pm.lastReson())
 
 sys = require "sys"
 sysplus = require "sysplus"
-require "sysplus"
 
 -- 添加硬狗防止程序卡死
 wdt.init(9000)
 sys.timerLoopStart(wdt.feed, 3000)
 
+-- 设置电平输出 3.3V
+-- pm.ioVol(pm.IOVOL_ALL_GPIO, 3300)
+
 -- 设置 DNS
 socket.setDNS(nil, 1, "119.29.29.29")
 socket.setDNS(nil, 2, "223.5.5.5")
@@ -25,34 +27,30 @@ mobile.ipv6(true)
 
 -- POWERKEY
 local button_last_press_time, button_last_release_time = 0, 0
-gpio.setup(
-    35,
-    function()
-        local current_time = mcu.ticks()
-        -- 按下
-        if gpio.get(35) == 0 then
-            button_last_press_time = current_time -- 记录最后一次按下时间
-            return
-        end
-        -- 释放
-        if button_last_press_time == 0 then -- 开机前已经按下, 开机后释放
-            return
-        end
-        if current_time - button_last_release_time < 250 then -- 防止连按
-            return
-        end
-        local duration = current_time - button_last_press_time -- 按键持续时间
-        button_last_release_time = current_time -- 记录最后一次释放时间
-        if duration > 2000 then
-            log.debug("EVENT.POWERKEY_LONG_PRESS", duration)
-            sys.publish("POWERKEY_LONG_PRESS", duration)
-        elseif duration > 50 then
-            log.debug("EVENT.POWERKEY_SHORT_PRESS", duration)
-            sys.publish("POWERKEY_SHORT_PRESS", duration)
-        end
-    end,
-    gpio.PULLUP
-)
+gpio.setup(35, function()
+    local current_time = mcu.ticks()
+    -- 按下
+    if gpio.get(35) == 0 then
+        button_last_press_time = current_time -- 记录最后一次按下时间
+        return
+    end
+    -- 释放
+    if button_last_press_time == 0 then -- 开机前已经按下, 开机后释放
+        return
+    end
+    if current_time - button_last_release_time < 250 then -- 防止连按
+        return
+    end
+    local duration = current_time - button_last_press_time -- 按键持续时间
+    button_last_release_time = current_time -- 记录最后一次释放时间
+    if duration > 2000 then
+        log.debug("EVENT.POWERKEY_LONG_PRESS", duration)
+        sys.publish("POWERKEY_LONG_PRESS", duration)
+    elseif duration > 50 then
+        log.debug("EVENT.POWERKEY_SHORT_PRESS", duration)
+        sys.publish("POWERKEY_SHORT_PRESS", duration)
+    end
+end, gpio.PULLUP)
 
 -- 加载模块
 config = require "config"
@@ -62,80 +60,118 @@ util_mobile = require "util_mobile"
 util_location = require "util_location"
 util_notify = require "util_notify"
 
--- 短信接收回调
-sms.setNewSmsCb(
-    function(sender_number, sms_content, m)
-        local time = string.format("%d/%02d/%02d %02d:%02d:%02d", m.year + 2000, m.mon, m.day, m.hour, m.min, m.sec)
-        log.info("smsCallback", time, sender_number, sms_content)
-
-        -- 短信控制
-        local is_sms_ctrl = false
-        local receiver_number, sms_content_to_be_sent = sms_content:match("^SMS,(+?%d+),(.+)$")
-        receiver_number, sms_content_to_be_sent = receiver_number or "", sms_content_to_be_sent or ""
-        if sms_content_to_be_sent ~= "" and receiver_number ~= "" and #receiver_number >= 5 and #receiver_number <= 20 then
-            sms.send(receiver_number, sms_content_to_be_sent)
-            is_sms_ctrl = true
+-- 由于 NOTIFY_TYPE 支持多个配置, 需按照包含来判断
+local containsValue = function(t, value)
+    if t == value then return true end
+    if type(t) ~= "table" then return false end
+    for k, v in pairs(t) do if v == value then return true end end
+    return false
+end
+
+if containsValue(config.NOTIFY_TYPE, "serial") then
+    -- 串口配置
+    uart.setup(1, 115200, 8, 1, uart.NONE)
+    -- 串口接收回调
+    uart.on(1, "receive", function(id, len)
+        local data = uart.read(id, len)
+        log.info("uart read:", id, len, data)
+        if config.ROLE == "MASTER" then
+            -- 主机, 通过队列发送数据
+            util_notify.add(data)
+        else
+            -- 从机, 通过串口发送数据
+            uart.write(1, data)
         end
+    end)
+end
 
-        -- 发送通知
-        util_notify.add(
-            {
-                sms_content,
-                "",
-                "发件号码: " .. sender_number,
-                "发件时间: " .. time,
-                "#SMS" .. (is_sms_ctrl and " #CTRL" or "")
-            }
-        )
+-- 短信接收回调
+sms.setNewSmsCb(function(sender_number, sms_content, m)
+    local time = string.format("%d/%02d/%02d %02d:%02d:%02d", m.year + 2000, m.mon, m.day, m.hour, m.min, m.sec)
+    log.info("smsCallback", time, sender_number, sms_content)
+
+    -- 短信控制
+    local is_sms_ctrl = false
+    local receiver_number, sms_content_to_be_sent = sms_content:match("^SMS,(+?%d+),(.+)$")
+    receiver_number, sms_content_to_be_sent = receiver_number or "", sms_content_to_be_sent or ""
+    if sms_content_to_be_sent ~= "" and receiver_number ~= "" and #receiver_number >= 5 and #receiver_number <= 20 then
+        sms.send(receiver_number, sms_content_to_be_sent)
+        is_sms_ctrl = true
     end
-)
 
-sys.taskInit(
-    function()
-        -- 等待网络环境准备就绪
-        sys.waitUntil("IP_READY")
+    -- 发送通知
+    util_notify.add({ sms_content, "", "发件号码: " .. sender_number, "发件时间: " .. time, "#SMS" .. (is_sms_ctrl and " #CTRL" or "") })
+end)
 
-        util_netled.init()
+sys.taskInit(function()
+    -- 等待网络环境准备就绪
+    sys.waitUntil("IP_READY", 20000)
 
-        -- 开机通知
-        if config.BOOT_NOTIFY then
-            util_notify.add("#BOOT")
-        end
+    util_netled.init()
 
-        -- 定时查询流量
-        if config.QUERY_TRAFFIC_INTERVAL and config.QUERY_TRAFFIC_INTERVAL >= 1000 * 60 then
-            sys.timerLoopStart(util_mobile.queryTraffic, config.QUERY_TRAFFIC_INTERVAL)
-        end
+    -- 开机通知
+    if config.BOOT_NOTIFY then sys.timerStart(util_notify.add, 1000 * 5, "#BOOT") end
 
-        -- 定时基站定位
-        if config.LOCATION_INTERVAL and config.LOCATION_INTERVAL >= 1000 * 30 then
-            sys.timerLoopStart(util_location.refresh, config.LOCATION_INTERVAL, 30)
-        end
+    -- 定时查询流量
+    if config.QUERY_TRAFFIC_INTERVAL and config.QUERY_TRAFFIC_INTERVAL >= 1000 * 60 then
+        sys.timerLoopStart(util_mobile.queryTraffic, config.QUERY_TRAFFIC_INTERVAL)
+    end
 
-        -- 电源键短按发送测试通知
-        sys.subscribe(
-            "POWERKEY_SHORT_PRESS",
-            function()
-                util_notify.add("#ALIVE")
-            end
-        )
-        -- 电源键长按查询流量
-        sys.subscribe("POWERKEY_LONG_PRESS", util_mobile.queryTraffic)
-
-        -- 开启低功耗模式
-        if config.LOW_POWER_MODE then
-            sys.wait(1000 * 15)
-            log.warn("main", "即将关闭 usb 电源, 如需查看日志请在配置中关闭低功耗模式")
-            sys.wait(1000 * 5)
-            gpio.setup(23, nil)
-            gpio.close(33)
-            pm.power(pm.USB, false) -- 关闭 USB
-            pm.power(pm.GPS, false)
-            pm.power(pm.GPS_ANT, false)
-            pm.power(pm.DAC_EN, false)
-            pm.force(pm.LIGHT) -- 进入休眠
-        end
+    -- 定时基站定位
+    if config.LOCATION_INTERVAL and config.LOCATION_INTERVAL >= 1000 * 30 then
+        util_location.refresh(nil, true)
+        sys.timerLoopStart(util_location.refresh, config.LOCATION_INTERVAL)
+    end
+
+    -- 电源键短按发送测试通知
+    sys.subscribe("POWERKEY_SHORT_PRESS", function() util_notify.add("#ALIVE") end)
+    -- 电源键长按查询流量
+    sys.subscribe("POWERKEY_LONG_PRESS", util_mobile.queryTraffic)
+
+    -- 开启低功耗模式
+    if config.LOW_POWER_MODE then
+        sys.wait(1000 * 15)
+        log.warn("main", "即将关闭 usb 电源, 如需查看日志请在配置中关闭低功耗模式")
+        sys.wait(1000 * 5)
+        gpio.setup(23, nil)
+        gpio.close(33)
+        pm.power(pm.USB, false) -- 关闭 USB
+        pm.power(pm.GPS, false)
+        pm.power(pm.GPS_ANT, false)
+        pm.power(pm.DAC_EN, false)
+        pm.force(pm.LIGHT) -- 进入休眠
     end
-)
+end)
+
+-- 通话相关
+local is_calling = false
+
+sys.subscribe("CC_IND", function(status)
+    if cc == nil then return end
+
+    if status == "INCOMINGCALL" then
+        -- 来电事件, 期间会重复触发
+        if is_calling then return end
+        is_calling = true
+
+        log.info("cc_status", "INCOMINGCALL", "来电事件", cc.lastNum())
+
+        -- 发送通知
+        util_notify.add({ "来电号码: " .. cc.lastNum(), "来电时间: " .. os.date("%Y-%m-%d %H:%M:%S"), "#CALL #CALL_IN" })
+        return
+    end
+
+    if status == "DISCONNECTED" then
+        -- 挂断事件
+        is_calling = false
+        log.info("cc_status", "DISCONNECTED", "挂断事件", cc.lastNum())
+
+        -- 发送通知
+        util_notify.add({ "来电号码: " .. cc.lastNum(), "挂断时间: " .. os.date("%Y-%m-%d %H:%M:%S"), "#CALL #CALL_DISCONNECTED" })
+        return
+    end
+
+    log.info("cc_status", status)
+end)
 
 sys.run()

+ 7 - 7
script/util_http.lua

@@ -12,8 +12,10 @@ local http_running_count = 0
 -- @param headers 请求头
 -- @param body 请求体
 function util_http.fetch(timeout, method, url, headers, body)
+    collectgarbage("collect")
+
     timeout = timeout or 1000 * 25
-    local opts = {timeout = timeout}
+    local opts = { timeout = timeout }
 
     http_count = http_count + 1
     http_running_count = http_running_count + 1
@@ -27,14 +29,12 @@ function util_http.fetch(timeout, method, url, headers, body)
     res_code, res_headers, res_body = http.request(method, url, headers, body, opts).wait()
     log.debug("util_http.fetch", "请求结束", "id:", id, "code:", res_code)
 
-    if res_code == -8 then
-        log.warn("util_http.fetch", "请求超时", "id:", id)
-    end
+    if res_code == -8 then log.warn("util_http.fetch", "请求超时", "id:", id) end
 
     http_running_count = http_running_count - 1
-    if http_running_count == 0 then
-        util_netled.blink()
-    end
+    if http_running_count == 0 then util_netled.blink() end
+
+    collectgarbage("collect")
 
     return res_code, res_headers, res_body
 end

+ 22 - 122
script/util_location.lua

@@ -1,16 +1,9 @@
 local util_location = {}
 
--- 基站定位接口类型, 支持 openluat 和 cellocation
-local api_type = "openluat"
+PRODUCT_KEY = "v32xEAKsGTIEQxtqgwCldp5aPlcnPs3K"
+local lbsLoc = require("lbsLoc")
 
-local cache = {
-    cell_info_raw = {},
-    cell_info_formatted = "",
-    lbs_data = {
-        lat = 0,
-        lng = 0
-    }
-}
+local cache = { lbs_data = { lat = 0, lng = 0 } }
 
 --- 格式化经纬度 (保留小数点后 6 位, 去除末尾的 0)
 -- @param value 经纬度
@@ -28,55 +21,27 @@ end
 local function getMapLink(lat, lng)
     lat, lng = lat or 0, lng or 0
     local map_link = ""
-    if lat ~= 0 and lng ~= 0 then
-        map_link = "http://apis.map.qq.com/uri/v1/marker?coord_type=1&marker=title:+;coord:" .. lat .. "," .. lng
-    end
+    if lat ~= 0 and lng ~= 0 then map_link = "http://apis.map.qq.com/uri/v1/marker?coord_type=1&marker=title:+;coord:" .. lat .. "," .. lng end
     log.debug("util_location.getMapLink", map_link)
     return map_link
 end
 
---- 格式化基站信息
--- @param cell_info_raw 基站信息
--- @return 格式化后的基站信息
-local function formatCellInfo(cell_info_raw)
-    if api_type == "openluat" then
-        local cell_info_arr = {}
-        for i, v in ipairs(cell_info_raw) do
-            table.insert(cell_info_arr, {mcc = v.mcc, mnc = v.mnc, lac = v.tac, ci = v.cid, rxlevel = v.rsrp, hex = 10})
-        end
-        local cell_info_json = json.encode(cell_info_arr)
-        log.debug("util_location.formatCellInfo", api_type .. ":", cell_info_json)
-        return cell_info_json
-    end
-
-    if api_type == "cellocation" then
-        local str = ""
-        for i, v in ipairs(cell_info_raw) do
-            str = str .. (i == 1 and "" or ";")
-            str = str .. v.mcc .. "," .. v.mnc .. "," .. v.tac .. "," .. v.cid .. "," .. v.rsrp
-        end
-        log.debug("util_location.formatCellInfo", api_type .. ":", str)
-        return str
-    end
-end
-
---- 获取基站信息
--- @return 基站信息 or ""
-local function getCellInfo()
-    local cell_info_formatted = formatCellInfo(mobile.getCellInfo())
-    cache.cell_info_formatted = cell_info_formatted
-    return cell_info_formatted
+--- lbsLoc.request 回调
+local function getLocCb(result, lat, lng, addr, time, locType)
+    log.info("util_location.getLocCb", "result,lat,lng,time,locType:", result, lat, lng, time:toHex(), locType)
+    -- 获取经纬度成功, 坐标系WGS84
+    if result == 0 then cache.lbs_data = { lat, lng } end
 end
 
 --- 刷新基站信息
 -- @param timeout 超时时间(单位: 秒)
-function util_location.refreshCellInfo(timeout)
+local function refreshCellInfo(timeout)
     log.info("util_location.refreshCellInfo", "start")
     if cache.is_req_cell_info_running then
         log.info("util_location.refreshCellInfo", "running, wait...")
     else
         cache.is_req_cell_info_running = true
-        mobile.reqCellInfo(timeout or 30) -- 单位: 秒
+        mobile.reqCellInfo(timeout or 20) -- 单位: 秒
     end
     sys.waitUntil("CELL_INFO_UPDATE")
     cache.is_req_cell_info_running = false
@@ -84,76 +49,14 @@ function util_location.refreshCellInfo(timeout)
 end
 
 --- 刷新基站定位信息
--- @param timeout 超时时间(单位: 秒)
--- @return 刷新成功返回 true
-function util_location.refresh(timeout, is_refresh_cell_info_disabled)
-    timeout = type(timeout) == "number" and timeout * 1000 or nil
-
-    local openluat = function(cell_info_formatted)
-        local lbs_api = "http://bs.openluat.com/get_gpss"
-        local header = {
-            ["Content-Type"] = "application/x-www-form-urlencoded"
-        }
-        local body = "data=" .. cell_info_formatted
-        local code, headers, body = util_http.fetch(timeout, "POST", lbs_api, header, body)
-        log.info("util_location.refresh", api_type .. ":", "code:", code, "body:", body)
-
-        if code ~= 200 or body == nil or body == "" then
-            return
-        end
-
-        local lbs_data = json.decode(body) or {}
-        local status, lat, lng = lbs_data.status, lbs_data.lat, lbs_data.lng
-
-        if status ~= 0 or lat == nil or lng == nil or lat == "" or lng == "" then
-            return
-        end
-
-        return lat, lng
-    end
-
-    local cellocation = function(cell_info_formatted)
-        local lbs_api = "http://api.cellocation.com:83/loc/?output=json&cl=" .. cell_info_formatted
-        local code, headers, body = util_http.fetch(timeout, "GET", lbs_api)
-        log.info("util_location.refresh", api_type .. ":", "code:", code, "body:", body)
-
-        if code ~= 200 or body == nil or body == "" then
-            return
-        end
-
-        local lbs_data = json.decode(body) or {}
-        local errcode, lat, lng = lbs_data.errcode, lbs_data.lat, lbs_data.lon
-        if errcode ~= 0 or lat == nil or lng == nil or lat == "0.0" or lng == "0.0" then
-            return
-        end
-
-        return lat, lng
-    end
-
-    sys.taskInit(
-        function()
-            if not is_refresh_cell_info_disabled then
-                util_location.refreshCellInfo(timeout)
-            end
-            local old_cell_info_formatted = cache.cell_info_formatted
-            local cell_info_formatted = getCellInfo()
-
-            if cell_info_formatted == old_cell_info_formatted then
-                log.info("util_location.refresh", api_type .. ":", "cell_info 无变化, 不重新请求")
-                return
-            end
-
-            local lat, lng
-            if api_type == "openluat" then
-                lat, lng = openluat(cell_info_formatted)
-            elseif api_type == "cellocation" then
-                lat, lng = cellocation(cell_info_formatted)
-            end
-            if lat and lng then
-                cache.lbs_data = {lat, lng}
-            end
-        end
-    )
+-- @param timeout 超时时间(单位: 毫秒)
+function util_location.refresh(timeout)
+    timeout = type(timeout) == "number" and timeout or nil
+
+    sys.taskInit(function()
+        refreshCellInfo()
+        lbsLoc.request(getLocCb, nil, timeout, nil, "bs.air32.cn")
+    end)
 end
 
 --- 获取位置信息
@@ -166,11 +69,8 @@ function util_location.get()
     return lat, lng, getMapLink(lat, lng)
 end
 
-sys.subscribe(
-    "CELL_INFO_UPDATE",
-    function()
-        log.debug("EVENT.CELL_INFO_UPDATE")
-    end
-)
+sys.taskInit(refreshCellInfo)
+
+sys.subscribe("CELL_INFO_UPDATE", function() log.debug("EVENT.CELL_INFO_UPDATE") end)
 
 return util_location

+ 10 - 10
script/util_mobile.lua

@@ -3,19 +3,19 @@ local util_mobile = {}
 -- 运营商数据
 local oper_data = {
     -- 中国移动
-    ["46000"] = {"CM", "中国移动", {"10086", "CXLL"}},
-    ["46002"] = {"CM", "中国移动", {"10086", "CXLL"}},
-    ["46007"] = {"CM", "中国移动", {"10086", "CXLL"}},
+    ["46000"] = { "CM", "中国移动", { "10086", "CXLL" } },
+    ["46002"] = { "CM", "中国移动", { "10086", "CXLL" } },
+    ["46007"] = { "CM", "中国移动", { "10086", "CXLL" } },
     -- 中国联通
-    ["46001"] = {"CU", "中国联通", {"10010", "2082"}},
-    ["46006"] = {"CU", "中国联通", {"10010", "2082"}},
-    ["46009"] = {"CU", "中国联通", {"10010", "2082"}},
+    ["46001"] = { "CU", "中国联通", { "10010", "2082" } },
+    ["46006"] = { "CU", "中国联通", { "10010", "2082" } },
+    ["46009"] = { "CU", "中国联通", { "10010", "2082" } },
     -- 中国电信
-    ["46003"] = {"CT", "中国电信", {"10001", "108"}},
-    ["46005"] = {"CT", "中国电信", {"10001", "108"}},
-    ["46011"] = {"CT", "中国电信", {"10001", "108"}},
+    ["46003"] = { "CT", "中国电信", { "10001", "108" } },
+    ["46005"] = { "CT", "中国电信", { "10001", "108" } },
+    ["46011"] = { "CT", "中国电信", { "10001", "108" } },
     -- 中国广电
-    ["46015"] = {"CB", "中国广电"}
+    ["46015"] = { "CB", "中国广电" },
 }
 
 --- 获取 MCC 和 MNC

+ 29 - 39
script/util_netled.lua

@@ -9,63 +9,53 @@ local netled_interval = netled_default_interval
 local netled_inited = false
 
 -- 开机时呼吸灯效果
-sys.taskInit(
-    function()
-        local nums = {0, 1, 2, 4, 6, 12, 16, 21, 27, 34, 42, 51, 61, 72, 85, 100, 100}
-        local len = #nums
-        while true do
-            for i = 1, len, 1 do
-                pwm.open(4, 1000, nums[i])
-                result = sys.waitUntil("NET_LED_INIT", 25)
-                if result then
-                    pwm.close(4)
-                    return
-                end
+sys.taskInit(function()
+    local nums = { 0, 1, 2, 4, 6, 12, 16, 21, 27, 34, 42, 51, 61, 72, 85, 100, 100 }
+    local len = #nums
+    while true do
+        for i = 1, len, 1 do
+            pwm.open(4, 1000, nums[i])
+            result = sys.waitUntil("NET_LED_INIT", 25)
+            if result then
+                pwm.close(4)
+                return
             end
-            for i = len, 1, -1 do
-                pwm.open(4, 1000, nums[i])
-                result = sys.waitUntil("NET_LED_INIT", 25)
-                if result then
-                    pwm.close(4)
-                    return
-                end
+        end
+        for i = len, 1, -1 do
+            pwm.open(4, 1000, nums[i])
+            result = sys.waitUntil("NET_LED_INIT", 25)
+            if result then
+                pwm.close(4)
+                return
             end
         end
     end
-)
+end)
 
 -- 注册网络后开始闪烁
 function util_netled.init()
-    if netled_inited then
-        return
-    end
+    if netled_inited then return end
     netled_inited = true
     sys.publish("NET_LED_INIT")
 
-    sys.taskInit(
-        function()
-            local netled = gpio.setup(27, 0, gpio.PULLUP)
-            while true do
-                netled(1)
-                sys.waitUntil("NET_LED_UPDATE", netled_duration)
-                netled(0)
-                sys.waitUntil("NET_LED_UPDATE", netled_interval)
-            end
+    sys.taskInit(function()
+        local netled = gpio.setup(27, 0, gpio.PULLUP)
+        while true do
+            netled(1)
+            sys.waitUntil("NET_LED_UPDATE", netled_duration)
+            netled(0)
+            sys.waitUntil("NET_LED_UPDATE", netled_interval)
         end
-    )
+    end)
 end
 
 function util_netled.blink(duration, interval, restore)
-    if duration == netled_duration and interval == netled_interval then
-        return
-    end
+    if duration == netled_duration and interval == netled_interval then return end
     netled_duration = duration or netled_default_duration
     netled_interval = interval or netled_default_interval
     log.debug("EVENT.NET_LED_UPDATE", duration, interval, restore)
     sys.publish("NET_LED_UPDATE")
-    if restore then
-        sys.timerStart(util_netled.blink, restore)
-    end
+    if restore then sys.timerStart(util_netled.blink, restore) end
 end
 
 return util_netled

+ 153 - 125
script/util_notify.lua

@@ -1,3 +1,5 @@
+local lib_smtp = require "lib_smtp"
+
 local util_notify = {}
 
 -- 消息队列
@@ -14,22 +16,62 @@ local function urlencodeTab(params)
 end
 
 local notify = {
+    -- 发送到 custom_post
+    ["custom_post"] = function(msg)
+        if config.CUSTOM_POST_URL == nil or config.CUSTOM_POST_URL == "" then
+            log.error("util_notify", "未配置 `config.CUSTOM_POST_URL`")
+            return
+        end
+        if type(config.CUSTOM_POST_BODY_TABLE) ~= "table" then
+            log.error("util_notify", "未配置 `config.CUSTOM_POST_BODY_TABLE`")
+            return
+        end
+
+        local header = { ["content-type"] = config.CUSTOM_POST_CONTENT_TYPE }
+
+        local body = json.decode(json.encode(config.CUSTOM_POST_BODY_TABLE))
+        -- 遍历并替换其中的变量
+        local function traverse_and_replace(t)
+            for k, v in pairs(t) do
+                if type(v) == "table" then
+                    traverse_and_replace(v)
+                elseif type(v) == "string" then
+                    t[k] = string.gsub(v, "{msg}", msg)
+                end
+            end
+        end
+        traverse_and_replace(body)
+
+        -- 根据 content-type 进行编码, 默认为 application/x-www-form-urlencoded
+        if string.find(config.CUSTOM_POST_CONTENT_TYPE, "json") then
+            body = json.encode(body)
+            -- LuatOS Bug, json.encode 会将 \n 转换为 \b
+            body = string.gsub(body, "\\b", "\\n")
+        else
+            body = urlencodeTab(body)
+        end
+
+        log.info("util_notify", "POST", config.CUSTOM_POST_URL, config.CUSTOM_POST_CONTENT_TYPE, body)
+        return util_http.fetch(nil, "POST", config.CUSTOM_POST_URL, header, body)
+    end,
     -- 发送到 telegram
     ["telegram"] = function(msg)
-        if config.TELEGRAM_PROXY_API == nil or config.TELEGRAM_PROXY_API == "" then
-            log.error("util_notify", "未配置 `config.TELEGRAM_PROXY_API`")
+        if config.TELEGRAM_API == nil or config.TELEGRAM_API == "" then
+            log.error("util_notify", "未配置 `config.TELEGRAM_API`")
+            return
+        end
+        if config.TELEGRAM_CHAT_ID == nil or config.TELEGRAM_CHAT_ID == "" then
+            log.error("util_notify", "未配置 `config.TELEGRAM_CHAT_ID`")
             return
         end
 
-        local header = {
-            ["content-type"] = "text/plain",
-            ["x-disable-web-page-preview"] = "1",
-            ["x-chat-id"] = config.TELEGRAM_CHAT_ID or "",
-            ["x-token"] = config.TELEGRAM_TOKEN or ""
-        }
+        local header = { ["content-type"] = "application/json" }
+        local body = { ["chat_id"] = config.TELEGRAM_CHAT_ID, ["disable_web_page_preview"] = true, ["text"] = msg }
+        local json_data = json.encode(body)
+        -- json_data = string.gsub(json_data, "\\b", "\\n")
 
-        log.info("util_notify", "POST", config.TELEGRAM_PROXY_API)
-        return util_http.fetch(nil, "POST", config.TELEGRAM_PROXY_API, header, msg)
+        log.info("util_notify", "POST", config.TELEGRAM_API)
+        return util_http.fetch(nil, "POST", config.TELEGRAM_API, header, json_data)
     end,
     -- 发送到 gotify
     ["gotify"] = function(msg)
@@ -43,16 +85,10 @@ local notify = {
         end
 
         local url = config.GOTIFY_API .. "/message?token=" .. config.GOTIFY_TOKEN
-        local header = {
-            ["Content-Type"] = "application/json; charset=utf-8"
-        }
-        local body = {
-            title = config.GOTIFY_TITLE,
-            message = msg,
-            priority = config.GOTIFY_PRIORITY
-        }
+        local header = { ["Content-Type"] = "application/json; charset=utf-8" }
+        local body = { title = config.GOTIFY_TITLE, message = msg, priority = config.GOTIFY_PRIORITY }
         local json_data = json.encode(body)
-        json_data = string.gsub(json_data, "\\b", "\\n")
+        -- json_data = string.gsub(json_data, "\\b", "\\n")
 
         log.info("util_notify", "POST", config.GOTIFY_API)
         return util_http.fetch(nil, "POST", url, header, json_data)
@@ -68,14 +104,8 @@ local notify = {
             return
         end
 
-        local header = {
-            ["Content-Type"] = "application/x-www-form-urlencoded"
-        }
-        local body = {
-            pushkey = config.PUSHDEER_KEY or "",
-            type = "text",
-            text = msg
-        }
+        local header = { ["Content-Type"] = "application/x-www-form-urlencoded" }
+        local body = { pushkey = config.PUSHDEER_KEY or "", type = "text", text = msg }
 
         log.info("util_notify", "POST", config.PUSHDEER_API)
         return util_http.fetch(nil, "POST", config.PUSHDEER_API, header, urlencodeTab(body))
@@ -91,12 +121,8 @@ local notify = {
             return
         end
 
-        local header = {
-            ["Content-Type"] = "application/x-www-form-urlencoded"
-        }
-        local body = {
-            body = msg
-        }
+        local header = { ["Content-Type"] = "application/x-www-form-urlencoded" }
+        local body = { body = msg }
         local url = config.BARK_API .. "/" .. config.BARK_KEY
 
         log.info("util_notify", "POST", url)
@@ -109,21 +135,20 @@ local notify = {
             return
         end
 
-        local header = {
-            ["Content-Type"] = "application/json; charset=utf-8"
-        }
-        local body = {
-            msgtype = "text",
-            text = {
-                content = msg
-            }
-        }
-        local json_data = json.encode(body)
-        -- LuatOS Bug, json.encode 会将 \n 转换为 \b
-        json_data = string.gsub(json_data, "\\b", "\\n")
+        local url = config.DINGTALK_WEBHOOK
+        -- 如果配置了 config.DINGTALK_SECRET 则需要签名(加签), 没配置则为自定义关键词
+        if (config.DINGTALK_SECRET ~= nil and config.DINGTALK_SECRET ~= "") then
+            local timestamp = tostring(os.time()) .. "000"
+            local sign = crypto.hmac_sha256(timestamp .. "\n" .. config.DINGTALK_SECRET, config.DINGTALK_SECRET):fromHex():toBase64():urlEncode()
+            url = url .. "&timestamp=" .. timestamp .. "&sign=" .. sign
+        end
 
-        log.info("util_notify", "POST", config.DINGTALK_WEBHOOK)
-        return util_http.fetch(nil, "POST", config.DINGTALK_WEBHOOK, header, json_data)
+        local header = { ["Content-Type"] = "application/json; charset=utf-8" }
+        local body = { msgtype = "text", text = { content = msg } }
+        body = json.encode(body)
+
+        log.info("util_notify", "POST", url)
+        return util_http.fetch(nil, "POST", url, header, body)
     end,
     -- 发送到 feishu
     ["feishu"] = function(msg)
@@ -132,18 +157,11 @@ local notify = {
             return
         end
 
-        local header = {
-            ["Content-Type"] = "application/json; charset=utf-8"
-        }
-        local body = {
-            msg_type = "text",
-            content = {
-                text = msg
-            }
-        }
+        local header = { ["Content-Type"] = "application/json; charset=utf-8" }
+        local body = { msg_type = "text", content = { text = msg } }
         local json_data = json.encode(body)
         -- LuatOS Bug, json.encode 会将 \n 转换为 \b
-        json_data = string.gsub(json_data, "\\b", "\\n")
+        -- json_data = string.gsub(json_data, "\\b", "\\n")
 
         log.info("util_notify", "POST", config.FEISHU_WEBHOOK)
         return util_http.fetch(nil, "POST", config.FEISHU_WEBHOOK, header, json_data)
@@ -155,18 +173,11 @@ local notify = {
             return
         end
 
-        local header = {
-            ["Content-Type"] = "application/json; charset=utf-8"
-        }
-        local body = {
-            msgtype = "text",
-            text = {
-                content = msg
-            }
-        }
+        local header = { ["Content-Type"] = "application/json; charset=utf-8" }
+        local body = { msgtype = "text", text = { content = msg } }
         local json_data = json.encode(body)
         -- LuatOS Bug, json.encode 会将 \n 转换为 \b
-        json_data = string.gsub(json_data, "\\b", "\\n")
+        -- json_data = string.gsub(json_data, "\\b", "\\n")
 
         log.info("util_notify", "POST", config.WECOM_WEBHOOK)
         return util_http.fetch(nil, "POST", config.WECOM_WEBHOOK, header, json_data)
@@ -182,18 +193,12 @@ local notify = {
             return
         end
 
-        local header = {
-            ["Content-Type"] = "application/json; charset=utf-8"
-        }
-        local body = {
-            token = config.PUSHOVER_API_TOKEN,
-            user = config.PUSHOVER_USER_KEY,
-            message = msg
-        }
+        local header = { ["Content-Type"] = "application/json; charset=utf-8" }
+        local body = { token = config.PUSHOVER_API_TOKEN, user = config.PUSHOVER_USER_KEY, message = msg }
 
         local json_data = json.encode(body)
         -- LuatOS Bug, json.encode 会将 \n 转换为 \b
-        json_data = string.gsub(json_data, "\\b", "\\n")
+        -- json_data = string.gsub(json_data, "\\b", "\\n")
 
         local url = "https://api.pushover.net/1/messages.json"
 
@@ -243,9 +248,7 @@ local notify = {
             return
         end
 
-        local header = {
-            ["Content-Type"] = "application/x-www-form-urlencoded"
-        }
+        local header = { ["Content-Type"] = "application/x-www-form-urlencoded" }
         local body = {
             user = config.NEXT_SMTP_PROXY_USER,
             password = config.NEXT_SMTP_PROXY_PASSWORD,
@@ -254,22 +257,43 @@ local notify = {
             form_name = config.NEXT_SMTP_PROXY_FORM_NAME,
             to_email = config.NEXT_SMTP_PROXY_TO_EMAIL,
             subject = config.NEXT_SMTP_PROXY_SUBJECT,
-            text = msg
+            text = msg,
         }
 
         log.info("util_notify", "POST", config.NEXT_SMTP_PROXY_API)
         return util_http.fetch(nil, "POST", config.NEXT_SMTP_PROXY_API, header, urlencodeTab(body))
-    end
+    end,
+    ["smtp"] = function(msg)
+        local smtp_config = {
+            host = config.SMTP_HOST,
+            port = config.SMTP_PORT,
+            username = config.SMTP_USERNAME,
+            password = config.SMTP_PASSWORD,
+            mail_from = config.SMTP_MAIL_FROM,
+            mail_to = config.SMTP_MAIL_TO,
+            tls_enable = config.SMTP_TLS_ENABLE,
+        }
+        local result = lib_smtp.send(msg, config.SMTP_MAIL_SUBJECT, smtp_config)
+        log.info("util_notify", "SMTP", result.success, result.message, result.is_retry)
+        if result.success then return 200, nil, result.message end
+        if result.is_retry then return 500, nil, result.message end
+        return 400, nil, result.message
+    end,
+    -- 发送到 serial
+    ["serial"] = function(msg)
+        uart.write(1, msg)
+        log.info("util_notify", "serial", "消息已转发到串口")
+        sys.wait(1000)
+        return 200
+    end,
 }
 
 local function append()
     local msg = "\n"
 
     -- 本机号码
-    local number = mobile.number(mobile.simid())
-    if number then
-        msg = msg .. "\n本机号码: " .. number
-    end
+    local number = mobile.number(mobile.simid()) or config.FALLBACK_LOCAL_NUMBER
+    if number then msg = msg .. "\n本机号码: " .. number end
 
     -- 开机时长
     local ms = mcu.ticks()
@@ -279,43 +303,35 @@ local function append()
     seconds = seconds % 60
     minutes = minutes % 60
     local boot_time = string.format("%02d:%02d:%02d", hours, minutes, seconds)
-    if ms >= 0 then
-        msg = msg .. "\n开机时长: " .. boot_time
-    end
+    if ms >= 0 then msg = msg .. "\n开机时长: " .. boot_time end
 
     -- 运营商
     local oper = util_mobile.getOper(true)
-    if oper ~= "" then
-        msg = msg .. "\n运营商: " .. oper
-    end
+    if oper ~= "" then msg = msg .. "\n运营商: " .. oper end
 
     -- 信号
     local rsrp = mobile.rsrp()
-    if rsrp ~= 0 then
-        msg = msg .. "\n信号: " .. rsrp .. "dBm"
-    end
+    if rsrp ~= 0 then msg = msg .. "\n信号: " .. rsrp .. "dBm" end
 
     -- 频段
     local band = util_mobile.getBand()
-    if band >= 0 then
-        msg = msg .. "\n频段: B" .. band
-    end
+    if band >= 0 then msg = msg .. "\n频段: B" .. band end
 
     -- 流量统计
-    local uplinkGB, uplinkB, downlinkGB, downlinkB = mobile.dataTraffic()
-    uplinkB = uplinkGB * 1024 * 1024 * 1024 + uplinkB
-    downlinkB = downlinkGB * 1024 * 1024 * 1024 + downlinkB
-    local function formatBytes(bytes)
-        if bytes < 1024 then
-            return bytes .. "B"
-        elseif bytes < 1024 * 1024 then
-            return string.format("%.2fKB", bytes / 1024)
-        elseif bytes < 1024 * 1024 * 1024 then
-            return string.format("%.2fMB", bytes / 1024 / 1024)
-        else
-            return string.format("%.2fGB", bytes / 1024 / 1024 / 1024)
-        end
-    end
+    -- local uplinkGB, uplinkB, downlinkGB, downlinkB = mobile.dataTraffic()
+    -- uplinkB = uplinkGB * 1024 * 1024 * 1024 + uplinkB
+    -- downlinkB = downlinkGB * 1024 * 1024 * 1024 + downlinkB
+    -- local function formatBytes(bytes)
+    --     if bytes < 1024 then
+    --         return bytes .. "B"
+    --     elseif bytes < 1024 * 1024 then
+    --         return string.format("%.2fKB", bytes / 1024)
+    --     elseif bytes < 1024 * 1024 * 1024 then
+    --         return string.format("%.2fMB", bytes / 1024 / 1024)
+    --     else
+    --         return string.format("%.2fGB", bytes / 1024 / 1024 / 1024)
+    --     end
+    -- end
     -- msg = msg .. "\n流量: ↑" .. formatBytes(uplinkB) .. " ↓" .. formatBytes(downlinkB)
 
     -- 位置
@@ -350,11 +366,6 @@ function util_notify.send(msg, channel)
         return true
     end
 
-    -- 通知内容追加更多信息
-    if config.NOTIFY_APPEND_MORE_INFO then
-        msg = msg .. append()
-    end
-
     -- 发送通知
     local code, headers, body = notify[channel](msg)
     if code == nil then
@@ -381,21 +392,24 @@ end
 -- @param msg 消息内容
 -- @param channels 通知渠道
 function util_notify.add(msg, channels)
-    if type(msg) == "table" then
-        msg = table.concat(msg, "\n")
+    if type(msg) == "table" then msg = table.concat(msg, "\n") end
+
+    -- 通知内容追加更多信息, 若已经包含则不再追加
+    local is_append = true
+    if string.find(msg, "本机号码:") and string.find(msg, "开机时长:") then
+        log.info("util_notify.send", "不追加更多信息")
+        is_append = false
     end
+    if config.NOTIFY_APPEND_MORE_INFO and is_append then msg = msg .. append() end
 
     channels = channels or config.NOTIFY_TYPE
 
-    if type(channels) ~= "table" then
-        channels = {channels}
-    end
+    if type(channels) ~= "table" then channels = { channels } end
 
-    for _, channel in ipairs(channels) do
-        table.insert(msg_queue, {channel = channel, msg = msg, retry = 0})
-    end
+    for _, channel in ipairs(channels) do table.insert(msg_queue, { channel = channel, msg = msg, retry = 0 }) end
     sys.publish("NEW_MSG")
     log.debug("util_notify.add", "添加到消息队列, 当前队列长度:", #msg_queue)
+    log.debug("util_notify.add", "添加到消息队列的内容:", msg:gsub("\r", "\\r"):gsub("\n", "\\n"))
 end
 
 -- 轮询消息队列
@@ -403,9 +417,23 @@ end
 -- 发送失败则等待下次轮询
 local function poll()
     local item, result
+    local codes = {
+        [0] = "网络未注册",
+        [1] = "网络已注册",
+        [2] = "网络搜索中",
+        [3] = "网络注册被拒绝",
+        [4] = "网络状态未知",
+        [5] = "网络已注册,漫游",
+        [6] = "网络已注册,仅SMS",
+        [7] = "网络已注册,漫游,仅SMS",
+        [8] = "网络已注册,紧急服务",
+        [9] = "网络已注册,非主要服务",
+        [10] = "网络已注册,非主要服务,漫游",
+    }
     while true do
         -- 消息队列非空, 且网络已注册
-        if next(msg_queue) ~= nil and mobile.status() == 1 then
+        log.debug("mobile.status:", codes[mobile.status() or 0] or "未知网络状态")
+        if next(msg_queue) ~= nil and (mobile.status() == 1 or mobile.status() == 5) then
             log.debug("util_notify.poll", "轮询消息队列中, 当前队列长度:", #msg_queue)
 
             item = msg_queue[1]