From 37e9975503b25353446e2a3e4c129a68c3649638 Mon Sep 17 00:00:00 2001 From: Anton Chen Date: Wed, 27 Nov 2024 21:54:03 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BF=9D=E6=B4=BB=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E5=AE=9A=E6=97=B6=E5=8F=91=E9=80=81=E7=9F=AD?= =?UTF-8?q?=E4=BF=A1=E5=88=B0=E6=8C=87=E5=AE=9A=E5=8F=B7=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 185 ++++++++++++++++++++++++---------------------- air780_helper.lua | 52 ++++++++++++- config.lua | 17 +++-- constants.lua | 2 + main.lua | 12 ++- pdu_helper.lua | 14 ++++ renew.lua | 88 ++++++++++++++++++++++ 7 files changed, 274 insertions(+), 96 deletions(-) create mode 100644 renew.lua diff --git a/README.md b/README.md index b4f5654..a25d6a8 100644 --- a/README.md +++ b/README.md @@ -1,89 +1,96 @@ -# Air780E短信转发 - -利用ESP32驱动Air780E实现短信转发,兼容合宙ESP32S3和ESP32C3。 - -**⚠ 仅支持联通、移动网络,不支持电信网络 ⚠** - -# 功能 - -- [x] 自动转发收到的短信,短信内容支持多种语言(其实就是ASCII和UCS-2字符集),目前已测试过英文、中文、日语、俄语字符 -- [x] 支持多个推送平台,目前接入: - - [x] [LuatOS社区提供的推送服务器](https://push.luatos.org/) - - [x] Bark - - [x] Server酱 - - [x] 钉钉机器人 - - [x] 推送加 PushPlus - - [x] Telegram(感谢 [@wongJG](https://github.com/wongJG) 的 Pull Request) - - [x] 飞书机器人(感谢 [@mmdjiji](https://github.com/mmdjiji) 的 Pull Request) - -# 使用方法 - -## 硬件组装 - -- 短接POW键上方的焊盘实现通电即开机 - -![](/image/shorting_soldering_pad_for_power_key.jpg) - -- 按照下图方向为Air780e和ESP32焊上排针和排座。注意合宙不送排座,需要自己买。 - -| Air780E | ESP32S3 | -|--------------------------|--------------------------| -| ![](/image/air780e.jpeg) | ![](/image/esp32s3.jpeg) | - -- 按图示方向插入SIM卡 - -![](/image/sim_card_direction.jpeg) - -- 按图示方向将Air780E和ESP32组合 - -![](/image/put_together.jpeg) - -## 为Air780e刷入AT固件 - -USB连接Air780e,选择 `Luatools/resource/618_lua_lod/版本号` 目录下的AT固件,将其烧录到Air780e。 - -## 修改脚本,刷入ESP32 - -- 修改[`config.lua`](config.lua) - - 修改`config.board_type`为正确的型号,可选值见注释 - - 修改`config.wifi`,填入无线网络的SSID和密码 - - 修改`config.notification_channel`,将要启用的通知通道的`enabled`配置置为`true`,并填写推送平台相关配置 -- 烧录脚本 - - 将[`firmware`](firmware)目录中对应的固件烧入开发板 - - 将所有`lua`脚本下载至开发板 -![](/image/burning_firmware_and_scripts.png) - - 将开发板上电开机,等待初始化完成后,即可转发短信到配置的通知通道 - -# LED灯状态含义 - -- ESP32 - - C3的`D4`或S3的`LED A`为初始化状态灯,闪烁代表正在初始化,常亮代表初始化完成,准备转发短信 - - C3的`D5`或S3的`LED B`为工作状态灯,平时长灭,收到新短信后高频闪烁,转发完成后熄灭 - -| ESP32C3 | ESP32S3 | -|-----------------------------|-----------------------------| -| ![](/image/esp32c3_led.jpg) | ![](/image/esp32s3_led.png) | - -- Air780 - - `POW`灯为电源指示灯,通电后常亮。注意,这个LED不代表开机状态,只要板子有电这个灯就会亮 - - `NET`灯为网络状态指示灯,长亮短灭代表正在初始化蜂窝网络,短亮长灭代表网络注册成功,可以接收短信 - -# Firmware目录下的文件说明 - -- `LuatOS-SoC_V1004_ESP32C3_classic.soc`对应`ESP32C3 经典款` -- `LuatOS-SoC_V1004_ESP32C3_lite.soc`对应`ESP32C3 简约款` -- `LuatOS-SoC_V1004_ESP32S3.soc`对应`ESP32S3` - -固件均通过[合宙云编译](https://wiki.luatos.com/develop/compile/Cloud_compilation.html)精简掉了不需要的功能,以保证内存空间充足。`LuaTools`自动下载的固件不能用,系统启动之后内存就不够用了,发不出去HTTP请求。 - -目前固件包含`gpio`、`uart`、`pwm`、`wdt`、`crypto`、`rtc`、`network`、`sntp`、`tls`、`wlan`、`pm`、`cjson`、`ntp`、`shell`、`dbg`。 - -# 致谢 - -本项目参考[低成本短信转发器](https://github.com/chenxuuu/sms_forwarding)而来,尤其是PDU相关代码,没有`chenxuuu`的这份项目和[50元内自制短信转发器(Air780E+ESP32C3)](https://www.chenxublog.com/2022/10/28/19-9-sms-forwarding-air780e-esp32c3.html)这篇文章,我不会这么快就完成开发。 - -# 赞助 - -| 支付宝 | 微信 | Bitcoin | -| ------ | ---- | ------- | -| ![](https://sat02pap001files.storage.live.com/y4mQubRjj6HwFcaRN5WA43bM81G13d2xI-3OAoLSsXXDxJQZ_inF6qA_OFDB51Pg3yfjXu8CSyioCTUI3StB_Dltd7vmBWNHRT0Ok8zMd9Rf_WU42mgDY-pJW_yCrJ0KEUsd32yi5xqB1wjR4lv8jzMboKmpphgwoeOpPR5xgnfhNbfU8ozvDcfnnEiCpvZ6rLk?width=548&height=542&cropmode=none) | ![](https://sat02pap001files.storage.live.com/y4mRChq9zMZbQZK0gVO19Smbyt74YG1QWTI9RAgewZpJKn6BOEg0GK-_AgR9LwdjDSJriEgnz05YSc9fYUiH09i-PKnb40lZI0AqbvtcyXJvqVSdiWbGpeqPFmIktJb2t-bjIXqrupCzZxXWPXmrrFXXdFzgSWstjebkOujhr-ByhKWoLvgn3GHu2WpnGzbKgXs?width=602&height=599&cropmode=none) | ![3H8yBE359vkbpvC4nSP5xwafWThUh4JvGB](https://sat02pap001files.storage.live.com/y4m7ll7ouERuCbkCXI1x-PQJMYTzonfgpFoEL7Odz8HwPC-O2DngJrulJd23PzD6dJnucGf1zC6zGp4PFyVZjJecRWVT69c06Y4OPdjpEh5Z3E6qkRNg1ZMuP9bxQ3R_YKt2HtjzG_BD3_a9gUkRwHm-zmNH1gxJxnSbysa_qbS8xoiFenQioB4RcU-tMZn71z8?width=1044&height=1098&cropmode=none) | +# Air780E短信转发 + +利用ESP32驱动Air780E实现短信转发,兼容合宙ESP32S3和ESP32C3。 + +**⚠ 仅支持联通、移动网络,不支持电信网络 ⚠** + +# 功能 + +- [x] 自动转发收到的短信,短信内容支持多种语言(其实就是ASCII和UCS-2字符集),目前已测试过英文、中文、日语、俄语字符 +- [x] 支持多个推送平台,目前接入: + - [x] [LuatOS社区提供的推送服务器](https://push.luatos.org/) + - [x] Bark + - [x] Server酱 + - [x] 钉钉机器人 + - [x] 推送加 PushPlus + - [x] Telegram(感谢 [@wongJG](https://github.com/wongJG) 的 Pull Request) + - [x] 飞书机器人(感谢 [@mmdjiji](https://github.com/mmdjiji) 的 Pull Request) + +# 使用方法 + +## 硬件组装 + +- 短接POW键上方的焊盘实现通电即开机 + +![](/image/shorting_soldering_pad_for_power_key.jpg) + +- 按照下图方向为Air780e和ESP32焊上排针和排座。注意合宙不送排座,需要自己买。 + +| Air780E | ESP32S3 | +|--------------------------|--------------------------| +| ![](/image/air780e.jpeg) | ![](/image/esp32s3.jpeg) | + +- 按图示方向插入SIM卡 + +![](/image/sim_card_direction.jpeg) + +- 按图示方向将Air780E和ESP32组合 + +![](/image/put_together.jpeg) + +## 为Air780e刷入AT固件 + +USB连接Air780e,选择 `Luatools/resource/618_lua_lod/版本号` 目录下的AT固件,将其烧录到Air780e。 + +## 修改脚本,刷入ESP32 + +- 修改[`config.lua`](config.lua) + - 修改`config.board_type`为正确的型号,可选值见注释 + - 修改`config.wifi`,填入无线网络的SSID和密码 + - 修改`config.notification_channel`,将要启用的通知通道的`enabled`配置置为`true`,并填写推送平台相关配置 +- 烧录脚本 + - 将[`firmware`](firmware)目录中对应的固件烧入开发板 + - 将所有`lua`脚本下载至开发板 +![](/image/burning_firmware_and_scripts.png) + - 将开发板上电开机,等待初始化完成后,即可转发短信到配置的通知通道 + +# LED灯状态含义 + +- ESP32 + - C3的`D4`或S3的`LED A`为初始化状态灯,闪烁代表正在初始化,常亮代表初始化完成,准备转发短信 + - C3的`D5`或S3的`LED B`为工作状态灯,平时长灭,收到新短信后高频闪烁,转发完成后熄灭 + +| ESP32C3 | ESP32S3 | +|-----------------------------|-----------------------------| +| ![](/image/esp32c3_led.jpg) | ![](/image/esp32s3_led.png) | + +- Air780 + - `POW`灯为电源指示灯,通电后常亮。注意,这个LED不代表开机状态,只要板子有电这个灯就会亮 + - `NET`灯为网络状态指示灯,长亮短灭代表正在初始化蜂窝网络,短亮长灭代表网络注册成功,可以接收短信 + +# Firmware目录下的文件说明 + +- `LuatOS-SoC_V1004_ESP32C3_classic.soc`对应`ESP32C3 经典款` +- `LuatOS-SoC_V1004_ESP32C3_lite.soc`对应`ESP32C3 简约款` +- `LuatOS-SoC_V1004_ESP32S3.soc`对应`ESP32S3` + +固件均通过[合宙云编译](https://wiki.luatos.com/develop/compile/Cloud_compilation.html)精简掉了不需要的功能,以保证内存空间充足。`LuaTools`自动下载的固件不能用,系统启动之后内存就不够用了,发不出去HTTP请求。 + +目前固件包含`gpio`、`uart`、`pwm`、`wdt`、`crypto`、`rtc`、`network`、`sntp`、`tls`、`wlan`、`pm`、`cjson`、`ntp`、`shell`、`dbg`。 + +# 保活 API 说明 + +API 提供 GET 和 POST 请求支持。 + +- GET 请求返回 POST 请求存储的时间戳 +- POST 请求接收 `{ "expiry": "1732622763" }`,并存储 + +# 致谢 + +本项目参考[低成本短信转发器](https://github.com/chenxuuu/sms_forwarding)而来,尤其是PDU相关代码,没有`chenxuuu`的这份项目和[50元内自制短信转发器(Air780E+ESP32C3)](https://www.chenxublog.com/2022/10/28/19-9-sms-forwarding-air780e-esp32c3.html)这篇文章,我不会这么快就完成开发。 + +# 赞助 + +| 支付宝 | 微信 | Bitcoin | +| ------ | ---- | ------- | +| ![](https://sat02pap001files.storage.live.com/y4mQubRjj6HwFcaRN5WA43bM81G13d2xI-3OAoLSsXXDxJQZ_inF6qA_OFDB51Pg3yfjXu8CSyioCTUI3StB_Dltd7vmBWNHRT0Ok8zMd9Rf_WU42mgDY-pJW_yCrJ0KEUsd32yi5xqB1wjR4lv8jzMboKmpphgwoeOpPR5xgnfhNbfU8ozvDcfnnEiCpvZ6rLk?width=548&height=542&cropmode=none) | ![](https://sat02pap001files.storage.live.com/y4mRChq9zMZbQZK0gVO19Smbyt74YG1QWTI9RAgewZpJKn6BOEg0GK-_AgR9LwdjDSJriEgnz05YSc9fYUiH09i-PKnb40lZI0AqbvtcyXJvqVSdiWbGpeqPFmIktJb2t-bjIXqrupCzZxXWPXmrrFXXdFzgSWstjebkOujhr-ByhKWoLvgn3GHu2WpnGzbKgXs?width=602&height=599&cropmode=none) | ![3H8yBE359vkbpvC4nSP5xwafWThUh4JvGB](https://sat02pap001files.storage.live.com/y4m7ll7ouERuCbkCXI1x-PQJMYTzonfgpFoEL7Odz8HwPC-O2DngJrulJd23PzD6dJnucGf1zC6zGp4PFyVZjJecRWVT69c06Y4OPdjpEh5Z3E6qkRNg1ZMuP9bxQ3R_YKt2HtjzG_BD3_a9gUkRwHm-zmNH1gxJxnSbysa_qbS8xoiFenQioB4RcU-tMZn71z8?width=1044&height=1098&cropmode=none) | diff --git a/air780_helper.lua b/air780_helper.lua index 671e692..bedd415 100644 --- a/air780_helper.lua +++ b/air780_helper.lua @@ -38,6 +38,10 @@ end) -- 发送AT指令 function air780_helper.send_at_command(command) uart.write(uart_id, command) + -- 如果是PDU模式下的短信内容,直接返回 + if command:sub(1, 6) == "001110" then + return + end uart.write(uart_id, "\r\n") log.debug("air780_helper", "发送AT指令\""..command.."\"") end @@ -80,6 +84,20 @@ sys.subscribe(constants.uart_ready_message, function() return end + -- 响应发送短信指令 + if current_line:find(">", 1, true) then + -- log.debug("air780_helper", "捕获到短信发送提示符") + sys.publish(constants.air780_helper_sms_send_ready) + return + end + + -- 响应发送短信成功 + if current_line:find("+CMGS:", 1, true) then + -- log.debug("air780_helper", "捕获到短信发送成功") + sys.publish(constants.air780_send_sms_success) + return + end + local urc = current_line:match("^%+(%w+)") if urc then -- URC上报 @@ -129,7 +147,7 @@ sys.subscribe(constants.uart_ready_message, function() end until #data == 0 end - end + end end end end) @@ -145,4 +163,36 @@ function air780_helper.send_at_command_and_wait(command, topic_listen_to, timeou end end +function air780_helper.topic_wait(topic, timeout) + local is_successful, r1, r2, r3 = sys.waitUntil(topic, timeout or 1000) + return is_successful +end + +function air780_helper.sent_sms(to, text) + local logging_tag = "air780_helper.sent_sms" + local data, len = pdu_helper.encode_pdu(to, text) + if not data or not len then + log.error(logging_tag, "短信编码失败") + return false + end + air780_helper.send_at_command("AT+CMGS=" .. len) + + -- 增加调试信息 + log.debug(logging_tag, "等待短信发送提示符") + local result = air780_helper.topic_wait(constants.air780_helper_sms_send_ready, 3000) + if not result then + log.error(logging_tag, "短信发送失败,AT+CMGS=" .. len .. " 超时") + return false + end + + air780_helper.send_at_command(data .. "\x1A") + local result = air780_helper.topic_wait(constants.air780_send_sms_success, 5000) + if result then + log.info(logging_tag, "短信发送成功") + return true + else + log.error(logging_tag, "短信发送失败") + return false + end +end return air780_helper diff --git a/config.lua b/config.lua index d88c479..9571982 100644 --- a/config.lua +++ b/config.lua @@ -18,15 +18,22 @@ config.disable_rndis = true -- 而1156版本AT固件第一次检测有概率检测不到SIM卡,需要重试 config.retry_sim_detection = true +-- 续期 API +config.renew_api = "https://api.example.com/sim/expiry" +-- 续期间隔 +config.renew_day = 180 +-- 续期检查间隔,单位小时。为 0 时禁用 +config.renew_check_interval = 0 +-- 续期接收号码 +config.renew_number = "+8613881388138" +-- 续期短信内容 +config.renew_content = "注意余额" + config.wifi = { { ssid = "Wi-Fi名", password = "Wi-Fi密码", }, - -- { - -- ssid = "", - -- password = "", - -- } } -- 手动配置DNS服务器 @@ -73,7 +80,7 @@ config.notification_channel = { -- telegram 机器人 telegram = { enabled = false, - -- Webhook地址 + -- Webhook地址, https://api.telegram.org/bot/sendMessage webhook_url = "", -- chat_id, 通过 https://api.telegram.org/bot/getUpdates 获取 chat_id = "" diff --git a/constants.lua b/constants.lua index 2d778fd..6869e45 100644 --- a/constants.lua +++ b/constants.lua @@ -32,5 +32,7 @@ constants.air780_message_topic_new_message_notification_configured = "NEW_MESSAG constants.air780_message_topic_network_connected = "NETWORK_CONNECTED" constants.air780_message_topic_new_sms_received = "NEW_SMS_RECEIVED" constants.air780_message_topic_new_notification_request = "NEW_NOTIFICATION_REQUEST" +constants.air780_helper_sms_send_ready = "SMS_SEND_READY" +constants.air780_send_sms_success = "SMS_SENT_SUCCESSFULLY" return constants diff --git a/main.lua b/main.lua index d4df959..9480c8d 100644 --- a/main.lua +++ b/main.lua @@ -110,7 +110,7 @@ sys.taskInit(function () log.info(logging_tag, "GPRS已附着") break else - log.info(logging_tag, "GPRS未附着,将在5秒后重新检查") + log.info(logging_tag, "GPRS未附着, 将在5秒后重新检查") sys.wait(5000) end end @@ -131,6 +131,16 @@ sys.taskInit(function () led_helper.light_status_led() end) +sys.taskInit(function() + renew = require("renew") + sys.waitUntil(constants.air780_message_topic_new_message_notification_configured) + sys.waitUntil("NTP_UPDATE") + if config.renew_check_interval > 0 then + log.info("renew", "自动续期启动") + renew.renew() + end +end) + --[[ long_sms_buffer = { [phone_number] = { diff --git a/pdu_helper.lua b/pdu_helper.lua index fac935d..3248a36 100644 --- a/pdu_helper.lua +++ b/pdu_helper.lua @@ -363,4 +363,18 @@ function pdu_helper.decode_pdu(pdu, len) return sender_number, sms_content_in_utf8, sms_receive_time, long_sms, total_message_count, current_idx, sms_id end +-- 生成PDU短信编码 +-- 仅支持单条短信,传入数据为utf8编码 +-- 返回值为pdu编码与长度 +function pdu_helper.encode_pdu(num,data) + data = utf8_to_ucs2(data):toHex() + local numlen, datalen, pducnt, pdu, pdulen, udhi = string.format("%02X", #num), #data / 2, 1, "", "", "" + if datalen > 140 then--短信内容太长啦 + data = data:sub(1, 140 * 2) + end + datalen = string.format("%02X", datalen) + pdu = "001110" .. numlen .. number_to_bcd_number(num) .. "000800" .. datalen .. data + return pdu, #pdu // 2 - 1 +end + return pdu_helper diff --git a/renew.lua b/renew.lua new file mode 100644 index 0000000..ee386b8 --- /dev/null +++ b/renew.lua @@ -0,0 +1,88 @@ +local renew = {} + +local sys = require("sys") +local sysplus = require("sysplus") +local air780 = require("air780_helper") +local config = require("config") + +-- 获取到期时间 +-- GET https://api.example.com/sim/expiry 1732622763 +function renew.get_expire_date() + local code, headers, body = http.request("GET", config.renew_api, {["User-Agent"] = "LuatOS/1.0.0 (Lua; ESP32; Air780)"}, nil, {ipv6=true}).wait() + + -- 如果 code 为负数,说明请求失败 + if code < 0 or code ~= 200 then + log.error("renew", "获取到期时间", "code=" .. code) + return nil + end + + -- 尝试将 body 转换为数字 + local date = tonumber(body) + if not date then + log.error("renew", "获取到期时间", "未知时间格式:" .. tostring(body)) + return nil + end + + return date +end + +-- 更新到期时间 +-- POST https://api.example.com/sim/expiry { "expiry": "1732622763" } +function renew.update_expire_date(date) + local body = { expiry = date } + local code, headers, body = http.request("POST", config.renew_api, {["Content-Type"] = "application/json", ["User-Agent"] = "LuatOS/1.0.0 (Lua; ESP32; Air780)"}, json.encode(body), {ipv6=true}).wait() + if code ~= 200 then + log.error("renew", "更新到期时间", "code=" .. code) + return false + end + return true +end + +-- 发送短信 +-- 发送短信后,更新到期时间,到期时间为当前时间戳 + config.renew_day +function renew.send_sms() + if air780.sent_sms(config.renew_number, config.renew_content) then + log.debug("renew", "sent_sms success") + if renew.update_expire_date(os.time() + config.renew_day * 24 * 60 * 60) then + log.debug("renew", "更新到期时间成功") + return true + else + log.error("renew", "更新到期时间失败") + return false + end + else + log.error("renew", "发送短信失败") + return false + end + return true +end + +-- 保活 +-- 如果 date 为空,则发送短信 +-- 如果当前时间戳大于等于 date,则发送短信 +-- 检查间隔为 config.renew_check_interval 单位小时 +function renew.renew() + while true do + local date = renew.get_expire_date() + if date == nil then + log.error("renew", "get_expire_date", "get_expire_date failed") + goto continue + end + if date == "" or os.time() >= date then + log.debug("renew", "已到期,开始保活") + if renew.send_sms() then + log.debug("renew", "保活成功") + else + log.error("renew", "保活失败") + end + + goto continue + end + log.debug("renew", "时间未到,无需保活") + + ::continue:: + sys.wait(config.renew_check_interval * 60 * 60 * 1000) + end +end + +return renew