通过mqtt使用webhook转发消息实现远程查看单片机日志
背景
我做的一个开机棒放在办公室,我在家里想远程唤醒开机,结果没开唤醒成功,mqtt也没回复唤醒结果,就想远程看一下开机棒的状态,开机棒又是用esp32做的,当然没法像ssh这种远程,就想可以把日志或者自身的状态发送给服务器,在服务器上查看信息。(最后发现是同事把我的开机棒连接的插座断电了o(╥﹏╥)o,像这种后续还要加一个掉电检测)
需要资源
只需一个公网服务器即可,当然你如果你都是在局域网下也可以,不过这样也就没有远程查看的需求了。
公网服务器搭建mqtt服务器,然后配置webhook转发,最后再写一个脚本监听webhook的转发达到存取日志的目的。
mqtt服务器
搭建
搭建mqtt服务器没什么好说的,记得搭建好之后写一个开机自启的服务,这样就能使用systemctl去控制了
shell
/lib/systemd/system/emqx.service
[Unit]
Description=emqxautostart
After=network.target
[Service]
Type=forking
Environment=HOME=/root/app/emqx # 换成自己的emqx路径
ExecStart=/root/app/emqx/bin/emqx start # 换成自己的emqx路径
#ExecReload=/root/app/emqx/bin/emqxt restart # 换成自己的emqx路径
ExecStop=/root/app/emqx/bin/emqx stop # 换成自己的emqx路径
PrivateTmp=true
[Install]
WantedBy=multi-user.target
配置webhook
http://服务器地址:端口号/url

规则:

监听服务
验证了js和py两种版本的,实现效果完全一样,看自己的喜好
py版本
python
import os
import json
from datetime import datetime
from flask import Flask, request
import logging
from logging.handlers import RotatingFileHandler
app = Flask(__name__)
def ensure_log_directory(date_str):
"""确保日志目录存在"""
script_dir = os.path.dirname(os.path.abspath(__file__))
log_dir = os.path.join(script_dir, 'logPy', date_str[:6]) # 使用年月作为子目录名
if not os.path.exists(log_dir):
os.makedirs(log_dir, exist_ok=True)
return log_dir
@app.route('/py_webhook', methods=['POST'])
def py_webhook():
# 获取请求体数据
if request.is_json:
received_data = request.get_json()
else:
received_data = json.loads(request.data.decode('utf-8'))
# 提取时间戳和有效载荷
timestamp_ms = received_data.get('timestamp')
payload = received_data.get('payload')
if timestamp_ms:
# 将毫秒时间戳转换为datetime对象
timestamp = datetime.fromtimestamp(timestamp_ms / 1000.0)
else:
# 如果没有提供时间戳,使用当前时间
timestamp = datetime.now()
log_time = timestamp.strftime('%Y-%m-%d %H:%M:%S')
# 使用年月格式作为目录名(如 "202601")
month_str = timestamp.strftime('%Y%m')
# 使用年月日格式作为文件名(如 "20260121.log")
date_str = timestamp.strftime('%Y%m%d')
# 确保日志目录存在
log_dir = ensure_log_directory(month_str)
log_file_path = os.path.join(log_dir, f'{date_str}.log')
# 创建日志条目
if isinstance(payload, str):
log_entry = f'[{log_time}] {payload}\n'
else:
log_entry = f'[{log_time}] {json.dumps(payload)}\n'
# 写入日志文件
try:
with open(log_file_path, 'a', encoding='utf-8') as log_file:
log_file.write(log_entry)
print(f'日志已写入: {log_file_path}',flush=True)
except Exception as e:
print(f'写入日志文件失败: {e}',flush=True)
print(f'Received webhook payload: {payload}',flush=True)
print(f'Received webhook timeStamp: {log_time}',flush=True)
# 返回响应
response_data = {'status': 'success', 'message': 'Webhook received successfully'}
return json.dumps(response_data), 200, {'Content-Type': 'application/json'}
if __name__ == '__main__':
# 确保主日志目录存在
script_dir = os.path.dirname(os.path.abspath(__file__))
main_log_dir = os.path.join(script_dir, 'logPy')
if not os.path.exists(main_log_dir):
os.makedirs(main_log_dir)
app.run(host='0.0.0.0', port=3001, debug=False) # 使用localhost不行
js版本
javascript
const express = require('express');
const mysql = require('mysql');
const fs = require('fs');
const path = require('path');
const bodyParser = require('body-parser');
const app = express();
// 使用 raw body parser 来捕获原始请求体 (既能处理json,又能文本)
app.use(express.raw({ type: () => true, limit: '10mb' }));
// 中间件:将原始 body 转换为适当的格式
app.use((req, res, next) => {
// 尝试解析为 JSON
try {
req.body = JSON.parse(req.body.toString());
} catch (e) {
// 如果不是 JSON,保持原始字符串
console.log("e : " + e + " " + req.body)
req.body = req.body.toString();
}
next();
});
// 创建日志目录的函数
function ensureLogDirectory(dateStr) {
const logDir = path.join(__dirname, 'logJs', dateStr);
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
return logDir;
}
// 处理Webhook请求的路由处理程序
app.post('/js_webhook', (req, res) => {
const receivedData = req.body; // 获取Webhook请求正文中的数据
// 提取时间戳和有效载荷
const timestampMs = receivedData.timestamp;
const payload = receivedData.payload;
let timeStamp = 0;
if (timestampMs) {
// 使用接收到的时间戳
timeStamp = new Date(timestampMs);
}
else {
timeStamp = new Date();
console.log("timestamp is null! body : " + receivedData);
}
// 将时间戳转换为日志格式 (yyyy-MM-dd HH:mm:ss)
const logTime = timeStamp.getFullYear() + '-' +
String(timeStamp.getMonth() + 1).padStart(2, '0') + '-' +
String(timeStamp.getDate()).padStart(2, '0') + ' ' +
String(timeStamp.getHours()).padStart(2, '0') + ':' +
String(timeStamp.getMinutes()).padStart(2, '0') + ':' +
String(timeStamp.getSeconds()).padStart(2, '0');
// 使用年月格式作为目录名(如 "202601")
const monthStr = timeStamp.getFullYear() +
String(timeStamp.getMonth() + 1).padStart(2, '0');
// 使用年月日格式作为文件名(如 "20260121.log")
const dateStr = timeStamp.getFullYear() +
String(timeStamp.getMonth() + 1).padStart(2, '0') +
String(timeStamp.getDate()).padStart(2, '0');
// 确保日志目录存在
const logDir = ensureLogDirectory(monthStr);
const logFilePath = path.join(logDir, `${dateStr}.log`); // 文件名格式为 YYYYMMDD.log
// 创建日志条目
const logEntry = `[${logTime}] ${JSON.stringify(payload)}\n`;
// 写入日志文件
fs.appendFile(logFilePath, logEntry, (err) => {
if (err) {
console.error('写入日志文件失败:', err);
} else {
console.log(`日志已写入: ${logFilePath}`);
}
});
console.log('Received webhook payload:', payload);
console.log('Received webhook timeStamp:' + logTime + " " + timestampMs);
res.status(200).json({ status: 'success', message: 'Webhook received successfully' });
});
// 启动服务器监听指定端口(例如:3000)
app.listen(3000, () => {
console.log('Server started on port 3000');
});
开机自启
可以跟emqx那种服务也行,也可以配置rc.local脚本也行,看自己喜好,我这里提供了rc.local方式,rc.local存放到/etc/rc.local位置
shell
# !!!都换成自己的路径!!!
# 我用的是nvm的node,系统自带的版本太低
nohup /root/.nvm/versions/node/v24.13.0/bin/node /root/sh/emqx_webhook/main.js > /root/sh/emqx_webhook/outputJs.log 2>&1 &
nohup python3 /root/sh/emqx_webhook/main.py > /root/sh/emqx_webhook/outputPy.log 2>&1 &
效果
可以看脚本运行的效果outputJs.log、outputPy.log,mqtt转发的日志按照日期文件夹存放到相应的文件夹

注意
都是血的教训
- 配置webhook时注意不要写成http://服务器地址/url:端口号
- 接收wenhook的脚本不运行,你在emqx的后台看webhook的状态一定是未连接的
- py脚本存放的路径一定要是绝对路径