1. 项目背景与核心价值
在物联网设备开发中,实现设备与云端服务的实时双向通信一直是个技术难点。传统方案通常采用轮询或MQTT协议,但这些方式要么实时性不足,要么开发复杂度高。本项目通过将微软SignalR框架移植到ESP32设备,实现了与.NET服务的无缝实时通信,同时整合了MCP(Model Context Protocol)服务能力。
SignalR作为微软推出的实时通信框架,原生支持WebSocket、Server-Sent Events和长轮询等多种传输方式,特别适合需要低延迟双向通信的场景。但在资源受限的嵌入式设备上运行SignalR客户端,面临着内存占用、线程模型和稳定性等多重挑战。
这个方案的核心创新点在于:
- 在ESP32上实现了完整的SignalR C++客户端
- 设计了基于JSON-RPC 2.0的MCP协议规范
- 开发了设备端MCP Server实现工具调用
- 实现了JWT认证和自动重连机制
2. 技术架构解析
2.1 整体架构设计
整个系统采用客户端-服务端架构,设备端运行在ESP32上,服务端基于.NET 10构建。架构图如下:
code复制┌──────────────────────┐ ┌──────────────────────┐
│ .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) │ │ - 显示通知 │ │
│ └────────────────┘ │ │ └────────────────┘ │
└──────────────────────┘ └──────────────────────┘
2.2 关键组件说明
SignalR Hub
服务端的核心组件,负责:
- 管理设备连接
- 处理设备消息
- 向设备推送指令
- 维护设备状态
MCP协议
基于JSON-RPC 2.0的自定义协议,定义了设备能力的调用规范,包括:
initialize:初始化连接tools/list:列出可用工具tools/call:调用特定工具
ESP32客户端
设备端实现包含:
- SignalR客户端连接管理
- MCP Server实现
- 消息处理分发
- 资源管理
3. 开发环境搭建
3.1 ESP32开发环境配置
推荐使用VS Code配合ESP-IDF插件进行开发:
-
安装VS Code和必要插件
bash复制# 下载安装Visual Studio Code https://code.visualstudio.com/ # 安装Espressif IDF插件 code --install-extension espressif.esp-idf-extension -
配置ESP-IDF工具链
- 按F1打开命令面板
- 输入"ESP-IDF: Configure ESP-IDF Extension"
- 选择"Express"安装方式
- 选择ESP-IDF版本(推荐v5.1+)
-
创建项目结构
code复制esp-signalr-example/ ├── main/ │ ├── main.cpp │ └── CMakeLists.txt ├── components/ │ └── signalr_client/ ├── CMakeLists.txt └── sdkconfig
3.2 .NET服务端环境
服务端需要.NET 10运行环境:
bash复制# 安装.NET 10 SDK
winget install Microsoft.DotNet.SDK.10
# 验证安装
dotnet --version
# 应输出: 10.0.x
4. 核心代码实现
4.1 SignalR服务端实现
Hub基础实现
csharp复制public class DeviceHub : Hub
{
private readonly ILogger<DeviceHub> _logger;
private static readonly ConcurrentDictionary<string, DeviceInfo> _devices = new();
public DeviceHub(ILogger<DeviceHub> logger)
{
_logger = logger;
}
public override async Task OnConnectedAsync()
{
var httpContext = Context.GetHttpContext();
var token = httpContext?.Request.Query["access_token"];
// JWT验证逻辑
var userId = ValidateToken(token);
await Groups.AddToGroupAsync(Context.ConnectionId, $"Users:{userId}");
_devices[Context.ConnectionId] = new DeviceInfo(userId);
await Clients.Caller.SendAsync("Notification", "连接成功");
}
public async Task SendDeviceCommand(string deviceId, string command)
{
await Clients.Group($"Users:{deviceId}")
.SendAsync("ExecuteCommand", command);
}
}
服务配置
csharp复制var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR(options => {
options.EnableDetailedErrors = true;
options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
});
var app = builder.Build();
app.MapHub<DeviceHub>("/deviceHub");
app.Run();
4.2 ESP32客户端实现
连接初始化
cpp复制void SignalRClient::Initialize(const std::string& hub_url, const std::string& token)
{
std::string auth_url = hub_url + "?access_token=" + token;
auto builder = signalr::hub_connection_builder::create(auth_url)
.with_websocket_factory([](auto& config) {
return std::make_shared<signalr::esp32_websocket_client>(config);
})
.skip_negotiation(true);
connection_ = std::make_unique<signalr::hub_connection>(builder.build());
}
消息处理
cpp复制void SetupHandlers()
{
connection_->on("ShowImage", [](const std::vector<signalr::value>& args) {
if(args.size() > 0) {
auto image_data = args[0].as_string();
Display::ShowImage(image_data);
}
});
connection_->on("PlayAudio", [](auto& args) {
if(!args.empty()) {
Audio::Play(args[0].as_string());
}
});
}
5. 关键技术难点与解决方案
5.1 内存管理优化
ESP32仅有520KB SRAM,需特别注意:
-
使用PSRAM扩展内存
cpp复制// 在sdkconfig中启用 CONFIG_ESP32_SPIRAM_SUPPORT=y // 代码中分配PSRAM void* psram_buffer = heap_caps_malloc(size, MALLOC_CAP_SPIRAM); -
内存池管理
cpp复制class MemoryPool { public: void* Allocate(size_t size) { if(free_blocks_.empty()) { auto block = heap_caps_malloc(size, MALLOC_CAP_SPIRAM); used_blocks_.insert(block); return block; } // 重用空闲块 } private: std::set<void*> used_blocks_; std::queue<void*> free_blocks_; };
5.2 自动重连机制
cpp复制void SignalRClient::StartConnection()
{
connection_->start([this](std::exception_ptr ex) {
if(ex) {
// 指数退避重连
int delay = std::min(30000, 1000 * (1 << retry_count_));
vTaskDelay(pdMS_TO_TICKS(delay));
StartConnection();
retry_count_++;
} else {
retry_count_ = 0;
}
});
}
6. 实际应用场景
6.1 设备控制流程
- 用户扫码获取JWT Token
- 设备使用Token连接SignalR Hub
- 服务端发送控制指令
- 设备执行并返回状态
6.2 消息推送示例
csharp复制// 服务端推送图片
await _hubContext.Clients.Group($"Users:{userId}")
.SendAsync("ShowImage", imageData);
// 设备端处理
connection_->on("ShowImage", [](auto& args) {
auto& display = Display::Instance();
display.Show(args[0].as_string());
});
7. 性能优化建议
-
消息压缩:对大型消息使用zlib压缩
cpp复制std::string Compress(const std::string& data) { z_stream zs = {0}; deflateInit(&zs, Z_BEST_COMPRESSION); // 压缩逻辑 } -
批量传输:音频等大数据分块发送
csharp复制// 服务端分块 foreach(var chunk in audioData.Chunk(1024)) { await client.SendAsync("AudioChunk", chunk); } -
连接复用:多个Hub共享同一物理连接
8. 常见问题排查
8.1 连接不稳定
- 检查WiFi信号强度
- 验证Token有效期
- 调整心跳间隔
csharp复制services.AddSignalR(options => { options.KeepAliveInterval = TimeSpan.FromSeconds(15); });
8.2 内存不足
- 监控内存使用
cpp复制ESP_LOGI("MEM", "Free heap: %d", esp_get_free_heap_size()); - 启用内存统计
bash复制
idf.py size-components
9. 项目演进方向
- 协议扩展:支持MQTT等更多传输协议
- 安全增强:实现DTLS加密传输
- 边缘计算:部分逻辑下沉到设备端
这个方案已经在小智AI设备上稳定运行,日均处理消息超过50万条。移植SignalR到ESP32的过程虽然充满挑战,但最终实现的实时双向通信能力为物联网设备开发提供了新的可能性。