亚马逊 AWS MQTT(S) 测试

树莓派接入

复制代码
import ssl
import sys
import time
import json
import socket
import paho.mqtt.client as mqtt

# ===== 这里改成实际参数 =====
AWS_ENDPOINT = "ambctxxx-ats.iot.ap-xxxxx-1.amazonaws.com"
PORT = 8883
CLIENT_ID = "test_device_01"
TOPIC_SUB = "wifi232"
TOPIC_PUB = "wifi232"

CA_FILE = "AmazonRootCA1.pem"
CERT_FILE = "7a20bd7ef0f99f3b662e33ac8273232856c401e0e7dfa76b883c7cacf94aeedf-certificate.pem.crt"
KEY_FILE = "7a20bd7ef0f99f3b662e33ac8273232856c401e0e7dfa76b883c7cacf94aeedf-private.pem.key"


def on_connect(client, userdata, flags, reason_code, properties=None):
    print(f"[INFO] Connected, reason_code={reason_code}")
    client.subscribe(TOPIC_SUB, qos=0)
    print(f"[INFO] Subscribed to topic: {TOPIC_SUB}")

    payload = {
        "message": "Hello from Raspberry Pi",
        "client_id": CLIENT_ID,
        "time": int(time.time())
    }
    result = client.publish(TOPIC_PUB, json.dumps(payload), qos=0)
    print(f"[INFO] Publish result rc={result.rc}, topic={TOPIC_PUB}")


def on_message(client, userdata, msg):
    try:
        payload = msg.payload.decode("utf-8", errors="ignore")
    except Exception:
        payload = str(msg.payload)
    print(f"[RECV] topic={msg.topic} payload={payload}")


def on_disconnect(client, userdata, disconnect_flags, reason_code, properties=None):
    print(f"[INFO] Disconnected, reason_code={reason_code}")


def on_log(client, userdata, level, buf):
    # 如需更详细调试可取消下一行注释
    # print(f"[LOG] {buf}")
    pass


def check_files():
    import os
    for f in [CA_FILE, CERT_FILE, KEY_FILE]:
        if not os.path.exists(f):
            print(f"[ERROR] File not found: {f}")
            sys.exit(1)


def check_dns():
    try:
        ip = socket.gethostbyname(AWS_ENDPOINT)
        print(f"[INFO] DNS resolved: {AWS_ENDPOINT} -> {ip}")
    except Exception as e:
        print(f"[ERROR] DNS resolve failed: {e}")
        sys.exit(1)


def main():
    check_files()
    check_dns()

    client = mqtt.Client(
        mqtt.CallbackAPIVersion.VERSION2,
        client_id=CLIENT_ID,
        protocol=mqtt.MQTTv311
    )

    client.on_connect = on_connect
    client.on_message = on_message
    client.on_disconnect = on_disconnect
    client.on_log = on_log

    client.tls_set(
        ca_certs=CA_FILE,
        certfile=CERT_FILE,
        keyfile=KEY_FILE,
        cert_reqs=ssl.CERT_REQUIRED,
        tls_version=ssl.PROTOCOL_TLS_CLIENT,
        ciphers=None
    )
    client.tls_insecure_set(False)

    print(f"[INFO] Connecting to {AWS_ENDPOINT}:{PORT} ...")
    client.connect(AWS_ENDPOINT, PORT, keepalive=60)

    try:
        client.loop_forever()
    except KeyboardInterrupt:
        print("\n[INFO] Exit by Ctrl+C")
        client.disconnect()


if __name__ == "__main__":
    main()

网页版本

复制代码
from __future__ import annotations

import json
import os
import socket
import ssl
import threading
import time
from collections import deque
from pathlib import Path
from typing import Any

from flask import Flask, jsonify, redirect, render_template_string, request, url_for
from werkzeug.utils import secure_filename
import paho.mqtt.client as mqtt

BASE_DIR = Path(__file__).resolve().parent
UPLOAD_DIR = BASE_DIR / "certs"
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
CONFIG_FILE = BASE_DIR / "config.json"

app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 10 * 1024 * 1024

DEFAULT_CONFIG = {
    "endpoint": "ambctu1a3ld5z-ats.iot.ap-northeast-1.amazonaws.com",
    "port": 8883,
    "client_id": "test_device_01",
    "topic_sub": "wifi232",
    "topic_pub": "wifi232",
    "keepalive": 60,
    "ca_file": "",
    "cert_file": "",
    "key_file": "",
    "auto_publish_on_connect": False,
}


def load_config() -> dict[str, Any]:
    if CONFIG_FILE.exists():
        try:
            data = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
            merged = DEFAULT_CONFIG.copy()
            merged.update(data)
            return merged
        except Exception:
            pass
    return DEFAULT_CONFIG.copy()


def save_config(data: dict[str, Any]) -> None:
    CONFIG_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")


class MQTTManager:
    def __init__(self) -> None:
        self.client: mqtt.Client | None = None
        self.connected = False
        self.connecting = False
        self.last_error = ""
        self.last_info = ""
        self.logs: deque[str] = deque(maxlen=300)
        self.messages: deque[dict[str, Any]] = deque(maxlen=200)
        self.subscriptions: set[str] = set()
        self.lock = threading.Lock()
        self.current_config = load_config()

    def log(self, text: str) -> None:
        ts = time.strftime("%Y-%m-%d %H:%M:%S")
        line = f"[{ts}] {text}"
        with self.lock:
            self.logs.appendleft(line)
            self.last_info = text
        print(line, flush=True)

    def set_error(self, text: str) -> None:
        with self.lock:
            self.last_error = text
        self.log(f"ERROR: {text}")

    def _abs_path(self, filename: str) -> str:
        return str((UPLOAD_DIR / filename).resolve())

    def _check_dns(self, host: str) -> str:
        ip = socket.gethostbyname(host)
        self.log(f"DNS resolved: {host} -> {ip}")
        return ip

    def _validate_files(self, config: dict[str, Any]) -> None:
        for key in ["ca_file", "cert_file", "key_file"]:
            value = (config.get(key) or "").strip()
            if not value:
                raise FileNotFoundError(f"缺少证书文件字段: {key}")
            path = UPLOAD_DIR / value
            if not path.exists():
                raise FileNotFoundError(f"证书文件不存在: {path.name}")

    def _build_client(self, config: dict[str, Any]) -> mqtt.Client:
        client = mqtt.Client(
            mqtt.CallbackAPIVersion.VERSION2,
            client_id=config["client_id"],
            protocol=mqtt.MQTTv311,
        )

        client.on_connect = self.on_connect
        client.on_message = self.on_message
        client.on_disconnect = self.on_disconnect
        client.on_subscribe = self.on_subscribe
        client.on_publish = self.on_publish
        client.on_log = self.on_log

        client.tls_set(
            ca_certs=self._abs_path(config["ca_file"]),
            certfile=self._abs_path(config["cert_file"]),
            keyfile=self._abs_path(config["key_file"]),
            cert_reqs=ssl.CERT_REQUIRED,
            tls_version=ssl.PROTOCOL_TLS_CLIENT,
            ciphers=None,
        )
        client.tls_insecure_set(False)
        return client

    def connect(self, config: dict[str, Any]) -> tuple[bool, str]:
        with self.lock:
            if self.connected:
                return False, "当前已连接,请先断开"
            if self.connecting:
                return False, "正在连接中,请稍后"
            self.connecting = True
            self.last_error = ""
            self.current_config = config.copy()

        try:
            self._validate_files(config)
            self._check_dns(config["endpoint"])
            client = self._build_client(config)
            self.client = client
            self.log(f"Connecting to {config['endpoint']}:{config['port']} ...")
            client.connect(config["endpoint"], int(config["port"]), keepalive=int(config["keepalive"]))
            thread = threading.Thread(target=client.loop_forever, daemon=True)
            thread.start()
            return True, "已发起连接"
        except Exception as e:
            with self.lock:
                self.connecting = False
            self.set_error(str(e))
            return False, str(e)

    def disconnect(self) -> tuple[bool, str]:
        try:
            if self.client is not None:
                self.client.disconnect()
                self.log("Disconnect requested")
                return True, "已请求断开"
            return False, "当前没有活动连接"
        except Exception as e:
            self.set_error(str(e))
            return False, str(e)

    def publish(self, topic: str, payload: str, qos: int = 0) -> tuple[bool, str]:
        if not self.client or not self.connected:
            return False, "MQTT 未连接"
        try:
            info = self.client.publish(topic, payload, qos=qos)
            self.log(f"Publish queued rc={info.rc}, topic={topic}")
            return True, f"发布请求已发送, rc={info.rc}"
        except Exception as e:
            self.set_error(str(e))
            return False, str(e)

    def subscribe(self, topic: str, qos: int = 0) -> tuple[bool, str]:
        if not self.client or not self.connected:
            return False, "MQTT 未连接"
        try:
            result, mid = self.client.subscribe(topic, qos=qos)
            self.log(f"Subscribe requested rc={result}, mid={mid}, topic={topic}")
            return True, f"订阅请求已发送, rc={result}, mid={mid}"
        except Exception as e:
            self.set_error(str(e))
            return False, str(e)

    def on_connect(self, client, userdata, flags, reason_code, properties=None):
        with self.lock:
            self.connected = True
            self.connecting = False
            self.last_error = ""
        self.log(f"Connected, reason_code={reason_code}")

        topic_sub = (self.current_config.get("topic_sub") or "").strip()
        if topic_sub:
            client.subscribe(topic_sub, qos=0)
            self.log(f"Subscribed to default topic: {topic_sub}")
            self.subscriptions.add(topic_sub)

        if self.current_config.get("auto_publish_on_connect"):
            payload = {
                "message": "Hello from Raspberry Pi Web UI",
                "client_id": self.current_config.get("client_id", ""),
                "time": int(time.time()),
            }
            result = client.publish(self.current_config.get("topic_pub", ""), json.dumps(payload), qos=0)
            self.log(f"Auto publish result rc={result.rc}, topic={self.current_config.get('topic_pub', '')}")

    def on_message(self, client, userdata, msg):
        try:
            payload_text = msg.payload.decode("utf-8", errors="ignore")
        except Exception:
            payload_text = repr(msg.payload)
        entry = {
            "time": time.strftime("%Y-%m-%d %H:%M:%S"),
            "topic": msg.topic,
            "payload": payload_text,
            "qos": msg.qos,
        }
        with self.lock:
            self.messages.appendleft(entry)
        self.log(f"RECV topic={msg.topic} payload={payload_text}")

    def on_disconnect(self, client, userdata, disconnect_flags, reason_code, properties=None):
        with self.lock:
            self.connected = False
            self.connecting = False
        self.log(f"Disconnected, reason_code={reason_code}")

    def on_subscribe(self, client, userdata, mid, reason_code_list, properties=None):
        self.log(f"Subscribe acknowledged, mid={mid}, reason_codes={reason_code_list}")

    def on_publish(self, client, userdata, mid, reason_code=None, properties=None):
        self.log(f"Publish acknowledged, mid={mid}")

    def on_log(self, client, userdata, level, buf):
        if "PING" in buf or "Sending PINGREQ" in buf or "Received PINGRESP" in buf:
            return
        self.log(f"MQTT LOG: {buf}")

    def state(self) -> dict[str, Any]:
        with self.lock:
            return {
                "connected": self.connected,
                "connecting": self.connecting,
                "last_error": self.last_error,
                "last_info": self.last_info,
                "logs": list(self.logs),
                "messages": list(self.messages),
                "subscriptions": sorted(self.subscriptions),
                "config": self.current_config,
                "files": sorted([p.name for p in UPLOAD_DIR.iterdir() if p.is_file()]),
            }


mqtt_manager = MQTTManager()

PAGE = """
<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>AWS IoT MQTTS Web Tool</title>
  <style>
    :root {
      --bg1: #0f172a;
      --bg2: #1e293b;
      --card: rgba(255,255,255,0.10);
      --card-border: rgba(255,255,255,0.16);
      --text: #e5eefc;
      --muted: #b8c4d9;
      --ok: #22c55e;
      --warn: #f59e0b;
      --bad: #ef4444;
      --primary: #38bdf8;
      --primary2: #60a5fa;
      --input: rgba(255,255,255,0.08);
    }
    * { box-sizing: border-box; }
    body {
      margin: 0;
      font-family: Arial, Helvetica, sans-serif;
      color: var(--text);
      background:
        radial-gradient(circle at top left, rgba(56,189,248,0.18), transparent 28%),
        radial-gradient(circle at top right, rgba(96,165,250,0.12), transparent 22%),
        linear-gradient(135deg, var(--bg1), var(--bg2));
      min-height: 100vh;
    }
    .wrap {
      max-width: 1400px;
      margin: 0 auto;
      padding: 24px;
    }
    .hero {
      display: flex;
      justify-content: space-between;
      gap: 16px;
      align-items: center;
      margin-bottom: 20px;
      padding: 22px 24px;
      border: 1px solid var(--card-border);
      border-radius: 20px;
      background: linear-gradient(135deg, rgba(255,255,255,0.12), rgba(255,255,255,0.05));
      backdrop-filter: blur(10px);
      box-shadow: 0 18px 50px rgba(0,0,0,0.25);
    }
    .hero h1 {
      margin: 0 0 8px;
      font-size: 30px;
    }
    .hero p {
      margin: 0;
      color: var(--muted);
    }
    .status {
      min-width: 260px;
      padding: 14px 16px;
      border-radius: 16px;
      background: rgba(255,255,255,0.08);
      border: 1px solid var(--card-border);
    }
    .status strong { display: block; font-size: 18px; margin-bottom: 6px; }
    .grid {
      display: grid;
      grid-template-columns: 420px 1fr;
      gap: 18px;
    }
    .card {
      background: var(--card);
      border: 1px solid var(--card-border);
      border-radius: 20px;
      padding: 18px;
      backdrop-filter: blur(10px);
      box-shadow: 0 12px 32px rgba(0,0,0,0.20);
    }
    .card h2 {
      margin: 0 0 14px;
      font-size: 20px;
    }
    .section-title {
      margin: 18px 0 10px;
      padding-top: 16px;
      border-top: 1px solid rgba(255,255,255,0.12);
      font-size: 15px;
      color: #cfe5ff;
    }
    label {
      display: block;
      margin: 10px 0 6px;
      font-size: 14px;
      color: #dce9fb;
    }
    input, select, textarea, button {
      font: inherit;
    }
    input[type=text], input[type=number], textarea, select {
      width: 100%;
      padding: 10px 12px;
      border-radius: 12px;
      border: 1px solid rgba(255,255,255,0.15);
      background: var(--input);
      color: var(--text);
      outline: none;
    }
    textarea {
      min-height: 140px;
      resize: vertical;
    }
    .row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
    .row3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; }
    .inline {
      display: flex;
      gap: 10px;
      align-items: center;
      flex-wrap: wrap;
    }
    .btn {
      padding: 10px 16px;
      border: 0;
      border-radius: 12px;
      cursor: pointer;
      color: white;
      background: linear-gradient(135deg, var(--primary), var(--primary2));
      box-shadow: 0 8px 20px rgba(56,189,248,0.25);
    }
    .btn.secondary { background: rgba(255,255,255,0.12); box-shadow: none; }
    .btn.warn { background: linear-gradient(135deg, #f97316, #ef4444); }
    .pill {
      display: inline-flex;
      align-items: center;
      gap: 8px;
      padding: 6px 10px;
      border-radius: 999px;
      border: 1px solid rgba(255,255,255,0.14);
      background: rgba(255,255,255,0.08);
      color: var(--muted);
      font-size: 13px;
      margin-right: 6px;
      margin-bottom: 6px;
    }
    .dot {
      width: 10px;
      height: 10px;
      border-radius: 50%;
      display: inline-block;
      background: var(--warn);
      box-shadow: 0 0 12px rgba(245,158,11,0.8);
    }
    .dot.ok {
      background: var(--ok);
      box-shadow: 0 0 12px rgba(34,197,94,0.8);
    }
    .dot.bad {
      background: var(--bad);
      box-shadow: 0 0 12px rgba(239,68,68,0.8);
    }
    .list {
      max-height: 240px;
      overflow: auto;
      border-radius: 14px;
      background: rgba(0,0,0,0.16);
      border: 1px solid rgba(255,255,255,0.10);
      padding: 10px;
    }
    .item {
      padding: 10px 12px;
      border-bottom: 1px solid rgba(255,255,255,0.08);
      font-family: Consolas, monospace;
      white-space: pre-wrap;
      word-break: break-word;
    }
    .item:last-child { border-bottom: 0; }
    .msg {
      margin-bottom: 12px;
      padding: 12px 14px;
      border-radius: 14px;
      border: 1px solid rgba(255,255,255,0.1);
      background: rgba(255,255,255,0.06);
    }
    .muted { color: var(--muted); }
    .files {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
    }
    .right-grid {
      display: grid;
      grid-template-rows: auto auto 1fr;
      gap: 18px;
    }
    @media (max-width: 1100px) {
      .grid { grid-template-columns: 1fr; }
      .hero { flex-direction: column; align-items: flex-start; }
    }
  </style>
</head>
<body>
  <div class="wrap">
    <div class="hero">
      <div>
        <h1>AWS IoT MQTTS Web Tool</h1>
        <p>上传证书,配置 Endpoint / ClientID / Topic,直接在网页完成连接、订阅、发布和收发调试。</p>
      </div>
      <div class="status">
        <strong>连接状态</strong>
        {% if state.connected %}
          <div class="pill"><span class="dot ok"></span> 已连接</div>
        {% elif state.connecting %}
          <div class="pill"><span class="dot"></span> 连接中</div>
        {% else %}
          <div class="pill"><span class="dot bad"></span> 未连接</div>
        {% endif %}
        <div class="muted" style="margin-top:8px; line-height:1.6;">
          ClientID: {{ cfg.client_id }}<br>
          Endpoint: {{ cfg.endpoint }}<br>
          Default SUB: {{ cfg.topic_sub }}<br>
          Default PUB: {{ cfg.topic_pub }}
        </div>
      </div>
    </div>

    <div class="grid">
      <div class="card">
        <h2>连接配置</h2>
        {% if message %}
          <div class="msg">{{ message }}</div>
        {% endif %}
        {% if error %}
          <div class="msg" style="border-color: rgba(239,68,68,0.35); background: rgba(127,29,29,0.35);">{{ error }}</div>
        {% endif %}

        <form method="post" action="/save_config" enctype="multipart/form-data">
          <div class="row">
            <div>
              <label>AWS Endpoint</label>
              <input type="text" name="endpoint" value="{{ cfg.endpoint }}" required>
            </div>
            <div>
              <label>Port</label>
              <input type="number" name="port" value="{{ cfg.port }}" min="1" max="65535" required>
            </div>
          </div>

          <div class="row">
            <div>
              <label>Client ID</label>
              <input type="text" name="client_id" value="{{ cfg.client_id }}" required>
            </div>
            <div>
              <label>Keepalive</label>
              <input type="number" name="keepalive" value="{{ cfg.keepalive }}" min="1" max="65535" required>
            </div>
          </div>

          <div class="row">
            <div>
              <label>默认订阅主题</label>
              <input type="text" name="topic_sub" value="{{ cfg.topic_sub }}">
            </div>
            <div>
              <label>默认发布主题</label>
              <input type="text" name="topic_pub" value="{{ cfg.topic_pub }}">
            </div>
          </div>

          <div class="section-title">证书文件</div>
          <div class="muted" style="margin-bottom: 10px;">已有文件会保留;重新选择文件后会覆盖原配置。AWS IoT 对应关系:CA = AmazonRootCA1.pem,客户端证书 = certificate.pem.crt,私钥 = private.pem.key。</div>

          <label>CA 根证书</label>
          <input type="file" name="ca_upload">
          <label>客户端证书</label>
          <input type="file" name="cert_upload">
          <label>客户端私钥</label>
          <input type="file" name="key_upload">

          <div class="section-title">当前证书映射</div>
          <div class="row3">
            <div>
              <label>CA 文件名</label>
              <input type="text" name="ca_file" value="{{ cfg.ca_file }}" placeholder="例如 AmazonRootCA1.pem">
            </div>
            <div>
              <label>证书文件名</label>
              <input type="text" name="cert_file" value="{{ cfg.cert_file }}" placeholder="例如 xxx-certificate.pem.crt">
            </div>
            <div>
              <label>私钥文件名</label>
              <input type="text" name="key_file" value="{{ cfg.key_file }}" placeholder="例如 xxx-private.pem.key">
            </div>
          </div>

          <label style="margin-top: 14px;">
            <input type="checkbox" name="auto_publish_on_connect" value="1" {% if cfg.auto_publish_on_connect %}checked{% endif %}>
            连接成功后自动向默认发布主题发一条测试消息
          </label>

          <div class="inline" style="margin-top: 16px;">
            <button class="btn" type="submit">保存配置</button>
            <button class="btn secondary" type="submit" formaction="/connect">连接 AWS</button>
            <button class="btn warn" type="submit" formaction="/disconnect">断开连接</button>
          </div>
        </form>

        <div class="section-title">已上传文件</div>
        <div class="files">
          {% for name in state.files %}
            <span class="pill">{{ name }}</span>
          {% else %}
            <span class="muted">还没有上传文件</span>
          {% endfor %}
        </div>
      </div>

      <div class="right-grid">
        <div class="card">
          <h2>发布与订阅</h2>
          <div class="row">
            <form method="post" action="/subscribe">
              <label>新增订阅主题</label>
              <input type="text" name="topic" value="{{ cfg.topic_sub }}" placeholder="例如 wifi232">
              <button class="btn" type="submit" style="margin-top: 12px;">订阅</button>
            </form>
            <form method="post" action="/publish">
              <label>发布主题</label>
              <input type="text" name="topic" value="{{ cfg.topic_pub }}" placeholder="例如 wifi232">
              <label>消息内容</label>
              <textarea name="payload">{
  "message": "Hello from Web UI",
  "client_id": "{{ cfg.client_id }}",
  "time": {{ now_ts }}
}</textarea>
              <button class="btn" type="submit" style="margin-top: 12px;">发布消息</button>
            </form>
          </div>
          <div class="section-title">当前订阅</div>
          <div>
            {% for topic in state.subscriptions %}
              <span class="pill">{{ topic }}</span>
            {% else %}
              <span class="muted">当前没有额外订阅记录,连接时会自动订阅默认主题。</span>
            {% endfor %}
          </div>
        </div>

        <div class="card">
          <h2>接收消息</h2>
          <div class="list" style="max-height: 300px;">
            {% for msg in state.messages %}
              <div class="item">[{{ msg.time }}] topic={{ msg.topic }} qos={{ msg.qos }}
{{ msg.payload }}</div>
            {% else %}
              <div class="item muted">还没有收到消息</div>
            {% endfor %}
          </div>
        </div>

        <div class="card">
          <h2>运行日志</h2>
          <div class="list" style="max-height: 420px;">
            {% for line in state.logs %}
              <div class="item">{{ line }}</div>
            {% else %}
              <div class="item muted">还没有日志</div>
            {% endfor %}
          </div>
        </div>
      </div>
    </div>
  </div>
</body>
</html>
"""


def handle_upload(field_name: str) -> str:
    uploaded = request.files.get(field_name)
    if not uploaded or not uploaded.filename:
        return ""
    filename = secure_filename(uploaded.filename)
    if not filename:
        return ""
    dst = UPLOAD_DIR / filename
    uploaded.save(dst)
    return filename


@app.get("/")
def index():
    state = mqtt_manager.state()
    cfg = load_config()
    message = request.args.get("message", "")
    error = request.args.get("error", state.get("last_error", ""))
    return render_template_string(PAGE, state=state, cfg=cfg, message=message, error=error, now_ts=int(time.time()))


@app.post("/save_config")
def save_config_route():
    cfg = load_config()
    cfg["endpoint"] = request.form.get("endpoint", "").strip()
    cfg["port"] = int(request.form.get("port", cfg["port"]))
    cfg["client_id"] = request.form.get("client_id", "").strip()
    cfg["topic_sub"] = request.form.get("topic_sub", "").strip()
    cfg["topic_pub"] = request.form.get("topic_pub", "").strip()
    cfg["keepalive"] = int(request.form.get("keepalive", cfg["keepalive"]))
    cfg["auto_publish_on_connect"] = request.form.get("auto_publish_on_connect") == "1"

    ca_uploaded = handle_upload("ca_upload")
    cert_uploaded = handle_upload("cert_upload")
    key_uploaded = handle_upload("key_upload")

    cfg["ca_file"] = ca_uploaded or request.form.get("ca_file", cfg.get("ca_file", "")).strip()
    cfg["cert_file"] = cert_uploaded or request.form.get("cert_file", cfg.get("cert_file", "")).strip()
    cfg["key_file"] = key_uploaded or request.form.get("key_file", cfg.get("key_file", "")).strip()

    save_config(cfg)
    mqtt_manager.current_config = cfg.copy()
    return redirect(url_for("index", message="配置已保存"))


@app.post("/connect")
def connect_route():
    save_config_route_internal()
    cfg = load_config()
    ok, msg = mqtt_manager.connect(cfg)
    if ok:
        return redirect(url_for("index", message=msg))
    return redirect(url_for("index", error=msg))


@app.post("/disconnect")
def disconnect_route():
    save_config_route_internal()
    ok, msg = mqtt_manager.disconnect()
    if ok:
        return redirect(url_for("index", message=msg))
    return redirect(url_for("index", error=msg))


@app.post("/publish")
def publish_route():
    topic = request.form.get("topic", "").strip()
    payload = request.form.get("payload", "")
    ok, msg = mqtt_manager.publish(topic, payload, qos=0)
    if ok:
        return redirect(url_for("index", message=msg))
    return redirect(url_for("index", error=msg))


@app.post("/subscribe")
def subscribe_route():
    topic = request.form.get("topic", "").strip()
    ok, msg = mqtt_manager.subscribe(topic, qos=0)
    if ok:
        mqtt_manager.subscriptions.add(topic)
        return redirect(url_for("index", message=msg))
    return redirect(url_for("index", error=msg))


@app.get("/api/state")
def api_state():
    return jsonify(mqtt_manager.state())


def save_config_route_internal() -> None:
    cfg = load_config()
    endpoint = request.form.get("endpoint")
    if endpoint is None:
        return
    cfg["endpoint"] = endpoint.strip()
    cfg["port"] = int(request.form.get("port", cfg["port"]))
    cfg["client_id"] = request.form.get("client_id", "").strip()
    cfg["topic_sub"] = request.form.get("topic_sub", "").strip()
    cfg["topic_pub"] = request.form.get("topic_pub", "").strip()
    cfg["keepalive"] = int(request.form.get("keepalive", cfg["keepalive"]))
    cfg["auto_publish_on_connect"] = request.form.get("auto_publish_on_connect") == "1"

    ca_uploaded = handle_upload("ca_upload")
    cert_uploaded = handle_upload("cert_upload")
    key_uploaded = handle_upload("key_upload")

    cfg["ca_file"] = ca_uploaded or request.form.get("ca_file", cfg.get("ca_file", "")).strip()
    cfg["cert_file"] = cert_uploaded or request.form.get("cert_file", cfg.get("cert_file", "")).strip()
    cfg["key_file"] = key_uploaded or request.form.get("key_file", cfg.get("key_file", "")).strip()
    save_config(cfg)
    mqtt_manager.current_config = cfg.copy()


if __name__ == "__main__":
    print("Open http://0.0.0.0:8888 or http://<RaspberryPi_IP>:8888")
    app.run(host="0.0.0.0", port=8888, debug=False, threaded=True)

普通 EMQX MQTT 测试

复制代码
from flask import Flask, render_template_string, request, jsonify
import paho.mqtt.client as mqtt
import threading
import time

app = Flask(__name__)

mqtt_client = None
mqtt_connected = False
recv_messages = []
recv_lock = threading.Lock()

HTML = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>MQTT Web Tool</title>
<style>
body{
    font-family: Arial;
    background:#0f172a;
    color:white;
    padding:20px;
}
.panel{
    background:#1e293b;
    padding:20px;
    border-radius:10px;
    margin-bottom:20px;
}
input,button{
    padding:8px;
    margin:5px;
    border-radius:6px;
    border:none;
}
button{
    background:#3b82f6;
    color:white;
}
#log{
    background:black;
    height:300px;
    overflow:auto;
    padding:10px;
    font-family:monospace;
}
</style>
</head>

<body>

<h2>MQTT Web Client</h2>

<div class="panel">
Broker
<input id="host" value="broker.emqx.io">
Port
<input id="port" value="1883">
ClientID
<input id="clientid" value="web_test">
Topic
<input id="topic" value="wifi232">

<button onclick="connectMQTT()">Connect</button>
<button onclick="subscribeMQTT()">Subscribe</button>
</div>

<div class="panel">
Payload
<input id="payload" style="width:300px">
<button onclick="publishMQTT()">Publish</button>
</div>

<div class="panel">
<div id="log"></div>
</div>

<script>
function log(t){
    let logDiv = document.getElementById("log");
    logDiv.innerHTML += t + "<br>";
    logDiv.scrollTop = logDiv.scrollHeight;
}

function connectMQTT(){
    fetch("/connect", {
        method: "POST",
        headers: {"Content-Type":"application/json"},
        body: JSON.stringify({
            host: document.getElementById("host").value,
            port: document.getElementById("port").value,
            clientid: document.getElementById("clientid").value
        })
    })
    .then(r => r.json())
    .then(d => log(d.msg))
    .catch(e => log("Connect error: " + e));
}

function subscribeMQTT(){
    fetch("/subscribe", {
        method: "POST",
        headers: {"Content-Type":"application/json"},
        body: JSON.stringify({
            topic: document.getElementById("topic").value
        })
    })
    .then(r => r.json())
    .then(d => log(d.msg))
    .catch(e => log("Subscribe error: " + e));
}

function publishMQTT(){
    fetch("/publish", {
        method: "POST",
        headers: {"Content-Type":"application/json"},
        body: JSON.stringify({
            topic: document.getElementById("topic").value,
            payload: document.getElementById("payload").value
        })
    })
    .then(r => r.json())
    .then(d => log(d.msg))
    .catch(e => log("Publish error: " + e));
}

setInterval(() => {
    fetch("/recv")
    .then(r => r.json())
    .then(data => {
        data.forEach(m => log(m));
    })
    .catch(() => {});
}, 1000);
</script>

</body>
</html>
"""

def on_connect(client, userdata, flags, reason_code, properties=None):
    global mqtt_connected
    if reason_code == 0:
        mqtt_connected = True
        with recv_lock:
            recv_messages.append("[INFO] MQTT connected")
    else:
        mqtt_connected = False
        with recv_lock:
            recv_messages.append(f"[ERROR] MQTT connect failed, code={reason_code}")

def on_disconnect(client, userdata, disconnect_flags, reason_code, properties=None):
    global mqtt_connected
    mqtt_connected = False
    with recv_lock:
        recv_messages.append(f"[INFO] MQTT disconnected, code={reason_code}")

def on_message(client, userdata, msg):
    payload = msg.payload.decode(errors="ignore")
    with recv_lock:
        recv_messages.append(f"[RECV] {msg.topic} : {payload}")

@app.route("/")
def index():
    return render_template_string(HTML)

@app.route("/connect", methods=["POST"])
def connect():
    global mqtt_client, mqtt_connected
    data = request.get_json(silent=True) or {}

    host = data.get("host", "").strip()
    port = data.get("port", 1883)
    clientid = data.get("clientid", "").strip()

    if not host:
        return jsonify({"msg": "Host is empty"}), 400
    if not clientid:
        return jsonify({"msg": "ClientID is empty"}), 400

    try:
        port = int(port)
    except Exception:
        return jsonify({"msg": "Port invalid"}), 400

    try:
        if mqtt_client is not None:
            try:
                mqtt_client.loop_stop()
                mqtt_client.disconnect()
            except Exception:
                pass

        mqtt_connected = False

        mqtt_client = mqtt.Client(
            callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
            client_id=clientid
        )
        mqtt_client.on_connect = on_connect
        mqtt_client.on_disconnect = on_disconnect
        mqtt_client.on_message = on_message

        mqtt_client.connect(host, port, 60)
        mqtt_client.loop_start()

        # 等待最多3秒确认连接结果
        for _ in range(30):
            if mqtt_connected:
                return jsonify({"msg": f"Connected to {host}:{port}"})
            time.sleep(0.1)

        return jsonify({"msg": "Connect request sent, but not confirmed yet"}), 500

    except Exception as e:
        mqtt_client = None
        mqtt_connected = False
        return jsonify({"msg": f"Connect failed: {str(e)}"}), 500

@app.route("/subscribe", methods=["POST"])
def subscribe():
    global mqtt_client, mqtt_connected
    data = request.get_json(silent=True) or {}
    topic = data.get("topic", "").strip()

    if not topic:
        return jsonify({"msg": "Topic is empty"}), 400

    if mqtt_client is None or not mqtt_connected:
        return jsonify({"msg": "MQTT not connected"}), 400

    try:
        mqtt_client.subscribe(topic)
        return jsonify({"msg": f"Subscribed {topic}"})
    except Exception as e:
        return jsonify({"msg": f"Subscribe failed: {str(e)}"}), 500

@app.route("/publish", methods=["POST"])
def publish():
    global mqtt_client, mqtt_connected
    data = request.get_json(silent=True) or {}
    topic = data.get("topic", "").strip()
    payload = data.get("payload", "")

    if not topic:
        return jsonify({"msg": "Topic is empty"}), 400

    if mqtt_client is None or not mqtt_connected:
        return jsonify({"msg": "MQTT not connected"}), 400

    try:
        info = mqtt_client.publish(topic, payload)
        if info.rc == mqtt.MQTT_ERR_SUCCESS:
            return jsonify({"msg": f"Published: {payload}"})
        else:
            return jsonify({"msg": f"Publish failed, rc={info.rc}"}), 500
    except Exception as e:
        return jsonify({"msg": f"Publish failed: {str(e)}"}), 500

@app.route("/recv")
def recv():
    global recv_messages
    with recv_lock:
        msgs = recv_messages[:]
        recv_messages = []
    return jsonify(msgs)

if __name__ == "__main__":
    print("Web MQTT Tool running")
    print("Open browser: http://树莓派IP:5000")
    app.run(host="0.0.0.0", port=5000, debug=False)
相关推荐
亚马逊云开发者2 小时前
不买服务器也能跑 AI?Lambda + Bedrock 这套组合真香
aws
夜月yeyue2 小时前
Linux 文件设备类型分析
linux·运维·网络·单片机
IpdataCloud2 小时前
游戏开服遭遇DDoS后,如何通过IP数据定位攻击来源?
网络·安全·游戏·ddos·ip
没有bug.的程序员2 小时前
黑客僵尸网络的降维打击:Spring Cloud Gateway 自定义限流剿杀 Sentinel 内存黑洞
java·网络·spring·gateway·sentinel
llddycidy2 小时前
通过强化关键线路来提高德克萨斯州电网抵御极端风暴的能力
网络·人工智能·pytorch
白山云北诗2 小时前
互联网常见网络攻击如何防护
网络·网络安全·ddos·waf·cc·安全防护
以太浮标2 小时前
华为eNSP模拟器综合实验之- 虚拟路由冗余协议VRRP(Virtual Router Redundancy Protocol)解析
网络·网络协议·华为·智能路由器·信息与通信
遗憾随她而去.2 小时前
前后端通信核心方案:轮询、WebSocket、SSE
网络·websocket·网络协议
yeshihouhou2 小时前
websocket实现进度条功能
网络·websocket·网络协议