diff --git a/README-ZH.md b/README-ZH.md index 4b32e1d1..3f1cc9e8 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -154,6 +154,7 @@ Best practice of MQTT in various clients. ## [MQTT Integration (Eco & Integration)](https://www.emqx.com/zh/blog/category/eco-and-integration) Explore more with & via EMQ. +- [ESP32 + MCP over MQTT:通过大模型控制智能硬件设备](https://www.emqx.com/zh/blog/esp32-and-mcp-over-mqtt-3) ([Edit](https://github.com/emqx/blog/blob/main/zh/202508/esp32-and-mcp-over-mqtt-3.md)) - [ESP32 + MCP over MQTT:从 0 到 1 打造情感陪伴智能体之「硬件设备能力封装」](https://www.emqx.com/zh/blog/esp32-and-mcp-over-mqtt-2) ([Edit](https://github.com/emqx/blog/blob/main/zh/202508/esp32-and-mcp-over-mqtt-2.md)) - [ESP32 + MCP over MQTT:从 0 到 1 打造情感陪伴智能体](https://www.emqx.com/zh/blog/esp32-and-mcp-over-mqtt) ([Edit](https://github.com/emqx/blog/blob/main/zh/202507/esp32-and-mcp-over-mqtt.md)) - [EMQX Cloud、TDengine Cloud 实现数据互通!联手打造端到端云上大数据解决方案](https://www.emqx.com/zh/blog/emqx-cloud-and-tdengine-cloud) ([Edit](https://github.com/emqx/blog/blob/main/zh/202505/emqx-cloud-and-tdengine-cloud.md)) @@ -202,6 +203,7 @@ Explore more with & via EMQ. ## [EMQX Open Source | Broker](https://www.emqx.com/zh/blog/category/emqx) EMQX is the world's most scalable open-source MQTT broker with a high performance that connects 100M+ IoT devices in 1 cluster, while maintaining 1M message per second throughput and sub-millisecond latency. +- [多云集群与跨云迁移:通过集群连接摆脱云供应商锁定](https://www.emqx.com/zh/blog/multi-cloud-clusters-and-cross-cloud-migration) ([Edit](https://github.com/emqx/blog/blob/main/zh/202508/multi-cloud-clusters-and-cross-cloud-migration.md)) - [200 万 WebSocket 连接测试:EMQX 并发性能深度解析](https://www.emqx.com/zh/blog/a-deep-dive-into-emqx-s-websocket-performance) ([Edit](https://github.com/emqx/blog/blob/main/zh/202507/a-deep-dive-into-emqx-s-websocket-performance.md)) - [突破连接边界:EMQX 实现 MQTT 和 NATS 协议双向互通](https://www.emqx.com/zh/blog/emqx-nats-gateway) ([Edit](https://github.com/emqx/blog/blob/main/zh/202507/emqx-nats-gateway.md)) - [EMQX + Amazon S3 Tables:从实时物联网数据到数据湖仓](https://www.emqx.com/zh/blog/from-real-time-iot-data-to-big-data-analytics) ([Edit](https://github.com/emqx/blog/blob/main/zh/202507/from-real-time-iot-data-to-big-data-analytics.md)) diff --git a/en/202306/connect-to-mqtt-broker-with-websocket.md b/en/202306/connect-to-mqtt-broker-with-websocket.md index b7a9f344..450632c8 100644 --- a/en/202306/connect-to-mqtt-broker-with-websocket.md +++ b/en/202306/connect-to-mqtt-broker-with-websocket.md @@ -35,7 +35,7 @@ Before proceeding, please ensure you have an [MQTT broker](https://www.emqx.com/ [EMQX](https://github.com/emqx/emqx) is the most scalable [open-source MQTT broker](https://www.emqx.com/en/blog/a-comprehensive-comparison-of-open-source-mqtt-brokers-in-2023) for IoT, [IIoT](https://www.emqx.com/en/blog/iiot-explained-examples-technologies-benefits-and-challenges), and connected vehicles. You can run the following Docker command to install EMQX. ``` - docker run -d --name emqx -p 1883:1883 -p 8083:8083 -p 8084:8084 -p 8883:888 -p 18083:18083 emqx/emqx + docker run -d --name emqx -p 1883:1883 -p 8083:8083 -p 8084:8084 -p 8883:8883 -p 18083:18083 emqx/emqx ``` - **Fully managed cloud service** diff --git a/zh/202508/esp32-and-mcp-over-mqtt-3.md b/zh/202508/esp32-and-mcp-over-mqtt-3.md new file mode 100644 index 00000000..0e5744e8 --- /dev/null +++ b/zh/202508/esp32-and-mcp-over-mqtt-3.md @@ -0,0 +1,531 @@ +## **本系列教程路线图** + +| 篇章 | 功能 | 难度 | +| :--- | :----------------------------------------------------- | :--- | +| 1 | 整体介绍:背景 + 环境准备 + 设备上线 | ★ | +| 2 | 从“命令式控制”到“语义控制”:MCP over MQTT 封装设备能力 | ★★ | +| 3 | **接入 LLM,实现“自然语言 → 设备控制”** | ★★ | +| 4 | 语音 I/O:麦克风数据上传 + 语音识别 + 语音合成回放 | ★★★ | +| 5 | 人格、情感、记忆:从“控制器”到“陪伴体” | ★★★ | +| 6 | 给智能体增加“眼睛”:图像采集 + 多模态理解 | ★★★ | + +## 回顾:MCP over MQTT 封装设备能力 + +在上一篇文章中,我们介绍了如何使用 MCP over MQTT 协议,完成 ESP32 设备能力开发,并向 EMQX 注册与发现。 + +- 开发 MCP Server,完成音量工具的封装,并向 EMQX 注册; +- 云端的 MCP 客户端应用可以通过 Python SDK 拉取设备注册的工具能力; +- 打通了“我知道你能做什么”的第一步。 + +本篇将在此基础上,让大模型接管控制逻辑,实现「你说我做」的效果。 + +## 本篇目标:让大模型能「自然」地控制设备 + +本篇将介绍如何借助大语言模型(以下简称:LLM),实现通过自然语言对设备的控制。例如: + +- “声音太吵了” → 推理出需调用 `set_volume(40)` +- “刚才那个声音再调低一点” → 支持上下文和多轮对话 + +具体目标是: + +- 利用自然语言触发 MCP 中注册的工具函数; +- 通过 LLM 的 MCP 调用自动转化为对应的 MQTT 消息; +- 构建一个具备上下文理解能力的交互式设备控制智能体。 + +## 为什么要接入大语言模型? + +在 LLM 出现之前,设备控制主要依赖结构化输入: + +- 用户必须点击预设选项或输入固定命令; +- 逻辑死板,缺乏上下文理解; +- 每新增一个功能,都需增加 UI 或代码逻辑,扩展成本高。 + +即便语音识别结合 API 调用能实现部分「智能控制」,但一旦语句稍有偏差或接口发生变化,就容易“失灵”,造成用户挫败感。相比之下,LLM 天然具备语义理解与上下文保持能力,能带来: + +- 更自然的交互方式; +- 更强的指令容错能力; +- 更低的开发和维护成本。 + +接入 LLM 后,设备控制的智能化能力大幅提升。它不仅能根据模糊语言推断对应函数调用,还能灵活应对不同厂商,以及硬件升级导致的接口变化,让设备的智能体验更加流畅丝滑。 + +## 程序执行过程 + +![image.png](https://assets.emqx.com/images/ca524c807c9c0607848e269176e3ba8d.png) + +1. 通过自然语言表达意图,LLM 负责理解语义。 +2. 将用户意图转化为结构化 MCP 工具调用(Tool Call), 并且提供合适的参数。 +3. 工具调用通过 MCP 协议被封装成标准的 MQTT 消息,经由 EMQX 转发到对应的 ESP32 设备。 +4. ESP 执行具体的控制逻辑代码。 +5. 将工具的调用结果传给 LLM,由 LLM 给用户返回相关的调用结果,并由 APP 展示给用户。 + +整个链路简单直接,并且具备良好的解耦性和可扩展性,便于快速集成多种设备。 + +## 自然语言调用 MCP 工具 + +### 硬件 + +与[上一篇文章](https://www.emqx.com/zh/blog/esp32-and-mcp-over-mqtt-2)所需硬件一致,无需调整。 + +### 软件 + +- LlamaIndex 相关的 MCP tools 调用库文件。 +- MCP over MQTT 相关的库文件。 +- 大模型:本文选择的是 DeepSeek R1,是由 SiliconFlow 提供的公有服务;读者也可以根据自己的情况换成别的 LLM 服务提供商的服务,比如:阿里云提供的通义千问服务。 + +#### SiliconFlow API 密钥申请 + +- 进入 [SiliconFlow](https://www.siliconflow.cn/) 官网([硅基流动 SiliconFlow - 致力于成为全球领先的 AI 能力提供商](https://www.siliconflow.cn/)) ,注册账号并登陆。 +- 登陆后在左侧,账户管理 → API 密钥中新建密钥即可。 + +### ESP32 控制音量 MCP 服务 + +```c# +#include +#include + +#include "freertos/FreeRTOS.h" + +#include "esp_log.h" +#include "esp_system.h" +#include "nvs_flash.h" + +#include "mcp.h" +#include "mcp_server.h" +#include "radio.h" +#include "wifi.h" + +const char *set_volume(int n_args, property_t *args) +{ + if (n_args < 1) { + return "At least one argument is required"; + } + + if (args[0].type != PROPERTY_INTEGER) { + return "Volume argument must be an integer"; + } + + int volume = (int) args[0].value.integer_value; + if (volume < 0 || volume > 100) { + return "Volume must be between 0 and 100"; + } + + esp_err_t ret = max98357_set_volume_percent((uint8_t) volume); + if (ret != ESP_OK) { + return "Failed to set volume"; + } + + ESP_LOGI("mcp server", "Setting volume to: %d%%", volume); + return "Volume set successfully"; +} + +void app_main(void) +{ + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || + ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + ret = max98357_init(); + if (ret != ESP_OK) { + ESP_LOGE("main", "MAX98357 init error: %s", esp_err_to_name(ret)); + } + + max98357_set_volume_percent(50); + + wifi_station_init("wifi_ssid", "wifi_password"); + + vTaskDelay(pdMS_TO_TICKS(1000)); + mcp_server_t *server = mcp_server_init( + "ESP32 Demo Server", "A demo server for ESP32 using MCP over MQTT", + "mqtt://broker.emqx.io", "esp32-demo-server-client", NULL, NULL, NULL); + + mcp_tool_t tools[] = { + { .name = "set_volume", + .description = "Set the volume of the device, range 0 to 100", + .property_count = 1, + .properties = + (property_t[]) { + { .name = "volume", + .description = "Volume level (0-100)", + .type = PROPERTY_INTEGER, + .value.integer_value = 50 }, + }, + .call = set_volume }, + }; + + mcp_server_register_tool(server, sizeof(tools) / sizeof(mcp_tool_t), tools); + + mcp_server_run(server); +} +``` + +使用 MCP Over MQTT Component 注册调整音量的 Tool,初始化 MCP Server 与 音频播放模块,注意修改 MCP Server 的参数,连接到 Serverless,以及修改 WIFI 的 SSID 与 Password。更多详细代码请参考 [ESP32 Demo](https://github.com/mqtt-ai/esp32-mcp-mqtt-tutorial/tree/main/samples/blog_3)。 + +### 云端 MCP Client 对接大模型调用 MCP 工具 - agent.py + +```python +import asyncio +import anyio +import logging +import os +from typing import List, Optional, Union, cast +from dataclasses import dataclass + +import mcp.client.mqtt as mcp_mqtt +from mcp.shared.mqtt import configure_logging +import mcp.types as types + +from llama_index.llms.siliconflow import SiliconFlow +from llama_index.core.llms import ChatMessage, MessageRole +from llama_index.core.agent import AgentRunner +from llama_index.core.tools import BaseTool, FunctionTool +from llama_index.core.settings import Settings + +configure_logging(level="DEBUG") +logger = logging.getLogger(__name__) + + +async def on_mcp_server_discovered(client: mcp_mqtt.MqttTransportClient, server_name): + logger.info(f"Discovered {server_name}, connecting ...") + await client.initialize_mcp_server(server_name) + +async def on_mcp_connect(client, server_name, connect_result): + capabilities = client.get_session(server_name).server_info.capabilities + logger.info(f"Capabilities of {server_name}: {capabilities}") + if capabilities.prompts: + prompts = await client.list_prompts(server_name) + logger.info(f"Prompts of {server_name}: {prompts}") + if capabilities.resources: + resources = await client.list_resources(server_name) + logger.info(f"Resources of {server_name}: {resources}") + resource_templates = await client.list_resource_templates(server_name) + logger.info(f"Resources templates of {server_name}: {resource_templates}") + if capabilities.tools: + toolsResult = await client.list_tools(server_name) + tools = toolsResult.tools + logger.info(f"Tools of {server_name}: {tools}") + +async def on_mcp_disconnect(client, server_name): + logger.info(f"Disconnected from {server_name}") + +client = None +api_key = "sk-***" + +async def get_mcp_tools(mcp_client: mcp_mqtt.MqttTransportClient) -> List[BaseTool]: + all_tools = [] + try: + try: + tools_result = await mcp_client.list_tools("ESP32 Demo Server") + + if tools_result is False: + return all_tools + + list_tools_result = cast(types.ListToolsResult, tools_result) + tools = list_tools_result.tools + + for tool in tools: + logger.info(f"tool: {tool.name} - {tool.description}") + + def create_mcp_tool_wrapper(client_ref, server_name, tool_name): + async def mcp_tool_wrapper(**kwargs): + try: + result = await client_ref.call_tool(server_name, tool_name, kwargs) + if result is False: + return f"call {tool_name} failed" + + call_result = cast(types.CallToolResult, result) + + if hasattr(call_result, 'content') and call_result.content: + content_parts = [] + for content_item in call_result.content: + if hasattr(content_item, 'type'): + if content_item.type == 'text': + text_content = cast(types.TextContent, content_item) + content_parts.append(text_content.text) + elif content_item.type == 'image': + image_content = cast(types.ImageContent, content_item) + content_parts.append(f"[image: {image_content.mimeType}]") + elif content_item.type == 'resource': + resource_content = cast(types.EmbeddedResource, content_item) + content_parts.append(f"[resource: {resource_content.resource}]") + else: + content_parts.append(str(content_item)) + else: + content_parts.append(str(content_item)) + + result_text = '\n'.join(content_parts) + + if hasattr(call_result, 'isError') and call_result.isError: + return f"tool return error: {result_text}" + else: + return result_text + else: + return str(call_result) + + except Exception as e: + error_msg = f"call {tool_name} error: {e}" + logger.error(error_msg) + return error_msg + + return mcp_tool_wrapper + + wrapper_func = create_mcp_tool_wrapper(mcp_client, "ESP32 Demo Server", tool.name) + + try: + llamaindex_tool = FunctionTool.from_defaults( + fn=wrapper_func, + name=f"mcp_{tool.name}", + description=tool.description or f"MCP tool: {tool.name}", + async_fn=wrapper_func + ) + all_tools.append(llamaindex_tool) + logger.info(f"call tool success: mcp_{tool.name}") + + except Exception as e: + logger.error(f"create tool {tool.name} error: {e}") + + except Exception as e: + logger.error(f"Get tool list error: {e}") + + except Exception as e: + logger.error(f"Get tool list error: {e}") + + return all_tools + +class ConversationalAgent: + def __init__(self, mcp_client: Optional[mcp_mqtt.MqttTransportClient] = None): + self.llm = SiliconFlow(api_key=api_key, model="deepseek-ai/DeepSeek-R1", temperature=0.6, max_tokens=4000, timeout=180) + Settings.llm = self.llm + + self.mcp_client = mcp_client + self.tools = [] + + self.agent = AgentRunner.from_llm( + llm=self.llm, + tools=self.tools, + verbose=True + ) + + self.mcp_tools_loaded = False + + async def load_mcp_tools(self): + if not self.mcp_tools_loaded and self.mcp_client: + try: + mcp_tools = await get_mcp_tools(self.mcp_client) + if mcp_tools: + self.tools.extend(mcp_tools) + self.agent = AgentRunner.from_llm( + llm=self.llm, + tools=self.tools, + verbose=True + ) + logger.info(f"load {len(mcp_tools)} tools") + self.mcp_tools_loaded = True + except Exception as e: + logger.error(f"load tool error: {e}") + + async def chat(self, message: str) -> str: + try: + if not self.mcp_tools_loaded: + await self.load_mcp_tools() + + logger.info(f"user input: {message}") + user_message = ChatMessage(role=MessageRole.USER, content=message) + + response = await self.agent.achat(message) + logger.info(f"Agent response: {response}") + return str(response) + + except Exception as e: + error_msg = f"error: {e}" + logger.error(error_msg) + return error_msg + + +async def main(): + try: + async with mcp_mqtt.MqttTransportClient( + "test_client", + auto_connect_to_mcp_server=True, + on_mcp_server_discovered=on_mcp_server_discovered, + on_mcp_connect=on_mcp_connect, + on_mcp_disconnect=on_mcp_disconnect, + mqtt_options=mcp_mqtt.MqttOptions( + host="broker.emqx.io", + ) + ) as mcp_client: + await mcp_client.start() + await anyio.sleep(3) + + agent = ConversationalAgent(mcp_client) + + print("input 'exit' or 'quit' exit") + print("input 'tools' show available tools") + print("="*50) + + while True: + try: + user_input = input("\nuser: ").strip() + + if user_input.lower() in ['exit', 'quit']: + break + + if user_input.lower() == 'tools': + print(f"available tools: {len(agent.tools)}") + for tool in agent.tools: + tool_name = getattr(tool.metadata, 'name', str(tool)) + tool_desc = getattr(tool.metadata, 'description', 'No description') + print(f"- {tool_name}: {tool_desc}") + continue + + if not user_input: + continue + + response = await agent.chat(user_input) + print(f"\nAgent: {response}") + + except KeyboardInterrupt: + break + except Exception as e: + print(f"error: {e}") + + except Exception as e: + print(f"agent init error: {e}") + + +if __name__ == "__main__": + anyio.run(main) + +``` + +上述代码基于 [MCP over MQTT Python SDK](https://github.com/emqx/mcp-python-sdk) 实现。 + +首先,初始化 MCP Client 并连接到 MQTT Broker(请将其中的 Broker 地址替换为你申请的 EMQX Serverless 实例地址)。客户端会自动发现并连接到已注册的 MCP Server。 + +随后,代码将 MCP Server 中注册的工具封装为 LlamaIndex 可调用的函数接口,供 LLM 使用。同时,还实现了一个简易的对话示例,用于展示可用工具列表及 LLM 的推理与调用过程。 + +- 注意替换 **api_key** 为 SiliconFlow 官网申请的 API 密钥。 +- LlamaIndex 已经封装好了很多的大模型调用,可以参考 [LlamaIndex 支持的 LLM 文档](https://docs.llamaindex.ai/en/stable/api_reference/llms/)获取更多信息。 + +### ESP32 程序运行和部署 + +```shell +# ESP32 程序编译以及烧录 +$ idf.py build & idf.flash +# ESP32 监控设备输出 +$ idf.py monitor + +# 运行 Agent 代码,使用 uv 运行 +$ uv run agent.py +``` + +### 云端 MCP Client 展示可用 MCP 工具 + +``` +# 运行 agent.py 后,先输入任意内容,触发 Agent 更新连接的 MCP Server 信息 +# 然后输入 tools 查看可用的 MCP Tools + +2025-08-06 02:29:39,958 - Agent response: Hello! How can I assist you today? + +Agent: Hello! How can I assist you today? + +user: tools +available tools: 1 +- mcp_set_volume: Set the volume of the device, range 0 to 100 +``` + +如上述所示,查询到一个 Tool,在 ESP32 上提供的调整设备的音量的工具。 + +### **示例对话 1:模糊控制** + +> 用户:设备声音太小了 +> LLM:调用 `set_volume(80)` +> ESP32:音量已调高 + +**Agent 端日志输出:**Agent 识别到用户的意图,调用 set_volume 调整音量到 80。 + +``` +user: 设备声音太小了 +2025-08-06 02:37:29,134 - user input: 设备声音太小了 +> Running step 0701ab58-6c7e-41be-8a2e-210eab9b870f. Step input: 设备声音太小了 +Thought: The current language of the user is Chinese. I need to use a tool to adjust device volume since they reported it's too low. +Action: mcp_set_volume +Action Input: {'kwargs': AttributedDict([('volume', 80)])} +2025-08-06 02:38:24,926 - Got msg from session for server_id: esp32-demo-server-client, msg: root=JSONRPCRequest(method='tools/call', params={'name': 'set_volume', 'arguments': {'kwargs': {'volume': 80}}}, jsonrpc='2.0', id=3) +2025-08-06 02:38:25,076 - Received message on topic $mcp-rpc/4d75d589169a41849fb86b51953110e3/esp32-demo-server-client/ESP32 Demo Server: {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"Volume set successfully"}]}} +2025-08-06 02:38:25,079 - Sending msg to session for server_id: esp32-demo-server-client, msg: root=JSONRPCResponse(jsonrpc='2.0', id=3, result={'content': [{'type': 'text', 'text': 'Volume set successfully'}]}) +Observation: Volume set successfully +> Running step 71688059-5007-4087-84e1-a98993000ff7. Step input: None +Thought: The user reported that the device sound is too low. I've successfully increased the volume to 80. Now I can respond directly in Chinese without further tools. +Answer: 已成功将设备音量调整到80,请检查是否合适。如果还需要进一步调整,请随时告诉我! +``` + +**ESP32 端日志输出:**ESP32 接收到 set_volume 的工具调用,并把音量调整为 80。 + +``` +I (503477) mcp_server: MCP client initialized: 4d75d589169a41849fb86b51953110e3 +I (503487) mcp_server: MCP client initialized: 4d75d589169a41849fb86b51953110e3 +I (503747) mcp_server: tools/list request received from $mcp-rpc/4d75d589169a41849fb86b51953110e3/esp32-demo-server-client/ESP32 Demo Server +I (508897) mcp_server: tools/list request received from $mcp-rpc/4d75d589169a41849fb86b51953110e3/esp32-demo-server-client/ESP32 Demo Server +I (585187) mcp_server: tools/call request received from $mcp-rpc/4d75d589169a41849fb86b51953110e3/esp32-demo-server-client/ESP32 Demo Server +I (585187) MAX98357: Set volume to 80%, 0 +I (585197) mcp server: Setting volume to: 80% +``` + +### 示例对话2:多轮对话 + +为了支持“声音有点大”这类模糊指令,需要 LLM 保持对话上下文,设置正确的参数进行调整。 + +- 框架记住上次调整的音量是 80; +- 当前语义推导为调整 -15; +- 最终调用 `set_volume(65)`。 + +**Agent 输出:**Agent 识别到用户意图,调整音量为 65,并调用 set_volume 调整音量。 + +``` +user: 声音有点大了 +2025-08-06 02:38:58,092 - user input: 声音有点大了 +> Running step be73cc62-e732-4209-8421-2bccdd09cd70. Step input: 声音有点大了 +Thought: The current language of the user is Chinese. I need to adjust the volume lower since the user mentioned it's a bit loud. I'll decrease it from the previous setting (80) to a more comfortable level. +Action: mcp_set_volume +Action Input: {'kwargs': AttributedDict([('volume', 65)])} +2025-08-06 02:39:40,075 - Got msg from session for server_id: esp32-demo-server-client, msg: root=JSONRPCRequest(method='tools/call', params={'name': 'set_volume', 'arguments': {'kwargs': {'volume': 65}}}, jsonrpc='2.0', id=4) +2025-08-06 02:39:40,236 - Received message on topic $mcp-rpc/4d75d589169a41849fb86b51953110e3/esp32-demo-server-client/ESP32 Demo Server: {"jsonrpc":"2.0","id":4,"result":{"content":[{"type":"text","text":"Volume set successfully"}]}} +2025-08-06 02:39:40,237 - Sending msg to session for server_id: esp32-demo-server-client, msg: root=JSONRPCResponse(jsonrpc='2.0', id=4, result={'content': [{'type': 'text', 'text': 'Volume set successfully'}]}) +Observation: Volume set successfully +``` + +**ESP32 端打印的日志内容:**ESP32 设备接收到 set_volume 调用,并把音量调整为 65。 + +``` +I (660347) mcp_server: tools/call request received from $mcp-rpc/4d75d589169a41849fb86b51953110e3/esp32-demo-server-client/ESP32 Demo Server +I (660357) MAX98357: Set volume to 65%, 0 +I (660357) mcp server: Setting volume to: 65% +``` + +您也可以试试其他模糊的对话方式,充分了解一下 LLM 对于自然语言的理解,以及把语义理解转换为工具调用的能力。 + +恭喜你,完成了本次任务:使用 LLM 实现用自然语言对设备进行控制。 + +## 下篇预告 + +通过本篇内容,我们打通了自然语言控制的完整路径:LLM 理解并调用部署在 ESP32 设备上的 MCP 工具,MCP 将调用转为 MQTT 指令发送给硬件设备,智能设备响应并执行对应指令。 + +下一篇中,我们将为智能硬件设备引入语音能力,结合语音识别和语音合成等第三方服务,探索如何在 ESP32 中实现真正的自然语音对话。 + +## 资源 + +- 硅基流动:[硅基流动 SiliconFlow - 致力于成为全球领先的 AI 能力提供商](https://siliconflow.cn/) +- 本文相关源码:https://github.com/mqtt-ai/esp32-mcp-mqtt-tutorial/tree/main/samples/blog_3 + + + +
+
+ 咨询 EMQ 技术专家 +
+ 联系我们 → +
diff --git a/zh/202508/multi-cloud-clusters-and-cross-cloud-migration.md b/zh/202508/multi-cloud-clusters-and-cross-cloud-migration.md new file mode 100644 index 00000000..05dd08e4 --- /dev/null +++ b/zh/202508/multi-cloud-clusters-and-cross-cloud-migration.md @@ -0,0 +1,119 @@ +想象一下,仅仅因为一次云服务的中断就让业务停摆,造成数百万美元的收入损失。2025 年 6 月,Google Cloud Platform(GCP)的全球性中断让无数企业陷入服务瘫痪,充分暴露了单一云架构的脆弱性。 + +正因如此,多云战略成为企业的新选择:通过将工作负载分布在阿里云、华为云和腾讯云等不同云平台之间,企业不仅能规避供应商锁定,还能优化成本效率并消除单点故障。然而,多云部署也面临着管理复杂性和数据可移植性等挑战。 + +Cluster Linking「集群连接」作为 EMQX 平台的核心功能,提供了跨云集群的无缝连接方案,为物联网基础设施赋予灵活性、可扩展性和高可用性,助您真正实现多云架构的价值。 + +## 集群连接对于多云部署至关重要 + +集群连接解决了跨不同云环境的分布式系统固有的挑战,将独立的集群转变为具有凝聚力和弹性的消息传递平台。 + +- **构建统一、高可用的平台:** + + 通过集群连接功能,将部署在不同云平台和区域的 EMQX 集群连接起来,创建了一个所有集群都能充当统一 MQTT 消息平台的系统,无需考虑集群的云供应商。无论客户连接到哪个集群,都能与连接到其他集群的设备进行无缝通信,同时**显著增强高可用性和灾难恢复能力**。如果一个云平台或区域发生故障,另一个云中相互连接的集群可以无缝接管,确保业务连续性和服务零中断。 + +- **实现无缝跨云通信和路由:** + + 由于不同云平台之间的网络隔离,传统的 MQTT 消息跨云通信复杂且效率低下。集群连接提供了一个优化的内置解决方案,可在这些不同的云集群之间**高效可靠地路由 MQTT 消息**。无论用户连接到哪个云平台,都能确保消息从发布者准确地传递给用户。 + +- **实现真正的多云架构并减少供应商锁定:** + + 如果没有集群连接功能,跨多个云的 EMQX 部署仍将是孤立、独立的集群。**集群连接是实现真正、统一和协作式多云 MQTT 基础架构的关键组件。**通过这种互联性,企业可以避免过度依赖单一云供应商,从而更灵活地选择最合适、最具成本效益的云服务来满足其需求。 + +- **简化运维:** + + 在不同的云环境中手动管理消息同步和路由非常复杂,而且容易出错。**集群连接功能将这一复杂性抽象化,简化了多云部署的操作和维护流程,并提高了可控性。** + +## 多云集群连接实例 + +本节演示了如何在多个云提供商之间部署 EMQX 集群。请注意,本演示侧重于在同一 EMQX 平台账户下的集群连接。有关跨不同账户或链接到自托管服务器的详细说明,请参阅[官方文档](https://docs.emqx.com/zh/cloud/latest/cluster_linking/cluster_linking.html)。 + +### 前提条件 + +1. 在不同的云平台创建两个专用部署:`deployment-aliyun` 和 `deployment-huawei`。有关设置专用部署的详细步骤,请参阅 [EMQX Cloud 文档中有关创建部署的说明。](https://docs.emqx.com/zh/cloud/latest/create/dedicated.html) +2. 确保通过 [NAT](https://docs.emqx.com/zh/cloud/latest/vas/nat-gateway.html) 网关访问公网: + - 每个部署都必须设置 NAT 网关。 + - NAT 网关必须处于 `Running` 状态,并允许外部连接。 + +### 创建集群连接 + +1. 在 `deployment-aliyun` 控制台中打开**「集群连接」**页面。 + +2. 单击**「新建」**以创建新的集群连接。 + +3. 在新建集群连接页面上,配置以下选项: + + - **集群连接类型**:选择**同账号**。 + - **集群名称**:选择要链接到的 `deployment-huawei` 专用部署。 + - **地址**:要链接的部署的 MQTT 主机和端口,会根据所选部署自动填充。 + - **用户名/密码**:使用 `deployment-huawei` 配置的有效凭据来验证连接。 + - **客户端 ID 前缀**:为客户端 ID 设置唯一的前缀,例如 `from-aliyun`。 + + 根据集群规模和配置,可能会建立多个 MQTT 客户端连接到 `deployment-huawei`,每个客户端必须具有唯一的 ClientID。您可以通过设置这些连接的客户端 ID 前缀*,*例如:`from-aliyun-1`,来控制这些 ClientID 的分配方式。 + + - **主题**:MQTT 主题过滤器列表,指定当前部署将从远程部署接收哪些消息,例如 `from-cluster`。 + - **高级设置**:配置其他设置,例如 [MQTT 协议](https://www.emqx.com/zh/blog/the-easiest-guide-to-getting-started-with-mqtt)参数。 + +4. 点击**确认**。新的链接将出现在**集群连接**页面上,并默认启用。 + + ![image.png](https://assets.emqx.com/images/5fca23d099b62ae68840da31a798742d.png) + +5. 在 `deployment-huawei` 重复此过程,创建返回 `deployment-aliyun` 的反向链接。 + + ![image.png](https://assets.emqx.com/images/0b29bac4e38209ba99b3dd14c850e089.png) + +### 技术原理 + +集群连接建立后,EMQX 使用 MQTT 协议在集群之间桥接消息。例如,当连接到 `deployment-aliyun` 的客户端向 `from-cluster` 主题发布消息时,EMQX 会通过配置的链接将其路由到 `deployment-huawei` 集群,确保两个集群上的订阅者都能收到该消息。此过程利用 MQTT 的轻量级发布-订阅模型实现高效的实时通信。 + +### 验证多云集群连接 + +要确认集群连接成功,请使用免费 MQTT 客户端工具 MQTTX 用于测试连接:[MQTTX 下载](https://mqttx.app/zh/downloads) + +1. 在 MQTTX 中创建名为 `aliyun-connection` 的连接,使用其 MQTT 主机和凭据连接 `deployment-aliyun`。 +2. 创建第二个名为 `huawei-connection` 的连接,连接到 `deployment-huawei`。 +3. 将两个客户端订阅到集群连接期间配置的 `from-cluster` 主题。 +4. 使用 `aliyun-connection`,向 `from-cluster` 发布测试消息(例如:“来自阿里云的问候”)。 +5. 验证 `aliyun-connection` 和 `huawei-connection` 是否接收消息,确认跨云的双向消息路由。 + +## 网络互联解决方案 + +为了在跨不同云平台的 EMQX 集群之间的实现无缝通信,强大的网络互连解决方案至关重要。本节探讨了三种常见方案:NAT 网关、VPN 连接和专线/云互联,重点介绍它们的优缺点和理想用例,帮助您为多云 MQTT 部署选择最佳方案。 + +### 默认 NAT 网关 + +网络地址转换(NAT)网关是跨不同云平台连接 EMQX 集群的默认方法。它通过公网 IP 路由流量,隐藏私有 IP 地址,同时支持互联网访问。这种方法设置简单,适合需要快速连接的小型企业或概念验证部署。 + +- **安全性**:由于流量通过公共网络传输,因此必须采取如防火墙、VPN 或加密等强大的安全措施,以保护数据安全。 +- **性能**:由于翻译和路由流量的开销,NAT 连接通常会有延迟。与专用网络连接相比,它可能无法提供最佳性能。 +- **用例**:非常适合初创企业或小规模物联网部署,在成本可控的前提下,提供低到中等的性能。 + +### **VPN 连接** + +对于希望确保云环境之间通信安全的企业来说,虚拟专用网络(VPN)隧道提供了一种可靠的解决方案。通过在云供应商的虚拟专用云(VPC)之间创建加密隧道,VPN 可提供安全稳定的连接。这种方法非常适合需要为工业传感器网络等物联网工作负载提供安全、经济高效通信的中型企业。 + +- **安全性**:加密隧道确保云平台之间的所有流量保持私密并免受外部威胁。 +- **稳定性**:VPN 通常比基于公共互联网的连接更稳定,并且可以支持多种流量类型。然而,它们仍然依赖于公共互联网,这意味着拥塞或路由效率低下可能会影响性能。 +- **用例**:最适合需要安全、可靠连接且无需承担高昂专用网络成本的中型企业。 + +### **直接连接 / 云互连** + +对于需要高性能连接的企业,**直接连接**或**云互联**可在数据中心和云环境之间提供专用的私有网络连接。虽然这些解决方案成本较高,但它们绕过了公共互联网,具有显著优势。这种解决方案是关键物联网应用的理想选择。 + +- **性能**:与 NAT 或 VPN 解决方案相比,直接专用连接可提供更高的带宽、更低的延迟和更可靠的性能,非常适合构建关键任务应用程序。 +- **服务水平协议(SLA)**:这些解决方案保证了正常运行时间的 SLA,这对于具有严格可用性要求的企业来说至关重要。 +- **成本**:较高的成本使其适合优先考虑性能而非预算的大型企业。 +- **用例**:最适合需要 SLA 低时延和拥有高吞吐量 IoT 应用程序的企业。 + +## 结论 + +**EMQX 集群连接**功能对于确保无缝多云部署、高可用性和业务弹性至关重要。通过支持灵活的跨云迁移、跨平台路由消息以及提供高效、统一的 MQTT 基础设施,它使企业能够摆脱供应商锁定,增强灾难恢复能力并简化运营。最终,集群连接简化了多云管理的复杂性并确保了业务连续性,使其成为构建强大而灵活的物联网架构的重要组成部分。 + + + +
+
+ 咨询 EMQ 技术专家 +
+ 联系我们 → +