Ubuntu安装Mosquitto服务器并导出运行日志,web查看运行日志并导出为Excel(未写),本文需要同一局域网环境,公网访问需要使用公网IP或穿透
注意:Linux下创建新文件请在使用前确认权限
所有操作请注意路径、用户名、密码等
目录
1 一台运行Ubuntu的主机,主机完成基础配置,ssh等(本文使用树莓派5)
3 连接至Linux服务器(本文使用Windows MQTTX)
一,准备工作
1 一台运行Ubuntu的主机,主机完成基础配置,ssh等(本文使用树莓派5)
2 一台其他支持mqtt收发的设备用来测试
二,安装
1 确保操作系统是最新的
sudo apt update
sudo apt upgrade -y
2 安装Mosquitto
sudo apt install mosquitto mosquitto-clients -y
安装完成后查看运行状态会显示runing
sudo systemctl status mosquitto
3 测试(可跳过)
分别运行以下指令测试Mosquitto服务器
mosquitto_pub -h localhost -t "test/topic" -m "Hello, MQTT"
mosquitto_sub -h localhost -t "test/topic"
4 配置防火墙(默认1883端口)
sudo ufw allow 1883
5 设置Mosquitto开机启动
sudo systemctl enable mosquitto
6 设置Mosquitto配置文件(重点)
6.1打开配置文件
sudo nano /etc/mosquitto/mosquitto.conf
6.2修改文件如下
allow_anonymous false 设置为禁止匿名连接(部分库匿名连接容易崩溃)
password_file /etc/mosquitto/pwfile 选择密码文件(等会创建)
bind_address 0.0.0.0 不限制设备
# Place your local configuration in /etc/mosquitto/conf.d/
#
# A full description of the configuration file is at
# /usr/share/doc/mosquitto/examples/mosquitto.conf.example
#pid_file /run/mosquitto/mosquitto.pid
persistence true
persistence_location /var/lib/mosquitto/
log_dest file /var/log/mosquitto/mosquitto.log
include_dir /etc/mosquitto/conf.d
allow_anonymous false
password_file /etc/mosquitto/pwfile
bind_address 0.0.0.0
7 创建密码文件
系统会提示输入密码,your_username就是用户名
创建密码文件
sudo mosquitto_passwd -c /etc/mosquitto/pwfile your_username
设置文件权限
sudo chown mosquitto:mosquitto /etc/mosquitto/pwfile
sudo chmod 600 /etc/mosquitto/pwfile
8 重启服务使配置文件生效
sudo systemctl restart mosquitto
三、使用
1 确认正常监听端口
安装 net-tools 包
sudo apt install net-tools -y
运行 netstat 命令
sudo netstat -tuln | grep 1883
2 确认连接状态(windows powershell)
Test-NetConnection -ComputerName 192.168.66.161 -Port 1883
3 连接至Linux服务器(本文使用Windows MQTTX)
服务器地址填写Linux设备的IP,用户名和密码为先前设置,弹出已连接则正常
4 接收测试
mosquitto_sub 订阅 -p 端口 -t "订阅话题" -u "用户名" -P "密码"
mosquitto_sub -p 1883 -t "test/topic" -u "your_username" -P "password"
话题与命令行一致,命令行中可以看到订阅的消息
5 发送测试
mosquitto_pub 发送 -m "内容"
mosquitto_pub -p 1883 -t "test/winrec" -m "HELLO" -u "your
_username" -P "password"
6 接收数据并导出文件
6.1 导出为JSON
路径等请自行更换并设置好权限
mosquitto_sub -p 1883 -t "collect/updata" -u "pi5" -P "password" | while read -r msg; do echo "{\"timestamp\": \"$(date +'%Y-%m-%dT%H:%M:%S')\", \"message\": \"$msg\"}" >> "/home/pi5/mqtt/data/$(date +'%y_%m_%d').json"; done
文件夹创建参考
sudo mkdir -p /home/mqtt/data
sudo chmod -R 777 /home/mqtt/data
6.2 导出为txt
mosquitto_sub -p 1883 -t "test/topic" -u "your_username" -P "password" >> "/home/mqtt/data/$(date +'%y_%m_%d').txt"
四、运行演示
运行导出为JSON命令并发送一段数据
sudo nano /home/mqtt/data/25_02_22.json 查看运行结果
五、固定Linux设备ip
依次运行将Xiaomi 13网络下的设备IP固定为192.168.66.66
sudo nmcli con mod "Xiaomi 13" ipv4.addresses 192.168.66.66/24
sudo nmcli con mod "Xiaomi 13" ipv4.gateway 192.168.66.1
sudo nmcli con mod "Xiaomi 13" ipv4.dns "8.8.8.8"
sudo nmcli con mod "Xiaomi 13" ipv4.method manual
设置完成后重连WiFi
sudo nmcli con up "Xiaomi 13"
六、ESP32连接等注意事项
使用PubSubClient库时需要设置参数,否则esp32会崩溃,以下参数测试可行,例程如下
//PubSubClient ESP32-Linux
//PubSubClient.h 中修改MQTT_MAX_PACKET_SIZE 512 MQTT_KEEPALIVE 180
#include <WiFi.h>
#include <PubSubClient.h>
#include "mqttserver.h"
// 平台配置
const char* mqttServer = "192.168.66.66";
const int mqttPort = 1883;
const char* mqttClientId = "ESP32_1";
const char* mqttUsername = "your_username";
const char* mqttPassword = "password";
// Topic配置
String topic_control = "test/dowload"; // 属性设置Topic
String topic_property_post = "test/update"; // 属性上报Topic
WiFiClient espClient; // 创建一个WiFi客户端实例,用于与WiFi网络连接
PubSubClient client(espClient); // 创建一个PubSubClient实例,将WiFi客户端传递给它以进行MQTT通信
// 物联网参数
float depth_water;
float humi, temp;
void mqtt_init(void) {
// 设置MQTT服务器地址和端口(告诉客户端在哪里找到MQTT代理)
client.setServer(mqttServer, mqttPort);
// 设置MQTT消息回调函数(用于处理接收到的MQTT消息。当客户端接收到消息时,这个函数会被自动调用)
client.setCallback(callback);
}
//MQTT状态判断,断连则重连,连接正常则上报数据
void mqtt_server(void) {
// 检查MQTT连接状态
if (!client.connected()) {
connectMQTT(); // 如果未连接,则尝试连接MQTT服务器
}
client.loop(); // 处理MQTT消息(MQTT客户端的核心循环,处理所有待处理的消息和回调。如果没有调用这个函数,客户端将无法接收消息。)
reportDeviceStatus(); // 报告设备状态
}
// 处理收到的控制命令
void callback(char* topic, byte* payload, unsigned int length) {
// 将payload转换为字符串
String message;
for (int i = 0; i < length; i++) {
message += (char)payload[i];
}
Serial.print("Message received: ");
Serial.println(message);
}
// MQTT服务器连接
void connectMQTT(void) {
// 只要未连接到MQTT服务器,就持续尝试连接
while (!client.connected()) {
Serial.println("Reconnecting to MQTT..."); // 输出重新连接的信息
// 尝试连接到MQTT服务器(使用提供的客户端ID、用户名和密码)
if (client.connect(mqttClientId, mqttUsername, mqttPassword)) {
Serial.println("Reconnected to MQTT server"); // 输出连接成功的信息
client.subscribe(topic_control.c_str()); // 重新订阅控制命令主题
} else {
// 如果连接失败,输出错误状态
Serial.print("MQTT reconnection failed, state: ");
Serial.println(client.state());
delay(2000); // 等待2秒再尝试重新连接
}
}
}
// 上传数据
void reportDeviceStatus(void) {
// 构造JSON格式的负载
String payload = "{\"depth_water\":" + String(depth_water) + ",\"humi\":" + String(humi) +",\"temp\":" + String(temp) +"}";
// 通过MQTT发布负载
if (client.publish(topic_property_post.c_str(), payload.c_str())) {
Serial.println("Device status reported successfully"); // 输出成功信息
} else {
Serial.println("Failed to report device status"); // 输出失败信息
}
}
七、Python解析json并从网页导出日志
1 安装所需python库
sudo apt install python3-pip python3-flask
sudo apt install python3-pandas python3-openpyxl
2 选择一个目录创建并写入前后端代码
cd /home/pi5
touch app.py
touch index.html
app.py
from flask import Flask, request, jsonify, send_from_directory
import os
import json
import re
import pandas as pd
from datetime import datetime
app = Flask(__name__)
# 设置存储 JSON 文件的目录
DATA_DIR = '/home/pi5/mqtt/data/'
# 获取所有日期的文件名
def get_json_files():
files = [f for f in os.listdir(DATA_DIR) if f.endswith('.json')]
return sorted(files)
# 解析指定日期的 JSON 文件
def parse_json_file(file_name):
try:
data_list = []
with open(os.path.join(DATA_DIR, file_name), 'r') as f:
for line in f:
line = line.strip()
if not line:
continue
try:
# 尝试直接解析行数据
data = json.loads(line)
except json.JSONDecodeError:
# 使用正则表达式修复 message 字段的格式问题
corrected_line = re.sub(
r'"message":\s*"(\{.*?\})"',
r'"message": \1',
line,
flags=re.DOTALL
)
try:
data = json.loads(corrected_line)
except json.JSONDecodeError as e:
print(f"Error decoding corrected line in {file_name}: {e}")
continue
# 检查 message 字段是否为字符串,尝试进一步解析
if 'message' in data and isinstance(data['message'], str):
try:
data['message'] = json.loads(data['message'])
except json.JSONDecodeError:
print(f"Failed to parse message field in {file_name}")
data_list.append(data)
return data_list
except Exception as e:
print(f"Error reading {file_name}: {e}")
return None
# 查询指定日期范围的数据
@app.route('/query', methods=['GET'])
def query_data():
start_date = request.args.get('start_date') # 获取查询的开始日期
end_date = request.args.get('end_date') # 获取查询的结束日期
# 解析日期格式 (yyyy-mm-dd)
try:
start_date = datetime.strptime(start_date, '%Y-%m-%d')
end_date = datetime.strptime(end_date, '%Y-%m-%d')
except ValueError:
return jsonify({'error': 'Invalid date format. Please use yyyy-mm-dd.'}), 400
result = []
# 遍历所有文件,根据时间范围过滤文件
for file_name in get_json_files():
# 提取文件日期(假设文件名是类似 "25_02_22.json")
file_date_str = file_name.split('.')[0]
try:
file_date = datetime.strptime(file_date_str, '%y_%m_%d')
except ValueError:
print(f"Skipping invalid filename: {file_name}")
continue
# 如果文件日期在指定的时间范围内,解析并返回数据
if start_date <= file_date <= end_date:
parsed_data = parse_json_file(file_name)
if parsed_data:
for entry in parsed_data:
# 提取 timestamp 字段并仅保留时间部分
timestamp = entry.get("timestamp", "")
time_part = timestamp.split('T')[1] if 'T' in timestamp else ""
# 提取 message 字段,并格式化为 "key: value | key: value"
message = entry.get("message", {})
message_str = " | ".join([f"{key}: {value}" for key, value in message.items()])
# 将时间和格式化后的 message 拼接
result.append({
'date': file_date.strftime('%Y-%m-%d'),
'data': f"{time_part} {message_str}" # 拼接时间和消息内容
})
return jsonify(result)
# 导出数据为 Excel
@app.route('/export_excel', methods=['GET'])
def export_excel():
start_date = request.args.get('start_date') # 获取查询的开始日期
end_date = request.args.get('end_date') # 获取查询的结束日期
# 解析日期格式 (yyyy-mm-dd)
try:
start_date = datetime.strptime(start_date, '%Y-%m-%d')
end_date = datetime.strptime(end_date, '%Y-%m-%d')
except ValueError:
return jsonify({'error': 'Invalid date format. Please use yyyy-mm-dd.'}), 400
result = []
# 遍历所有文件,根据时间范围过滤文件
for file_name in get_json_files():
# 提取文件日期(假设文件名是类似 "25_02_22.json")
file_date_str = file_name.split('.')[0]
try:
file_date = datetime.strptime(file_date_str, '%y_%m_%d')
except ValueError:
print(f"Skipping invalid filename: {file_name}")
continue
# 如果文件日期在指定的时间范围内,解析并返回数据
if start_date <= file_date <= end_date:
parsed_data = parse_json_file(file_name)
if parsed_data:
for entry in parsed_data:
result.append({
'date': file_date.strftime('%Y-%m-%d'),
'data': entry
})
# 将查询结果转换为 pandas DataFrame
df_list = []
for item in result:
date_str = item['date']
data = item['data']
# 检查 'message' 是否为字符串,如果是,则解析它为字典
if isinstance(data.get("message", ""), str):
try:
data['message'] = json.loads(data['message']) # 解析为字典
except json.JSONDecodeError:
print(f"Failed to parse message field in {item['date']}")
# 提取 'timestamp' 并只保留 'T' 后的部分,改为 'time'
timestamp = data.get("timestamp", "")
time_part = timestamp.split('T')[1] if 'T' in timestamp else ""
# 提取字段,并确保获取有效的数据
df_list.append({
'date': date_str,
'time': time_part, # 只保留 'T' 后的部分
'humi1': data.get("message", {}).get("humi1", ""),
'temp2': data.get("message", {}).get("temp2", ""),
#'temp': data.get("message", {}).get("temp", ""),
})
# 将数据保存到 Excel 文件
df = pd.DataFrame(df_list)
file_path = "data.xlsx" # 使用相对路径
df.to_excel(file_path, index=False, engine='openpyxl')
# 使用 send_from_directory 发送文件,确保文件位于当前工作目录
return send_from_directory(os.getcwd(), file_path, as_attachment=True)
# 返回静态文件(index.html)
@app.route('/')
def serve_index():
return send_from_directory(os.getcwd(), 'index.html')
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MQTT Data Query</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
.container {
max-width: 600px;
margin: auto;
}
input, button {
padding: 8px;
margin: 5px;
}
.result {
margin-top: 20px;
}
table {
width: 100%;
border-collapse: collapse;
}
table, th, td {
border: 1px solid #ddd;
}
th, td {
padding: 8px;
text-align: left;
}
</style>
</head>
<body>
<div class="container">
<h1>Data Query</h1>
<label for="start_date">Start Date:</label>
<input type="date" id="start_date" required>
<label for="end_date">End Date:</label>
<input type="date" id="end_date" required>
<br>
<button onclick="fetchData()">Query Data</button>
<button onclick="exportExcel()">Export to Excel</button>
<div class="result">
<h3>Results</h3>
<div id="data-table"></div>
</div>
</div>
<script>
async function fetchData() {
const start_date = document.getElementById('start_date').value;
const end_date = document.getElementById('end_date').value;
if (!start_date || !end_date) {
alert("Please select both start and end dates.");
return;
}
const response = await fetch(`/query?start_date=${start_date}&end_date=${end_date}`);
const data = await response.json();
let tableHTML = '<table><tr><th>Date</th><th>Data</th></tr>';
data.forEach(item => {
tableHTML += `<tr><td>${item.date}</td><td>${JSON.stringify(item.data)}</td></tr>`;
});
tableHTML += '</table>';
document.getElementById('data-table').innerHTML = tableHTML;
}
function exportExcel() {
const start_date = document.getElementById('start_date').value;
const end_date = document.getElementById('end_date').value;
if (!start_date || !end_date) {
alert("Please select both start and end dates.");
return;
}
window.location.href = `/export_excel?start_date=${start_date}&end_date=${end_date}`;
}
</script>
</body>
</html>
3 运行并访问
运行
python app.py
访问
设备IP:5000