用途: 将 config 全局结构体所有字段写入 NVS。
NVS Namespace: "sms_config"
写入模式: false(读写模式)
写入的 Key 列表:
| NVS Key | 类型 | 对应字段 |
|---|---|---|
smtpServer |
String | config.smtpServer |
smtpPort |
Int | config.smtpPort |
smtpUser |
String | config.smtpUser |
smtpPass |
String | config.smtpPass |
smtpSendTo |
String | config.smtpSendTo |
adminPhone |
String | config.adminPhone |
webUser |
String | config.webUser |
webPass |
String | config.webPass |
numBlkList |
String | config.numberBlackList |
push{i}en |
Bool | 通道 i 是否启用 |
push{i}type |
UChar | 通道 i 推送类型 |
push{i}url |
String | 通道 i URL |
push{i}name |
String | 通道 i 名称 |
push{i}k1 |
String | 通道 i 参数1 |
push{i}k2 |
String | 通道 i 参数2 |
push{i}body |
String | 通道 i 自定义模板 |
用途: 从 NVS 加载所有配置到 config 结构体。包含旧版本迁移逻辑。
兼容迁移: 如果存在旧 key httpUrl 且第一个推送通道未启用,自动迁移到通道 1。
默认值:
smtpPort: 465webUser:"admin"(DEFAULT_WEB_USER)webPass:"admin123"(DEFAULT_WEB_PASS)- 通道名称:
"通道1"~"通道5" - 通道类型:
PUSH_TYPE_POST_JSON(1)
用途: 检查单个推送通道配置是否完整可用。
校验规则:
| 推送类型 | 必填字段 |
|---|---|
| POST_JSON / BARK / GET / DINGTALK / FEISHU / CUSTOM | url 非空 |
| PUSHPLUS / SERVERCHAN | key1 非空 |
| GOTIFY | url 非空 且 key1 非空 |
| TELEGRAM | key1 非空 且 key2 非空 |
前提: ch.enabled == true,否则直接返回 false。
用途: 检查系统是否有至少一种可用的通知方式。
返回 true 条件: 邮件配置完整(4 个 SMTP 字段均非空)或 至少一个推送通道通过 isPushChannelValid() 校验。
返回: "http://{WiFi.localIP}/"
参数:
cmd: AT 指令字符串(不含\r\n,函数自动追加)timeout: 等待超时(毫秒)
行为:
- 清空 Serial1 缓冲区
- 发送
cmd\r\n - 等待直到收到 "OK" 或 "ERROR" 或超时
- 收到 OK/ERROR 后额外
delay(50)读取剩余数据
返回: 模组完整响应字符串(含 OK/ERROR)
行为:
- 配置
MODEM_EN_PIN为输出 - 拉低 1200ms → 关闭模组
- 拉高 6000ms → 开启模组并等待启动
注意: 调用后需清空 Serial1 缓冲区(该函数不自动清空)
行为:
- 调用
modemPowerCycle() - 清空 Serial1
- 循环 10 次尝试
sendATandWaitOK("AT", 1000) - 打印恢复结果
与 sendATCommand 区别: 仅返回 true/false,不做额外 delay。用于初始化流程中的幂等检查。
用途: 轮询 AT+CEREG? 直到网络注册成功。
注册状态码:
1: 已注册本地网络 → true5: 已注册漫游 → true0,2,3,4: 未注册 → false
超时: 单次轮询 2000ms(调用方在外层循环调用)
行为: LED 亮 50ms → 灭 → 等待 gap_time ms。
流程:
pdu.setSCAnumber()使用默认短信中心pdu.encodePDU(phoneNumber, message)编码- 发送
AT+CMGS=<pduLen> - 等待
>提示符(5 秒超时) - 发送 PDU 数据 +
Ctrl+Z(0x1A) - 等待 OK/ERROR(30 秒超时)
返回: true=成功, false=PDU编码失败/无提示符/ERROR/超时
前提检查: SMTP 四个字段均非空,否则打印跳过日志。
实现:
- 创建
smtp.connect(server, port, callback) smtp.authenticate(user, pass, readymail_auth_password)- 构造
SMTPMessage,设置 from/to/subject/body/timestamp smtp.send(msg)
from 格式: "sms notify <user@example.com>"
to 格式: "your_email <receiver@example.com>"
timestamp: 使用 time(nullptr)(需 NTP 已同步)
行为:
- 检查 WiFi 连接
- 检查是否有启用的有效通道
- 遍历所有通道,对每个有效通道调用
sendToChannel() - 通道间 delay(100ms)
void sendToChannel(const PushChannel& channel, const char* sender, const char* message, const char* timestamp)
核心推送函数。根据 channel.type 分发到 10 种推送方式之一。每个 case 构建对应的 HTTP 请求(URL/Header/Body),使用 HTTPClient 发送,打印响应码和内容。
签名相关:
- 钉钉:
HMAC-SHA256(timestamp+"\n"+secret)→ Base64 → URLEncode → 追加到 URL - 飞书:
HMAC-SHA256(timestamp+"\n"+secret)→ Base64 → 放入 JSON body
占位符: 自定义模板(PUSH_TYPE_CUSTOM)支持 {sender} {message} {timestamp} 占位符替换。
标准 URL 编码(空格 → +, 非字母数字 → %XX)
JSON 字符串转义:" → \", \ → \\, \n → \\n, \r → \\r, \t → \\t
HMAC-SHA256(timestamp + "\n" + secret, secret) → Base64 → URLEncode
通过 gettimeofday() 获取 UTC 毫秒级时间戳,失败则回退 time(nullptr) * 1000
将 concatBuffer[MAX_CONCAT_MESSAGES] 全部标记为未使用,清空所有分段。
查找逻辑:
- 遍历缓冲区,找已用且 refNumber+sender 匹配的槽位 → 直接返回
- 遍历缓冲区,找未使用槽位 → 初始化并返回
- 无空闲槽位 → 覆盖最老的槽位(按 firstPartTime 比较)
按顺序拼接 concatBuffer[slot].parts[0..totalParts-1],缺失分段标记 [缺失分段N]
将该槽位所有字段复位。
遍历所有使用中的槽位,若 millis() - firstPartTime >= 30000ms,强制合并转发不完整消息,并清空槽位。
逐字节读取串口,遇 \n 返回行,\r 跳过,超长行保护(超过 SERIAL_BUFFER_SIZE 归零),无完整行返回空串。
注意: 使用 static 缓冲区,仅适合单线程调用。
检查字符串是否全为十六进制字符 [0-9A-Fa-f]。
匹配逻辑: 逐行读取 config.numberBlackList(换行符分隔),支持 +86 前缀的模糊匹配:
+8613800138000匹配黑名单中的1380013800013800138000匹配黑名单中的+8613800138000
比较发送者与 config.adminPhone,忽略 +86 前缀差异。
支持命令:
| 命令格式 | 行为 |
|---|---|
SMS:号码:内容 |
调用 sendSMS() 代发短信,邮件通知结果 |
RESET |
发送通知邮件 → resetModule() → ESP.restart() |
处理顺序:
isInNumberBlackList()→ 忽略isAdmin()+ 命令格式检测 →processAdminCommand()→ 不再发普通通知sendSMSToServer()— 推送所有启用的通道sendEmailNotification()— 邮件通知
状态机: IDLE ↔ WAIT_PDU
IDLE 状态检测 +CMTI: 行 → 标记尽快 CMGL 补收存储短信;检测 +CMT: 行 → 转入 WAIT_PDU(兼容直推模式)。
WAIT_PDU 状态读取 PDU hex 数据 → pdu.decodePDU() 解析 → 根据 concatInfo 判断长短信/普通短信 → 恢复 IDLE。
用途: 输出日志,但不换行。内容追加到行缓冲区 _logLine,不会立即写入环形缓冲区。同时输出到 Serial。
使用场景: 与 logCaptureLn() 配合,构建一行完整日志:
logCapture("目标号码: ");
logCaptureLn(String(phoneNumber));
// 环形缓冲区中只有一行: "目标号码: 13800138000"用途: 输出日志并换行。将 _logLine + msg 提交到环形缓冲区,然后清空行缓冲区。同时输出到 Serial.println。
注意: 环形缓冲区每调用一次 logCaptureLn 即可产生一行日志,因此 logCapture() 和 logCaptureLn() 的日志会在环形缓冲区中合并为同一行。
用途: 格式化日志输出(类似 printf)。支持 %d/%s/%f 等格式。如果格式化字符串以 \n 结尾,自动提交整行到缓冲区。
用途: HTTP 端点 GET /log,返回环形缓冲区中所有日志行。
返回格式: application/json
["行1", "行2", "行3", ...]鉴权: 需要 HTTP Basic Auth。最多返回 120 行(环形缓冲区容量)。
HTTP Basic Authentication,账号密码来自 config.webUser / config.webPass。
返回 SPA 主页 HTML。通过 html.replace("%KEY%", value) 替换模板占位符,动态生成 5 个推送通道的表单。页面包含 10 个可切换面板(系统概览/账号管理/邮件通知/推送通道/管理员黑名单/发送短信/模组诊断/网络测试/模组控制/AT终端/系统日志),通过侧边栏 JS 切换显示。
模板占位符:
| 占位符 | 数据来源 |
|---|---|
%IP% |
WiFi.localIP().toString() |
%WEB_USER% / %WEB_PASS% |
config.webUser / config.webPass |
%SMTP_SERVER% ~ %SMTP_SEND_TO% |
config.smtp* |
%ADMIN_PHONE% |
config.adminPhone |
%NUMBER_BLACK_LIST% |
config.numberBlackList |
%SMTP_CHECK% |
邮件配置是否完整 |
%PUSH_COUNT% |
已启用的有效推送通道数 |
%PUSH_CHANNELS% |
循环生成 5 个通道的 HTML 表单 |
兼容旧链接,直接委托给 handleRoot() 返回同一 SPA 页面。
解析 POST 表单 → 写入 config(写入区在 gWorkMux 内,与 worker 的快照读互斥)→ saveConfig() → 重新校验 → 快速返回成功页面(3 秒跳转)。不发送通知邮件(避免 SMTP 阻塞前端;连通性请用各通道“测试推送”验证)。
根据 ?action= 控制飞行模式:
| action | AT 指令 | 说明 |
|---|---|---|
query |
AT+CFUN? |
查询当前 CFUN 值 |
toggle |
查询 + AT+CFUN=1/4 |
1↔4 切换 |
on |
AT+CFUN=4 |
强制开启 |
off |
AT+CFUN=1 |
强制关闭 |
通过 ?cmd= 透传 AT 指令到 sendATCommand(),返回 JSON {success, message}。
解析 POST 表单 phone/content → enqueueOutgoingSms() 入队(HTTP 立即返回)→ 真正的 AT+CMGS 由 loop 的 processOutgoingSmsQueue() 异步执行,避免浏览器长等。
网页“蜂窝 HTTP payload”诊断。handlePing() 只启动/查询后台任务并立即返回;真正流量在 loop 的 processPingJob() 执行:
AT+CGACT=1,1激活数据连接fetchCellularKeepAliveUrl():AT+MHTTPCREATE→AT+MHTTPREQUEST→ 统计+MHTTPURC:"content"下载字节AT+CGACT=0,1关闭数据连接
?action=status 轮询进度。流量保号复用同一 payload URL,URL 保存在 config.kaUrl,诊断可临时覆盖。
下列路由同样注册于 setup()(均经 checkAuth()):
| 路由 | 处理函数 | 说明 |
|---|---|---|
GET /status |
handleStatus() |
健康状态 JSON(版本/信号/堆水位/各队列深度/统计/复位原因/时间) |
GET /messages |
handleMessages() |
收件箱/已发送 JSON(?box=sent),流式分块输出 |
POST /resend POST /delete |
handleResend() / handleDeleteMsg() |
重发/删除收件箱某条(?id=) |
GET /keepalive |
handleKeepAlive() |
保号:?action=status|run|reset;run 入队由 loop 执行(默认动作 = 蜂窝 HTTP payload 下载) |
GET /testpush |
handleTestPush() |
通道测试:?ch= 入队,?action=status 轮询;真实发送在 worker |
GET /ussd |
handleUssd() |
USSD 查询(AT+CUSD,同步最长 ~20s) |
GET /modem GET /wifi GET /wifiscan |
— | 模组信息 / WiFi 状态 / 扫描 |
POST /wificonfig |
ssid、pass、可选 mdnsHost |
保存 WiFi 与设备名称;省略名称保留原值,成功响应返回实际 mdnsHost。AP 模式原地连接,STA 模式保存后重启 |
GET /apstatus |
— | 配网状态:connected、ip、mdnsHost,不含 WiFi 凭据;AP 模式免密,STA 模式需登录 |
POST /reboot POST /factory |
handleReboot() / handleFactory() |
重启 / 恢复出厂(清 NVS) |
GET /export POST /import |
handleExport() / handleImport() |
配置导出 / 导入(含凭据,注意保管) |
GET /logdownload |
handleLogDownload() |
纯文本下载全部日志 |
POST /update |
handleOtaUpload() / handleOtaFinish() |
网页 OTA 固件升级 |
并发说明:/testpush 真正的推送发送在后台 pushWorkerTask;/keepalive run、/sendsms、/ping 的模组/网络动作仍在 loop 任务(经队列异步)。日志相关读取(/log、/logdownload)与写入用 gLogMux 保护。