带你手搓玩具。

小智原本这套架构有个局限性:MCP工具执行完之后,只能同步返回结果或者通过异步邮件通知,设备无法被动接收服务端的消息。比如我想让服务端主动给设备推送一张图片、播放一段语音、或者发送一个文本通知,在之前的架构下是做不到的。

所以我就决定改造小智客户端,集成SignalR实时通信框架。这次改造的核心价值是:通过SignalR消息通道,让设备可以接收各种类型的消息(声音、图片、文本通知),服务端的MCP工具执行成功后,可以根据用户ID推送数据到对应的用户通道

整个改造涉及SignalR C++客户端的集成、JWT Token认证、扫码登录(基于ESP32本地MCP工具实现)、以及服务端消息推送逻辑。客户端代码都是C++实现的,不过现在AI辅助编程很强大,帮我节省了大量时间。

img

问题解答

Q: 为什么选择SignalR而不是直接用WebSocket?

A: 起初我确实考虑过直接用WebSocket,但SignalR提供了很多开箱即用的功能:

  • Hub抽象:服务端可以轻松实现群组管理,按用户ID推送消息,比如Clients.Group($"Users:{userId}").SendAsync("Notification", message)
  • 消息路由:不需要自己写消息分发逻辑,SignalR的Hub方法调用和事件推送已经很完善了
  • 类型化调用:相比原始WebSocket的字符串消息,SignalR提供了类似RPC的调用体验,代码更清晰

虽然ESP32没有现成的SignalR库,但我找到了微软官方的C++ SignalR客户端(半成品),将它与ESP32的WebSocket组件整合后,就能用上SignalR的这些特性了。至于SignalR自带的重连机制,我没用,小智有自己的循环重连逻辑,更可控一些。

Q: 改造的核心价值是什么?解决了什么问题?

A: 改造前,ESP32的MCP工具调用完成后,只能通过两种方式通知:

  1. 同步返回:工具执行结果直接返回给调用方
  2. 异步邮件:通过邮件发送执行结果

这两种方式都无法满足实时推送的需求。比如我想让服务端在生图完成后立即推送图片给设备显示,或者播放一段语音提示,之前的架构做不到。

改造后,通过SignalR建立了一条服务端到设备的实时消息通道

  • 服务端的MCP工具执行成功后,可以调用_hubContext.Clients.Group($"Users:{userId}").SendAsync("ShowImage", imageData)将图片推送给设备
  • 设备通过SignalR的事件监听接收消息:connection->on("ShowImage", [](const std::vector<signalr::value>& args) { ... })
  • 支持推送任意类型的数据:文本、图片(Base64)、语音URL、JSON通知等

这才是这次改造的核心价值:让设备具备被动接收服务端消息的能力,而不仅仅是主动调用和同步返回。

Q: 扫码登录是怎么实现的?

A: 扫码登录功能是基于ESP32本地MCP工具实现的,这是小智的固有功能,我只是进行了拓展:

  1. 设备启动时检查是否有JWT Token
  2. 如果没有Token,调用本地MCP工具display_qrcode在屏幕上显示二维码
  3. 二维码内容包含设备ID和服务端地址:https://mcp-server.com/device-login?deviceId=xxx
  4. 用户用手机扫码,完成授权。
  5. 设备获取Token后保存到NVS(Non-Volatile Storage),下次启动直接使用

这样就实现了设备的快速认证,用户体验很好。扫码认证的服务端是使用开源的keycloak做的,对接了设备认证类型。

img

名词解释

核心概念

  • SignalR:微软提供的实时通信框架,封装了WebSocket、Server‑Sent Events和长轮询等传输方式,支持Hub模型、自动重连与消息序列化。适合实现双向、低延迟的实时消息系统。将它移植到嵌入式设备时需考虑客户端实现的体积、内存消耗与线程模型。

  • Hub(集线器):SignalR的核心抽象,类似于MVC中的Controller。服务端通过Hub定义方法供客户端调用,客户端也可以注册事件监听服务端推送。例如ChatHub.SendMessage(user, message)就是一个典型的Hub方法。

  • MCP(Model Context Protocol):一种基于JSON-RPC 2.0的协议,用于定义客户端和服务端之间的工具调用规范。在IoT场景中,设备可以作为MCP Server暴露能力(如重启、显示图片),而云端服务作为MCP Client调用这些能力。

  • JSON-RPC 2.0:一种轻量级的远程过程调用协议,使用JSON编码。MCP协议基于此标准,定义了initializetools/listtools/call等方法。每个请求必须包含jsonrpc: "2.0"methodid字段。

ESP32相关

  • FreeRTOS:一个开源、轻量级的实时操作系统内核,常用于微控制器平台(如ESP32)。提供任务调度、优先级、互斥锁、信号量、队列、软件定时器等实时特性,便于在资源受限设备上实现并发与确定性行为。使用时需注意堆栈大小、中断安全和任务优先级设计。

  • ESP32 PSRAM:ESP32可选的外部伪静态RAM(Pseudo-SRAM),用于扩展设备可用内存(常见4MB/8MB/16MB)。适合存放大对象、图像缓存、网络缓冲和动态分配数据。在ESP-IDF中需启用并正确配置,分配时也可使用不同的堆区域(如heap_caps_malloc(size, MALLOC_CAP_SPIRAM))来控制放置与性能/DMA限制。

  • WebSocket:一种基于TCP的全双工通信协议,通过HTTP握手升级建立连接。SignalR默认优先使用WebSocket作为传输层,在ESP32上通过esp_websocket_client组件实现。需要注意的是ESP32的WebSocket客户端不支持自动重连,需要在应用层实现。

认证相关

  • Bearer Token:一种HTTP认证方案,将Token放在Authorization头中:Authorization: Bearer <token>。在SignalR中,通常将Token作为查询参数传递:/hub?access_token=YOUR_TOKEN

  • JWT(JSON Web Token):一种开放标准(RFC 7512),用于在各方之间安全地传输信息。在Verdure MCP中,使用Keycloak签发的JWT进行用户认证,Token中包含用户ID、角色、过期时间等Claim信息。

  • API Token:一种简单的认证方式,后续连接时携带此Token验证身份。Verdure MCP同时支持API Token和JWT两种方式。

img

核心技术架构

整个改造的架构可以用一张图说明:

┌──────────────────────┐                          ┌──────────────────────┐
│   .NET MCP Service   │                          │   ESP32 Device       │
│   (Verdure MCP)      │◄─────SignalR Hub────────►│   (小智客户端)       │
│                      │                          │                      │
│  ┌────────────────┐  │  ① JWT Token认证         │  ┌────────────────┐  │
│  │  DeviceHub.cs  │  │◄─────────────────────────│  │  扫码登录      │  │
│  │                │  │                          │  │  (本地MCP工具) │  │
│  │ OnConnected    │  │                          │  └────────────────┘  │
│  │ (验证Token)    │  │                          │          ↓           │
│  └────────────────┘  │  ② 建立连接               │  ┌────────────────┐  │
│          ↓           │◄─────────────────────────│  │ SignalR Client │  │
│  ┌────────────────┐  │                          │  │ - connection   │  │
│  │  群组管理      │  │                          │  │ - on() events  │  │
│  │ Users:{userId} │  │                          │  └────────────────┘  │
│  └────────────────┘  │                          │                      │
│          ↓           │                          │                      │
│  ┌────────────────┐  │  ③ MCP工具执行后推送     │  ┌────────────────┐  │
│  │  消息推送      │  │─────────────────────────►│  │ 消息接收处理   │  │
│  │ SendAsync()    │  │  ShowImage(imageData)    │  │ - 显示图片     │  │
│  │                │  │  PlayAudio(audioUrl)     │  │ - 播放语音     │  │
│  │                │  │  Notification(text)      │  │ - 显示通知     │  │
│  └────────────────┘  │                          │  └────────────────┘  │
└──────────────────────┘                          └──────────────────────┘

关键流程:

  1. 扫码登录:设备启动后,如果没有Token,调用本地MCP工具显示二维码,用户扫码后获取JWT Token
  2. 建立连接:携带JWT Token连接SignalR Hub,服务端验证后加入用户群组Users:{userId}
  3. 消息推送:服务端MCP工具执行完成后,通过SignalR将结果推送给设备
    • _hubContext.Clients.Group($"Users:{userId}").SendAsync("ShowImage", imageData)
    • 设备监听事件并处理:connection->on("ShowImage", handler)

这套架构的核心价值就是让服务端可以主动推送消息给设备,而不仅仅是等待设备轮询或同步返回。

开发环境准备

ESP32开发环境(VS Code方式)

最简单的方式是使用VS Code的ESP-IDF插件:

  1. 安装VS Code和插件

  2. 配置ESP-IDF

    • F1打开命令面板,输入 ESP-IDF: Configure ESP-IDF Extension
    • 选择 Express 快速配置
    • 选择ESP-IDF版本(推荐v5.1或更高)
    • 等待安装完成(会自动下载工具链、Python环境等)
  3. 创建/打开项目

    • F1 → ESP-IDF: Show Examples Projects
    • 或直接打开 esp-signalr-example 项目文件夹
  4. 编译和烧录

    • 点击底部状态栏的 BuildFlashMonitor 按钮
    • 或按快捷键:Ctrl+E B(编译)、Ctrl+E F(烧录)

这种方式比命令行简单很多,适合.NET开发者快速上手ESP32开发。

img

.NET开发环境

服务端使用.NET 10开发:

# Windows: 下载安装器 https://dotnet.microsoft.com/download/dotnet/10.0

# 验证安装
dotnet --version  # 应该输出 10.0.x

核心代码实现

本章节将代码分为示例代码实际整合代码两个部分进行讲解:

  • 示例代码:用于理解核心概念的简化版本,便于学习和快速上手
  • 实际整合代码:生产环境中的完整实现,包含完善的错误处理、状态管理等

关于示例仓库

为了帮助开发者快速上手ESP32的SignalR集成,我创建了一个完整的示例仓库:

🔗 仓库地址https://github.com/maker-community/esp-signalr-example

📦 仓库结构

esp-signalr-example/
├── main/                    # ESP32 C++客户端代码
│   ├── main.cpp            # 主程序(WiFi连接、SignalR初始化)
│   └── CMakeLists.txt      # ESP-IDF构建配置
├── signalr-server/         # .NET C# 服务端代码
│   ├── Program.cs          # ASP.NET Core服务器配置
│   ├── ChatHub.cs          # SignalR Hub实现
│   └── signalr-server.csproj
├── docs/                   # 文档
│   ├── QUICKSTART.md       # 5分钟快速开始指南
│   ├── TEST_SERVER_SETUP.md # 测试服务器详细设置
│   └── TROUBLESHOOTING.md  # 常见问题排查
└── README.md               # 项目说明

✨ 主要特性

  1. 开箱即用的服务器

    • 基于ASP.NET Core和SignalR构建
    • 支持消息广播
    • 完整的连接管理和日志输出
    • 提供RESTful API用于设备控制
  2. 简化的ESP32客户端

    • 使用Microsoft官方的C++ SignalR客户端库移植版
    • 通过menuconfig配置WiFi和服务器地址
    • 演示消息发送/接收、传感器数据上报
    • 清晰的日志输出和错误处理

🚀 快速开始示例(5分钟运行):

# 1. 克隆仓库
git clone https://github.com/maker-community/esp-signalr-example.git
cd esp-signalr-example

# 2. 启动服务器(需要.NET 9.0+)
cd signalr-server
dotnet run --urls "http://+:5000" 这个运行可以用ip访问
# 服务器运行在: http://0.0.0.0:5000/chatHub

# 3. 配置并烧录ESP32
cd ../
idf.py menuconfig
# 配置WiFi SSID、密码和服务器地址
idf.py build flash monitor

esp32的配置如下:

img

📊 运行效果

服务器输出:

img

✓ Client connected: abc123
  IP Address: 192.168.1.100
  Total Connections: 1

[10:30:25] Received from ESP32-Device: Test message #1 from ESP32
[10:30:35] Sensor Update - Temperature: 25.50

ESP32串口输出:

img

I (3520) SIGNALR_EXAMPLE: ✓✓✓ Connected to SignalR Hub! ✓✓✓
I (3525) SIGNALR_EXAMPLE: 🔔 Notification: Welcome!
I (14640) S

🎯 示例仓库的价值

  • 学习路径清晰:从简单的连接到复杂的数据传输,循序渐进
  • 可直接运行:不需要依赖外部服务,本地即可测试完整流程
  • 代码注释详细:关键部分都有中英文注释说明
  • 易于扩展:基于这个示例可以快速开发自己的应用

接下来的 5.1 节将基于这个示例仓库的代码进行讲解。

5.1 示例代码(教学简化版)

说明:以下代码来自开源示例仓库 esp-signalr-example,经过精简突出核心概念,方便理解SignalR与ESP32集成的基本原理。完整代码请参考仓库源码。

5.1.1 服务端:SignalR Hub基础实现

这是服务端的核心代码,实现了连接管理、消息广播和设备状态跟踪:

ChatHub.cs - Hub核心实现

using Microsoft.AspNetCore.SignalR;

public class ChatHub : Hub
{
    private readonly ILogger<ChatHub> _logger;
    private static int _connectionCount = 0;
    
    // 存储连接的设备信息
    private static readonly Dictionary<string, DeviceInfo> _connectedDevices = new();
    private static readonly object _devicesLock = new();

    public ChatHub(ILogger<ChatHub> logger)
    {
        _logger = logger;
    }

    /// <summary>
    /// 处理来自ESP32的消息
    /// </summary>
    public async Task SendMessage(string user, string message)
    {
        _logger.LogInformation("[{Time}] Received from {User}: {Message}", 
            DateTime.Now.ToString("HH:mm:ss"), user, message);
        
        // 广播到所有连接的客户端
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }

    /// <summary>
    /// 处理传感器数据更新
    /// </summary>
    public async Task UpdateSensor(string sensorId, double value)
    {
        _logger.LogInformation("[{Time}] Sensor Update - {SensorId}: {Value:F2}", 
            DateTime.Now.ToString("HH:mm:ss"), sensorId, value);
        
        // 广播传感器数据到所有客户端
        await Clients.All.SendAsync("UpdateSensorData", sensorId, value);
    }

    /// <summary>
    /// 处理ESP32状态更新
    /// </summary>
    public async Task UpdateDeviceStatus(string deviceId, string status, int freeHeap)
    {
        _logger.LogInformation("[{Time}] Device Status - {DeviceId}: {Status}, Free Heap: {FreeHeap} bytes", 
            DateTime.Now.ToString("HH:mm:ss"), deviceId, status, freeHeap);
        
        await Clients.All.SendAsync("DeviceStatusUpdate", deviceId, status, freeHeap);
    }

    /// <summary>
    /// 客户端连接时触发
    /// </summary>
    public override async Task OnConnectedAsync()
    {
        Interlocked.Increment(ref _connectionCount);
        
        var connectionId = Context.ConnectionId;
        var httpContext = Context.GetHttpContext();
        var ipAddress = httpContext?.Connection.RemoteIpAddress?.ToString();
        var userAgent = httpContext?.Request.Headers["User-Agent"].ToString();
        
        // 保存设备信息
        lock (_devicesLock)
        {
            _connectedDevices[connectionId] = new DeviceInfo
            {
                ConnectionId = connectionId,
                IpAddress = ipAddress,
                UserAgent = userAgent,
                ConnectedAt = DateTime.UtcNow
            };
        }
        
        _logger.LogInformation("✓ Client connected: {ConnectionId}", connectionId);
        _logger.LogInformation("  IP Address: {IpAddress}", ipAddress);
        _logger.LogInformation("  Total Connections: {Count}", _connectionCount);
        
        await base.OnConnectedAsync();
        
        // 发送欢迎消息(ESP32通过此消息确认连接成功)
        await Clients.Caller.SendAsync("Notification", 
            "Welcome to SignalR Test Server!");
    }

    /// <summary>
    /// 客户端断开时触发
    /// </summary>
    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        Interlocked.Decrement(ref _connectionCount);
        
        var connectionId = Context.ConnectionId;
        
        // 移除设备信息
        lock (_devicesLock)
        {
            _connectedDevices.Remove(connectionId);
        }
        
        _logger.LogInformation("✗ Client disconnected: {ConnectionId}", connectionId);
        if (exception != null)
        {
            _logger.LogWarning("  Disconnection reason: {Message}", exception.Message);
        }
        _logger.LogInformation("  Remaining Connections: {Count}", _connectionCount);
        
        await base.OnDisconnectedAsync(exception);
    }
}

/// <summary>
/// 设备连接信息
/// </summary>
public class DeviceInfo
{
    public string ConnectionId { get; set; } = "";
    public string? IpAddress { get; set; }
    public string? UserAgent { get; set; }
    public DateTime ConnectedAt { get; set; }
}

Program.cs - SignalR服务配置

var builder = WebApplication.CreateBuilder(args);

// 添加SignalR服务
builder.Services.AddSignalR(options =>
{
    options.EnableDetailedErrors = true;  // 开发环境启用
    options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);  // 客户端超时
    options.KeepAliveInterval = TimeSpan.FromSeconds(15);  // 心跳间隔
});

// 添加CORS支持(允许ESP32跨域连接)
builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(policy =>
    {
        policy.AllowAnyOrigin()
              .AllowAnyHeader()
              .AllowAnyMethod();
    });
});

var app = builder.Build();

app.UseCors();
app.MapHub<ChatHub>("/chatHub");

// 监听所有网络接口(重要:局域网内ESP32能访问)
app.Urls.Add("http://0.0.0.0:5000");

Console.WriteLine("SignalR Server: http://0.0.0.0:5000/chatHub");
app.Run();

关键点说明

  1. 连接确认机制:服务器在 OnConnectedAsync 中发送 Notification 消息,ESP32收到此消息才认为连接成功
  2. 消息广播:使用 Clients.All.SendAsync() 向所有连接的客户端广播消息
  3. 连接跟踪:使用静态字典 _connectedDevices 跟踪所有连接的设备信息
5.1.2 服务端:设备控制API(通过SignalR推送消息)

示例仓库提供了完整的设备控制API,演示如何通过SignalR向ESP32推送各种类型的消息:

Program.cs - 设备控制API端点

// ============================================================================
// 设备控制 API - 用于向设备发送 CustomMessage
// ============================================================================

// 获取所有连接的设备
app.MapGet("/api/device/connections", () =>
{
    return Results.Ok(ChatHub.ConnectedDevices);
})
.WithName("GetConnections")
.WithDescription("获取所有连接的设备列表");

// 发送通知
app.MapPost("/api/device/notification", async (
    NotificationRequest request, 
    IHubContext<ChatHub> hubContext, 
    ILogger<Program> logger) =>
{
    var message = new
    {
        action = "notification",
        title = request.Title ?? "通知",
        content = request.Content ?? "",
        emotion = request.Emotion ?? "bell",
        sound = request.Sound ?? "popup"
    };

    await SendCustomMessage(hubContext, logger, request.ConnectionId, message);
    return Results.Ok(new { success = true, message = "Notification sent" });
})
.WithDescription("发送通知到设备 (sound: popup/success/vibration/exclamation/low_battery/none)");

// 发送图片
app.MapPost("/api/device/image", async (
    ImageRequest request, 
    IHubContext<ChatHub> hubContext, 
    ILogger<Program> logger) =>
{
    var message = new
    {
        action = "image",
        url = request.Url
    };

    await SendCustomMessage(hubContext, logger, request.ConnectionId, message);
    return Results.Ok(new { success = true, message = "Image sent" });
})
.WithDescription("发送图片URL到设备显示 (支持JPG/PNG, 最大1MB)");

// 发送音频
app.MapPost("/api/device/audio", async (
    AudioRequest request, 
    IHubContext<ChatHub> hubContext, 
    ILogger<Program> logger) =>
{
    var message = new
    {
        action = "audio",
        url = request.Url
    };

    await SendCustomMessage(hubContext, logger, request.ConnectionId, message);
    return Results.Ok(new { success = true, message = "Audio sent" });
})
.WithDescription("发送音频URL到设备播放 (OGG格式, 最大512KB)");

// 发送命令
app.MapPost("/api/device/command", async (
    CommandRequest request, 
    IHubContext<ChatHub> hubContext, 
    ILogger<Program> logger) =>
{
    var message = new
    {
        action = "command",
        command = request.Command
    };

    await SendCustomMessage(hubContext, logger, request.ConnectionId, message);
    return Results.Ok(new { success = true, message = "Command sent" });
})
.WithDescription("发送命令到设备 (command: reboot/wake/listen/stop)");

// 显示二维码
app.MapPost("/api/device/qrcode", async (
    QRCodeRequest request, 
    IHubContext<ChatHub> hubContext, 
    ILogger<Program> logger) =>
{
    var message = new
    {
        action = "qrcode",
        content = request.Content,
        title = request.Title ?? "扫码"
    };

    await SendCustomMessage(hubContext, logger, request.ConnectionId, message);
    return Results.Ok(new { success = true, message = "QRCode sent" });
})
.WithDescription("显示二维码到设备屏幕");

// 辅助方法:发送 CustomMessage
async Task SendCustomMessage(
    IHubContext<ChatHub> hubContext, 
    ILogger<Program> logger, 
    string? connectionId, 
    object message)
{
    var json = JsonSerializer.Serialize(message);
    logger.LogInformation("📤 Sending CustomMessage to {Target}: {Message}", 
        string.IsNullOrEmpty(connectionId) ? "ALL" : connectionId, json);

    if (string.IsNullOrEmpty(connectionId))
    {
        // 发送给所有连接的设备
        await hubContext.Clients.All.SendAsync("CustomMessage", json);
    }
    else
    {
        // 发送给指定连接
        await hubContext.Clients.Client(connectionId).SendAsync("CustomMessage", json);
    }
}

// ============================================================================
// 请求模型
// ============================================================================

record NotificationRequest
{
    public string? ConnectionId { get; init; }
    public string? Title { get; init; }
    public string Content { get; init; } = "";
    public string? Emotion { get; init; }
    public string? Sound { get; init; }
}

record ImageRequest
{
    public string? ConnectionId { get; init; }
    public string Url { get; init; } = "";
}

record AudioRequest
{
    public string? ConnectionId { get; init; }
    public string Url { get; init; } = "";
}

record CommandRequest
{
    public string? ConnectionId { get; init; }
    public string Command { get; init; } = "";
}

record QRCodeRequest
{
    public string? ConnectionId { get; init; }
    public string Content { get; init; } = "";
    public string? Title { get; init; }
}

关键点说明

  1. IHubContext注入:使用 IHubContext<ChatHub> 在非Hub类中发送SignalR消息
  2. 消息格式:使用JSON格式的 CustomMessage 事件,包含 action 字段标识消息类型
  3. 定向推送
    • Clients.All.SendAsync() - 广播给所有连接的设备
    • Clients.Client(connectionId).SendAsync() - 发送给指定设备
    • Clients.Group(groupName).SendAsync() - 发送给群组(如 Users:{userId}
  4. RESTful API设计:提供HTTP端点控制设备,便于其他服务调用

服务端的接口图片如下可以直接操作测试:

img

5.1.3 客户端(ESP32):连接SignalR并接收消息

这是ESP32端的核心代码,演示如何连接SignalR Hub并接收各种类型的消息:

main.cpp - SignalR连接与消息处理

#include <stdio.h>
#include <memory>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_log.h"
#include "nvs_flash.h"

#include "hub_connection_builder.h"
#include "esp32_websocket_client.h"
#include "esp32_http_client.h"

// =============================================================================
// 配置项(通过menuconfig设置)
// =============================================================================

#define WIFI_SSID      CONFIG_EXAMPLE_WIFI_SSID
#define WIFI_PASSWORD  CONFIG_EXAMPLE_WIFI_PASSWORD
#define SIGNALR_HUB_URL CONFIG_EXAMPLE_SIGNALR_HUB_URL

static const char* TAG = "SIGNALR_EXAMPLE";

// SignalR连接对象
static std::unique_ptr<signalr::hub_connection> g_connection;
static bool g_is_connected = false;

// =============================================================================
// 消息处理器
// =============================================================================

/**
 * 处理服务器发送的消息
 */
static void on_receive_message(const std::vector<signalr::value>& args)
{
    ESP_LOGI(TAG, "==============================================");
    ESP_LOGI(TAG, "📩 Message received from server:");
    
    if (args.size() >= 2) {
        std::string user = args[0].as_string();
        std::string message = args[1].as_string();
        
        ESP_LOGI(TAG, "   From: %s", user.c_str());
        ESP_LOGI(TAG, "   Text: %s", message.c_str());
    } else if (args.size() == 1) {
        ESP_LOGI(TAG, "   Message: %s", args[0].as_string().c_str());
    }
    
    ESP_LOGI(TAG, "==============================================");
}

/**
 * 处理通知消息(连接确认)
 */
static void on_notification(const std::vector<signalr::value>& args)
{
    if (args.empty()) return;
    
    std::string notification = args[0].as_string();
    ESP_LOGI(TAG, "🔔 Notification: %s", notification.c_str());
    
    // 通过Notification消息确认连接成功
    if (!g_is_connected) {
        g_is_connected = true;
        ESP_LOGI(TAG, "==============================================");
        ESP_LOGI(TAG, "✓✓✓ Connected to SignalR Hub! ✓✓✓");
        ESP_LOGI(TAG, "==============================================");
    }
}

/**
 * 处理传感器数据更新
 */
static void on_sensor_update(const std::vector<signalr::value>& args)
{
    if (args.size() < 2) return;
    
    std::string sensor_id = args[0].as_string();
    double value = args[1].as_double();
    
    ESP_LOGI(TAG, "📊 Sensor Update: %s = %.2f", sensor_id.c_str(), value);
}

/**
 * 处理设备状态更新
 */
static void on_device_status(const std::vector<signalr::value>& args)
{
    if (args.size() < 3) return;
    
    std::string device_id = args[0].as_string();
    std::string status = args[1].as_string();
    int free_heap = static_cast<int>(args[2].as_double());
    
    ESP_LOGI(TAG, "📱 Device Status: %s - %s (Free Heap: %d bytes)", 
             device_id.c_str(), status.c_str(), free_heap);
}

// =============================================================================
// SignalR连接管理
// =============================================================================

/**
 * 初始化SignalR连接
 */
static void init_signalr(void)
{
    ESP_LOGI(TAG, "Initializing SignalR connection to: %s", SIGNALR_HUB_URL);

    try {
        // 创建hub_connection(使用make_unique)
        g_connection = std::make_unique<signalr::hub_connection>(
            signalr::hub_connection_builder::create(SIGNALR_HUB_URL)
                .with_websocket_factory([](const signalr::signalr_client_config& config) {
                    return std::make_shared<signalr::esp32_websocket_client>(config);
                })
                .with_http_client_factory([](const signalr::signalr_client_config& config) {
                    return std::make_shared<signalr::esp32_http_client>(config);
                })
                .with_automatic_reconnect()  // 启用自动重连
                .skip_negotiation(true)      // 跳过协商,直接WebSocket
                .build());

        ESP_LOGI(TAG, "✓ SignalR connection object created");
        
    } catch (const std::exception& e) {
        ESP_LOGE(TAG, "Failed to create SignalR connection: %s", e.what());
    }
}

/**
 * 注册消息处理器
 */
static void setup_message_handlers(void)
{
    if (!g_connection) {
        ESP_LOGE(TAG, "Connection not initialized");
        return;
    }

    // 注册 "ReceiveMessage" 事件
    g_connection->on("ReceiveMessage", on_receive_message);
    ESP_LOGI(TAG, "✓ Registered handler: ReceiveMessage");

    // 注册 "Notification" 事件(用于连接确认)
    g_connection->on("Notification", on_notification);
    ESP_LOGI(TAG, "✓ Registered handler: Notification");

    // 注册 "UpdateSensorData" 事件
    g_connection->on("UpdateSensorData", on_sensor_update);
    ESP_LOGI(TAG, "✓ Registered handler: UpdateSensorData");

    // 注册 "DeviceStatusUpdate" 事件
    g_connection->on("DeviceStatusUpdate", on_device_status);
    ESP_LOGI(TAG, "✓ Registered handler: DeviceStatusUpdate");
}

/**
 * 启动SignalR连接
 */
static void start_signalr_connection(void)
{
    if (!g_connection) {
        ESP_LOGE(TAG, "Connection not initialized");
        return;
    }
    
    ESP_LOGI(TAG, "Starting SignalR connection...");

    try {
        // 启动连接(异步)
        g_connection->start([](std::exception_ptr exception) {
            if (exception) {
                ESP_LOGE(TAG, "Connection failed in callback");
            } else {
                ESP_LOGI(TAG, "Connection started successfully");
            }
        });
        
        ESP_LOGI(TAG, "Waiting for Notification message to confirm connection...");

    } catch (const std::exception& e) {
        ESP_LOGE(TAG, "Exception starting connection: %s", e.what());
    }
}

// =============================================================================
// 测试任务:定期发送消息
// =============================================================================

static void signalr_test_task(void* param)
{
    int message_count = 1;
    
    while (true) {
        // 等待10秒
        vTaskDelay(pdMS_TO_TICKS(10000));
        
        // 检查连接状态
        if (!g_connection || !g_is_connected) {
            ESP_LOGW(TAG, "Not connected, skipping message send");
            continue;
        }
        
        // 发送消息到服务器
        std::string message = "Test message #" + std::to_string(message_count++) + " from ESP32";
        
        ESP_LOGI(TAG, "📤 Sending message...");
        ESP_LOGI(TAG, "   User: ESP32-Device");
        ESP_LOGI(TAG, "   Message: %s", message.c_str());
        
        try {
            std::vector<signalr::value> args;
            args.push_back(signalr::value("ESP32-Device"));
            args.push_back(signalr::value(message));
            
            // 调用服务器的 SendMessage 方法
            g_connection->invoke("SendMessage", args, 
                [](const signalr::value& result, std::exception_ptr exception) {
                    if (exception) {
                        ESP_LOGE(TAG, "✗ Failed to send message");
                    } else {
                        ESP_LOGI(TAG, "✓ Message sent successfully!");
                    }
                });
                
        } catch (const std::exception& e) {
            ESP_LOGE(TAG, "Exception sending message: %s", e.what());
        }
    }
}

// =============================================================================
// 主程序
// =============================================================================

extern "C" void app_main(void)
{
    ESP_LOGI(TAG, "========================================");
    ESP_LOGI(TAG, " ESP32 SignalR Client Test Example");
    ESP_LOGI(TAG, "========================================");
    
    // 1. 初始化WiFi(省略WiFi连接代码,参考完整示例)
    // wifi_init_sta();
    
    // 2. 初始化SignalR连接对象
    ESP_LOGI(TAG, "Step 1: Initializing SignalR...");
    init_signalr();
    
    // 3. 注册消息处理器
    ESP_LOGI(TAG, "Step 2: Setting up message handlers...");
    setup_message_handlers();
    
    // 4. 启动连接
    ESP_LOGI(TAG, "Step 3: Starting connection...");
    start_signalr_connection();
    
    // 5. 创建测试任务(定期发送消息)
    ESP_LOGI(TAG, "Step 4: Creating test task...");
    xTaskCreate(signalr_test_task, "signalr_test", 8192, NULL, 5, NULL);
    
    ESP_LOGI(TAG, "Setup complete. Check logs for connection status.");
}

关键点说明

  1. 连接创建:使用 hub_connection_builder 构建连接,配置WebSocket客户端工厂
  2. 跳过协商skip_negotiation(true) 直接使用WebSocket,提高连接速度
  3. 消息处理器注册:使用 connection->on("EventName", handler) 注册事件监听器
  4. 连接确认:通过接收 Notification 消息判断连接成功(服务器在 OnConnectedAsync 中发送)
  5. 调用服务器方法:使用 invoke() 调用Hub方法,如 SendMessage

完整运行流程

1. WiFi连接成功
   ↓
2. 创建SignalR连接对象
   ↓
3. 注册消息处理器(ReceiveMessage、Notification等)
   ↓
4. 调用 connection->start() 启动连接
   ↓
5. 等待服务器发送 Notification 消息
   ↓
6. 收到 Notification,标记连接成功
   ↓
7. 定期调用 invoke("SendMessage") 发送消息
   ↓
8. 接收服务器广播的消息,触发对应处理器

示例输出

I (3480) SIGNALR_EXAMPLE: ✓ Registered handler: ReceiveMessage
I (3485) SIGNALR_EXAMPLE: ✓ Registered handler: Notification
I (3490) SIGNALR_EXAMPLE: Starting SignalR connection...
I (4520) SIGNALR_EXAMPLE: ==============================================
I (4520) SIGNALR_EXAMPLE: ✓✓✓ Connected to SignalR Hub! ✓✓✓
I (4525) SIGNALR_EXAMPLE: ==============================================
I (4530) SIGNALR_EXAMPLE: 🔔 Notification: Welcome to SignalR!
I (14530) SIGNALR_EXAMPLE: 📤 Sending message...
I (14640) SIGNALR_EXAMPLE: ✓ Message sent successfully!
I (14650) SIGNALR_EXAMPLE: 📩 Message received from server:
I (14655) SIGNALR_EXAMPLE:    From: ESP32-Device
I (14660) SIGNALR_EXAMPLE:    Text: Test message #1 from ESP32

5.2 实际整合代码(生产环境完整实现)

说明:以下代码来自小智AI项目的实际生产代码,包含了完整的错误处理、状态管理、JWT认证和自动重连机制。

实际项目代码分为三个主要仓库:

5.2.1 小智ESP32设备代码

仓库地址

注意:SignalR功能主要在 signalr 和 signalr-update-audio 两个分支中实现,这两个分支都是SignalR集成相关的开发分支。

核心文件

  • main/signalr_client.cc / main/signalr_client.h - SignalR客户端核心实现
  • main/application.cc / main/application.h - 主应用程序逻辑和状态管理
  • main/protocols/websocket_protocol.cc - WebSocket协议实现
  • main/protocols/mqtt_protocol.cc - MQTT协议实现
  • main/mcp_server.cc - MCP服务器实现

实际实现特点

与示例代码相比,生产环境实现增加了:

  1. 完整的生命周期管理

    • 连接建立、断开重连、资源清理
    • 设备状态机管理(空闲、连接中、监听、说话等)
  2. 协议版本支持

    • 支持WebSocket和MQTT两种传输协议
    • 协议层抽象,易于扩展新协议
  3. 音频流处理

    • 实时音频数据的编码、传输和接收
    • 音频分块传输(重要!)- 解决大数据传输导致连接断开的问题
    • 支持Opus编解码
  4. MCP工具集成

    • 完整的MCP Server实现
    • 工具注册、调用和响应机制
    • 支持异步工具执行
  5. SignalR客户端封装

    • 完整的连接生命周期管理
    • JWT Token认证
    • 自动重连机制(指数退避)
    • 设备注册和心跳保持
    • 自定义消息处理
SignalR客户端核心实现 (signalr_client.cc)

这是整个SignalR集成的核心代码,封装了所有与SignalR通信相关的逻辑。

完整代码signalr_client.cc (850行)

关键实现要点

1. 单例模式管理 - 全局唯一实例

SignalRClient& SignalRClient::GetInstance() {
    static SignalRClient instance;
    return instance;
}

2. JWT Token认证 - 通过Query String传递

bool SignalRClient::Initialize(const std::string& hub_url, const std::string& token) {
    // 🔐 Build URL with token as query parameter (ASP.NET Core SignalR standard method)
    std::string final_hub_url = hub_url;
    if (!token.empty()) {
        ESP_LOGI(TAG, "========== SignalR Token Authentication ==========");
        
        // Remove "Bearer " prefix if present
        std::string token_value = token;
        if (token_value.find("Bearer ") == 0) {
            token_value = token_value.substr(7);
        }
        
        // Append token to URL
        final_hub_url += "?access_token=" + token_value;
    }
    
    // Create hub connection builder
    auto builder = signalr::hub_connection_builder::create(final_hub_url);
    
    // Set WebSocket factory (使用ESP32的WebSocket实现)
    builder.with_websocket_factory([](const signalr::signalr_client_config& config) {
        auto client = std::make_shared<signalr::esp32_websocket_client>(config);
        return client;
    });
    
    // Skip negotiation (direct WebSocket connection)
    builder.skip_negotiation(true);
    
    // Build connection
    connection_ = std::make_unique<signalr::hub_connection>(builder.build());
}

3. 超时和心跳配置

signalr::signalr_client_config cfg;
cfg.set_server_timeout(std::chrono::seconds(60));     // server expects 60s idle
cfg.set_keepalive_interval(std::chrono::seconds(15)); // send ping every 15s
cfg.set_handshake_timeout(std::chrono::seconds(5));   // short handshake timeout

// IMPORTANT: Disable library's auto-reconnect! It has race condition bugs
cfg.enable_auto_reconnect(false);
connection_->set_client_config(cfg);

4. 连接确认和自动注册

// Register Notification handler to confirm connection
connection_->on("Notification", [this](const std::vector<signalr::value>& args) {
    if (args.empty()) return;
    
    std::string message = args[0].as_string();
    ESP_LOGI(TAG, "🔔 Notification from server: %s", message.c_str());
    
    if (!connection_confirmed_) {
        connection_confirmed_ = true;
        ESP_LOGI(TAG, "✓✓✓ SIGNALR CONNECTION CONFIRMED BY SERVER! ✓✓✓");
        
        // 🔄 Auto-register device info after connection confirmed
        std::string mac_address = DeviceInfo::GetMacAddress();
        std::string metadata = DeviceInfo::BuildMetadataJson();
        
        RegisterDevice(mac_address, "", metadata, [](bool success, const std::string& result) {
            if (success) {
                ESP_LOGI(TAG, "✅ Device auto-registration successful");
            }
        });
    }
});

5. 自定义消息处理

connection_->on("CustomMessage", [this](const std::vector<signalr::value>& args) {
    if (args.empty()) return;
    
    try {
        std::string json_str = args[0].as_string();
        ESP_LOGI(TAG, "📨 Received CustomMessage: %s", json_str.c_str());
        
        auto root = cJSON_Parse(json_str.c_str());
        if (root) {
            if (on_custom_message_) {
                on_custom_message_(root);  // 调用用户设置的回调
            }
            cJSON_Delete(root);
        }
    } catch (const std::exception& e) {
        ESP_LOGE(TAG, "Exception handling CustomMessage: %s", e.what());
    }
});

6. 自动重连机制 - 使用PSRAM栈的后台任务

void SignalRClient::StartReconnectTask() {
    ESP_LOGI(TAG, "Starting SignalR reconnect background task (PSRAM stack)...");
    reconnect_task_running_.store(true, std::memory_order_release);
    
    // Allocate task stack from PSRAM (reusable)
    reconnect_task_stack_ = (StackType_t*)heap_caps_malloc(
        RECONNECT_TASK_STACK_SIZE, MALLOC_CAP_SPIRAM);
    
    // Create task with static allocation (stack in PSRAM)
    reconnect_task_handle_ = xTaskCreateStatic(
        ReconnectTaskEntry, "signalr_reconn",
        RECONNECT_TASK_STACK_SIZE / sizeof(StackType_t),
        this, 2, reconnect_task_stack_, reconnect_task_buffer_
    );
}

void SignalRClient::ReconnectTaskLoop() {
    while (reconnect_task_running_.load(std::memory_order_acquire)) {
        vTaskDelay(pdMS_TO_TICKS(1000));
        
        if (!reconnect_requested_.load() || IsConnected()) {
            continue;
        }
        
        // Apply exponential backoff
        ESP_LOGI(TAG, "Attempting connection (backoff=%dms)...", reconnect_backoff_ms_);
        
        if (Connect() && IsConnected()) {
            reconnect_backoff_ms_ = 1000;  // Reset backoff on success
        } else {
            vTaskDelay(pdMS_TO_TICKS(reconnect_backoff_ms_));
            reconnect_backoff_ms_ = std::min(reconnect_backoff_ms_ * 2, 
                MAX_RECONNECT_BACKOFF_MS);  // Exponential backoff
        }
    }
}

7. 设备注册和心跳

void SignalRClient::RegisterDevice(
    const std::string& mac_address,
    const std::string& device_token,
    const std::string& metadata,
    std::function<void(bool, const std::string&)> callback) {
    
    if (!IsConnected()) {
        if (callback) callback(false, "Not connected");
        return;
    }
    
    std::vector<signalr::value> args;
    args.push_back(signalr::value(mac_address));
    args.push_back(signalr::value(device_token));
    args.push_back(signalr::value(metadata));
    
    connection_->invoke("RegisterDevice", args,
        [callback](const signalr::value& result, std::exception_ptr ex) {
            if (ex) {
                if (callback) callback(false, "Registration failed");
            } else {
                if (callback) callback(true, "Registration sent");
            }
        });
}

void SignalRClient::SendHeartbeat(
    std::function<void(bool, const std::string&)> callback) {
    
    if (!IsConnected()) {
        if (callback) callback(false, "Not connected");
        return;
    }
    
    std::vector<signalr::value> args;
    connection_->invoke("Heartbeat", args,
        [callback](const signalr::value& result, std::exception_ptr ex) {
            if (!ex) {
                ESP_LOGD(TAG, "💓 Heartbeat sent");
                if (callback) callback(true, "Success");
            }
        });
}

SignalR客户端类定义 (signalr_client.h):

class SignalRClient {
public:
    static SignalRClient& GetInstance();
    
    // 连接管理
    bool Initialize(const std::string& hub_url, const std::string& token);
    bool Connect();
    void Disconnect();
    void Reset();
    void RequestReconnect();
    
    // 状态查询
    bool IsInitialized() const;
    bool IsConnecting() const;
    bool IsConnected() const;
    std::string GetConnectionState() const;
    
    // 回调设置
    void OnCustomMessage(std::function<void(const cJSON*)> callback);
    void OnDeviceRegistered(std::function<void(const cJSON*)> callback);
    
    // Hub方法调用
    void RegisterDevice(const std::string& mac_address,
                       const std::string& device_token,
                       const std::string& metadata,
                       std::function<void(bool, const std::string&)> callback);
    void SendHeartbeat(std::function<void(bool, const std::string&)> callback);
    void InvokeHubMethod(const std::string& method_name,
                        const std::string& args_json,
                        std::function<void(bool, const std::string&)> callback);

private:
    SignalRClient();
    ~SignalRClient();
    
    std::unique_ptr<signalr::hub_connection> connection_;
    std::string hub_url_;
    std::string token_;
    bool initialized_ = false;
    bool connection_confirmed_ = false;
    std::atomic<bool> reconnect_requested_{false};
    
    // 回调函数
    std::function<void(const cJSON*)> on_custom_message_;
    std::function<void(const cJSON*)> on_device_registered_;
    
    // 重连任务
    TaskHandle_t reconnect_task_handle_ = nullptr;
    int reconnect_backoff_ms_ = 1000;
};

使用示例

// 在主应用中使用SignalR客户端
void Application::InitializeSignalR() {
    auto& client = SignalRClient::GetInstance();
    
    // 设置消息回调
    client.OnCustomMessage([this](const cJSON* json) {
        ESP_LOGI(TAG, "Received message from server");
        HandleServerMessage(json);
    });
    
    // 初始化并连接
    std::string hub_url = "wss://your-server.com/devicehub";
    std::string token = GetJwtToken();  // 从NVS读取或扫码获取
    
    if (client.Initialize(hub_url, token)) {
        if (client.Connect()) {
            client.RequestReconnect();  // 启动自动重连任务
        }
    }
}
Application层集成代码

核心代码片段 (application.cc):


void Application::HandleSignalRMessage(const std::string& message) {
    ESP_LOGI(TAG, "Handling SignalR message: %s", message.c_str());
    
    auto root = cJSON_Parse(message.c_str());
    if (!root) {
        ESP_LOGE(TAG, "Failed to parse SignalR message JSON");
        return;
    }
    
    auto display = Board::GetInstance().GetDisplay();
    
    // Check message action/type
    auto action = cJSON_GetObjectItem(root, "action");
    if (cJSON_IsString(action)) {
        if (strcmp(action->valuestring, "notification") == 0) {
            // Handle notification
            // JSON: {"action":"notification", "title":"标题", "content":"内容", "emotion":"bell", "sound":"popup"}
            auto title = cJSON_GetObjectItem(root, "title");
            auto content = cJSON_GetObjectItem(root, "content");
            auto emotion = cJSON_GetObjectItem(root, "emotion");
            auto sound = cJSON_GetObjectItem(root, "sound");
            
            const char* title_str = cJSON_IsString(title) ? title->valuestring : Lang::Strings::INFO;
            const char* content_str = cJSON_IsString(content) ? content->valuestring : "";
            const char* emotion_str = cJSON_IsString(emotion) ? emotion->valuestring : "bell";
            
            // Select sound based on "sound" field
            std::string_view sound_view = Lang::Sounds::OGG_POPUP;
            if (cJSON_IsString(sound)) {
                if (strcmp(sound->valuestring, "success") == 0) {
                    sound_view = Lang::Sounds::OGG_SUCCESS;
                } else if (strcmp(sound->valuestring, "vibration") == 0) {
                    sound_view = Lang::Sounds::OGG_VIBRATION;
                } else if (strcmp(sound->valuestring, "exclamation") == 0) {
                    sound_view = Lang::Sounds::OGG_EXCLAMATION;
                } else if (strcmp(sound->valuestring, "low_battery") == 0) {
                    sound_view = Lang::Sounds::OGG_LOW_BATTERY;
                } else if (strcmp(sound->valuestring, "none") == 0) {
                    sound_view = "";
                }
                // default: popup
            }
            
            Alert(title_str, content_str, emotion_str, sound_view);
            
        } else if (strcmp(action->valuestring, "command") == 0) {
            // Handle command
            // JSON: {"action":"command", "command":"reboot|wake|listen|stop"}
            auto cmd = cJSON_GetObjectItem(root, "command");
            if (cJSON_IsString(cmd)) {
                if (strcmp(cmd->valuestring, "reboot") == 0) {
                    Reboot();
                } else if (strcmp(cmd->valuestring, "wake") == 0) {
                    // Trigger wake word detection
                    xEventGroupSetBits(event_group_, MAIN_EVENT_WAKE_WORD_DETECTED);
                } else if (strcmp(cmd->valuestring, "listen") == 0) {
                    StartListening();
                } else if (strcmp(cmd->valuestring, "stop") == 0) {
                    StopListening();
                } else {
                    ESP_LOGW(TAG, "Unknown SignalR command: %s", cmd->valuestring);
                }
            }
            
        } else if (strcmp(action->valuestring, "display") == 0) {
            // Display custom content
            // JSON: {"action":"display", "content":"文本内容", "role":"system"}
            auto content = cJSON_GetObjectItem(root, "content");
            auto role = cJSON_GetObjectItem(root, "role");
            const char* role_str = cJSON_IsString(role) ? role->valuestring : "system";
            if (cJSON_IsString(content)) {
                display->SetChatMessage(role_str, content->valuestring);
            }
            
        } else if (strcmp(action->valuestring, "emotion") == 0) {
            // Change emotion/expression
            // JSON: {"action":"emotion", "emotion":"happy"}
            auto emotion = cJSON_GetObjectItem(root, "emotion");
            if (cJSON_IsString(emotion)) {
                display->SetEmotion(emotion->valuestring);
            }
            
        } else if (strcmp(action->valuestring, "image") == 0) {
            // Display image from URL
            // JSON: {"action":"image", "url":"https://example.com/image.jpg"}
            auto url = cJSON_GetObjectItem(root, "url");
            if (cJSON_IsString(url)) {
                HandleSignalRImageMessage(url->valuestring);
            } else {
                ESP_LOGW(TAG, "Image action requires 'url' field");
            }
            
        } else if (strcmp(action->valuestring, "audio") == 0) {
            // Play audio from URL (OGG format)
            // JSON: {"action":"audio", "url":"https://example.com/sound.ogg"}
            auto url = cJSON_GetObjectItem(root, "url");
            if (cJSON_IsString(url)) {
                HandleSignalRAudioMessage(url->valuestring);
            } else {
                ESP_LOGW(TAG, "Audio action requires 'url' field");
            }
            
        } else if (strcmp(action->valuestring, "qrcode") == 0) {
            // Show QR code
            // JSON: {"action":"qrcode", "data":"https://...", "title":"标题", "subtitle":"副标题"}
            auto data = cJSON_GetObjectItem(root, "data");
            auto title = cJSON_GetObjectItem(root, "title");
            auto subtitle = cJSON_GetObjectItem(root, "subtitle");
            if (cJSON_IsString(data)) {
                const char* title_str = cJSON_IsString(title) ? title->valuestring : nullptr;
                const char* subtitle_str = cJSON_IsString(subtitle) ? subtitle->valuestring : nullptr;
                display->ShowQRCode(data->valuestring, title_str, subtitle_str);
            } else {
                ESP_LOGW(TAG, "QRCode action requires 'data' field");
            }
            
        } else if (strcmp(action->valuestring, "hide_qrcode") == 0) {
            // Hide QR code
            // JSON: {"action":"hide_qrcode"}
            display->HideQRCode();
            
        } else {
            // Default: display as system message
            char* display_str = cJSON_Print(root);
            if (display_str) {
                display->SetChatMessage("system", display_str);
                cJSON_free(display_str);
            }
        }
    } else {
        // No action specified, display raw message
        char* display_str = cJSON_Print(root);
        if (display_str) {
            display->SetChatMessage("system", display_str);
            cJSON_free(display_str);
        }
    }
    
    cJSON_Delete(root);
}

完整的Application类功能

  • ✅ 设备状态管理 (状态机)
  • ✅ 网络事件处理 (连接/断开)
  • ✅ 音频服务集成 (编解码、流处理)
  • ✅ 唤醒词检测
  • ✅ 协议层抽象 (WebSocket/MQTT)
  • ✅ MCP消息路由
  • ✅ 错误处理和恢复
  • ✅ 资源管理和清理
  • ✅ 线程安全的消息调度
5.2.2 MCP服务器代码 (verdure-mcp)

仓库地址verdure-mcp

目录结构

src/Verdure.Mcp.Server/
├── Hubs/
│   └── DeviceHub.cs          # SignalR Hub实现
├── Tools/
│   ├── MusicTool.cs          # 音乐播放控制
│   ├── EmailTool.cs          # 邮件发送
│   ├── WeatherTool.cs        # 天气查询
│   └── SmartHomeTool.cs      # 智能家居控制
├── Services/
│   ├── DeviceService.cs      # 设备管理服务
│   ├── McpExecutor.cs        # MCP工具执行器
│   └── TokenService.cs       # JWT令牌服务
└── Models/
    ├── DeviceConnection.cs   # 设备连接记录
    ├── DeviceInfo.cs         # 设备信息
    └── McpToolLog.cs         # 工具调用日志

DeviceHub完整实现 (Hubs/DeviceHub.cs):

生产环境的DeviceHub实际实现特点:

  • ✅ 数据库持久化 (Entity Framework Core + PostgreSQL)
  • ✅ 设备注册和状态跟踪
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐