├── .gitignore ├── .idea ├── .gitignore ├── iotgateway.iml ├── modules.xml └── vcs.xml ├── README.md ├── conf └── config.go ├── consts └── consts.go ├── device.go ├── docs ├── README.md ├── developer-guide.md ├── protocol-development.md └── quick-reference.md ├── events ├── const.go └── pushEvents.go ├── gateway.go ├── go.mod ├── go.sum ├── lib ├── strings.go └── strings_test.go ├── log └── log.go ├── model ├── device.go └── gateway.go ├── mqttClient ├── client.go ├── data.go ├── reconnect.go └── setconfig.go ├── mqttProtocol ├── eventModel.go ├── gatewayModel.go ├── propertyModel.go ├── serviceCallModel.go └── tools.go ├── network ├── option.go ├── packet.go ├── protocol.go ├── server.go ├── tcp.go └── udp.go ├── service.go ├── set.go ├── vars ├── clientListMap.go ├── deviceListAllMap.go ├── deviceMessageMap.go ├── deviceMessageMap_test.go └── vars.go └── version └── version.go /.gitignore: -------------------------------------------------------------------------------- 1 | .buildpath 2 | .hgignore.swp 3 | .project 4 | .orig 5 | .swp 6 | .idea/ 7 | .settings/ 8 | .vscode/ 9 | *.iml 10 | vendor/ 11 | **/.DS_Store 12 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # 默认忽略的文件 2 | /shelf/ 3 | /workspace.xml 4 | # 基于编辑器的 HTTP 客户端请求 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/iotgateway.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SagooIOT 网关基础服务库代码 2 | 3 | 开发SagooIOT专属网关时,可以引用此项目,以便快速开发。创建一个空的工程,按下面的步骤完成自己专属网关的开发。 4 | 5 | 详细使用可以参考示例工程:[iotgateway-example](https://github.com/sagoo-cloud/iotgateway-example) 6 | 7 | 8 | ## SagooIOT的网关开发说明 9 | 10 | ```go 11 | go get -u github.com/sagoo-cloud/iotgateway 12 | ``` 13 | 14 | ## 实现入口程序 15 | 16 | 参考如下: 17 | 18 | ```go 19 | 20 | package main 21 | 22 | import ( 23 | "github.com/gogf/gf/v2/frame/g" 24 | "github.com/gogf/gf/v2/os/gctx" 25 | "github.com/gogf/gf/v2/os/glog" 26 | "github.com/sagoo-cloud/iotgateway" 27 | "github.com/sagoo-cloud/iotgateway/version" 28 | ) 29 | 30 | // 定义编译时的版本信息 31 | var ( 32 | BuildVersion = "0.0" 33 | BuildTime = "" 34 | CommitID = "" 35 | ) 36 | 37 | func main() { 38 | //初始化日志 39 | glog.SetDefaultLogger(g.Log()) 40 | //显示版本信息 41 | version.ShowLogo(BuildVersion, BuildTime, CommitID) 42 | ctx := gctx.GetInitCtx() 43 | 44 | //需要解析的协议,可以根据需要添加,如果不需要实现自定义解析协议,可以不添加,可以为nil 45 | chargeProtocol := protocol.ChargeProtocol{} 46 | 47 | //创建网关 48 | gateway, err := iotgateway.NewGateway(ctx, chargeProtocol) 49 | if err != nil { 50 | panic(err) 51 | } 52 | //初始化事件 53 | events.Init() 54 | 55 | // 初始化个性网关需要实现的其它服务 56 | 57 | //启动网关 58 | gateway.Start() 59 | 60 | } 61 | 62 | 63 | ``` 64 | 65 | 66 | ## 实现protocol接口 67 | 68 | 实现protocol接口处理接收到的数据。在Decode方法中,需要将接收到的数据进行解析,然后返回解析后的数据。在Encode方法中,需要将需要发送的数据进行编码,然后返回编码后的数据。 69 | 70 | ```go 71 | type ChargeProtocol struct { 72 | } 73 | 74 | func (c *ChargeProtocol) Encode(args []byte) (res []byte, err error) { 75 | return args, nil 76 | } 77 | 78 | func (c *ChargeProtocol) Decode(conn net.Conn, buffer []byte) (res []byte, err error) { 79 | return buffer, nil 80 | } 81 | 82 | ``` 83 | 84 | 85 | ## 向SagooIOT服务推送数据 86 | 87 | 在需要推送数据的地方准备好事件相关数据,然后触发推送事件。 88 | 89 | **事件数据上报:** 90 | 91 | 触发的是 `consts.PushAttributeDataToMQTT` 事件 92 | 93 | 94 | ```go 95 | 96 | //准备事件返回数据 97 | var eventData = make(map[string]interface{}) 98 | eventData["XXX字段1"] = "XXX值1" 99 | eventData["XXX字段2"] = "XXX值2" 100 | 101 | var eventDataList = make(map[string]interface{}) 102 | eventDataList["XXX事件标识字串"] = eventData 103 | 104 | out := g.Map{ 105 | "DeviceKey": deviceKey, 106 | "EventDataList": eventDataList, 107 | } 108 | //触发属性上报事件 109 | event.MustFire(consts.PushAttributeDataToMQTT, out) 110 | 111 | ``` 112 | **属性数据上报** 113 | 114 | 触发的是 `consts.PushAttributeDataToMQTT` 事件 115 | 116 | ```go 117 | //准备上报的数据 118 | var propertieData = make(map[string]interface{}) 119 | propertieData["XXX字段1"] = "XXX值1" 120 | propertieData["XXX字段2"] = "XXX值2" 121 | 122 | out := g.Map{ 123 | "DeviceKey": deviceKey, 124 | "PropertieDataList": propertieData, 125 | } 126 | //触发属性上报事件 127 | event.MustFire(consts.PushAttributeDataToMQTT, out) 128 | 129 | ``` 130 | 131 | ## 从SagooIOT平台下发调用回复 132 | 133 | 在SagooIoT系统中向设备端下发有两种情况,1. 服务下发,2. 属性设置下发。 134 | 135 | ### 服务下发 136 | 137 | 如果需要完成SagooIoT端向设备进行服务调用,需要在网关程序中完成订阅服务下发事件。 138 | 触发的是 `consts.PushServiceResDataToMQTT` 事件。 139 | 140 | 一 、在获取到设备key的地方订阅服务下发事件。 141 | 142 | ```go 143 | //订阅网关设备服务下发事件 144 | iotgateway.ServerGateway.SubscribeServiceEvent(传入获取的设备key) 145 | 146 | ``` 147 | 二、在对设备进行处理后,需要回复SagooIOT平台。 148 | 149 | 由SagooIOT平台端下发后回复: 150 | 触发的是 `consts.PushServiceResDataToMQTT` 事件 151 | 152 | ```go 153 | //准备回复数据 154 | var replyData = make(map[string]interface{}) 155 | replyData["XXX字段1"] = "XXX值1" 156 | replyData["XXX字段2"] = "XXX值1" 157 | outData := g.Map{ 158 | "DeviceKey": deviceKey, 159 | "ReplyData": replyData, 160 | } 161 | //出发回复的事件 162 | event.MustFire(consts.PushServiceResDataToMQTT, outData) 163 | ``` 164 | 165 | ### 属性设置下发 166 | 167 | 如果需要完成SagooIoT端向设备进行服务调用,需要在网关程序中完成订阅服务下发事件。 168 | 触发的是 `consts.PropertySetEvent` 事件。 169 | 170 | 一 、在获取到设备key的地方订阅服务下发事件。 171 | 172 | ```go 173 | //订阅网关设备服务下发事件 174 | iotgateway.ServerGateway.SubscribeSetEvent(传入获取的设备key) 175 | 176 | ``` 177 | 二、在对设备进行处理后,需要回复SagooIOT平台。 178 | 179 | 由SagooIOT平台端下发后回复: 180 | 触发的是 `consts.PushSetResDataToMQTT` 事件 181 | 182 | ```go 183 | //准备回复数据 184 | var replyData = make(map[string]interface{}) 185 | replyData["XXX字段1"] = "XXX值1" 186 | replyData["XXX字段2"] = "XXX值1" 187 | outData := g.Map{ 188 | "DeviceKey": deviceKey, 189 | "ReplyData": replyData, 190 | } 191 | //出发回复的事件 192 | event.MustFire(consts.PushSetResDataToMQTT, outData) 193 | ``` 194 | 195 | ### SagooIoT平台接收到回复的数据处理 196 | 197 | 在SagooIoT平台,对服务下发后,会收到回复数据。需要在对应的功能定义设置输入参数。参数标识与数据类型要与回服务回复的数据保持一致。 198 | 199 | 200 | ## 默认服务下发功能 201 | 202 | 网关中已经有一些默认的服务下发功能。 203 | 204 | ### 获取网关版本信息 205 | 206 | 功能标识:`getGatewayVersion` 207 | 功能描述:获取网关版本信息 208 | 功能输入参数:无 209 | 功能输出参数: 210 | 211 | | 参数标识 | 参数名称 | 类型 | 212 | | --------- | -------- | ------ | 213 | | Version | 版本 | string | 214 | | BuildTime | 编译时间 | string | 215 | | CommitID | 提交ID | string | -------------------------------------------------------------------------------- /conf/config.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type GatewayConfig struct { 8 | GatewayServerConfig GatewayServerConfig `json:"server"` 9 | MqttConfig MqttConfig `json:"mqtt"` 10 | } 11 | 12 | type GatewayServerConfig struct { 13 | Name string `json:"name"` // 网关服务名称,启动时显示 14 | Addr string `json:"addr"` // 网关服务地址 15 | NetType string `json:"netType"` // 网关服务类型 16 | SerUpTopic string `json:"serUpTopic"` // 服务上行Topic 17 | SerDownTopic string `json:"serDownTopic"` // 服务下行Topic 18 | Duration time.Duration `json:"duration"` // 网关服务心跳时长 19 | ProductKey string `json:"productKey"` // 网关产品标识 20 | DeviceKey string `json:"deviceKey"` // 网关实例标识 21 | DeviceName string `json:"deviceName"` // 网关系统名称 22 | Description string `json:"description"` // 网关系统描述 23 | DeviceType string `json:"deviceType"` // 网关系统类型 24 | Manufacturer string `json:"manufacturer"` // 网关系统厂商 25 | PacketConfig PacketConfig `json:"packetConfig"` 26 | } 27 | 28 | // PacketHandlingType 定义了处理粘包的方法类型 29 | type PacketHandlingType int 30 | 31 | // PacketConfig 定义了处理粘包的配置 32 | type PacketConfig struct { 33 | Type PacketHandlingType 34 | FixedLength int // 用于 FixedLength 类型 35 | HeaderLength int // 用于 HeaderBodySeparate 类型 36 | Delimiter string // 用于 Delimiter 类型 37 | } 38 | 39 | type MqttConfig struct { 40 | Address string `json:"address"` // mqtt服务地址 41 | Username string `json:"username"` // mqtt服务用户名 42 | Password string `json:"password"` // mqtt服务密码 43 | ClientId string `json:"clientId"` // mqtt客户端标识 44 | ClientCertificateKey string `json:"clientCertificateKey"` // mqtt客户端证书密钥 45 | ClientCertificateCert string `json:"clientCertificateCert"` // mqtt客户端证书 46 | KeepAliveDuration time.Duration `json:"keepAliveDuration"` // mqtt客户端保持连接时长 47 | Duration time.Duration `json:"duration"` // mqtt客户端心跳时长 48 | } 49 | -------------------------------------------------------------------------------- /consts/consts.go: -------------------------------------------------------------------------------- 1 | package consts 2 | 3 | const ( 4 | PushAttributeDataToMQTT = "PushAttributeDataToMQTT" //属性上报 5 | PushServiceResDataToMQTT = "PushServiceResDataToMQTT" //服务调用结果上报 6 | PushSetResDataToMQTT = "PushSetResDataToMQTT" //属性设置结果上报 7 | 8 | NetTypeTcpServer = "tcp" 9 | NetTypeUDPServer = "udp" 10 | NetTypeMqttServer = "mqtt" 11 | ) 12 | -------------------------------------------------------------------------------- /device.go: -------------------------------------------------------------------------------- 1 | package iotgateway 2 | 3 | import ( 4 | "github.com/sagoo-cloud/iotgateway/model" 5 | "github.com/sagoo-cloud/iotgateway/vars" 6 | ) 7 | 8 | // GetDevice 获取设备 9 | func GetDevice(deviceKey string) *model.Device { 10 | if deviceKey != "" { 11 | device, err := vars.GetDevice(deviceKey) 12 | if err != nil { 13 | return nil 14 | } 15 | return device 16 | } 17 | return nil 18 | } 19 | 20 | // SaveDevice 保存设置 21 | func SaveDevice(deviceKey string, device *model.Device) { 22 | vars.UpdateDeviceMap(deviceKey, device) 23 | } 24 | 25 | // GetDeviceCount 获取设备统计 26 | func GetDeviceCount() int { 27 | return vars.CountDevices() 28 | } 29 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # SagooIOT 网关 SDK 文档中心 2 | 3 | 欢迎使用 SagooIOT 网关 SDK!本文档中心提供了完整的开发指南和参考资料,帮助您快速上手并开发出高质量的IoT网关应用。 4 | 5 | ## 📚 文档导航 6 | 7 | ### 🚀 快速开始 8 | - **[开发手册](./developer-guide.md)** - 完整的开发指南,从入门到精通 9 | - **[快速参考](./quick-reference.md)** - 常用API和代码片段速查 10 | - **[协议开发指南](./protocol-development.md)** - 详细的协议处理器开发教程 11 | 12 | ### 📖 核心概念 13 | 14 | #### 网关架构 15 | - 整体架构设计 16 | - 核心组件介绍 17 | - 数据流向说明 18 | 19 | #### 协议处理 20 | - 协议接口定义 21 | - 数据解析与编码 22 | - 错误处理机制 23 | 24 | #### 事件系统 25 | - 事件驱动架构 26 | - 数据上报机制 27 | - 服务调用处理 28 | 29 | #### MQTT集成 30 | - 连接管理 31 | - Topic规范 32 | - 数据格式 33 | 34 | ### 🛠️ 开发指南 35 | 36 | #### 环境搭建 37 | ```bash 38 | # 安装SDK 39 | go get -u github.com/sagoo-cloud/iotgateway 40 | 41 | # 创建项目 42 | mkdir my-gateway && cd my-gateway 43 | go mod init my-gateway 44 | ``` 45 | 46 | #### 最小示例 47 | ```go 48 | package main 49 | 50 | import ( 51 | "github.com/gogf/gf/v2/os/gctx" 52 | "github.com/sagoo-cloud/iotgateway" 53 | ) 54 | 55 | func main() { 56 | ctx := gctx.GetInitCtx() 57 | gateway, _ := iotgateway.NewGateway(ctx, nil) 58 | gateway.Start() 59 | } 60 | ``` 61 | 62 | ### 📋 功能特性 63 | 64 | | 特性 | 描述 | 文档链接 | 65 | |------|------|----------| 66 | | 🌐 多协议支持 | TCP、UDP、MQTT等多种网络协议 | [网络层开发](./developer-guide.md#网络层开发) | 67 | | 🔧 可扩展架构 | 插件化的协议处理器设计 | [协议开发](./protocol-development.md) | 68 | | 📡 MQTT集成 | 与SagooIOT平台无缝集成 | [MQTT集成](./developer-guide.md#mqtt集成) | 69 | | 🎯 事件驱动 | 基于事件的异步处理机制 | [事件系统](./developer-guide.md#事件系统) | 70 | | 🛡️ 高可靠性 | 内置连接管理、错误恢复、内存优化 | [最佳实践](./developer-guide.md#最佳实践) | 71 | | 📊 监控友好 | 完善的日志和统计信息 | [故障排查](./developer-guide.md#故障排查) | 72 | 73 | ### 🎯 使用场景 74 | 75 | #### 工业物联网 76 | - 工厂设备数据采集 77 | - 生产线监控 78 | - 设备状态管理 79 | 80 | #### 智能家居 81 | - 家电设备接入 82 | - 环境监测 83 | - 安防系统 84 | 85 | #### 智慧城市 86 | - 路灯控制 87 | - 环境监测 88 | - 交通管理 89 | 90 | ### 📝 开发流程 91 | 92 | ```mermaid 93 | graph TD 94 | A[创建项目] --> B[实现协议处理器] 95 | B --> C[配置网关参数] 96 | C --> D[注册事件处理器] 97 | D --> E[启动网关服务] 98 | E --> F[设备接入测试] 99 | F --> G[数据上报验证] 100 | G --> H[服务调用测试] 101 | H --> I[部署上线] 102 | ``` 103 | 104 | ### 🔧 配置示例 105 | 106 | #### 基础配置 107 | ```yaml 108 | server: 109 | name: "IoT网关" 110 | addr: ":8080" 111 | netType: "tcp" 112 | productKey: "your_product_key" 113 | deviceKey: "your_device_key" 114 | 115 | mqtt: 116 | address: "tcp://localhost:1883" 117 | username: "username" 118 | password: "password" 119 | clientId: "gateway_client" 120 | ``` 121 | 122 | #### 高级配置 123 | ```yaml 124 | server: 125 | packetConfig: 126 | type: 3 # 分隔符处理 127 | delimiter: "\r\n" 128 | duration: 60s 129 | 130 | mqtt: 131 | keepAliveDuration: 30s 132 | clientCertificateKey: "path/to/key.pem" 133 | clientCertificateCert: "path/to/cert.pem" 134 | ``` 135 | 136 | ### 📊 性能指标 137 | 138 | | 指标 | 数值 | 说明 | 139 | |------|------|------| 140 | | 并发连接数 | 10,000+ | 单实例支持的最大设备连接数 | 141 | | 消息吞吐量 | 50,000 msg/s | 每秒处理的消息数量 | 142 | | 内存占用 | < 100MB | 基础运行时内存占用 | 143 | | 响应延迟 | < 10ms | 平均消息处理延迟 | 144 | 145 | ### 🚨 常见问题 146 | 147 | #### Q: 设备连接不上网关? 148 | **A:** 检查网络配置、防火墙设置和协议实现。详见 [故障排查](./developer-guide.md#故障排查) 149 | 150 | #### Q: 数据解析失败? 151 | **A:** 验证数据格式、协议实现和日志输出。参考 [协议开发](./protocol-development.md) 152 | 153 | #### Q: MQTT连接异常? 154 | **A:** 检查服务器地址、认证信息和证书配置。查看 [MQTT集成](./developer-guide.md#mqtt集成) 155 | 156 | #### Q: 内存使用过高? 157 | **A:** 启用缓存清理、检查设备离线处理。参考 [最佳实践](./developer-guide.md#最佳实践) 158 | 159 | ### 📞 技术支持 160 | 161 | - **QQ群**: 686637608 162 | - **微信公众号**: sagoocn 163 | - **在线文档**: [iotdoc.sagoo.cn](https://iotdoc.sagoo.cn) 164 | 165 | ### 📄 许可证 166 | 167 | 本项目采用 MIT 许可证,详情请查看 [LICENSE](../LICENSE) 文件。 168 | 169 | --- 170 | 171 | ## 🎉 开始您的IoT网关开发之旅 172 | 173 | 选择适合您的文档开始学习: 174 | 175 | 1. **新手入门** → [开发手册](./developer-guide.md) 176 | 2. **快速查阅** → [快速参考](./quick-reference.md) 177 | 3. **协议开发** → [协议开发指南](./protocol-development.md) 178 | 179 | 祝您开发愉快!🚀 -------------------------------------------------------------------------------- /docs/developer-guide.md: -------------------------------------------------------------------------------- 1 | # SagooIOT 网关 SDK 开发手册 2 | 3 | ## 目录 4 | 5 | 1. [概述](#概述) 6 | 2. [快速开始](#快速开始) 7 | 3. [架构设计](#架构设计) 8 | 4. [核心组件](#核心组件) 9 | 5. [协议开发](#协议开发) 10 | 6. [配置管理](#配置管理) 11 | 7. [事件系统](#事件系统) 12 | 8. [网络层开发](#网络层开发) 13 | 9. [MQTT集成](#mqtt集成) 14 | 10. [最佳实践](#最佳实践) 15 | 11. [故障排查](#故障排查) 16 | 12. [API参考](#api参考) 17 | 18 | --- 19 | 20 | ## 概述 21 | 22 | SagooIOT 网关 SDK 是一个用于快速开发物联网网关的 Go 语言框架。它提供了完整的网关基础设施,包括网络通信、协议解析、MQTT集成、事件处理等核心功能。 23 | 24 | ### 主要特性 25 | 26 | - 🚀 **多协议支持** - 支持TCP、UDP、MQTT等多种网络协议 27 | - 🔧 **可扩展架构** - 插件化的协议处理器设计 28 | - 📡 **MQTT集成** - 与SagooIOT平台无缝集成 29 | - 🎯 **事件驱动** - 基于事件的异步处理机制 30 | - 🛡️ **高可靠性** - 内置连接管理、错误恢复、内存优化 31 | - 📊 **监控友好** - 完善的日志和统计信息 32 | 33 | ### 系统要求 34 | 35 | - Go 1.23.0 或更高版本 36 | - 支持的操作系统:Linux、Windows、macOS 37 | - 内存:建议 512MB 以上 38 | - 网络:支持TCP/UDP/MQTT协议 39 | 40 | --- 41 | 42 | ## 快速开始 43 | 44 | ### 1. 安装SDK 45 | 46 | ```bash 47 | go get -u github.com/sagoo-cloud/iotgateway 48 | ``` 49 | 50 | ### 2. 创建基础项目 51 | 52 | 创建一个新的Go项目并初始化: 53 | 54 | ```bash 55 | mkdir my-gateway 56 | cd my-gateway 57 | go mod init my-gateway 58 | ``` 59 | 60 | ### 3. 编写主程序 61 | 62 | 创建 `main.go` 文件: 63 | 64 | ```go 65 | package main 66 | 67 | import ( 68 | "context" 69 | "github.com/gogf/gf/v2/frame/g" 70 | "github.com/gogf/gf/v2/os/gctx" 71 | "github.com/gogf/gf/v2/os/glog" 72 | "github.com/sagoo-cloud/iotgateway" 73 | "github.com/sagoo-cloud/iotgateway/version" 74 | ) 75 | 76 | // 编译时版本信息 77 | var ( 78 | BuildVersion = "1.0.0" 79 | BuildTime = "" 80 | CommitID = "" 81 | ) 82 | 83 | func main() { 84 | // 初始化日志 85 | glog.SetDefaultLogger(g.Log()) 86 | 87 | // 显示版本信息 88 | version.ShowLogo(BuildVersion, BuildTime, CommitID) 89 | 90 | ctx := gctx.GetInitCtx() 91 | 92 | // 创建协议处理器(可选,如果不需要自定义协议可以传nil) 93 | protocol := &MyProtocol{} 94 | 95 | // 创建网关实例 96 | gateway, err := iotgateway.NewGateway(ctx, protocol) 97 | if err != nil { 98 | panic(err) 99 | } 100 | 101 | // 初始化自定义事件处理 102 | initEvents() 103 | 104 | // 启动网关 105 | gateway.Start() 106 | } 107 | 108 | // 初始化事件处理 109 | func initEvents() { 110 | // 在这里注册自定义事件处理器 111 | } 112 | ``` 113 | 114 | ### 4. 实现协议处理器 115 | 116 | 创建 `protocol.go` 文件: 117 | 118 | ```go 119 | package main 120 | 121 | import ( 122 | "github.com/sagoo-cloud/iotgateway/model" 123 | ) 124 | 125 | type MyProtocol struct{} 126 | 127 | // Init 初始化协议处理器 128 | func (p *MyProtocol) Init(device *model.Device, data []byte) error { 129 | // 设备初始化逻辑 130 | if device != nil { 131 | device.DeviceKey = "device_001" // 设置设备标识 132 | } 133 | return nil 134 | } 135 | 136 | // Decode 解码设备上行数据 137 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) { 138 | // 解析设备数据 139 | // 这里实现你的协议解析逻辑 140 | 141 | // 示例:解析后触发数据上报 142 | // pushDeviceData(device.DeviceKey, parsedData) 143 | 144 | return nil, nil // 如果不需要回复数据,返回nil 145 | } 146 | 147 | // Encode 编码下行数据 148 | func (p *MyProtocol) Encode(device *model.Device, data interface{}, param ...string) ([]byte, error) { 149 | // 编码要发送给设备的数据 150 | // 这里实现你的数据编码逻辑 151 | 152 | return []byte("encoded data"), nil 153 | } 154 | ``` 155 | 156 | ### 5. 配置文件 157 | 158 | 创建 `config/config.yaml` 文件: 159 | 160 | ```yaml 161 | server: 162 | name: "我的IoT网关" 163 | addr: ":8080" 164 | netType: "tcp" 165 | duration: 60s 166 | productKey: "your_product_key" 167 | deviceKey: "your_device_key" 168 | deviceName: "Gateway Device" 169 | description: "IoT Gateway" 170 | deviceType: "gateway" 171 | manufacturer: "YourCompany" 172 | packetConfig: 173 | type: 3 # Delimiter类型 174 | delimiter: "\r\n" 175 | 176 | mqtt: 177 | address: "tcp://localhost:1883" 178 | username: "your_username" 179 | password: "your_password" 180 | clientId: "gateway_client" 181 | keepAliveDuration: 30s 182 | duration: 60s 183 | ``` 184 | 185 | ### 6. 运行网关 186 | 187 | ```bash 188 | go run . 189 | ``` 190 | 191 | --- 192 | 193 | ## 架构设计 194 | 195 | ### 整体架构 196 | 197 | ``` 198 | ┌─────────────────────────────────────────────────────────────┐ 199 | │ SagooIOT 网关 SDK │ 200 | ├─────────────────────────────────────────────────────────────┤ 201 | │ 应用层 (Application Layer) │ 202 | │ ┌─────────────────┐ ┌─────────────────┐ │ 203 | │ │ 事件处理器 │ │ 业务逻辑 │ │ 204 | │ └─────────────────┘ └─────────────────┘ │ 205 | ├─────────────────────────────────────────────────────────────┤ 206 | │ 网关核心层 (Gateway Core Layer) │ 207 | │ ┌─────────────────┐ ┌─────────────────┐ │ 208 | │ │ 协议处理器 │ │ 设备管理 │ │ 209 | │ └─────────────────┘ └─────────────────┘ │ 210 | ├─────────────────────────────────────────────────────────────┤ 211 | │ 通信层 (Communication Layer) │ 212 | │ ┌─────────────────┐ ┌─────────────────┐ │ 213 | │ │ 网络服务器 │ │ MQTT客户端 │ │ 214 | │ │ (TCP/UDP) │ │ │ │ 215 | │ └─────────────────┘ └─────────────────┘ │ 216 | ├─────────────────────────────────────────────────────────────┤ 217 | │ 基础设施层 (Infrastructure Layer) │ 218 | │ ┌─────────────────┐ ┌─────────────────┐ │ 219 | │ │ 配置管理 │ │ 日志系统 │ │ 220 | │ └─────────────────┘ └─────────────────┘ │ 221 | └─────────────────────────────────────────────────────────────┘ 222 | ``` 223 | 224 | ### 数据流向 225 | 226 | ``` 227 | 设备 ──TCP/UDP──> 网络服务器 ──> 协议处理器 ──> 事件系统 ──> MQTT ──> SagooIOT平台 228 | ↑ │ 229 | └──────────────── 协议处理器 <── 事件系统 <── MQTT <──────────────────────┘ 230 | ``` 231 | 232 | --- 233 | 234 | ## 核心组件 235 | 236 | ### 1. Gateway 网关核心 237 | 238 | `Gateway` 是整个SDK的核心组件,负责协调各个子系统的工作。 239 | 240 | ```go 241 | type Gateway struct { 242 | Address string // 网关地址 243 | Version string // 版本信息 244 | Status string // 运行状态 245 | MQTTClient mqtt.Client // MQTT客户端 246 | Server network.NetworkServer // 网络服务器 247 | Protocol network.ProtocolHandler // 协议处理器 248 | } 249 | ``` 250 | 251 | **主要方法:** 252 | 253 | - `NewGateway(ctx, protocol)` - 创建网关实例 254 | - `Start()` - 启动网关服务 255 | - `SubscribeServiceEvent(deviceKey)` - 订阅服务下发事件 256 | - `SubscribeSetEvent(deviceKey)` - 订阅属性设置事件 257 | 258 | ### 2. 设备模型 259 | 260 | ```go 261 | type Device struct { 262 | DeviceKey string // 设备唯一标识 263 | ClientID string // 客户端ID 264 | OnlineStatus bool // 在线状态 265 | Conn net.Conn // 网络连接 266 | Metadata map[string]interface{} // 元数据 267 | Info map[string]interface{} // 设备信息 268 | AlarmInfo map[string]interface{} // 报警信息 269 | LastActive time.Time // 最后活跃时间 270 | } 271 | ``` 272 | 273 | ### 3. 消息模型 274 | 275 | ```go 276 | // 上行消息 277 | type UpMessage struct { 278 | MessageID string `json:"messageId"` // 消息ID 279 | SendTime int64 `json:"sendTime"` // 发送时间 280 | RequestCode string `json:"requestCode"` // 请求代码 281 | MethodName string `json:"methodName"` // 方法名称 282 | Topic string `json:"topic"` // MQTT主题 283 | } 284 | ``` 285 | 286 | --- 287 | 288 | ## 协议开发 289 | 290 | ### 协议接口定义 291 | 292 | 所有自定义协议都需要实现 `ProtocolHandler` 接口: 293 | 294 | ```go 295 | type ProtocolHandler interface { 296 | Init(device *model.Device, data []byte) error 297 | Encode(device *model.Device, data interface{}, param ...string) ([]byte, error) 298 | Decode(device *model.Device, data []byte) ([]byte, error) 299 | } 300 | ``` 301 | 302 | ### 协议开发步骤 303 | 304 | #### 1. 实现Init方法 305 | 306 | ```go 307 | func (p *MyProtocol) Init(device *model.Device, data []byte) error { 308 | // 1. 解析设备标识信息 309 | deviceKey := parseDeviceKey(data) 310 | if device != nil { 311 | device.DeviceKey = deviceKey 312 | device.OnlineStatus = true 313 | } 314 | 315 | // 2. 订阅平台下发事件 316 | if deviceKey != "" { 317 | iotgateway.ServerGateway.SubscribeServiceEvent(deviceKey) 318 | iotgateway.ServerGateway.SubscribeSetEvent(deviceKey) 319 | } 320 | 321 | return nil 322 | } 323 | ``` 324 | 325 | #### 2. 实现Decode方法 326 | 327 | ```go 328 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) { 329 | // 1. 解析数据包 330 | packet, err := parsePacket(data) 331 | if err != nil { 332 | return nil, err 333 | } 334 | 335 | // 2. 根据数据类型处理 336 | switch packet.Type { 337 | case "heartbeat": 338 | return p.handleHeartbeat(device, packet) 339 | case "data": 340 | return p.handleData(device, packet) 341 | case "alarm": 342 | return p.handleAlarm(device, packet) 343 | } 344 | 345 | return nil, nil 346 | } 347 | 348 | func (p *MyProtocol) handleData(device *model.Device, packet *Packet) ([]byte, error) { 349 | // 准备属性数据 350 | properties := map[string]interface{}{ 351 | "temperature": packet.Temperature, 352 | "humidity": packet.Humidity, 353 | "timestamp": time.Now().Unix(), 354 | } 355 | 356 | // 触发属性上报事件 357 | eventData := g.Map{ 358 | "DeviceKey": device.DeviceKey, 359 | "PropertieDataList": properties, 360 | } 361 | event.MustFire(consts.PushAttributeDataToMQTT, eventData) 362 | 363 | // 返回确认消息 364 | return []byte("ACK"), nil 365 | } 366 | ``` 367 | 368 | #### 3. 实现Encode方法 369 | 370 | ```go 371 | func (p *MyProtocol) Encode(device *model.Device, data interface{}, param ...string) ([]byte, error) { 372 | // 根据数据类型编码 373 | switch v := data.(type) { 374 | case map[string]interface{}: 375 | return p.encodeCommand(v) 376 | case string: 377 | return []byte(v), nil 378 | default: 379 | return json.Marshal(data) 380 | } 381 | } 382 | 383 | func (p *MyProtocol) encodeCommand(cmd map[string]interface{}) ([]byte, error) { 384 | // 构造命令包 385 | packet := CommandPacket{ 386 | Header: 0xAA, 387 | Command: cmd["command"].(string), 388 | Data: cmd["data"], 389 | CRC: 0, // 计算CRC 390 | } 391 | 392 | return packet.Serialize(), nil 393 | } 394 | ``` 395 | 396 | ### 协议开发最佳实践 397 | 398 | 1. **错误处理**:始终检查数据包的完整性和有效性 399 | 2. **设备标识**:确保能正确解析设备唯一标识 400 | 3. **状态管理**:及时更新设备在线状态 401 | 4. **数据验证**:验证数据格式和范围 402 | 5. **性能优化**:避免在协议处理中进行耗时操作 403 | 404 | --- 405 | 406 | ## 配置管理 407 | 408 | ### 配置结构 409 | 410 | ```go 411 | type GatewayConfig struct { 412 | GatewayServerConfig GatewayServerConfig `json:"server"` 413 | MqttConfig MqttConfig `json:"mqtt"` 414 | } 415 | ``` 416 | 417 | ### 服务器配置 418 | 419 | ```go 420 | type GatewayServerConfig struct { 421 | Name string `json:"name"` // 网关服务名称 422 | Addr string `json:"addr"` // 监听地址 423 | NetType string `json:"netType"` // 网络类型: tcp/udp/mqtt 424 | SerUpTopic string `json:"serUpTopic"` // 上行Topic 425 | SerDownTopic string `json:"serDownTopic"` // 下行Topic 426 | Duration time.Duration `json:"duration"` // 心跳间隔 427 | ProductKey string `json:"productKey"` // 产品标识 428 | DeviceKey string `json:"deviceKey"` // 设备标识 429 | PacketConfig PacketConfig `json:"packetConfig"` // 粘包处理配置 430 | } 431 | ``` 432 | 433 | ### 粘包处理配置 434 | 435 | ```go 436 | type PacketConfig struct { 437 | Type PacketHandlingType // 处理类型 438 | FixedLength int // 固定长度 439 | HeaderLength int // 头部长度 440 | Delimiter string // 分隔符 441 | } 442 | 443 | // 处理类型常量 444 | const ( 445 | NoHandling = 0 // 不处理 446 | FixedLength = 1 // 固定长度 447 | HeaderBodySeparate = 2 // 头部+体 448 | Delimiter = 3 // 分隔符 449 | ) 450 | ``` 451 | 452 | ### MQTT配置 453 | 454 | ```go 455 | type MqttConfig struct { 456 | Address string `json:"address"` // MQTT服务器地址 457 | Username string `json:"username"` // 用户名 458 | Password string `json:"password"` // 密码 459 | ClientId string `json:"clientId"` // 客户端ID 460 | ClientCertificateKey string `json:"clientCertificateKey"` // 客户端证书密钥 461 | ClientCertificateCert string `json:"clientCertificateCert"` // 客户端证书 462 | KeepAliveDuration time.Duration `json:"keepAliveDuration"` // 保持连接时长 463 | Duration time.Duration `json:"duration"` // 心跳间隔 464 | } 465 | ``` 466 | 467 | ### 配置文件示例 468 | 469 | ```yaml 470 | # config/config.yaml 471 | server: 472 | name: "智能网关" 473 | addr: ":8080" 474 | netType: "tcp" 475 | duration: 60s 476 | productKey: "smart_gateway" 477 | deviceKey: "gateway_001" 478 | deviceName: "智能网关设备" 479 | description: "用于工业设备接入的智能网关" 480 | deviceType: "gateway" 481 | manufacturer: "SagooIOT" 482 | packetConfig: 483 | type: 3 # 分隔符类型 484 | delimiter: "\r\n" 485 | 486 | mqtt: 487 | address: "tcp://mqtt.sagoo.cn:1883" 488 | username: "gateway_user" 489 | password: "gateway_pass" 490 | clientId: "gateway_001" 491 | keepAliveDuration: 30s 492 | duration: 60s 493 | ``` 494 | 495 | --- 496 | 497 | ## 事件系统 498 | 499 | ### 事件类型 500 | 501 | SDK提供了以下预定义事件: 502 | 503 | ```go 504 | const ( 505 | PushAttributeDataToMQTT = "PushAttributeDataToMQTT" // 属性上报 506 | PushServiceResDataToMQTT = "PushServiceResDataToMQTT" // 服务调用结果上报 507 | PushSetResDataToMQTT = "PushSetResDataToMQTT" // 属性设置结果上报 508 | ) 509 | ``` 510 | 511 | ### 属性数据上报 512 | 513 | ```go 514 | func pushAttributeData(deviceKey string, properties map[string]interface{}) { 515 | eventData := g.Map{ 516 | "DeviceKey": deviceKey, 517 | "PropertieDataList": properties, 518 | } 519 | event.MustFire(consts.PushAttributeDataToMQTT, eventData) 520 | } 521 | 522 | // 使用示例 523 | properties := map[string]interface{}{ 524 | "temperature": 25.6, 525 | "humidity": 60.5, 526 | "pressure": 1013.25, 527 | } 528 | pushAttributeData("device_001", properties) 529 | ``` 530 | 531 | ### 事件数据上报 532 | 533 | ```go 534 | func pushEventData(deviceKey string, events map[string]interface{}) { 535 | eventData := g.Map{ 536 | "DeviceKey": deviceKey, 537 | "EventDataList": events, 538 | } 539 | event.MustFire(consts.PushAttributeDataToMQTT, eventData) 540 | } 541 | 542 | // 使用示例 543 | events := map[string]interface{}{ 544 | "alarm": map[string]interface{}{ 545 | "level": "high", 546 | "message": "温度过高", 547 | "time": time.Now().Unix(), 548 | }, 549 | } 550 | pushEventData("device_001", events) 551 | ``` 552 | 553 | ### 服务调用响应 554 | 555 | ```go 556 | func handleServiceCall(e event.Event) error { 557 | deviceKey := gconv.String(e.Data()["DeviceKey"]) 558 | messageId := gconv.String(e.Data()["MessageID"]) 559 | params := e.Data() 560 | 561 | // 处理服务调用逻辑 562 | result := processService(deviceKey, params) 563 | 564 | // 发送响应 565 | replyData := g.Map{ 566 | "DeviceKey": deviceKey, 567 | "MessageID": messageId, // 重要:传递消息ID确保精确匹配 568 | "ReplyData": result, 569 | } 570 | event.Async(consts.PushServiceResDataToMQTT, replyData) 571 | 572 | return nil 573 | } 574 | 575 | // 注册服务调用处理器 576 | func initEvents() { 577 | event.On("restart", event.ListenerFunc(handleRestartService), event.Normal) 578 | event.On("getStatus", event.ListenerFunc(handleGetStatus), event.Normal) 579 | } 580 | ``` 581 | 582 | ### 自定义事件处理 583 | 584 | ```go 585 | // 定义自定义事件 586 | const CustomDataEvent = "CustomDataEvent" 587 | 588 | // 注册事件处理器 589 | func initCustomEvents() { 590 | event.On(CustomDataEvent, event.ListenerFunc(handleCustomData), event.Normal) 591 | } 592 | 593 | func handleCustomData(e event.Event) error { 594 | data := e.Data() 595 | // 处理自定义数据 596 | log.Printf("收到自定义数据: %+v", data) 597 | return nil 598 | } 599 | 600 | // 触发自定义事件 601 | func triggerCustomEvent(data map[string]interface{}) { 602 | event.MustFire(CustomDataEvent, data) 603 | } 604 | ``` 605 | 606 | --- 607 | 608 | ## 网络层开发 609 | 610 | ### 支持的网络类型 611 | 612 | 1. **TCP服务器** - 适用于长连接设备 613 | 2. **UDP服务器** - 适用于短连接或广播设备 614 | 3. **MQTT客户端** - 适用于MQTT协议设备 615 | 616 | ### TCP服务器配置 617 | 618 | ```yaml 619 | server: 620 | netType: "tcp" 621 | addr: ":8080" 622 | packetConfig: 623 | type: 3 # 分隔符处理 624 | delimiter: "\r\n" 625 | ``` 626 | 627 | ### UDP服务器配置 628 | 629 | ```yaml 630 | server: 631 | netType: "udp" 632 | addr: ":8080" 633 | ``` 634 | 635 | ### MQTT客户端配置 636 | 637 | ```yaml 638 | server: 639 | netType: "mqtt" 640 | serUpTopic: "device/+/data" # 设备上行数据Topic 641 | serDownTopic: "device/+/command" # 设备下行命令Topic 642 | ``` 643 | 644 | ### 粘包处理 645 | 646 | SDK提供了多种粘包处理方式: 647 | 648 | #### 1. 固定长度 649 | 650 | ```yaml 651 | packetConfig: 652 | type: 1 # FixedLength 653 | fixedLength: 64 654 | ``` 655 | 656 | #### 2. 头部+体分离 657 | 658 | ```yaml 659 | packetConfig: 660 | type: 2 # HeaderBodySeparate 661 | headerLength: 4 # 头部4字节表示体长度 662 | ``` 663 | 664 | #### 3. 分隔符 665 | 666 | ```yaml 667 | packetConfig: 668 | type: 3 # Delimiter 669 | delimiter: "\r\n" 670 | ``` 671 | 672 | ### 自定义网络处理 673 | 674 | ```go 675 | // 实现自定义网络选项 676 | func WithCustomTimeout(timeout time.Duration) network.Option { 677 | return func(s *network.BaseServer) { 678 | s.SetTimeout(timeout) 679 | } 680 | } 681 | 682 | // 创建自定义网络服务器 683 | server := network.NewTCPServer( 684 | network.WithTimeout(2*time.Minute), 685 | network.WithProtocolHandler(protocol), 686 | network.WithCleanupInterval(5*time.Minute), 687 | WithCustomTimeout(30*time.Second), 688 | ) 689 | ``` 690 | 691 | --- 692 | 693 | ## MQTT集成 694 | 695 | ### 连接管理 696 | 697 | SDK自动管理MQTT连接,包括: 698 | 699 | - 自动重连 700 | - 心跳保持 701 | - 连接状态监控 702 | - 证书认证支持 703 | 704 | ### Topic规范 705 | 706 | #### 上行数据Topic 707 | 708 | ``` 709 | /sys/{productKey}/{deviceKey}/thing/event/property/pack/post 710 | ``` 711 | 712 | #### 服务调用Topic 713 | 714 | ``` 715 | /sys/{productKey}/{deviceKey}/thing/service/{serviceId} 716 | ``` 717 | 718 | #### 属性设置Topic 719 | 720 | ``` 721 | /sys/{productKey}/{deviceKey}/thing/service/property/set 722 | ``` 723 | 724 | ### 数据格式 725 | 726 | #### 属性上报格式 727 | 728 | ```json 729 | { 730 | "id": "message_id", 731 | "version": "1.0", 732 | "method": "thing.event.property.pack.post", 733 | "params": { 734 | "properties": { 735 | "temperature": { 736 | "value": 25.6, 737 | "time": 1640995200000 738 | } 739 | }, 740 | "subDevices": [ 741 | { 742 | "identity": { 743 | "productKey": "product_001", 744 | "deviceKey": "device_001" 745 | }, 746 | "properties": { 747 | "humidity": { 748 | "value": 60.5, 749 | "time": 1640995200000 750 | } 751 | } 752 | } 753 | ] 754 | } 755 | } 756 | ``` 757 | 758 | #### 服务调用格式 759 | 760 | ```json 761 | { 762 | "id": "service_call_id", 763 | "version": "1.0", 764 | "method": "thing.service.restart", 765 | "params": { 766 | "delay": 5 767 | } 768 | } 769 | ``` 770 | 771 | ### MQTT客户端使用 772 | 773 | ```go 774 | // 发布数据到指定Topic 775 | func publishToMQTT(topic string, data interface{}) error { 776 | client := iotgateway.ServerGateway.MQTTClient 777 | if client == nil || !client.IsConnected() { 778 | return errors.New("MQTT客户端未连接") 779 | } 780 | 781 | jsonData, err := json.Marshal(data) 782 | if err != nil { 783 | return err 784 | } 785 | 786 | token := client.Publish(topic, 1, false, jsonData) 787 | return token.Error() 788 | } 789 | ``` 790 | 791 | --- 792 | 793 | ## 最佳实践 794 | 795 | ### 1. 错误处理 796 | 797 | ```go 798 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) { 799 | defer func() { 800 | if r := recover(); r != nil { 801 | log.Errorf("协议解析异常: %v", r) 802 | } 803 | }() 804 | 805 | // 数据长度检查 806 | if len(data) < 4 { 807 | return nil, errors.New("数据包长度不足") 808 | } 809 | 810 | // 数据格式验证 811 | if !isValidPacket(data) { 812 | return nil, errors.New("无效的数据包格式") 813 | } 814 | 815 | // 解析数据 816 | packet, err := parsePacket(data) 817 | if err != nil { 818 | return nil, fmt.Errorf("解析数据包失败: %v", err) 819 | } 820 | 821 | return p.processPacket(device, packet) 822 | } 823 | ``` 824 | 825 | ### 2. 性能优化 826 | 827 | ```go 828 | // 使用对象池减少内存分配 829 | var packetPool = sync.Pool{ 830 | New: func() interface{} { 831 | return &Packet{} 832 | }, 833 | } 834 | 835 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) { 836 | // 从池中获取对象 837 | packet := packetPool.Get().(*Packet) 838 | defer packetPool.Put(packet) 839 | 840 | // 重置对象状态 841 | packet.Reset() 842 | 843 | // 解析数据 844 | err := packet.Parse(data) 845 | if err != nil { 846 | return nil, err 847 | } 848 | 849 | return p.processPacket(device, packet) 850 | } 851 | ``` 852 | 853 | ### 3. 并发安全 854 | 855 | ```go 856 | type SafeProtocol struct { 857 | mu sync.RWMutex 858 | devices map[string]*model.Device 859 | counters map[string]int64 860 | } 861 | 862 | func (p *SafeProtocol) updateDeviceCounter(deviceKey string) { 863 | p.mu.Lock() 864 | defer p.mu.Unlock() 865 | 866 | p.counters[deviceKey]++ 867 | } 868 | 869 | func (p *SafeProtocol) getDeviceCounter(deviceKey string) int64 { 870 | p.mu.RLock() 871 | defer p.mu.RUnlock() 872 | 873 | return p.counters[deviceKey] 874 | } 875 | ``` 876 | 877 | ### 4. 资源管理 878 | 879 | ```go 880 | func (p *MyProtocol) Init(device *model.Device, data []byte) error { 881 | // 设置设备清理回调 882 | if device != nil { 883 | device.Metadata = map[string]interface{}{ 884 | "cleanup": func() { 885 | // 清理设备相关资源 886 | p.cleanupDevice(device.DeviceKey) 887 | }, 888 | } 889 | } 890 | 891 | return nil 892 | } 893 | 894 | func (p *MyProtocol) cleanupDevice(deviceKey string) { 895 | // 清理缓存 896 | vars.ClearDeviceMessages(deviceKey) 897 | 898 | // 清理其他资源 899 | p.removeDeviceFromCache(deviceKey) 900 | } 901 | ``` 902 | 903 | ### 5. 日志记录 904 | 905 | ```go 906 | import ( 907 | "github.com/gogf/gf/v2/os/glog" 908 | "context" 909 | ) 910 | 911 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) { 912 | ctx := context.Background() 913 | 914 | glog.Debugf(ctx, "收到设备数据: deviceKey=%s, dataLen=%d", 915 | device.DeviceKey, len(data)) 916 | 917 | packet, err := parsePacket(data) 918 | if err != nil { 919 | glog.Errorf(ctx, "解析数据包失败: deviceKey=%s, error=%v", 920 | device.DeviceKey, err) 921 | return nil, err 922 | } 923 | 924 | glog.Infof(ctx, "成功解析数据包: deviceKey=%s, type=%s", 925 | device.DeviceKey, packet.Type) 926 | 927 | return p.processPacket(device, packet) 928 | } 929 | ``` 930 | 931 | --- 932 | 933 | ## 故障排查 934 | 935 | ### 常见问题 936 | 937 | #### 1. 设备连接失败 938 | 939 | **现象:** 设备无法连接到网关 940 | 941 | **排查步骤:** 942 | 1. 检查网关监听地址和端口 943 | 2. 检查防火墙设置 944 | 3. 检查网络连通性 945 | 4. 查看网关日志 946 | 947 | ```bash 948 | # 检查端口监听 949 | netstat -tlnp | grep 8080 950 | 951 | # 测试连接 952 | telnet gateway_ip 8080 953 | ``` 954 | 955 | #### 2. 数据解析错误 956 | 957 | **现象:** 收到数据但解析失败 958 | 959 | **排查步骤:** 960 | 1. 检查数据格式是否正确 961 | 2. 检查协议实现是否有误 962 | 3. 添加调试日志查看原始数据 963 | 964 | ```go 965 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) { 966 | // 添加调试日志 967 | glog.Debugf(context.Background(), "原始数据: %x", data) 968 | glog.Debugf(context.Background(), "数据字符串: %s", string(data)) 969 | 970 | // 解析逻辑... 971 | } 972 | ``` 973 | 974 | #### 3. MQTT连接问题 975 | 976 | **现象:** 无法连接到MQTT服务器 977 | 978 | **排查步骤:** 979 | 1. 检查MQTT服务器地址和端口 980 | 2. 检查用户名密码 981 | 3. 检查证书配置 982 | 4. 查看MQTT连接日志 983 | 984 | ```go 985 | // 添加MQTT连接状态监控 986 | func monitorMQTTConnection() { 987 | client := iotgateway.ServerGateway.MQTTClient 988 | if client != nil { 989 | isConnected := client.IsConnected() 990 | glog.Infof(context.Background(), "MQTT连接状态: %v", isConnected) 991 | } 992 | } 993 | ``` 994 | 995 | #### 4. 内存泄漏 996 | 997 | **现象:** 网关运行时间长后内存持续增长 998 | 999 | **排查步骤:** 1000 | 1. 检查缓存清理是否正常 1001 | 2. 检查设备离线清理 1002 | 3. 使用内存分析工具 1003 | 1004 | ```go 1005 | // 监控缓存状态 1006 | func monitorCacheStats() { 1007 | stats := vars.GetCacheStats() 1008 | glog.Infof(context.Background(), "缓存统计: %+v", stats) 1009 | 1010 | if expiredCount := stats["expiredCount"].(int); expiredCount > 100 { 1011 | glog.Warnf(context.Background(), "发现大量过期消息: %d", expiredCount) 1012 | } 1013 | } 1014 | ``` 1015 | 1016 | ### 调试工具 1017 | 1018 | #### 1. 启用调试日志 1019 | 1020 | ```yaml 1021 | # config/config.yaml 1022 | logger: 1023 | level: "debug" 1024 | stdout: true 1025 | ``` 1026 | 1027 | #### 2. 性能监控 1028 | 1029 | ```go 1030 | import _ "net/http/pprof" 1031 | import "net/http" 1032 | 1033 | func init() { 1034 | go func() { 1035 | log.Println(http.ListenAndServe("localhost:6060", nil)) 1036 | }() 1037 | } 1038 | ``` 1039 | 1040 | 访问 `http://localhost:6060/debug/pprof/` 查看性能数据。 1041 | 1042 | #### 3. 健康检查 1043 | 1044 | ```go 1045 | func healthCheck() map[string]interface{} { 1046 | return map[string]interface{}{ 1047 | "gateway_status": "running", 1048 | "mqtt_connected": iotgateway.ServerGateway.MQTTClient.IsConnected(), 1049 | "device_count": vars.CountDevices(), 1050 | "cache_stats": vars.GetCacheStats(), 1051 | "uptime": time.Since(startTime).String(), 1052 | } 1053 | } 1054 | ``` 1055 | 1056 | --- 1057 | 1058 | ## API参考 1059 | 1060 | ### 核心API 1061 | 1062 | #### Gateway 1063 | 1064 | ```go 1065 | // 创建网关实例 1066 | func NewGateway(ctx context.Context, protocol network.ProtocolHandler) (*Gateway, error) 1067 | 1068 | // 启动网关 1069 | func (gw *Gateway) Start() 1070 | 1071 | // 订阅服务下发事件 1072 | func (gw *Gateway) SubscribeServiceEvent(deviceKey string) 1073 | 1074 | // 订阅属性设置事件 1075 | func (gw *Gateway) SubscribeSetEvent(deviceKey string) 1076 | 1077 | // 向设备下发数据(MQTT模式) 1078 | func (gw *Gateway) DeviceDownData(data interface{}) 1079 | ``` 1080 | 1081 | #### 设备管理 1082 | 1083 | ```go 1084 | // 更新设备信息 1085 | func UpdateDeviceMap(key string, device *model.Device) 1086 | 1087 | // 获取设备信息 1088 | func GetDeviceMap(key string) (*model.Device, error) 1089 | 1090 | // 删除设备信息 1091 | func DeleteFromDeviceMap(key string) 1092 | 1093 | // 获取设备数量 1094 | func CountDevices() int 1095 | ``` 1096 | 1097 | #### 消息缓存 1098 | 1099 | ```go 1100 | // 存储消息 1101 | func UpdateUpMessageMap(key string, device model.UpMessage) 1102 | 1103 | // 获取消息 1104 | func GetUpMessageMap(key string) (model.UpMessage, error) 1105 | 1106 | // 根据复合键获取消息 1107 | func GetUpMessageByCompositeKey(deviceKey, messageId string) (model.UpMessage, error) 1108 | 1109 | // 删除消息 1110 | func DeleteFromUpMessageMap(key string) 1111 | 1112 | // 根据复合键删除消息 1113 | func DeleteFromUpMessageMapByCompositeKey(deviceKey, messageId string) 1114 | 1115 | // 清理设备所有消息 1116 | func ClearDeviceMessages(deviceKey string) 1117 | 1118 | // 获取缓存统计 1119 | func GetCacheStats() map[string]interface{} 1120 | ``` 1121 | 1122 | #### 事件系统 1123 | 1124 | ```go 1125 | // 触发事件 1126 | func event.MustFire(eventName string, data interface{}) 1127 | 1128 | // 异步触发事件 1129 | func event.Async(eventName string, data interface{}) 1130 | 1131 | // 注册事件监听器 1132 | func event.On(eventName string, listener event.Listener, priority event.Priority) 1133 | ``` 1134 | 1135 | ### 常量定义 1136 | 1137 | ```go 1138 | // 事件类型 1139 | const ( 1140 | PushAttributeDataToMQTT = "PushAttributeDataToMQTT" 1141 | PushServiceResDataToMQTT = "PushServiceResDataToMQTT" 1142 | PushSetResDataToMQTT = "PushSetResDataToMQTT" 1143 | ) 1144 | 1145 | // 网络类型 1146 | const ( 1147 | NetTypeTcpServer = "tcp" 1148 | NetTypeUDPServer = "udp" 1149 | NetTypeMqttServer = "mqtt" 1150 | ) 1151 | 1152 | // 粘包处理类型 1153 | const ( 1154 | NoHandling = 0 1155 | FixedLength = 1 1156 | HeaderBodySeparate = 2 1157 | Delimiter = 3 1158 | ) 1159 | ``` 1160 | 1161 | --- 1162 | 1163 | ## 版本历史 1164 | 1165 | ### v1.0.0 (当前版本) 1166 | - ✅ 基础网关功能 1167 | - ✅ TCP/UDP/MQTT支持 1168 | - ✅ 协议处理器接口 1169 | - ✅ 事件驱动架构 1170 | - ✅ MQTT集成 1171 | - ✅ 消息缓存优化 1172 | - ✅ 内存泄漏防护 1173 | -------------------------------------------------------------------------------- /docs/protocol-development.md: -------------------------------------------------------------------------------- 1 | # 协议开发指南 2 | 3 | ## 概述 4 | 5 | 本指南详细介绍如何为SagooIOT网关SDK开发自定义协议处理器,包括协议接口实现、数据解析、编码处理等核心内容。 6 | 7 | ## 协议接口定义 8 | 9 | 所有协议处理器都必须实现 `ProtocolHandler` 接口: 10 | 11 | ```go 12 | type ProtocolHandler interface { 13 | Init(device *model.Device, data []byte) error 14 | Encode(device *model.Device, data interface{}, param ...string) ([]byte, error) 15 | Decode(device *model.Device, data []byte) ([]byte, error) 16 | } 17 | ``` 18 | 19 | ## 方法详解 20 | 21 | ### 1. Init 方法 22 | 23 | **作用:** 初始化设备连接,设置设备标识,订阅平台事件 24 | 25 | **调用时机:** 设备首次连接或发送数据时 26 | 27 | **参数说明:** 28 | - `device`: 设备对象,可能为nil(需要判断) 29 | - `data`: 初始数据包 30 | 31 | **实现要点:** 32 | 33 | ```go 34 | func (p *MyProtocol) Init(device *model.Device, data []byte) error { 35 | // 1. 数据有效性检查 36 | if len(data) < 4 { 37 | return errors.New("初始数据包长度不足") 38 | } 39 | 40 | // 2. 解析设备标识 41 | deviceKey, err := p.parseDeviceKey(data) 42 | if err != nil { 43 | return fmt.Errorf("解析设备标识失败: %v", err) 44 | } 45 | 46 | // 3. 设置设备信息 47 | if device != nil { 48 | device.DeviceKey = deviceKey 49 | device.OnlineStatus = true 50 | device.LastActive = time.Now() 51 | 52 | // 设置设备元数据 53 | device.Metadata = map[string]interface{}{ 54 | "protocol": "MyProtocol", 55 | "version": "1.0", 56 | } 57 | } 58 | 59 | // 4. 订阅平台下发事件 60 | if deviceKey != "" { 61 | iotgateway.ServerGateway.SubscribeServiceEvent(deviceKey) 62 | iotgateway.ServerGateway.SubscribeSetEvent(deviceKey) 63 | } 64 | 65 | // 5. 记录设备上线日志 66 | glog.Infof(context.Background(), "设备上线: %s", deviceKey) 67 | 68 | return nil 69 | } 70 | ``` 71 | 72 | ### 2. Decode 方法 73 | 74 | **作用:** 解析设备上行数据,触发相应的数据上报事件 75 | 76 | **调用时机:** 每次收到设备数据时 77 | 78 | **参数说明:** 79 | - `device`: 设备对象 80 | - `data`: 接收到的原始数据 81 | 82 | **返回值:** 83 | - `[]byte`: 需要回复给设备的数据(可以为nil) 84 | - `error`: 解析错误 85 | 86 | **实现模板:** 87 | 88 | ```go 89 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) { 90 | // 1. 异常恢复 91 | defer func() { 92 | if r := recover(); r != nil { 93 | glog.Errorf(context.Background(), "协议解析异常: %v", r) 94 | } 95 | }() 96 | 97 | // 2. 参数验证 98 | if device == nil { 99 | return nil, errors.New("设备对象为空") 100 | } 101 | 102 | if len(data) == 0 { 103 | return nil, errors.New("数据为空") 104 | } 105 | 106 | // 3. 更新设备活跃时间 107 | device.LastActive = time.Now() 108 | 109 | // 4. 解析数据包 110 | packet, err := p.parsePacket(data) 111 | if err != nil { 112 | glog.Errorf(context.Background(), "解析数据包失败: deviceKey=%s, error=%v", 113 | device.DeviceKey, err) 114 | return nil, err 115 | } 116 | 117 | // 5. 根据数据包类型处理 118 | switch packet.Type { 119 | case PacketTypeHeartbeat: 120 | return p.handleHeartbeat(device, packet) 121 | case PacketTypeData: 122 | return p.handleData(device, packet) 123 | case PacketTypeAlarm: 124 | return p.handleAlarm(device, packet) 125 | case PacketTypeResponse: 126 | return p.handleResponse(device, packet) 127 | default: 128 | return nil, fmt.Errorf("未知数据包类型: %d", packet.Type) 129 | } 130 | } 131 | ``` 132 | 133 | ### 3. Encode 方法 134 | 135 | **作用:** 编码下行数据,将平台指令转换为设备可识别的格式 136 | 137 | **调用时机:** 需要向设备发送数据时 138 | 139 | **参数说明:** 140 | - `device`: 目标设备对象 141 | - `data`: 要发送的数据 142 | - `param`: 可选参数 143 | 144 | **实现模板:** 145 | 146 | ```go 147 | func (p *MyProtocol) Encode(device *model.Device, data interface{}, param ...string) ([]byte, error) { 148 | if device == nil { 149 | return nil, errors.New("设备对象为空") 150 | } 151 | 152 | // 根据数据类型进行编码 153 | switch v := data.(type) { 154 | case map[string]interface{}: 155 | return p.encodeCommand(device, v) 156 | case string: 157 | return p.encodeString(device, v) 158 | case []byte: 159 | return v, nil 160 | default: 161 | // 默认JSON编码 162 | return json.Marshal(data) 163 | } 164 | } 165 | ``` 166 | 167 | ## 协议开发实例 168 | 169 | ### 示例1:简单文本协议 170 | 171 | 假设设备使用简单的文本协议,格式为:`DEVICE_ID|DATA_TYPE|DATA|CRC` 172 | 173 | ```go 174 | package protocol 175 | 176 | import ( 177 | "errors" 178 | "fmt" 179 | "strconv" 180 | "strings" 181 | "time" 182 | 183 | "github.com/gogf/gf/v2/frame/g" 184 | "github.com/gogf/gf/v2/os/glog" 185 | "github.com/gogf/gf/v2/util/gconv" 186 | "github.com/gookit/event" 187 | "github.com/sagoo-cloud/iotgateway" 188 | "github.com/sagoo-cloud/iotgateway/consts" 189 | "github.com/sagoo-cloud/iotgateway/model" 190 | ) 191 | 192 | type TextProtocol struct{} 193 | 194 | // 数据包类型 195 | const ( 196 | DataTypeHeartbeat = "HB" 197 | DataTypeData = "DATA" 198 | DataTypeAlarm = "ALARM" 199 | DataTypeResponse = "RESP" 200 | ) 201 | 202 | // 数据包结构 203 | type TextPacket struct { 204 | DeviceID string 205 | DataType string 206 | Data string 207 | CRC string 208 | } 209 | 210 | func (p *TextProtocol) Init(device *model.Device, data []byte) error { 211 | // 解析初始数据包获取设备ID 212 | packet, err := p.parseTextPacket(data) 213 | if err != nil { 214 | return err 215 | } 216 | 217 | if device != nil { 218 | device.DeviceKey = packet.DeviceID 219 | device.OnlineStatus = true 220 | device.LastActive = time.Now() 221 | } 222 | 223 | // 订阅平台事件 224 | if packet.DeviceID != "" { 225 | iotgateway.ServerGateway.SubscribeServiceEvent(packet.DeviceID) 226 | iotgateway.ServerGateway.SubscribeSetEvent(packet.DeviceID) 227 | } 228 | 229 | glog.Infof(context.Background(), "文本协议设备上线: %s", packet.DeviceID) 230 | return nil 231 | } 232 | 233 | func (p *TextProtocol) Decode(device *model.Device, data []byte) ([]byte, error) { 234 | defer func() { 235 | if r := recover(); r != nil { 236 | glog.Errorf(context.Background(), "文本协议解析异常: %v", r) 237 | } 238 | }() 239 | 240 | if device == nil || len(data) == 0 { 241 | return nil, errors.New("无效参数") 242 | } 243 | 244 | // 解析数据包 245 | packet, err := p.parseTextPacket(data) 246 | if err != nil { 247 | return nil, err 248 | } 249 | 250 | // 验证CRC 251 | if !p.verifyCRC(packet) { 252 | return nil, errors.New("CRC校验失败") 253 | } 254 | 255 | // 更新设备活跃时间 256 | device.LastActive = time.Now() 257 | 258 | // 处理不同类型的数据 259 | switch packet.DataType { 260 | case DataTypeHeartbeat: 261 | return p.handleHeartbeat(device, packet) 262 | case DataTypeData: 263 | return p.handleData(device, packet) 264 | case DataTypeAlarm: 265 | return p.handleAlarm(device, packet) 266 | default: 267 | return nil, fmt.Errorf("未知数据类型: %s", packet.DataType) 268 | } 269 | } 270 | 271 | func (p *TextProtocol) Encode(device *model.Device, data interface{}, param ...string) ([]byte, error) { 272 | if device == nil { 273 | return nil, errors.New("设备对象为空") 274 | } 275 | 276 | switch v := data.(type) { 277 | case map[string]interface{}: 278 | return p.encodeCommand(device, v) 279 | case string: 280 | return p.encodeString(device, v) 281 | default: 282 | return nil, errors.New("不支持的数据类型") 283 | } 284 | } 285 | 286 | // 解析文本数据包 287 | func (p *TextProtocol) parseTextPacket(data []byte) (*TextPacket, error) { 288 | text := strings.TrimSpace(string(data)) 289 | parts := strings.Split(text, "|") 290 | 291 | if len(parts) != 4 { 292 | return nil, errors.New("数据包格式错误") 293 | } 294 | 295 | return &TextPacket{ 296 | DeviceID: parts[0], 297 | DataType: parts[1], 298 | Data: parts[2], 299 | CRC: parts[3], 300 | }, nil 301 | } 302 | 303 | // 验证CRC 304 | func (p *TextProtocol) verifyCRC(packet *TextPacket) bool { 305 | // 简单的CRC校验实现 306 | expected := p.calculateCRC(packet.DeviceID + "|" + packet.DataType + "|" + packet.Data) 307 | return expected == packet.CRC 308 | } 309 | 310 | // 计算CRC 311 | func (p *TextProtocol) calculateCRC(data string) string { 312 | // 简单的校验和实现 313 | sum := 0 314 | for _, b := range []byte(data) { 315 | sum += int(b) 316 | } 317 | return fmt.Sprintf("%04X", sum&0xFFFF) 318 | } 319 | 320 | // 处理心跳数据 321 | func (p *TextProtocol) handleHeartbeat(device *model.Device, packet *TextPacket) ([]byte, error) { 322 | glog.Debugf(context.Background(), "收到心跳: deviceKey=%s", device.DeviceKey) 323 | 324 | // 回复心跳确认 325 | response := fmt.Sprintf("%s|HB_ACK|OK|%s", 326 | device.DeviceKey, 327 | p.calculateCRC(device.DeviceKey+"|HB_ACK|OK")) 328 | 329 | return []byte(response), nil 330 | } 331 | 332 | // 处理数据上报 333 | func (p *TextProtocol) handleData(device *model.Device, packet *TextPacket) ([]byte, error) { 334 | // 解析数据字段 335 | dataFields := strings.Split(packet.Data, ",") 336 | properties := make(map[string]interface{}) 337 | 338 | for _, field := range dataFields { 339 | kv := strings.Split(field, "=") 340 | if len(kv) == 2 { 341 | // 尝试转换为数值 342 | if val, err := strconv.ParseFloat(kv[1], 64); err == nil { 343 | properties[kv[0]] = val 344 | } else { 345 | properties[kv[0]] = kv[1] 346 | } 347 | } 348 | } 349 | 350 | // 添加时间戳 351 | properties["timestamp"] = time.Now().Unix() 352 | 353 | // 触发属性上报事件 354 | eventData := g.Map{ 355 | "DeviceKey": device.DeviceKey, 356 | "PropertieDataList": properties, 357 | } 358 | event.MustFire(consts.PushAttributeDataToMQTT, eventData) 359 | 360 | glog.Infof(context.Background(), "设备数据上报: deviceKey=%s, data=%+v", 361 | device.DeviceKey, properties) 362 | 363 | // 回复确认 364 | response := fmt.Sprintf("%s|DATA_ACK|OK|%s", 365 | device.DeviceKey, 366 | p.calculateCRC(device.DeviceKey+"|DATA_ACK|OK")) 367 | 368 | return []byte(response), nil 369 | } 370 | 371 | // 处理报警数据 372 | func (p *TextProtocol) handleAlarm(device *model.Device, packet *TextPacket) ([]byte, error) { 373 | // 解析报警数据 374 | alarmData := map[string]interface{}{ 375 | "level": "high", 376 | "message": packet.Data, 377 | "time": time.Now().Unix(), 378 | } 379 | 380 | events := map[string]interface{}{ 381 | "alarm": alarmData, 382 | } 383 | 384 | // 触发事件上报 385 | eventData := g.Map{ 386 | "DeviceKey": device.DeviceKey, 387 | "EventDataList": events, 388 | } 389 | event.MustFire(consts.PushAttributeDataToMQTT, eventData) 390 | 391 | glog.Warnf(context.Background(), "设备报警: deviceKey=%s, alarm=%s", 392 | device.DeviceKey, packet.Data) 393 | 394 | // 回复确认 395 | response := fmt.Sprintf("%s|ALARM_ACK|OK|%s", 396 | device.DeviceKey, 397 | p.calculateCRC(device.DeviceKey+"|ALARM_ACK|OK")) 398 | 399 | return []byte(response), nil 400 | } 401 | 402 | // 编码命令 403 | func (p *TextProtocol) encodeCommand(device *model.Device, cmd map[string]interface{}) ([]byte, error) { 404 | command := gconv.String(cmd["command"]) 405 | params := gconv.String(cmd["params"]) 406 | 407 | data := fmt.Sprintf("%s,%s", command, params) 408 | packet := fmt.Sprintf("%s|CMD|%s", device.DeviceKey, data) 409 | crc := p.calculateCRC(packet) 410 | 411 | result := fmt.Sprintf("%s|%s", packet, crc) 412 | return []byte(result), nil 413 | } 414 | 415 | // 编码字符串 416 | func (p *TextProtocol) encodeString(device *model.Device, str string) ([]byte, error) { 417 | packet := fmt.Sprintf("%s|MSG|%s", device.DeviceKey, str) 418 | crc := p.calculateCRC(packet) 419 | 420 | result := fmt.Sprintf("%s|%s", packet, crc) 421 | return []byte(result), nil 422 | } 423 | ``` 424 | 425 | ### 示例2:二进制协议 426 | 427 | 假设设备使用二进制协议,格式为:`[Header(2)][Length(2)][DeviceID(4)][Type(1)][Data(N)][CRC(2)]` 428 | 429 | ```go 430 | package protocol 431 | 432 | import ( 433 | "bytes" 434 | "encoding/binary" 435 | "errors" 436 | "fmt" 437 | "time" 438 | 439 | "github.com/gogf/gf/v2/frame/g" 440 | "github.com/gogf/gf/v2/os/glog" 441 | "github.com/gookit/event" 442 | "github.com/sagoo-cloud/iotgateway" 443 | "github.com/sagoo-cloud/iotgateway/consts" 444 | "github.com/sagoo-cloud/iotgateway/model" 445 | ) 446 | 447 | type BinaryProtocol struct{} 448 | 449 | // 协议常量 450 | const ( 451 | ProtocolHeader = 0xAABB 452 | MinPacketLength = 11 // 最小包长度 453 | 454 | // 数据类型 455 | TypeHeartbeat = 0x01 456 | TypeData = 0x02 457 | TypeAlarm = 0x03 458 | TypeCommand = 0x04 459 | ) 460 | 461 | // 二进制数据包结构 462 | type BinaryPacket struct { 463 | Header uint16 464 | Length uint16 465 | DeviceID uint32 466 | Type uint8 467 | Data []byte 468 | CRC uint16 469 | } 470 | 471 | func (p *BinaryProtocol) Init(device *model.Device, data []byte) error { 472 | packet, err := p.parseBinaryPacket(data) 473 | if err != nil { 474 | return err 475 | } 476 | 477 | if device != nil { 478 | device.DeviceKey = fmt.Sprintf("device_%08X", packet.DeviceID) 479 | device.OnlineStatus = true 480 | device.LastActive = time.Now() 481 | } 482 | 483 | deviceKey := fmt.Sprintf("device_%08X", packet.DeviceID) 484 | if deviceKey != "" { 485 | iotgateway.ServerGateway.SubscribeServiceEvent(deviceKey) 486 | iotgateway.ServerGateway.SubscribeSetEvent(deviceKey) 487 | } 488 | 489 | glog.Infof(context.Background(), "二进制协议设备上线: %s", deviceKey) 490 | return nil 491 | } 492 | 493 | func (p *BinaryProtocol) Decode(device *model.Device, data []byte) ([]byte, error) { 494 | defer func() { 495 | if r := recover(); r != nil { 496 | glog.Errorf(context.Background(), "二进制协议解析异常: %v", r) 497 | } 498 | }() 499 | 500 | if device == nil || len(data) < MinPacketLength { 501 | return nil, errors.New("无效参数") 502 | } 503 | 504 | packet, err := p.parseBinaryPacket(data) 505 | if err != nil { 506 | return nil, err 507 | } 508 | 509 | // 验证CRC 510 | if !p.verifyCRC(packet) { 511 | return nil, errors.New("CRC校验失败") 512 | } 513 | 514 | device.LastActive = time.Now() 515 | 516 | switch packet.Type { 517 | case TypeHeartbeat: 518 | return p.handleHeartbeat(device, packet) 519 | case TypeData: 520 | return p.handleData(device, packet) 521 | case TypeAlarm: 522 | return p.handleAlarm(device, packet) 523 | default: 524 | return nil, fmt.Errorf("未知数据类型: %d", packet.Type) 525 | } 526 | } 527 | 528 | func (p *BinaryProtocol) Encode(device *model.Device, data interface{}, param ...string) ([]byte, error) { 529 | if device == nil { 530 | return nil, errors.New("设备对象为空") 531 | } 532 | 533 | // 解析设备ID 534 | var deviceID uint32 535 | fmt.Sscanf(device.DeviceKey, "device_%08X", &deviceID) 536 | 537 | switch v := data.(type) { 538 | case map[string]interface{}: 539 | return p.encodeCommand(deviceID, v) 540 | case []byte: 541 | return p.encodeData(deviceID, TypeCommand, v) 542 | default: 543 | return nil, errors.New("不支持的数据类型") 544 | } 545 | } 546 | 547 | // 解析二进制数据包 548 | func (p *BinaryProtocol) parseBinaryPacket(data []byte) (*BinaryPacket, error) { 549 | if len(data) < MinPacketLength { 550 | return nil, errors.New("数据包长度不足") 551 | } 552 | 553 | reader := bytes.NewReader(data) 554 | packet := &BinaryPacket{} 555 | 556 | // 读取头部 557 | if err := binary.Read(reader, binary.BigEndian, &packet.Header); err != nil { 558 | return nil, err 559 | } 560 | 561 | if packet.Header != ProtocolHeader { 562 | return nil, errors.New("无效的协议头") 563 | } 564 | 565 | // 读取长度 566 | if err := binary.Read(reader, binary.BigEndian, &packet.Length); err != nil { 567 | return nil, err 568 | } 569 | 570 | if int(packet.Length) != len(data) { 571 | return nil, errors.New("数据包长度不匹配") 572 | } 573 | 574 | // 读取设备ID 575 | if err := binary.Read(reader, binary.BigEndian, &packet.DeviceID); err != nil { 576 | return nil, err 577 | } 578 | 579 | // 读取类型 580 | if err := binary.Read(reader, binary.BigEndian, &packet.Type); err != nil { 581 | return nil, err 582 | } 583 | 584 | // 读取数据 585 | dataLen := int(packet.Length) - MinPacketLength 586 | if dataLen > 0 { 587 | packet.Data = make([]byte, dataLen) 588 | if _, err := reader.Read(packet.Data); err != nil { 589 | return nil, err 590 | } 591 | } 592 | 593 | // 读取CRC 594 | if err := binary.Read(reader, binary.BigEndian, &packet.CRC); err != nil { 595 | return nil, err 596 | } 597 | 598 | return packet, nil 599 | } 600 | 601 | // 验证CRC 602 | func (p *BinaryProtocol) verifyCRC(packet *BinaryPacket) bool { 603 | expected := p.calculateCRC(packet) 604 | return expected == packet.CRC 605 | } 606 | 607 | // 计算CRC 608 | func (p *BinaryProtocol) calculateCRC(packet *BinaryPacket) uint16 { 609 | // 简单的CRC16实现 610 | var crc uint16 = 0xFFFF 611 | 612 | // 计算除CRC外的所有字段 613 | buf := new(bytes.Buffer) 614 | binary.Write(buf, binary.BigEndian, packet.Header) 615 | binary.Write(buf, binary.BigEndian, packet.Length) 616 | binary.Write(buf, binary.BigEndian, packet.DeviceID) 617 | binary.Write(buf, binary.BigEndian, packet.Type) 618 | buf.Write(packet.Data) 619 | 620 | data := buf.Bytes() 621 | for _, b := range data { 622 | crc ^= uint16(b) 623 | for i := 0; i < 8; i++ { 624 | if crc&1 != 0 { 625 | crc = (crc >> 1) ^ 0xA001 626 | } else { 627 | crc >>= 1 628 | } 629 | } 630 | } 631 | 632 | return crc 633 | } 634 | 635 | // 处理心跳 636 | func (p *BinaryProtocol) handleHeartbeat(device *model.Device, packet *BinaryPacket) ([]byte, error) { 637 | glog.Debugf(context.Background(), "收到心跳: deviceKey=%s", device.DeviceKey) 638 | 639 | // 构造心跳回复 640 | return p.encodeData(packet.DeviceID, TypeHeartbeat, []byte{0x01}), nil 641 | } 642 | 643 | // 处理数据 644 | func (p *BinaryProtocol) handleData(device *model.Device, packet *BinaryPacket) ([]byte, error) { 645 | if len(packet.Data) < 8 { 646 | return nil, errors.New("数据长度不足") 647 | } 648 | 649 | // 解析数据(假设为温度和湿度) 650 | reader := bytes.NewReader(packet.Data) 651 | var temperature, humidity float32 652 | 653 | binary.Read(reader, binary.BigEndian, &temperature) 654 | binary.Read(reader, binary.BigEndian, &humidity) 655 | 656 | properties := map[string]interface{}{ 657 | "temperature": temperature, 658 | "humidity": humidity, 659 | "timestamp": time.Now().Unix(), 660 | } 661 | 662 | // 触发属性上报 663 | eventData := g.Map{ 664 | "DeviceKey": device.DeviceKey, 665 | "PropertieDataList": properties, 666 | } 667 | event.MustFire(consts.PushAttributeDataToMQTT, eventData) 668 | 669 | glog.Infof(context.Background(), "设备数据上报: deviceKey=%s, temp=%.2f, hum=%.2f", 670 | device.DeviceKey, temperature, humidity) 671 | 672 | // 回复确认 673 | return p.encodeData(packet.DeviceID, TypeData, []byte{0x01}), nil 674 | } 675 | 676 | // 处理报警 677 | func (p *BinaryProtocol) handleAlarm(device *model.Device, packet *BinaryPacket) ([]byte, error) { 678 | if len(packet.Data) < 1 { 679 | return nil, errors.New("报警数据为空") 680 | } 681 | 682 | alarmCode := packet.Data[0] 683 | alarmData := map[string]interface{}{ 684 | "code": alarmCode, 685 | "level": p.getAlarmLevel(alarmCode), 686 | "message": p.getAlarmMessage(alarmCode), 687 | "time": time.Now().Unix(), 688 | } 689 | 690 | events := map[string]interface{}{ 691 | "alarm": alarmData, 692 | } 693 | 694 | eventData := g.Map{ 695 | "DeviceKey": device.DeviceKey, 696 | "EventDataList": events, 697 | } 698 | event.MustFire(consts.PushAttributeDataToMQTT, eventData) 699 | 700 | glog.Warnf(context.Background(), "设备报警: deviceKey=%s, code=%d", 701 | device.DeviceKey, alarmCode) 702 | 703 | return p.encodeData(packet.DeviceID, TypeAlarm, []byte{0x01}), nil 704 | } 705 | 706 | // 编码数据 707 | func (p *BinaryProtocol) encodeData(deviceID uint32, dataType uint8, data []byte) []byte { 708 | length := uint16(MinPacketLength + len(data)) 709 | 710 | buf := new(bytes.Buffer) 711 | 712 | // 写入头部信息 713 | binary.Write(buf, binary.BigEndian, ProtocolHeader) 714 | binary.Write(buf, binary.BigEndian, length) 715 | binary.Write(buf, binary.BigEndian, deviceID) 716 | binary.Write(buf, binary.BigEndian, dataType) 717 | buf.Write(data) 718 | 719 | // 计算CRC 720 | packet := &BinaryPacket{ 721 | Header: ProtocolHeader, 722 | Length: length, 723 | DeviceID: deviceID, 724 | Type: dataType, 725 | Data: data, 726 | } 727 | crc := p.calculateCRC(packet) 728 | binary.Write(buf, binary.BigEndian, crc) 729 | 730 | return buf.Bytes() 731 | } 732 | 733 | // 编码命令 734 | func (p *BinaryProtocol) encodeCommand(deviceID uint32, cmd map[string]interface{}) ([]byte, error) { 735 | // 根据命令类型编码 736 | cmdType := cmd["type"].(string) 737 | 738 | switch cmdType { 739 | case "restart": 740 | return p.encodeData(deviceID, TypeCommand, []byte{0x01}), nil 741 | case "config": 742 | // 编码配置命令 743 | return p.encodeConfigCommand(deviceID, cmd) 744 | default: 745 | return nil, fmt.Errorf("不支持的命令类型: %s", cmdType) 746 | } 747 | } 748 | 749 | // 编码配置命令 750 | func (p *BinaryProtocol) encodeConfigCommand(deviceID uint32, cmd map[string]interface{}) ([]byte, error) { 751 | buf := new(bytes.Buffer) 752 | 753 | // 命令码 754 | buf.WriteByte(0x02) 755 | 756 | // 配置参数 757 | if interval, ok := cmd["interval"]; ok { 758 | binary.Write(buf, binary.BigEndian, uint16(interval.(int))) 759 | } 760 | 761 | if threshold, ok := cmd["threshold"]; ok { 762 | binary.Write(buf, binary.BigEndian, float32(threshold.(float64))) 763 | } 764 | 765 | return p.encodeData(deviceID, TypeCommand, buf.Bytes()), nil 766 | } 767 | 768 | // 获取报警级别 769 | func (p *BinaryProtocol) getAlarmLevel(code uint8) string { 770 | switch code { 771 | case 1: 772 | return "low" 773 | case 2: 774 | return "medium" 775 | case 3: 776 | return "high" 777 | default: 778 | return "unknown" 779 | } 780 | } 781 | 782 | // 获取报警消息 783 | func (p *BinaryProtocol) getAlarmMessage(code uint8) string { 784 | switch code { 785 | case 1: 786 | return "温度异常" 787 | case 2: 788 | return "湿度异常" 789 | case 3: 790 | return "设备故障" 791 | default: 792 | return "未知报警" 793 | } 794 | } 795 | ``` 796 | 797 | ## 协议开发最佳实践 798 | 799 | ### 1. 错误处理 800 | 801 | ```go 802 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) { 803 | // 1. 使用defer恢复panic 804 | defer func() { 805 | if r := recover(); r != nil { 806 | glog.Errorf(context.Background(), "协议解析panic: %v", r) 807 | } 808 | }() 809 | 810 | // 2. 参数验证 811 | if device == nil { 812 | return nil, errors.New("设备对象为空") 813 | } 814 | 815 | if len(data) == 0 { 816 | return nil, errors.New("数据为空") 817 | } 818 | 819 | // 3. 数据长度检查 820 | if len(data) < p.getMinPacketLength() { 821 | return nil, fmt.Errorf("数据包长度不足,期望至少%d字节,实际%d字节", 822 | p.getMinPacketLength(), len(data)) 823 | } 824 | 825 | // 4. 协议头验证 826 | if !p.isValidHeader(data) { 827 | return nil, errors.New("无效的协议头") 828 | } 829 | 830 | // 5. 详细的错误信息 831 | packet, err := p.parsePacket(data) 832 | if err != nil { 833 | return nil, fmt.Errorf("解析数据包失败: %v, 原始数据: %x", err, data) 834 | } 835 | 836 | return p.processPacket(device, packet) 837 | } 838 | ``` 839 | 840 | ### 2. 性能优化 841 | 842 | ```go 843 | // 使用对象池减少内存分配 844 | var packetPool = sync.Pool{ 845 | New: func() interface{} { 846 | return &Packet{} 847 | }, 848 | } 849 | 850 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) { 851 | // 从池中获取对象 852 | packet := packetPool.Get().(*Packet) 853 | defer func() { 854 | packet.Reset() // 重置对象状态 855 | packetPool.Put(packet) 856 | }() 857 | 858 | // 解析数据 859 | err := packet.Parse(data) 860 | if err != nil { 861 | return nil, err 862 | } 863 | 864 | return p.processPacket(device, packet) 865 | } 866 | 867 | // 使用字节缓冲池 868 | var bufferPool = sync.Pool{ 869 | New: func() interface{} { 870 | return bytes.NewBuffer(make([]byte, 0, 1024)) 871 | }, 872 | } 873 | 874 | func (p *MyProtocol) Encode(device *model.Device, data interface{}, param ...string) ([]byte, error) { 875 | buf := bufferPool.Get().(*bytes.Buffer) 876 | defer func() { 877 | buf.Reset() 878 | bufferPool.Put(buf) 879 | }() 880 | 881 | // 编码数据到缓冲区 882 | err := p.encodeToBuffer(buf, data) 883 | if err != nil { 884 | return nil, err 885 | } 886 | 887 | // 复制数据(因为缓冲区会被重用) 888 | result := make([]byte, buf.Len()) 889 | copy(result, buf.Bytes()) 890 | 891 | return result, nil 892 | } 893 | ``` 894 | 895 | ### 3. 并发安全 896 | 897 | ```go 898 | type SafeProtocol struct { 899 | mu sync.RWMutex 900 | devices map[string]*DeviceInfo 901 | counters map[string]int64 902 | } 903 | 904 | type DeviceInfo struct { 905 | LastSeen time.Time 906 | PacketCount int64 907 | ErrorCount int64 908 | } 909 | 910 | func (p *SafeProtocol) updateDeviceInfo(deviceKey string) { 911 | p.mu.Lock() 912 | defer p.mu.Unlock() 913 | 914 | if info, exists := p.devices[deviceKey]; exists { 915 | info.LastSeen = time.Now() 916 | info.PacketCount++ 917 | } else { 918 | p.devices[deviceKey] = &DeviceInfo{ 919 | LastSeen: time.Now(), 920 | PacketCount: 1, 921 | ErrorCount: 0, 922 | } 923 | } 924 | } 925 | 926 | func (p *SafeProtocol) getDeviceInfo(deviceKey string) *DeviceInfo { 927 | p.mu.RLock() 928 | defer p.mu.RUnlock() 929 | 930 | if info, exists := p.devices[deviceKey]; exists { 931 | // 返回副本避免并发修改 932 | return &DeviceInfo{ 933 | LastSeen: info.LastSeen, 934 | PacketCount: info.PacketCount, 935 | ErrorCount: info.ErrorCount, 936 | } 937 | } 938 | return nil 939 | } 940 | ``` 941 | 942 | ### 4. 日志记录 943 | 944 | ```go 945 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) { 946 | ctx := context.Background() 947 | 948 | // 记录接收数据 949 | glog.Debugf(ctx, "收到设备数据: deviceKey=%s, dataLen=%d, data=%x", 950 | device.DeviceKey, len(data), data) 951 | 952 | packet, err := p.parsePacket(data) 953 | if err != nil { 954 | // 记录解析错误 955 | glog.Errorf(ctx, "解析数据包失败: deviceKey=%s, error=%v, data=%x", 956 | device.DeviceKey, err, data) 957 | return nil, err 958 | } 959 | 960 | // 记录解析成功 961 | glog.Infof(ctx, "成功解析数据包: deviceKey=%s, type=%s, dataLen=%d", 962 | device.DeviceKey, packet.GetTypeName(), len(packet.Data)) 963 | 964 | result, err := p.processPacket(device, packet) 965 | if err != nil { 966 | glog.Errorf(ctx, "处理数据包失败: deviceKey=%s, error=%v", 967 | device.DeviceKey, err) 968 | return nil, err 969 | } 970 | 971 | if result != nil { 972 | glog.Debugf(ctx, "回复设备数据: deviceKey=%s, replyLen=%d, reply=%x", 973 | device.DeviceKey, len(result), result) 974 | } 975 | 976 | return result, nil 977 | } 978 | ``` 979 | 980 | ### 5. 配置化协议 981 | 982 | ```go 983 | type ProtocolConfig struct { 984 | Header []byte `json:"header"` 985 | MinLength int `json:"minLength"` 986 | MaxLength int `json:"maxLength"` 987 | Timeout time.Duration `json:"timeout"` 988 | EnableCRC bool `json:"enableCRC"` 989 | ByteOrder string `json:"byteOrder"` // "big" or "little" 990 | } 991 | 992 | type ConfigurableProtocol struct { 993 | config *ProtocolConfig 994 | } 995 | 996 | func NewConfigurableProtocol(config *ProtocolConfig) *ConfigurableProtocol { 997 | return &ConfigurableProtocol{ 998 | config: config, 999 | } 1000 | } 1001 | 1002 | func (p *ConfigurableProtocol) isValidPacket(data []byte) bool { 1003 | if len(data) < p.config.MinLength { 1004 | return false 1005 | } 1006 | 1007 | if len(data) > p.config.MaxLength { 1008 | return false 1009 | } 1010 | 1011 | if len(p.config.Header) > 0 { 1012 | if len(data) < len(p.config.Header) { 1013 | return false 1014 | } 1015 | 1016 | if !bytes.Equal(data[:len(p.config.Header)], p.config.Header) { 1017 | return false 1018 | } 1019 | } 1020 | 1021 | return true 1022 | } 1023 | ``` 1024 | 1025 | ## 测试协议 1026 | 1027 | ### 单元测试 1028 | 1029 | ```go 1030 | package protocol 1031 | 1032 | import ( 1033 | "testing" 1034 | "github.com/sagoo-cloud/iotgateway/model" 1035 | ) 1036 | 1037 | func TestTextProtocol_ParsePacket(t *testing.T) { 1038 | protocol := &TextProtocol{} 1039 | 1040 | tests := []struct { 1041 | name string 1042 | data []byte 1043 | want *TextPacket 1044 | wantErr bool 1045 | }{ 1046 | { 1047 | name: "valid heartbeat packet", 1048 | data: []byte("DEVICE001|HB|OK|1234"), 1049 | want: &TextPacket{ 1050 | DeviceID: "DEVICE001", 1051 | DataType: "HB", 1052 | Data: "OK", 1053 | CRC: "1234", 1054 | }, 1055 | wantErr: false, 1056 | }, 1057 | { 1058 | name: "invalid packet format", 1059 | data: []byte("DEVICE001|HB"), 1060 | want: nil, 1061 | wantErr: true, 1062 | }, 1063 | } 1064 | 1065 | for _, tt := range tests { 1066 | t.Run(tt.name, func(t *testing.T) { 1067 | got, err := protocol.parseTextPacket(tt.data) 1068 | if (err != nil) != tt.wantErr { 1069 | t.Errorf("parseTextPacket() error = %v, wantErr %v", err, tt.wantErr) 1070 | return 1071 | } 1072 | 1073 | if !tt.wantErr && got != nil { 1074 | if got.DeviceID != tt.want.DeviceID || 1075 | got.DataType != tt.want.DataType || 1076 | got.Data != tt.want.Data || 1077 | got.CRC != tt.want.CRC { 1078 | t.Errorf("parseTextPacket() = %+v, want %+v", got, tt.want) 1079 | } 1080 | } 1081 | }) 1082 | } 1083 | } 1084 | 1085 | func TestTextProtocol_Decode(t *testing.T) { 1086 | protocol := &TextProtocol{} 1087 | device := &model.Device{ 1088 | DeviceKey: "DEVICE001", 1089 | } 1090 | 1091 | // 测试心跳数据 1092 | data := []byte("DEVICE001|HB|OK|1234") 1093 | result, err := protocol.Decode(device, data) 1094 | 1095 | if err != nil { 1096 | t.Errorf("Decode() error = %v", err) 1097 | } 1098 | 1099 | if result == nil { 1100 | t.Error("Decode() result should not be nil") 1101 | } 1102 | } 1103 | ``` 1104 | 1105 | ### 集成测试 1106 | 1107 | ```go 1108 | func TestProtocolIntegration(t *testing.T) { 1109 | // 创建测试网关 1110 | ctx := context.Background() 1111 | protocol := &TextProtocol{} 1112 | gateway, err := iotgateway.NewGateway(ctx, protocol) 1113 | if err != nil { 1114 | t.Fatalf("创建网关失败: %v", err) 1115 | } 1116 | 1117 | // 模拟设备连接 1118 | device := &model.Device{ 1119 | DeviceKey: "TEST_DEVICE", 1120 | ClientID: "client_001", 1121 | } 1122 | 1123 | // 测试初始化 1124 | initData := []byte("TEST_DEVICE|HB|INIT|1234") 1125 | err = protocol.Init(device, initData) 1126 | if err != nil { 1127 | t.Errorf("Init() error = %v", err) 1128 | } 1129 | 1130 | // 测试数据解析 1131 | testData := []byte("TEST_DEVICE|DATA|temp=25.6,hum=60.5|5678") 1132 | result, err := protocol.Decode(device, testData) 1133 | if err != nil { 1134 | t.Errorf("Decode() error = %v", err) 1135 | } 1136 | 1137 | if result == nil { 1138 | t.Error("Decode() should return response") 1139 | } 1140 | 1141 | // 测试编码 1142 | cmd := map[string]interface{}{ 1143 | "command": "restart", 1144 | "params": "delay=5", 1145 | } 1146 | encoded, err := protocol.Encode(device, cmd) 1147 | if err != nil { 1148 | t.Errorf("Encode() error = %v", err) 1149 | } 1150 | 1151 | if len(encoded) == 0 { 1152 | t.Error("Encode() should return data") 1153 | } 1154 | } 1155 | ``` 1156 | 1157 | ## 调试技巧 1158 | 1159 | ### 1. 数据包分析 1160 | 1161 | ```go 1162 | func (p *MyProtocol) debugPacket(data []byte) { 1163 | glog.Debugf(context.Background(), "原始数据包分析:") 1164 | glog.Debugf(context.Background(), " 长度: %d", len(data)) 1165 | glog.Debugf(context.Background(), " 十六进制: %x", data) 1166 | glog.Debugf(context.Background(), " 字符串: %q", string(data)) 1167 | 1168 | // 按字节分析 1169 | for i, b := range data { 1170 | glog.Debugf(context.Background(), " [%02d]: 0x%02X (%d) '%c'", 1171 | i, b, b, printableChar(b)) 1172 | } 1173 | } 1174 | 1175 | func printableChar(b byte) rune { 1176 | if b >= 32 && b <= 126 { 1177 | return rune(b) 1178 | } 1179 | return '.' 1180 | } 1181 | ``` 1182 | 1183 | ### 2. 性能监控 1184 | 1185 | ```go 1186 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) { 1187 | start := time.Now() 1188 | defer func() { 1189 | duration := time.Since(start) 1190 | if duration > 100*time.Millisecond { 1191 | glog.Warnf(context.Background(), "协议解析耗时过长: deviceKey=%s, duration=%v", 1192 | device.DeviceKey, duration) 1193 | } 1194 | }() 1195 | 1196 | // 解析逻辑... 1197 | } 1198 | ``` 1199 | 1200 | ### 3. 状态监控 1201 | 1202 | ```go 1203 | type ProtocolStats struct { 1204 | TotalPackets int64 1205 | SuccessPackets int64 1206 | ErrorPackets int64 1207 | LastError string 1208 | LastErrorTime time.Time 1209 | } 1210 | 1211 | func (p *MyProtocol) updateStats(success bool, err error) { 1212 | p.mu.Lock() 1213 | defer p.mu.Unlock() 1214 | 1215 | p.stats.TotalPackets++ 1216 | if success { 1217 | p.stats.SuccessPackets++ 1218 | } else { 1219 | p.stats.ErrorPackets++ 1220 | if err != nil { 1221 | p.stats.LastError = err.Error() 1222 | p.stats.LastErrorTime = time.Now() 1223 | } 1224 | } 1225 | } 1226 | 1227 | func (p *MyProtocol) GetStats() ProtocolStats { 1228 | p.mu.RLock() 1229 | defer p.mu.RUnlock() 1230 | 1231 | return p.stats 1232 | } 1233 | ``` 1234 | 1235 | 通过以上指南,您可以开发出高质量、高性能的协议处理器,满足各种设备接入需求。 -------------------------------------------------------------------------------- /docs/quick-reference.md: -------------------------------------------------------------------------------- 1 | # SagooIOT 网关 SDK 快速参考 2 | 3 | ## 快速开始 4 | 5 | ### 1. 安装依赖 6 | 7 | ```bash 8 | go get -u github.com/sagoo-cloud/iotgateway 9 | ``` 10 | 11 | ### 2. 最小化示例 12 | 13 | ```go 14 | package main 15 | 16 | import ( 17 | "github.com/gogf/gf/v2/frame/g" 18 | "github.com/gogf/gf/v2/os/gctx" 19 | "github.com/sagoo-cloud/iotgateway" 20 | ) 21 | 22 | func main() { 23 | ctx := gctx.GetInitCtx() 24 | gateway, _ := iotgateway.NewGateway(ctx, nil) 25 | gateway.Start() 26 | } 27 | ``` 28 | 29 | ## 协议处理器模板 30 | 31 | ```go 32 | type MyProtocol struct{} 33 | 34 | func (p *MyProtocol) Init(device *model.Device, data []byte) error { 35 | if device != nil { 36 | device.DeviceKey = "device_001" 37 | // 订阅平台下发事件 38 | iotgateway.ServerGateway.SubscribeServiceEvent(device.DeviceKey) 39 | iotgateway.ServerGateway.SubscribeSetEvent(device.DeviceKey) 40 | } 41 | return nil 42 | } 43 | 44 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) { 45 | // 解析设备数据 46 | // 触发数据上报 47 | properties := map[string]interface{}{ 48 | "temperature": 25.6, 49 | "humidity": 60.5, 50 | } 51 | 52 | eventData := g.Map{ 53 | "DeviceKey": device.DeviceKey, 54 | "PropertieDataList": properties, 55 | } 56 | event.MustFire(consts.PushAttributeDataToMQTT, eventData) 57 | 58 | return []byte("OK"), nil 59 | } 60 | 61 | func (p *MyProtocol) Encode(device *model.Device, data interface{}, param ...string) ([]byte, error) { 62 | return json.Marshal(data) 63 | } 64 | ``` 65 | 66 | ## 配置文件模板 67 | 68 | ```yaml 69 | # config/config.yaml 70 | server: 71 | name: "IoT网关" 72 | addr: ":8080" 73 | netType: "tcp" # tcp/udp/mqtt 74 | duration: 60s 75 | productKey: "your_product_key" 76 | deviceKey: "your_device_key" 77 | packetConfig: 78 | type: 3 # 0:无处理 1:固定长度 2:头部+体 3:分隔符 79 | delimiter: "\r\n" 80 | 81 | mqtt: 82 | address: "tcp://localhost:1883" 83 | username: "username" 84 | password: "password" 85 | clientId: "gateway_client" 86 | keepAliveDuration: 30s 87 | ``` 88 | 89 | ## 常用API 90 | 91 | ### 数据上报 92 | 93 | ```go 94 | // 属性数据上报 95 | func pushProperties(deviceKey string, data map[string]interface{}) { 96 | eventData := g.Map{ 97 | "DeviceKey": deviceKey, 98 | "PropertieDataList": data, 99 | } 100 | event.MustFire(consts.PushAttributeDataToMQTT, eventData) 101 | } 102 | 103 | // 事件数据上报 104 | func pushEvents(deviceKey string, events map[string]interface{}) { 105 | eventData := g.Map{ 106 | "DeviceKey": deviceKey, 107 | "EventDataList": events, 108 | } 109 | event.MustFire(consts.PushAttributeDataToMQTT, eventData) 110 | } 111 | ``` 112 | 113 | ### 服务调用处理 114 | 115 | ```go 116 | // 注册服务处理器 117 | func initServiceHandlers() { 118 | event.On("restart", event.ListenerFunc(handleRestart), event.Normal) 119 | event.On("getStatus", event.ListenerFunc(handleGetStatus), event.Normal) 120 | } 121 | 122 | func handleRestart(e event.Event) error { 123 | deviceKey := gconv.String(e.Data()["DeviceKey"]) 124 | messageId := gconv.String(e.Data()["MessageID"]) 125 | 126 | // 执行重启逻辑 127 | result := map[string]interface{}{ 128 | "status": "success", 129 | "message": "设备重启成功", 130 | } 131 | 132 | // 回复平台 133 | replyData := g.Map{ 134 | "DeviceKey": deviceKey, 135 | "MessageID": messageId, 136 | "ReplyData": result, 137 | } 138 | event.Async(consts.PushServiceResDataToMQTT, replyData) 139 | 140 | return nil 141 | } 142 | ``` 143 | 144 | ### 设备管理 145 | 146 | ```go 147 | // 设备上线 148 | func deviceOnline(deviceKey string, device *model.Device) { 149 | vars.UpdateDeviceMap(deviceKey, device) 150 | iotgateway.ServerGateway.SubscribeServiceEvent(deviceKey) 151 | iotgateway.ServerGateway.SubscribeSetEvent(deviceKey) 152 | } 153 | 154 | // 设备离线 155 | func deviceOffline(deviceKey string) { 156 | vars.DeleteFromDeviceMap(deviceKey) 157 | vars.ClearDeviceMessages(deviceKey) 158 | } 159 | 160 | // 获取设备状态 161 | func getDeviceStatus(deviceKey string) (*model.Device, error) { 162 | return vars.GetDeviceMap(deviceKey) 163 | } 164 | ``` 165 | 166 | ## 常用常量 167 | 168 | ```go 169 | // 事件类型 170 | consts.PushAttributeDataToMQTT // 属性上报 171 | consts.PushServiceResDataToMQTT // 服务调用响应 172 | consts.PushSetResDataToMQTT // 属性设置响应 173 | 174 | // 网络类型 175 | consts.NetTypeTcpServer // TCP服务器 176 | consts.NetTypeUDPServer // UDP服务器 177 | consts.NetTypeMqttServer // MQTT客户端 178 | 179 | // 粘包处理类型 180 | network.NoHandling // 不处理 181 | network.FixedLength // 固定长度 182 | network.HeaderBodySeparate // 头部+体 183 | network.Delimiter // 分隔符 184 | ``` 185 | 186 | ## 错误处理模板 187 | 188 | ```go 189 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) { 190 | defer func() { 191 | if r := recover(); r != nil { 192 | glog.Errorf(context.Background(), "协议解析异常: %v", r) 193 | } 194 | }() 195 | 196 | if len(data) == 0 { 197 | return nil, errors.New("数据为空") 198 | } 199 | 200 | if device == nil { 201 | return nil, errors.New("设备信息为空") 202 | } 203 | 204 | // 解析逻辑... 205 | 206 | return nil, nil 207 | } 208 | ``` 209 | 210 | ## 调试技巧 211 | 212 | ### 启用调试日志 213 | 214 | ```go 215 | import "github.com/gogf/gf/v2/os/glog" 216 | 217 | func init() { 218 | glog.SetLevel(glog.LEVEL_ALL) 219 | glog.SetStdoutPrint(true) 220 | } 221 | ``` 222 | 223 | ### 监控缓存状态 224 | 225 | ```go 226 | func monitorCache() { 227 | stats := vars.GetCacheStats() 228 | glog.Infof(context.Background(), "缓存统计: %+v", stats) 229 | } 230 | ``` 231 | 232 | ### 健康检查 233 | 234 | ```go 235 | func healthCheck() map[string]interface{} { 236 | return map[string]interface{}{ 237 | "status": "running", 238 | "mqtt_connected": iotgateway.ServerGateway.MQTTClient.IsConnected(), 239 | "device_count": vars.CountDevices(), 240 | "cache_stats": vars.GetCacheStats(), 241 | } 242 | } 243 | ``` 244 | 245 | ## 性能优化 246 | 247 | ### 对象池使用 248 | 249 | ```go 250 | var packetPool = sync.Pool{ 251 | New: func() interface{} { 252 | return &Packet{} 253 | }, 254 | } 255 | 256 | func parsePacket(data []byte) *Packet { 257 | packet := packetPool.Get().(*Packet) 258 | defer packetPool.Put(packet) 259 | 260 | packet.Reset() 261 | packet.Parse(data) 262 | return packet 263 | } 264 | ``` 265 | 266 | ### 批量数据处理 267 | 268 | ```go 269 | func batchProcessData(devices []string, data []map[string]interface{}) { 270 | for i, deviceKey := range devices { 271 | go func(key string, d map[string]interface{}) { 272 | pushProperties(key, d) 273 | }(deviceKey, data[i]) 274 | } 275 | } 276 | ``` 277 | 278 | ## 常见问题解决 279 | 280 | ### 1. 设备连接不上 281 | 282 | ```bash 283 | # 检查端口 284 | netstat -tlnp | grep 8080 285 | 286 | # 测试连接 287 | telnet gateway_ip 8080 288 | ``` 289 | 290 | ### 2. MQTT连接失败 291 | 292 | ```go 293 | // 检查MQTT连接状态 294 | if !iotgateway.ServerGateway.MQTTClient.IsConnected() { 295 | glog.Error(context.Background(), "MQTT连接断开") 296 | } 297 | ``` 298 | 299 | ### 3. 内存泄漏 300 | 301 | ```go 302 | // 定期清理过期缓存 303 | go func() { 304 | ticker := time.NewTicker(5 * time.Minute) 305 | for range ticker.C { 306 | stats := vars.GetCacheStats() 307 | if expiredCount := stats["expiredCount"].(int); expiredCount > 100 { 308 | glog.Warn(context.Background(), "发现大量过期消息") 309 | } 310 | } 311 | }() 312 | ``` 313 | 314 | ## 部署脚本 315 | 316 | ### Dockerfile 317 | 318 | ```dockerfile 319 | FROM golang:1.23-alpine AS builder 320 | WORKDIR /app 321 | COPY . . 322 | RUN go mod download 323 | RUN go build -o gateway . 324 | 325 | FROM alpine:latest 326 | RUN apk --no-cache add ca-certificates 327 | WORKDIR /root/ 328 | COPY --from=builder /app/gateway . 329 | COPY --from=builder /app/config ./config 330 | CMD ["./gateway"] 331 | ``` 332 | 333 | ### docker-compose.yml 334 | 335 | ```yaml 336 | version: '3.8' 337 | services: 338 | gateway: 339 | build: . 340 | ports: 341 | - "8080:8080" 342 | volumes: 343 | - ./config:/root/config 344 | - ./logs:/root/logs 345 | environment: 346 | - ENV=production 347 | restart: unless-stopped 348 | ``` 349 | 350 | ### 启动脚本 351 | 352 | ```bash 353 | #!/bin/bash 354 | # start.sh 355 | 356 | # 设置环境变量 357 | export GATEWAY_ENV=production 358 | 359 | # 创建日志目录 360 | mkdir -p logs 361 | 362 | # 启动网关 363 | ./gateway > logs/gateway.log 2>&1 & 364 | 365 | echo "网关已启动,PID: $!" 366 | ``` 367 | 368 | ## 监控脚本 369 | 370 | ```bash 371 | #!/bin/bash 372 | # monitor.sh 373 | 374 | while true; do 375 | # 检查进程是否存在 376 | if ! pgrep -f "gateway" > /dev/null; then 377 | echo "$(date): 网关进程不存在,正在重启..." 378 | ./start.sh 379 | fi 380 | 381 | # 检查内存使用 382 | mem_usage=$(ps -o pid,ppid,cmd,%mem,%cpu --sort=-%mem -C gateway | tail -n +2) 383 | echo "$(date): 内存使用情况: $mem_usage" 384 | 385 | sleep 60 386 | done 387 | ``` -------------------------------------------------------------------------------- /events/const.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | const ( 4 | PropertySetEvent = "property" // PropertySetEvent 属性设置下发事件,SagooIoT平台下发属性设置命令时触发 5 | 6 | GetGatewayVersionEvent = "getGatewayVersion" // ServiceCallEvent 服务调用下发事件,SagooIoT平台下发服务调用getGatewayVersion命令时触发 7 | GetGatewayConfig = "getGatewayConfig" // ServiceCallEvent 服务调用下发事件,SagooIoT平台下发服务调用getGatewayConfig命令时触发 8 | ) 9 | -------------------------------------------------------------------------------- /events/pushEvents.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | 9 | "github.com/gogf/gf/v2/encoding/gjson" 10 | "github.com/gogf/gf/v2/frame/g" 11 | "github.com/gogf/gf/v2/os/glog" 12 | "github.com/gogf/gf/v2/os/gtime" 13 | "github.com/gogf/gf/v2/util/gconv" 14 | "github.com/gogf/gf/v2/util/guid" 15 | "github.com/gookit/event" 16 | "github.com/sagoo-cloud/iotgateway/consts" 17 | "github.com/sagoo-cloud/iotgateway/lib" 18 | "github.com/sagoo-cloud/iotgateway/log" 19 | "github.com/sagoo-cloud/iotgateway/model" 20 | "github.com/sagoo-cloud/iotgateway/mqttClient" 21 | "github.com/sagoo-cloud/iotgateway/mqttProtocol" 22 | "github.com/sagoo-cloud/iotgateway/vars" 23 | "github.com/sagoo-cloud/iotgateway/version" 24 | ) 25 | 26 | // LoadingPublishEvent 加载发布事件 27 | func LoadingPublishEvent() { 28 | //推送属性数据到mqtt服务事件 29 | event.On(consts.PushAttributeDataToMQTT, event.ListenerFunc(pushAttributeDataToMQTT), event.Normal) 30 | //推送服务调用响应数据到mqtt服务事件 31 | event.On(consts.PushServiceResDataToMQTT, event.ListenerFunc(pushServiceResDataToMQTT), event.High) 32 | //推送设置属性响应数据到mqtt服务事件 33 | event.On(consts.PushSetResDataToMQTT, event.ListenerFunc(pushSetResDataToMQTT), event.High) 34 | 35 | // 服务下发获取网关配置信息事件 36 | event.On(GetGatewayVersionEvent, event.ListenerFunc(getGatewayVersionData), event.Normal) 37 | 38 | } 39 | 40 | // pushAttributeDataToMQTT 推送属性数据到mqtt服务 41 | func pushAttributeDataToMQTT(e event.Event) (err error) { 42 | deviceKey := gconv.String(e.Data()["DeviceKey"]) 43 | if deviceKey == "" { 44 | return errors.New("设备key为空") 45 | } 46 | // propertieData 属性信息 47 | var propertieData = make(map[string]interface{}) 48 | if e.Data()["PropertieDataList"] != nil { 49 | propertieDataLIst := gconv.Map(e.Data()["PropertieDataList"]) 50 | for k, v := range propertieDataLIst { 51 | var param = mqttProtocol.PropertyNode{} 52 | switch v.(type) { 53 | case int, int16, int32, int64, 54 | uint, uint16, uint32, uint64, 55 | float32, float64, 56 | bool, string: 57 | param.Value = v 58 | case mqttProtocol.PropertyNode: 59 | param.Value = v.(mqttProtocol.PropertyNode).Value 60 | param.CreateTime = v.(mqttProtocol.PropertyNode).CreateTime 61 | default: 62 | param.Value = gconv.Map(v) 63 | } 64 | if param.CreateTime == 0 { 65 | param.CreateTime = gtime.Timestamp() 66 | } 67 | 68 | propertieData[k] = param 69 | } 70 | } 71 | 72 | //eventsData 事件信息 73 | var eventsData = make(map[string]mqttProtocol.EventNode) 74 | if e.Data()["EventDataList"] != nil { 75 | eventDataList := gconv.Map(e.Data()["EventDataList"]) 76 | for k, v := range eventDataList { 77 | var param = mqttProtocol.EventNode{} 78 | switch v.(type) { 79 | case mqttProtocol.EventNode: 80 | param.Value = v.(mqttProtocol.EventNode).Value 81 | param.CreateTime = v.(mqttProtocol.PropertyNode).CreateTime 82 | default: 83 | param.Value = gconv.Map(v) 84 | } 85 | if param.CreateTime == 0 { 86 | param.CreateTime = gtime.Timestamp() 87 | } 88 | eventsData[k] = param 89 | } 90 | } 91 | 92 | //子设备 93 | subDevice := mqttProtocol.Sub{ 94 | Identity: mqttProtocol.Identity{ProductKey: "", DeviceKey: deviceKey}, 95 | Properties: propertieData, 96 | Events: eventsData, 97 | } 98 | 99 | builder := mqttProtocol.NewGatewayBatchReqBuilder() 100 | builder.SetId(guid.S()).SetVersion("1.0") 101 | builder.AddSubDevice(subDevice) 102 | builder.SetMethod("thing.event.property.pack.post") 103 | builder.Build() 104 | data := gconv.Map(builder.Build()) 105 | outData := gjson.New(data).MustToJson() 106 | log.Debug("设备Key:%v,推送【属性数据】到MQTT服务:%s", deviceKey, outData) 107 | if err = mqttClient.PublishData(deviceKey, outData); err != nil { 108 | log.Debug("pushAttributeDataToMQTT", err.Error()) 109 | return 110 | } 111 | return 112 | } 113 | 114 | // pushServiceResDataToMQTT 推送服务调用响应数据到mqtt服务 115 | func pushServiceResDataToMQTT(e event.Event) (err error) { 116 | deviceKey := gconv.String(e.Data()["DeviceKey"]) 117 | replyData := e.Data()["ReplyData"] 118 | replyDataMap := make(map[string]interface{}) 119 | if replyData != nil { 120 | replyDataMap = gconv.Map(replyData) 121 | } 122 | 123 | // 优先尝试使用指定的消息ID(如果提供的话) 124 | var msg model.UpMessage 125 | if messageId := gconv.String(e.Data()["MessageID"]); messageId != "" { 126 | // 使用复合键精确匹配 127 | msg, err = vars.GetUpMessageByCompositeKey(deviceKey, messageId) 128 | if err != nil { 129 | // 如果复合键没找到,fallback到原有方式 130 | msg, err = vars.GetUpMessageMap(deviceKey) 131 | } 132 | } else { 133 | // 使用原有方式获取消息(保持向下兼容) 134 | msg, err = vars.GetUpMessageMap(deviceKey) 135 | } 136 | 137 | if msg.MessageID != "" && err == nil { 138 | log.Debug("==5555==监听回复信息====", msg) 139 | mqData := mqttProtocol.ServiceCallOutputRes{} 140 | mqData.Id = msg.MessageID 141 | mqData.Code = 200 142 | mqData.Message = "success" 143 | mqData.Version = "1.0" 144 | mqData.Data = replyDataMap 145 | 146 | //推送数据到mqtt 147 | topic := msg.Topic + "_reply" 148 | 149 | log.Debug("设备Key:%v,推送【服务调用应答数据】到MQTT服务:%v", deviceKey, mqData) 150 | outData, err := json.Marshal(mqData) 151 | if err != nil { 152 | log.Debug("服务回调响应序列化失败:", err.Error()) 153 | return err 154 | } 155 | 156 | log.Debug("服务回调响应:", mqData) 157 | log.Debug("服务回调响应topic:", topic) 158 | err = mqttClient.Publish(topic, outData) 159 | if err != nil { 160 | log.Debug("发送服务回调响应失败:", err.Error()) 161 | } else { 162 | log.Debug("发送服务回调响应成功") 163 | // ✅ 添加缓存清理,防止内存泄漏 164 | if messageId := gconv.String(e.Data()["MessageID"]); messageId != "" { 165 | vars.DeleteFromUpMessageMapByCompositeKey(deviceKey, messageId) 166 | } else { 167 | // 兼容原有方式 168 | vars.DeleteFromUpMessageMap(deviceKey) 169 | } 170 | } 171 | } 172 | return 173 | } 174 | 175 | // pushSetResDataToMQTT 推送属性设置响应数据到mqtt服务 176 | func pushSetResDataToMQTT(e event.Event) (err error) { 177 | deviceKey := gconv.String(e.Data()["DeviceKey"]) 178 | replyData := e.Data()["ReplyData"] 179 | glog.Debugf(context.Background(), "【IotGateway】推送属性设置响应数据到mqtt服务:设备:%s,数据:%v", deviceKey, replyData) 180 | replyDataMap := make(map[string]interface{}) 181 | if replyData != nil { 182 | replyDataMap = gconv.Map(replyData) 183 | } 184 | 185 | // 优先尝试使用指定的消息ID(如果提供的话) 186 | var msg model.UpMessage 187 | if messageId := gconv.String(e.Data()["MessageID"]); messageId != "" { 188 | // 使用复合键精确匹配 189 | msg, err = vars.GetUpMessageByCompositeKey(deviceKey, messageId) 190 | if err != nil { 191 | // 如果复合键没找到,fallback到原有方式 192 | msg, err = vars.GetUpMessageMap(deviceKey) 193 | } 194 | } else { 195 | // 使用原有方式获取消息(保持向下兼容) 196 | msg, err = vars.GetUpMessageMap(deviceKey) 197 | } 198 | 199 | if msg.MessageID != "" && err == nil { 200 | glog.Debug(context.Background(), "【IotGateway】 监听回复信息", msg) 201 | mqData := mqttProtocol.ServiceCallOutputRes{} 202 | mqData.Id = msg.MessageID 203 | mqData.Code = 200 204 | mqData.Message = "success" 205 | mqData.Version = "1.0" 206 | mqData.Data = replyDataMap 207 | 208 | //推送数据到mqtt 209 | topic := msg.Topic + "_reply" 210 | outData, err := json.Marshal(mqData) 211 | if err != nil { 212 | glog.Debugf(context.Background(), "【IotGateway】属性设置响应序列化失败:%v", err.Error()) 213 | return err 214 | } 215 | glog.Debugf(context.Background(), "【IotGateway】向平推送属性设置应答数据Topic:%s", topic) 216 | glog.Debugf(context.Background(), "【IotGateway】设备Key:%v,推送【属性设置应答数据】到MQTT服务:%v", deviceKey, string(outData)) 217 | 218 | err = mqttClient.Publish(topic, outData) 219 | if err != nil { 220 | log.Debug("【IotGateway】向mqtt服务推送属性设置响应失败:", err.Error()) 221 | } else { 222 | log.Debug("【IotGateway】向mqtt服务推送属性设置响应成功") 223 | } 224 | 225 | // ✅ 优化缓存清理逻辑 226 | if messageId := gconv.String(e.Data()["MessageID"]); messageId != "" { 227 | vars.DeleteFromUpMessageMapByCompositeKey(deviceKey, messageId) 228 | } else { 229 | // 兼容原有方式 230 | vars.DeleteFromUpMessageMap(deviceKey) 231 | } 232 | } 233 | return 234 | } 235 | 236 | // getGatewayVersionData 获取网关版本信息事件 237 | func getGatewayVersionData(e event.Event) (err error) { 238 | // 获取设备KEY 239 | ok, deviceKey := lib.GetMapValueForKey(e.Data(), "DeviceKey") 240 | if !ok { 241 | glog.Debug(context.Background(), "获取设备KEY失败") 242 | return fmt.Errorf("获取设备KEY失败: %s", e.Data()) 243 | } 244 | //==== 平台端下发调用 应答==== 245 | ra, err := vars.GetUpMessageMap(deviceKey.(string)) 246 | if err == nil { 247 | if ra.MessageID != "" { 248 | 249 | var rd = make(map[string]interface{}) 250 | rd["Version"] = version.GetVersion() 251 | rd["BuildTime"] = version.GetBuildTime() 252 | rd["CommitID"] = version.CommitID 253 | 254 | outData := g.Map{ 255 | "DeviceKey": deviceKey, 256 | "ReplyData": rd, 257 | } 258 | event.Async(consts.PushServiceResDataToMQTT, outData) 259 | } 260 | } 261 | return 262 | } 263 | -------------------------------------------------------------------------------- /gateway.go: -------------------------------------------------------------------------------- 1 | package iotgateway 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | mqtt "github.com/eclipse/paho.mqtt.golang" 7 | "github.com/gogf/gf/v2/encoding/gjson" 8 | "github.com/gogf/gf/v2/frame/g" 9 | "github.com/gogf/gf/v2/os/glog" 10 | "github.com/gogf/gf/v2/util/gconv" 11 | "github.com/gogf/gf/v2/util/guid" 12 | "github.com/gookit/event" 13 | "github.com/sagoo-cloud/iotgateway/conf" 14 | "github.com/sagoo-cloud/iotgateway/consts" 15 | "github.com/sagoo-cloud/iotgateway/events" 16 | "github.com/sagoo-cloud/iotgateway/log" 17 | "github.com/sagoo-cloud/iotgateway/mqttClient" 18 | "github.com/sagoo-cloud/iotgateway/mqttProtocol" 19 | "github.com/sagoo-cloud/iotgateway/network" 20 | "github.com/sagoo-cloud/iotgateway/vars" 21 | "github.com/sagoo-cloud/iotgateway/version" 22 | "strings" 23 | "time" 24 | ) 25 | 26 | const ( 27 | propertyTopic = "/sys/%s/%s/thing/event/property/pack/post" 28 | serviceTopic = "/sys/+/%s/thing/service/#" 29 | setTopic = "/sys/+/%s/thing/service/property/set" 30 | ) 31 | 32 | type Gateway struct { 33 | Address string 34 | Version string 35 | Status string 36 | ctx context.Context // 上下文 37 | options *conf.GatewayConfig 38 | MQTTClient mqtt.Client 39 | Server network.NetworkServer 40 | Protocol network.ProtocolHandler 41 | cancel context.CancelFunc 42 | } 43 | 44 | var ServerGateway *Gateway 45 | 46 | func NewGateway(ctx context.Context, protocol network.ProtocolHandler) (gw *Gateway, err error) { 47 | 48 | //读取配置文件 49 | options := new(conf.GatewayConfig) 50 | confData, err := g.Cfg().Data(ctx) 51 | if err != nil { 52 | glog.Debug(context.Background(), "读取配置文件失败", err) 53 | return 54 | } 55 | err = gconv.Scan(confData, options) 56 | if err != nil { 57 | glog.Error(ctx, "读取配置文件失败", err) 58 | return 59 | } 60 | 61 | options.MqttConfig.ClientId = options.GatewayServerConfig.DeviceKey 62 | client, err := mqttClient.GetMQTTClient(options.MqttConfig) //初始化mqtt客户端 63 | if err != nil { 64 | log.Debug("mqttClient.GetMQTTClient error:", err) 65 | } 66 | if options.GatewayServerConfig.NetType == "" { 67 | options.GatewayServerConfig.NetType = consts.NetTypeTcpServer 68 | } 69 | vars.GatewayServerConfig = options.GatewayServerConfig 70 | 71 | gw = &Gateway{ 72 | options: options, 73 | Address: options.GatewayServerConfig.Addr, 74 | MQTTClient: client, // will be set later 75 | Server: nil, 76 | Protocol: protocol, 77 | } 78 | gw.ctx, gw.cancel = context.WithCancel(context.Background()) 79 | defer gw.cancel() 80 | 81 | //初始化事件 82 | defer func() { 83 | err := event.CloseWait() 84 | if err != nil { 85 | glog.Debugf(context.Background(), "event.CloseWait() error: %s", err.Error()) 86 | } 87 | }() 88 | events.LoadingPublishEvent() //加载发布事件 89 | 90 | ServerGateway = gw 91 | return 92 | } 93 | func (gw *Gateway) Start() { 94 | name := gw.options.GatewayServerConfig.Name 95 | if name == "" { 96 | name = "SagooIoT Gateway Server" 97 | } 98 | ctx, cancel := context.WithCancel(context.Background()) 99 | defer cancel() 100 | 101 | //订阅网关设备服务下发事件 102 | gw.SubscribeServiceEvent(gw.options.GatewayServerConfig.DeviceKey) 103 | 104 | go gw.heartbeat(gw.options.GatewayServerConfig.Duration) //启动心跳 105 | switch gw.options.GatewayServerConfig.NetType { 106 | case consts.NetTypeTcpServer: 107 | // 创建 TCP 服务器 108 | gw.Server = network.NewTCPServer( 109 | network.WithTimeout(1*time.Minute), 110 | network.WithProtocolHandler(gw.Protocol), 111 | network.WithCleanupInterval(5*time.Minute), 112 | network.WithPacketHandling(gw.options.GatewayServerConfig.PacketConfig), 113 | ) 114 | glog.Infof(ctx, "%s started Tcp listening on %v", name, gw.options.GatewayServerConfig.Addr) 115 | // 启动 TCP 服务器 116 | if err := gw.Server.Start(ctx, gw.options.GatewayServerConfig.Addr); err != nil { 117 | log.Info("TCP 服务器错误: %v", err) 118 | } 119 | 120 | case consts.NetTypeUDPServer: 121 | // 创建 UDP 服务器 122 | gw.Server = network.NewUDPServer( 123 | network.WithTimeout(1*time.Minute), 124 | network.WithProtocolHandler(gw.Protocol), 125 | network.WithCleanupInterval(5*time.Minute), 126 | ) 127 | glog.Infof(ctx, "%s started UDP listening on %v", name, gw.options.GatewayServerConfig.Addr) 128 | // 启动 UDP 服务器 129 | if err := gw.Server.Start(ctx, gw.options.GatewayServerConfig.Addr); err != nil { 130 | log.Info("UDP 服务器错误: %v", err) 131 | } 132 | case consts.NetTypeMqttServer: 133 | //启动mqtt类型的设备网关服务 134 | glog.Infof(context.Background(), "%s started listening ......", name) 135 | //log.Info("%s started listening ......") 136 | gw.SubscribeDeviceUpData() 137 | select {} 138 | } 139 | 140 | return 141 | } 142 | 143 | // heartbeat 网关服务心跳 144 | func (gw *Gateway) heartbeat(duration time.Duration) { 145 | if duration == 0 { 146 | duration = 60 147 | } 148 | ticker := time.NewTicker(time.Second * duration) 149 | 150 | // 立即发送一次心跳消息 151 | gw.sendHeartbeat() 152 | 153 | for { 154 | select { 155 | case <-ticker.C: 156 | // 发送心跳消息 157 | gw.sendHeartbeat() 158 | } 159 | } 160 | } 161 | 162 | // sendHeartbeat 发送心跳消息 163 | func (gw *Gateway) sendHeartbeat() { 164 | if gw.MQTTClient == nil { 165 | log.Error("【IotGateway】sendHeartbeat error: Client has lost connection with the MQTT broker.") 166 | return 167 | } 168 | if !gw.MQTTClient.IsConnected() { 169 | log.Error("【IotGateway】sendHeartbeat error: Client has lost connection with the MQTT broker.") 170 | return 171 | } 172 | 173 | // 设备数量 174 | versionInfo := version.GetVersion() 175 | if versionInfo == "" || versionInfo == "0.0" { 176 | versionInfo = "v0.0.1" 177 | } 178 | count := vars.CountDevices() 179 | builder := mqttProtocol.NewGatewayBatchReqBuilder().SetId(guid.S()) 180 | builder.SetVersion("1.0") 181 | builder.AddProperty("Status", 0) 182 | builder.AddProperty("Count", count) 183 | builder.AddProperty("Version", versionInfo) 184 | builder.SetMethod("thing.event.property.pack.post") 185 | data := gconv.Map(builder.Build()) 186 | outData := gjson.New(data).MustToJson() 187 | topic := fmt.Sprintf(propertyTopic, vars.GatewayServerConfig.ProductKey, vars.GatewayServerConfig.DeviceKey) 188 | token := gw.MQTTClient.Publish(topic, 1, false, outData) 189 | if token.Error() != nil { 190 | glog.Errorf(context.Background(), "【IotGateway】publish error: %s", token.Error()) 191 | } 192 | glog.Debugf(context.Background(), "【IotGateway】网关向平台发送心跳数据:%s", string(outData)) 193 | 194 | } 195 | 196 | // SubscribeDeviceUpData 在mqtt网络类型的设备情况下,订阅设备上传数据 197 | func (gw *Gateway) SubscribeDeviceUpData() { 198 | if gw.MQTTClient == nil || !gw.MQTTClient.IsConnected() { 199 | log.Error("【IotGateway】SubscribeDeviceUpData error: Client has lost connection with the MQTT broker.") 200 | return 201 | } 202 | log.Debug("订阅设备上传数据topic: ", gw.options.GatewayServerConfig.SerUpTopic) 203 | if gw.options.GatewayServerConfig.SerUpTopic != "" { 204 | token := gw.MQTTClient.Subscribe(gw.options.GatewayServerConfig.SerUpTopic, 1, onDeviceUpDataMessage) 205 | if token.Error() != nil { 206 | log.Debug("subscribe error: ", token.Error()) 207 | } 208 | } 209 | 210 | } 211 | 212 | // onDeviceUpDataMessage 设备上传数据 213 | var onDeviceUpDataMessage mqtt.MessageHandler = func(client mqtt.Client, msg mqtt.Message) { 214 | //忽略_reply结尾的topic 215 | if strings.HasSuffix(msg.Topic(), "_reply") { 216 | return 217 | } 218 | if msg != nil { 219 | ServerGateway.Protocol.Decode(nil, msg.Payload()) 220 | } 221 | } 222 | 223 | // DeviceDownData 在mqtt网络类型的设备情况下,向设备下发数据 224 | func (gw *Gateway) DeviceDownData(data interface{}) { 225 | if gw.MQTTClient == nil || !gw.MQTTClient.IsConnected() { 226 | log.Error("【IotGateway】DeviceDownData error: Client has lost connection with the MQTT broker.") 227 | return 228 | } 229 | if gw.options.GatewayServerConfig.SerDownTopic != "" { 230 | token := gw.MQTTClient.Publish(gw.options.GatewayServerConfig.SerDownTopic, 1, false, data) 231 | if token.Error() != nil { 232 | log.Error("publish error: %s", token.Error()) 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sagoo-cloud/iotgateway 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/eclipse/paho.mqtt.golang v1.5.0 7 | github.com/fatih/color v1.18.0 8 | github.com/gogf/gf/v2 v2.9.0 9 | github.com/gookit/event v1.1.2 10 | golang.org/x/text v0.23.0 11 | ) 12 | 13 | require ( 14 | github.com/BurntSushi/toml v1.4.0 // indirect 15 | github.com/clbanning/mxj/v2 v2.7.0 // indirect 16 | github.com/emirpasic/gods v1.18.1 // indirect 17 | github.com/fsnotify/fsnotify v1.7.0 // indirect 18 | github.com/go-logr/logr v1.4.2 // indirect 19 | github.com/go-logr/stdr v1.2.2 // indirect 20 | github.com/google/uuid v1.6.0 // indirect 21 | github.com/gorilla/websocket v1.5.3 // indirect 22 | github.com/grokify/html-strip-tags-go v0.1.0 // indirect 23 | github.com/kr/text v0.2.0 // indirect 24 | github.com/magiconair/properties v1.8.9 // indirect 25 | github.com/mattn/go-colorable v0.1.13 // indirect 26 | github.com/mattn/go-isatty v0.0.20 // indirect 27 | github.com/mattn/go-runewidth v0.0.16 // indirect 28 | github.com/olekukonko/tablewriter v0.0.5 // indirect 29 | github.com/rivo/uniseg v0.4.7 // indirect 30 | go.opentelemetry.io/otel v1.32.0 // indirect 31 | go.opentelemetry.io/otel/metric v1.32.0 // indirect 32 | go.opentelemetry.io/otel/sdk v1.32.0 // indirect 33 | go.opentelemetry.io/otel/trace v1.32.0 // indirect 34 | golang.org/x/net v0.32.0 // indirect 35 | golang.org/x/sync v0.12.0 // indirect 36 | golang.org/x/sys v0.28.0 // indirect 37 | gopkg.in/yaml.v3 v3.0.1 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= 2 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 3 | github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= 4 | github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o= 7 | github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk= 8 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 9 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 10 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 11 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 12 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 13 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 14 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 15 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 16 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 17 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 18 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 19 | github.com/gogf/gf/v2 v2.8.3 h1:h9Px3lqJnnH6It0AqHRz4/1hx0JmvaSf1IvUir5x1rA= 20 | github.com/gogf/gf/v2 v2.8.3/go.mod h1:n++xPYGUUMadw6IygLEgGZqc6y6DRLrJKg5kqCrPLWY= 21 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 22 | github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= 23 | github.com/gookit/event v1.1.2 h1:cYZWKJeoJWnP1ZxW1G+36GViV+hH9ksEorLqVw901Nw= 24 | github.com/gookit/event v1.1.2/go.mod h1:YIYR3fXnwEq1tey3JfepMt19Mzm2uxmqlpc7Dj6Ekng= 25 | github.com/gookit/goutil v0.6.15 h1:mMQ0ElojNZoyPD0eVROk5QXJPh2uKR4g06slgPDF5Jo= 26 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 27 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 28 | github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4= 29 | github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= 30 | github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= 31 | github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 32 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 33 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 34 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 35 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 36 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 37 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 38 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 39 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 40 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 41 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 42 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 43 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 44 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 45 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 46 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 47 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 48 | go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= 49 | go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= 50 | go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= 51 | go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= 52 | go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= 53 | go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= 54 | go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= 55 | go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= 56 | golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= 57 | golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= 58 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 59 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 60 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 61 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 62 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 63 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 64 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 65 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 66 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 67 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 68 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 69 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 70 | -------------------------------------------------------------------------------- /lib/strings.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "golang.org/x/text/encoding/simplifiedchinese" 10 | "golang.org/x/text/transform" 11 | "io" 12 | "strings" 13 | ) 14 | 15 | // ChineseToHex 将中文转为GBK编码的十六进制 16 | func ChineseToHex(src string) (string, error) { 17 | // 创建一个将文本转换为GBK编码的转换器 18 | t := simplifiedchinese.GBK.NewEncoder() 19 | reader := transform.NewReader(strings.NewReader(src), t) 20 | 21 | // 读取所有经过转换的内容 22 | res, err := io.ReadAll(reader) 23 | if err != nil { 24 | return "", errors.New("read transformation result failed: " + err.Error()) 25 | } 26 | 27 | // 将GBK编码转换为十六进制 28 | hexStr := hex.EncodeToString(res) 29 | 30 | return hexStr, nil 31 | } 32 | 33 | // GetBytesByInt 按两个字节进行取值转为数组 34 | func GetBytesByInt(data []byte) []uint16 { 35 | // 创建一个 uint16 切片来保存16位的值 36 | values := make([]uint16, len(data)/2) 37 | // 每两个字节创建一个16位的值,存入values切片 38 | for i := 0; i < len(data); i += 2 { 39 | values[i/2] = binary.BigEndian.Uint16(data[i : i+2]) 40 | } 41 | return values 42 | } 43 | 44 | // HexToBytes 将十六进制字符串转为[]byte 45 | func HexToBytes(hexData string) ([]byte, error) { 46 | // 如果有0x前缀,去掉 47 | if strings.HasPrefix(hexData, "0x") { 48 | hexData = hexData[2:] 49 | } 50 | // 使用encoding/hex包解码 51 | return hex.DecodeString(hexData) 52 | } 53 | 54 | // GetTopicInfo 通过topic获取设备KEY 3,deviceKey 55 | func GetTopicInfo(valueName, topic string) string { 56 | splits := strings.Split(topic, "/") 57 | if len(splits) >= 3 { 58 | switch valueName { 59 | case "deviceKey": 60 | return splits[3] 61 | case "productKey": 62 | return splits[2] 63 | } 64 | } 65 | return "" 66 | } 67 | 68 | // RandString 生成随机字符串 69 | func RandString(n int) string { 70 | b := make([]byte, n) 71 | rand.Read(b) 72 | return fmt.Sprintf("%x", b) 73 | } 74 | 75 | // GetMapValueForKey 76 | // 从指定的map中获取指定的值,它接受一个map和一个要检查的key 77 | // 如果key存在,则返回true和对应的值 78 | // 如果key不存在,则返回false和nil 79 | func GetMapValueForKey(myMap map[string]any, key string) (bool, any) { 80 | value, ok := myMap[key] 81 | return ok, value 82 | } 83 | -------------------------------------------------------------------------------- /lib/strings_test.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import "testing" 4 | 5 | func TestRandString(t *testing.T) { 6 | t.Log(RandString(16)) 7 | } 8 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/fatih/color" 9 | ) 10 | 11 | // Level 定义日志级别 12 | type Level uint8 13 | 14 | const ( 15 | DEBUG Level = iota // 调试级别 16 | INFO // 信息级别 17 | WARNING // 警告级别 18 | ERROR // 错误级别 19 | FATAL // 致命错误级别 20 | ) 21 | 22 | // Logger 是日志结构体 23 | type Logger struct { 24 | level Level // 日志级别 25 | } 26 | 27 | // 全局默认logger 28 | var std = New(INFO) 29 | 30 | // New 创建一个logger 31 | func New(level Level) *Logger { 32 | return &Logger{level: level} 33 | } 34 | 35 | // 写入日志 36 | func (l *Logger) log(lv Level, format string, a ...any) { 37 | if lv < l.level { 38 | return 39 | } 40 | timeStr := time.Now().Format("2006-01-02 15:04:05.000") // 当前时间 41 | msg := fmt.Sprintf(format, a...) 42 | 43 | // 构建日志内容 44 | var log string 45 | switch lv { 46 | case DEBUG: 47 | log = timeStr + color.BlueString(" [DEBU] ") + msg 48 | 49 | case INFO: 50 | log = timeStr + color.GreenString(" [INFO] ") + msg 51 | 52 | case WARNING: 53 | log = timeStr + color.YellowString(" [WARN] ") + msg 54 | 55 | case ERROR: 56 | log = timeStr + color.RedString(" [ERRO] ") + msg 57 | 58 | case FATAL: 59 | log = timeStr + color.RedString(" [FATA] ") + msg 60 | 61 | } 62 | 63 | fmt.Fprintln(os.Stderr, log) 64 | } 65 | 66 | // Debug 调试级别日志 67 | func (l *Logger) Debug(format string, a ...any) { 68 | l.log(DEBUG, format, a...) 69 | } 70 | 71 | // Info 信息级别日志 72 | func (l *Logger) Info(format string, a ...any) { 73 | l.log(INFO, format, a...) 74 | } 75 | 76 | // Warn 警告级别日志 77 | func (l *Logger) Warn(format string, a ...any) { 78 | l.log(WARNING, format, a...) 79 | } 80 | 81 | // Error 错误级别日志 82 | func (l *Logger) Error(format string, a ...any) { 83 | l.log(ERROR, format, a...) 84 | } 85 | 86 | // Fatal 致命级别日志并退出 87 | func (l *Logger) Fatal(format string, a ...any) { 88 | l.log(FATAL, format, a...) 89 | os.Exit(1) 90 | } 91 | 92 | // SetLevel 设置日志级别 93 | func (l *Logger) SetLevel(lv Level) { 94 | l.level = lv 95 | } 96 | 97 | // GetLevel 获取日志级别 98 | func (l *Logger) GetLevel() Level { 99 | return l.level 100 | } 101 | 102 | // Debug 包级别日志函数 103 | func Debug(format string, a ...any) { 104 | std.Debug(format, a) 105 | } 106 | 107 | func Info(format string, a ...any) { 108 | std.Info(format, a) 109 | } 110 | 111 | func Warn(format string, a ...any) { 112 | std.Warn(format, a) 113 | } 114 | 115 | func Error(format string, a ...any) { 116 | std.Error(format, a) 117 | } 118 | 119 | func Fatal(format string, a ...any) { 120 | std.Fatal(format, a) 121 | } 122 | -------------------------------------------------------------------------------- /model/device.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "net" 5 | "time" 6 | ) 7 | 8 | // Device 设备数据 9 | type Device struct { 10 | DeviceKey string // 设备唯一标识 11 | ClientID string // 客户端ID 12 | OnlineStatus bool // 在线状态 13 | Conn net.Conn // 连接 14 | Metadata map[string]interface{} // 元数据 15 | Info map[string]interface{} // 设备信息 16 | AlarmInfo map[string]interface{} // 报警信息 17 | LastActive time.Time // 最后活跃时间 18 | } 19 | -------------------------------------------------------------------------------- /model/gateway.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // GatewayInfo 网关数据 4 | type GatewayInfo struct { 5 | ProductKey string `json:"productKey"` 6 | DeviceKey string `json:"deviceKey"` 7 | DeviceName string `json:"deviceName"` 8 | Description string `json:"description"` 9 | DeviceType string `json:"deviceType"` 10 | Version string `json:"version"` 11 | Manufacturer string `json:"manufacturer"` 12 | } 13 | 14 | // UpMessage 上行消息 15 | type UpMessage struct { 16 | MessageID string `json:"messageId"` 17 | SendTime int64 `json:"sendTime"` 18 | RequestCode string `json:"requestCode"` 19 | MethodName string `json:"methodName"` 20 | Topic string `json:"topic"` 21 | } 22 | 23 | // DownMessage 下行消息 24 | type DownMessage struct { 25 | FuncCode string `json:"funcCode"` 26 | ChannelNumber string `json:"channelNumber"` 27 | ErrorCode string `json:"errorCode"` 28 | } 29 | -------------------------------------------------------------------------------- /mqttClient/client.go: -------------------------------------------------------------------------------- 1 | package mqttClient 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | mqtt "github.com/eclipse/paho.mqtt.golang" 9 | "github.com/sagoo-cloud/iotgateway/conf" 10 | ) 11 | 12 | // 保证只有一个MQTT客户端实例的互斥锁 13 | var singleInstanceLock sync.Mutex 14 | 15 | // MQTT客户端单例 16 | var client mqtt.Client 17 | 18 | var ( 19 | cancel context.CancelFunc 20 | ) 21 | 22 | // GetMQTTClient 获取MQTT客户端单例 23 | func GetMQTTClient(cf conf.MqttConfig) (mqttClient mqtt.Client, err error) { 24 | singleInstanceLock.Lock() 25 | defer singleInstanceLock.Unlock() 26 | 27 | // 如果客户端已存在但未连接,尝试重连 28 | if client != nil { 29 | if client.IsConnected() { 30 | return client, nil 31 | } 32 | // 如果存在重连管理器,尝试重连 33 | if reconnectManager != nil { 34 | if err := reconnectManager.Reconnect(); err != nil { 35 | return nil, fmt.Errorf("重连失败: %v", err) 36 | } 37 | return client, nil 38 | } 39 | } 40 | 41 | connOpts, err := getMqttClientConfig(cf) 42 | if err != nil { 43 | return nil, fmt.Errorf("failed to get MQTT client config: %v", err) 44 | } 45 | 46 | // 创建连接 47 | client = mqtt.NewClient(connOpts) 48 | if client == nil { 49 | return nil, fmt.Errorf("failed to create MQTT client") 50 | } 51 | 52 | // 初始化重连管理器 53 | reconnectManager = NewReconnectManager(client) 54 | 55 | // 建立连接 56 | if token := client.Connect(); token.Wait() && token.Error() != nil { 57 | // 提供详细的错误信息 58 | return nil, fmt.Errorf("failed to connect to MQTT broker: %v", token.Error()) 59 | } 60 | 61 | return client, nil 62 | } 63 | 64 | // Publish 向mqtt服务推送数据 65 | func Publish(topic string, payload []byte) (err error) { 66 | if client == nil { 67 | return fmt.Errorf("【IotGateway】Publish err: client is nil. Client has lost connection with the MQTT broker.") 68 | } 69 | if !client.IsConnected() { 70 | // 尝试重连 71 | if reconnectManager != nil { 72 | if err := reconnectManager.Reconnect(); err != nil { 73 | return fmt.Errorf("【IotGateway】Publish err: 重连失败: %v", err) 74 | } 75 | // 重连成功后重试发布 76 | pubToken := client.Publish(topic, 1, false, payload) 77 | return pubToken.Error() 78 | } 79 | return fmt.Errorf("【IotGateway】Publish err: client is close. Client has lost connection with the MQTT broker.") 80 | } 81 | 82 | pubToken := client.Publish(topic, 1, false, payload) 83 | return pubToken.Error() 84 | } 85 | 86 | // GetReconnectStatus 获取重连状态 87 | func GetReconnectStatus() map[string]interface{} { 88 | if reconnectManager != nil { 89 | return reconnectManager.GetReconnectStatus() 90 | } 91 | return nil 92 | } 93 | 94 | // Stop 停止MQTT客户端 95 | func Stop() { 96 | if reconnectManager != nil { 97 | reconnectManager.Stop() 98 | } 99 | if client != nil && client.IsConnected() { 100 | client.Disconnect(250) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /mqttClient/data.go: -------------------------------------------------------------------------------- 1 | package mqttClient 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/gogf/gf/v2/os/glog" 7 | "github.com/sagoo-cloud/iotgateway/vars" 8 | ) 9 | 10 | // PublishData 向mqtt服务推送属性数据 11 | func PublishData(deviceKey string, payload []byte) (err error) { 12 | gateWayProductKey := vars.GatewayServerConfig.ProductKey 13 | topic := fmt.Sprintf(propertyTopic, gateWayProductKey, deviceKey) 14 | err = Publish(topic, payload) 15 | if err != nil { 16 | return fmt.Errorf("【IotGateway】publish error: %s", err.Error()) 17 | } 18 | glog.Debugf(context.Background(), "【IotGateway】属性上报,topic: %s,推送的数据:%s", topic, string(payload)) 19 | return 20 | } 21 | -------------------------------------------------------------------------------- /mqttClient/reconnect.go: -------------------------------------------------------------------------------- 1 | package mqttClient 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "time" 8 | 9 | mqtt "github.com/eclipse/paho.mqtt.golang" 10 | "github.com/gogf/gf/v2/os/glog" 11 | ) 12 | 13 | // ReconnectManager MQTT重连管理器 14 | type ReconnectManager struct { 15 | initialBackoff time.Duration 16 | maxBackoff time.Duration 17 | maxRetries int 18 | currentRetry int 19 | lastReconnectTime time.Time 20 | mu sync.Mutex 21 | client mqtt.Client 22 | ctx context.Context 23 | cancel context.CancelFunc 24 | isReconnecting bool 25 | } 26 | 27 | // NewReconnectManager 创建新的重连管理器 28 | func NewReconnectManager(client mqtt.Client) *ReconnectManager { 29 | ctx, cancel := context.WithCancel(context.Background()) 30 | return &ReconnectManager{ 31 | initialBackoff: time.Second, 32 | maxBackoff: time.Minute * 5, 33 | maxRetries: 10, 34 | client: client, 35 | ctx: ctx, 36 | cancel: cancel, 37 | } 38 | } 39 | 40 | // Reconnect 执行重连操作 41 | func (m *ReconnectManager) Reconnect() error { 42 | m.mu.Lock() 43 | if m.isReconnecting { 44 | m.mu.Unlock() 45 | return fmt.Errorf("已经在重连中") 46 | } 47 | m.isReconnecting = true 48 | m.mu.Unlock() 49 | 50 | defer func() { 51 | m.mu.Lock() 52 | m.isReconnecting = false 53 | m.mu.Unlock() 54 | }() 55 | 56 | // 计算当前应该等待的时间 57 | backoff := m.initialBackoff 58 | for i := 0; i < m.currentRetry; i++ { 59 | backoff *= 2 60 | if backoff > m.maxBackoff { 61 | backoff = m.maxBackoff 62 | break 63 | } 64 | } 65 | 66 | // 等待计算出的时间 67 | select { 68 | case <-m.ctx.Done(): 69 | return fmt.Errorf("重连被取消") 70 | case <-time.After(backoff): 71 | } 72 | 73 | // 尝试重连 74 | if token := m.client.Connect(); token.Wait() && token.Error() != nil { 75 | m.currentRetry++ 76 | if m.currentRetry >= m.maxRetries { 77 | return fmt.Errorf("达到最大重试次数: %d", m.maxRetries) 78 | } 79 | return token.Error() 80 | } 81 | 82 | // 重连成功,重置计数器 83 | m.currentRetry = 0 84 | m.lastReconnectTime = time.Now() 85 | return nil 86 | } 87 | 88 | // StartReconnectLoop 启动重连循环 89 | func (m *ReconnectManager) StartReconnectLoop() { 90 | m.mu.Lock() 91 | if m.isReconnecting { 92 | m.mu.Unlock() 93 | return 94 | } 95 | m.mu.Unlock() 96 | 97 | go func() { 98 | for { 99 | select { 100 | case <-m.ctx.Done(): 101 | return 102 | default: 103 | if !m.client.IsConnected() { 104 | if err := m.Reconnect(); err != nil { 105 | glog.Error(m.ctx, "【IotGateway】MQTT重连失败: %v", err) 106 | } else { 107 | glog.Info(m.ctx, "【IotGateway】MQTT重连成功") 108 | // 重连成功后等待一段时间再检查 109 | time.Sleep(time.Second * 5) 110 | } 111 | } else { 112 | // 已连接,等待较长时间再检查 113 | time.Sleep(time.Second * 30) 114 | } 115 | } 116 | } 117 | }() 118 | } 119 | 120 | // Stop 停止重连管理器 121 | func (m *ReconnectManager) Stop() { 122 | if m.cancel != nil { 123 | m.cancel() 124 | } 125 | } 126 | 127 | // GetReconnectStatus 获取重连状态 128 | func (m *ReconnectManager) GetReconnectStatus() map[string]interface{} { 129 | m.mu.Lock() 130 | defer m.mu.Unlock() 131 | 132 | return map[string]interface{}{ 133 | "currentRetry": m.currentRetry, 134 | "maxRetries": m.maxRetries, 135 | "lastReconnectTime": m.lastReconnectTime, 136 | "isConnected": m.client.IsConnected(), 137 | "isReconnecting": m.isReconnecting, 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /mqttClient/setconfig.go: -------------------------------------------------------------------------------- 1 | package mqttClient 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "fmt" 7 | "os" 8 | "time" 9 | 10 | mqtt "github.com/eclipse/paho.mqtt.golang" 11 | "github.com/sagoo-cloud/iotgateway/conf" 12 | "github.com/sagoo-cloud/iotgateway/log" 13 | ) 14 | 15 | const ( 16 | propertyTopic = "/sys/%s/%s/thing/event/property/pack/post" 17 | serviceTopic = "/sys/+/%s/thing/service/#" 18 | ) 19 | 20 | var ( 21 | reconnectManager *ReconnectManager 22 | ) 23 | 24 | // getMqttClientConfig 获取mqtt客户端配置 25 | func getMqttClientConfig(cf conf.MqttConfig) (connOpts *mqtt.ClientOptions, err error) { 26 | if cf.Address == "" && cf.Username == "" && cf.Password == "" && cf.ClientId == "" { 27 | return nil, fmt.Errorf("mqtt配置信息不完整") 28 | } 29 | 30 | connOpts = mqtt.NewClientOptions().AddBroker(fmt.Sprintf("tcp://%s", cf.Address)) 31 | connOpts.SetUsername(cf.Username) 32 | connOpts.SetPassword(cf.Password) 33 | connOpts.SetClientID(cf.ClientId) 34 | 35 | if cf.ClientCertificateKey != "" { 36 | connOpts.AddBroker(fmt.Sprintf("ssl://%s", cf.Address)) 37 | tlsConfig := NewTlsConfig(cf.ClientCertificateKey, cf.ClientCertificateCert) 38 | connOpts.SetTLSConfig(tlsConfig) 39 | } 40 | 41 | connOpts.SetKeepAlive(cf.KeepAliveDuration * time.Second) 42 | connOpts.SetConnectTimeout(time.Second * 10) 43 | connOpts.SetMaxReconnectInterval(time.Minute * 5) 44 | connOpts.SetAutoReconnect(true) 45 | connOpts.SetConnectRetryInterval(time.Second * 5) 46 | 47 | // 连接成功回调 48 | connOpts.OnConnect = func(client mqtt.Client) { 49 | log.Debug("MQTT服务连接成功") 50 | if reconnectManager != nil { 51 | reconnectManager.currentRetry = 0 52 | // 连接成功后,启动重连循环以确保连接持续 53 | reconnectManager.StartReconnectLoop() 54 | } 55 | } 56 | 57 | // 连接断开回调 58 | connOpts.OnConnectionLost = func(client mqtt.Client, err error) { 59 | log.Debug("MQTT服务连接已断开: %v", err) 60 | if reconnectManager != nil { 61 | // 连接断开时,确保重连循环正在运行 62 | reconnectManager.StartReconnectLoop() 63 | } 64 | } 65 | 66 | // 重连回调 67 | connOpts.OnReconnecting = func(client mqtt.Client, o *mqtt.ClientOptions) { 68 | log.Debug("正在尝试重新连接MQTT服务...") 69 | } 70 | 71 | return 72 | } 73 | 74 | // NewTlsConfig 生成tls配置 75 | func NewTlsConfig(clientCertificateKey, clientCertificateCert string) *tls.Config { 76 | certPool := x509.NewCertPool() 77 | ca, err := os.ReadFile("ca.pem") 78 | if err != nil { 79 | log.Error(err.Error()) 80 | } 81 | certPool.AppendCertsFromPEM(ca) 82 | if clientCertificateKey != "" && clientCertificateCert != "" { 83 | clientKeyPair, err := tls.LoadX509KeyPair(clientCertificateCert, clientCertificateKey) 84 | if err != nil { 85 | panic(err) 86 | } 87 | return &tls.Config{ 88 | RootCAs: certPool, 89 | ClientAuth: tls.NoClientCert, 90 | ClientCAs: nil, 91 | InsecureSkipVerify: true, 92 | Certificates: []tls.Certificate{clientKeyPair}, 93 | } 94 | } 95 | return &tls.Config{ 96 | RootCAs: certPool, 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /mqttProtocol/eventModel.go: -------------------------------------------------------------------------------- 1 | package mqttProtocol 2 | 3 | // 事件上报结构体 4 | type ( 5 | ReportEventReq struct { 6 | Id string `json:"id"` 7 | Version string `json:"version"` 8 | Sys SysInfo `json:"sys"` 9 | Params ReportEventParams `json:"params"` 10 | } 11 | ReportEventParams struct { 12 | Value map[string]string `json:"value"` 13 | CreateAt int64 `json:"time"` 14 | } 15 | ReportEventReply struct { 16 | Code int `json:"code"` 17 | Data struct { 18 | } `json:"data"` 19 | Id string `json:"id"` 20 | Message string `json:"message"` 21 | Method string `json:"method"` 22 | Version string `json:"version"` 23 | } 24 | ) 25 | -------------------------------------------------------------------------------- /mqttProtocol/gatewayModel.go: -------------------------------------------------------------------------------- 1 | package mqttProtocol 2 | 3 | // 网关批量上报结构体 4 | type ( 5 | //设备上报请求 6 | GatewayBatchReq struct { 7 | Id string `json:"id"` 8 | Version string `json:"version"` 9 | Sys SysInfo `json:"sys"` 10 | Params PropertyInfo `json:"params"` 11 | Method string `json:"method"` 12 | } 13 | 14 | PropertyInfo struct { 15 | Properties map[string]interface{} `json:"properties"` 16 | Events map[string]EventNode `json:"events"` 17 | SubDevices []Sub `json:"subDevices"` 18 | } 19 | Sub struct { 20 | Identity Identity `json:"identity"` 21 | Properties map[string]interface{} `json:"properties"` 22 | Events map[string]EventNode `json:"events"` 23 | } 24 | Identity struct { 25 | ProductKey string `json:"productKey"` 26 | DeviceKey string `json:"deviceKey"` 27 | } 28 | EventNode struct { 29 | Value map[string]interface{} `json:"value"` 30 | CreateTime int64 `json:"time"` 31 | } 32 | 33 | // GatewayBatchReply 设备上报回复 34 | GatewayBatchReply struct { 35 | Code int `json:"code"` 36 | Data struct { 37 | } `json:"data"` 38 | Id string `json:"id"` 39 | Message string `json:"message"` 40 | Method string `json:"method"` 41 | Version string `json:"version"` 42 | } 43 | ) 44 | -------------------------------------------------------------------------------- /mqttProtocol/propertyModel.go: -------------------------------------------------------------------------------- 1 | package mqttProtocol 2 | 3 | type ( 4 | SysInfo struct { 5 | Ack int `json:"ack"` 6 | } 7 | 8 | PropertyNode struct { 9 | Value interface{} `json:"value"` 10 | CreateTime int64 `json:"time"` 11 | } 12 | ) 13 | 14 | // 属性上报结构体 15 | type ( 16 | ReportPropertyReq struct { 17 | Id string `json:"id"` 18 | Version string `json:"version"` 19 | Sys SysInfo `json:"sys"` 20 | Params map[string]interface{} `json:"params"` 21 | Method string `json:"method"` 22 | } 23 | ReportPropertyReply struct { 24 | Code int `json:"code"` 25 | Data struct { 26 | } `json:"data"` 27 | Id string `json:"id"` 28 | Message string `json:"message"` 29 | Method string `json:"method"` 30 | Version string `json:"version"` 31 | } 32 | ) 33 | 34 | // 属性设置结构体 35 | type ( 36 | PropertySetRequest struct { 37 | Id string `json:"id"` 38 | Version string `json:"version"` 39 | Params map[string]interface{} `json:"params"` 40 | Method string `json:"method"` 41 | } 42 | PropertySetRes struct { 43 | Code int `json:"code"` 44 | Data map[string]interface{} `json:"data"` 45 | Id string `json:"id"` 46 | Message string `json:"message"` 47 | Version string `json:"version"` 48 | } 49 | ) 50 | -------------------------------------------------------------------------------- /mqttProtocol/serviceCallModel.go: -------------------------------------------------------------------------------- 1 | package mqttProtocol 2 | 3 | // 服务调用结构体 4 | type ( 5 | ServiceCallRequest struct { 6 | Id string `json:"id"` 7 | Version string `json:"version"` 8 | Params map[string]interface{} `json:"params"` 9 | Method string `json:"method"` 10 | } 11 | 12 | ServiceCallOutputRes struct { 13 | Code int `json:"code"` 14 | Data map[string]interface{} `json:"data"` 15 | Id string `json:"id"` 16 | Message string `json:"message"` 17 | Version string `json:"version"` 18 | } 19 | ) 20 | -------------------------------------------------------------------------------- /mqttProtocol/tools.go: -------------------------------------------------------------------------------- 1 | package mqttProtocol 2 | 3 | type GatewayBatchReqBuilder struct { 4 | batchReq GatewayBatchReq 5 | } 6 | 7 | // NewGatewayBatchReqBuilder 创建一个新的GatewayBatchReqBuilder 8 | func NewGatewayBatchReqBuilder() *GatewayBatchReqBuilder { 9 | return &GatewayBatchReqBuilder{ 10 | batchReq: GatewayBatchReq{ 11 | Sys: SysInfo{}, 12 | Params: PropertyInfo{ 13 | Properties: make(map[string]interface{}), 14 | Events: make(map[string]EventNode), 15 | SubDevices: make([]Sub, 0), 16 | }, 17 | }, 18 | } 19 | } 20 | 21 | func (b *GatewayBatchReqBuilder) SetId(id string) *GatewayBatchReqBuilder { 22 | b.batchReq.Id = id 23 | return b 24 | } 25 | 26 | func (b *GatewayBatchReqBuilder) SetVersion(version string) *GatewayBatchReqBuilder { 27 | b.batchReq.Version = version 28 | return b 29 | } 30 | 31 | func (b *GatewayBatchReqBuilder) SetSys(sys SysInfo) *GatewayBatchReqBuilder { 32 | b.batchReq.Sys = sys 33 | return b 34 | } 35 | 36 | func (b *GatewayBatchReqBuilder) AddProperty(key string, value interface{}) *GatewayBatchReqBuilder { 37 | b.batchReq.Params.Properties[key] = value 38 | return b 39 | } 40 | 41 | func (b *GatewayBatchReqBuilder) AddEvent(key string, event EventNode) *GatewayBatchReqBuilder { 42 | b.batchReq.Params.Events[key] = event 43 | return b 44 | } 45 | 46 | func (b *GatewayBatchReqBuilder) AddSubDevice(sub Sub) *GatewayBatchReqBuilder { 47 | b.batchReq.Params.SubDevices = append(b.batchReq.Params.SubDevices, sub) 48 | return b 49 | } 50 | 51 | func (b *GatewayBatchReqBuilder) SetMethod(method string) *GatewayBatchReqBuilder { 52 | b.batchReq.Method = method 53 | return b 54 | } 55 | 56 | func (b *GatewayBatchReqBuilder) Build() GatewayBatchReq { 57 | return b.batchReq 58 | } 59 | -------------------------------------------------------------------------------- /network/option.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "github.com/sagoo-cloud/iotgateway/conf" 5 | "time" 6 | ) 7 | 8 | // Option 定义了服务器配置的选项函数类型 9 | type Option func(*BaseServer) 10 | 11 | // WithTimeout 设置超时选项 12 | func WithTimeout(timeout time.Duration) Option { 13 | return func(s *BaseServer) { 14 | s.timeout = timeout 15 | } 16 | } 17 | 18 | // WithProtocolHandler 设置协议处理器选项 19 | func WithProtocolHandler(handler ProtocolHandler) Option { 20 | return func(s *BaseServer) { 21 | s.protocolHandler = handler 22 | } 23 | } 24 | 25 | // WithCleanupInterval 设置清理间隔选项 26 | func WithCleanupInterval(interval time.Duration) Option { 27 | return func(s *BaseServer) { 28 | s.cleanupInterval = interval 29 | } 30 | } 31 | 32 | // WithPacketHandling 设置粘包处理选项 33 | func WithPacketHandling(config conf.PacketConfig) Option { 34 | return func(s *BaseServer) { 35 | s.packetConfig = config 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /network/packet.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import "github.com/sagoo-cloud/iotgateway/conf" 4 | 5 | const ( 6 | NoHandling conf.PacketHandlingType = iota 7 | FixedLength // 定长 8 | HeaderBodySeparate // 头部+体 9 | Delimiter // 分隔符 10 | ) 11 | -------------------------------------------------------------------------------- /network/protocol.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import "github.com/sagoo-cloud/iotgateway/model" 4 | 5 | // ProtocolHandler 接口定义了协议处理方法 6 | type ProtocolHandler interface { 7 | Init(device *model.Device, data []byte) error 8 | Encode(device *model.Device, data interface{}, param ...string) ([]byte, error) 9 | Decode(device *model.Device, data []byte) ([]byte, error) 10 | } 11 | -------------------------------------------------------------------------------- /network/server.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net" 7 | "sync" 8 | "time" 9 | 10 | "github.com/gogf/gf/v2/os/glog" 11 | "github.com/sagoo-cloud/iotgateway/conf" 12 | "github.com/sagoo-cloud/iotgateway/model" 13 | "github.com/sagoo-cloud/iotgateway/vars" 14 | ) 15 | 16 | // NetworkServer 接口定义了网络服务器的通用方法 17 | type NetworkServer interface { 18 | Start(ctx context.Context, addr string) error 19 | Stop() error 20 | SendData(device *model.Device, data interface{}, param ...string) error 21 | } 22 | 23 | // BaseServer 结构体包含 TCP 和 UDP 服务器的共同字段 24 | type BaseServer struct { 25 | devices sync.Map 26 | timeout time.Duration 27 | protocolHandler ProtocolHandler 28 | cleanupInterval time.Duration 29 | packetConfig conf.PacketConfig 30 | } 31 | 32 | // NewBaseServer 创建一个新的基础服务器实例 33 | func NewBaseServer(options ...Option) *BaseServer { 34 | s := &BaseServer{ 35 | timeout: 30 * time.Second, 36 | cleanupInterval: 5 * time.Minute, 37 | packetConfig: conf.PacketConfig{Type: Delimiter, Delimiter: "\r\n"}, 38 | } 39 | 40 | for _, option := range options { 41 | option(s) 42 | } 43 | 44 | return s 45 | } 46 | 47 | // handleConnect 处理设备上线事件 48 | func (s *BaseServer) handleConnect(clientID string, conn net.Conn) *model.Device { 49 | device := &model.Device{ClientID: clientID, OnlineStatus: true, Conn: conn, LastActive: time.Now()} 50 | s.devices.Store(clientID, device) 51 | glog.Debugf(context.Background(), "设备 %s 上线\n", clientID) 52 | return device 53 | } 54 | 55 | // getDevice 获取设备实例 56 | func (s *BaseServer) getDevice(clientID string) *model.Device { 57 | if device, ok := s.devices.Load(clientID); ok { 58 | // 将接口类型断言为*Device类型 59 | if device, ok := device.(*model.Device); ok { 60 | return device 61 | } 62 | } 63 | return nil 64 | } 65 | 66 | // handleDisconnect 处理设备离线事件 67 | func (s *BaseServer) handleDisconnect(device *model.Device) { 68 | if _, ok := s.devices.LoadAndDelete(device.ClientID); ok { 69 | device.OnlineStatus = false 70 | glog.Debugf(context.Background(), "设备 %s 离线, %s\n", device.DeviceKey, device.ClientID) 71 | 72 | // ✅ 清理设备相关的所有消息缓存,防止内存泄漏 73 | if device.DeviceKey != "" { 74 | vars.ClearDeviceMessages(device.DeviceKey) 75 | } 76 | } 77 | } 78 | 79 | // handleReceiveData 处理接收数据事件 80 | func (s *BaseServer) handleReceiveData(device *model.Device, data []byte) (resData interface{}, err error) { 81 | if s.protocolHandler == nil { 82 | return nil, errors.New("未设置协议处理器") 83 | } 84 | s.protocolHandler.Init(device, data) // 初始化协议处理器 85 | if device != nil { 86 | device.OnlineStatus = true 87 | device.LastActive = time.Now() // 更新设备最后活跃时间 88 | if device.DeviceKey != "" { 89 | vars.UpdateDeviceMap(device.DeviceKey, device) // 更新到全局设备列表 90 | } 91 | } 92 | return s.protocolHandler.Decode(device, data) // 解码数据 93 | } 94 | 95 | // cleanupInactiveDevices 清理不活跃的设备 96 | func (s *BaseServer) cleanupInactiveDevices(ctx context.Context) { 97 | ticker := time.NewTicker(s.cleanupInterval) 98 | defer ticker.Stop() 99 | 100 | for { 101 | select { 102 | case <-ctx.Done(): 103 | return 104 | case <-ticker.C: 105 | now := time.Now() 106 | s.devices.Range(func(key, value interface{}) bool { 107 | device := value.(*model.Device) 108 | if now.Sub(device.LastActive) > s.timeout*2 { 109 | s.handleDisconnect(device) 110 | } 111 | return true 112 | }) 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /network/tcp.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "encoding/binary" 8 | "errors" 9 | "fmt" 10 | "github.com/gogf/gf/v2/os/glog" 11 | "github.com/sagoo-cloud/iotgateway/model" 12 | "github.com/sagoo-cloud/iotgateway/vars" 13 | "io" 14 | "net" 15 | "sync" 16 | "time" 17 | ) 18 | 19 | // TCPServer 结构体表示 TCP 服务器 20 | type TCPServer struct { 21 | *BaseServer 22 | listener net.Listener 23 | conns sync.Map 24 | } 25 | 26 | // NewTCPServer 创建一个新的 TCP 服务器实例 27 | func NewTCPServer(options ...Option) NetworkServer { 28 | return &TCPServer{ 29 | BaseServer: NewBaseServer(options...), 30 | } 31 | } 32 | 33 | // Start 启动 TCP 服务器 34 | func (s *TCPServer) Start(ctx context.Context, addr string) error { 35 | var err error 36 | s.listener, err = net.Listen("tcp", addr) 37 | if err != nil { 38 | return fmt.Errorf("TCP 监听失败: %v", err) 39 | } 40 | 41 | go s.cleanupInactiveDevices(ctx) 42 | 43 | go func() { 44 | <-ctx.Done() 45 | s.Stop() 46 | }() 47 | 48 | for { 49 | conn, err := s.listener.Accept() 50 | if err != nil { 51 | if errors.Is(err, net.ErrClosed) { 52 | return nil // 正常关闭 53 | } 54 | glog.Debugf(context.Background(), "接受 TCP 连接失败: %v", err) 55 | continue 56 | } 57 | go s.handleConnection(ctx, conn) 58 | } 59 | } 60 | 61 | // Stop 停止 TCP 服务器 62 | func (s *TCPServer) Stop() error { 63 | if s.listener != nil { 64 | err := s.listener.Close() 65 | s.conns.Range(func(key, value interface{}) bool { 66 | conn := value.(net.Conn) 67 | conn.Close() 68 | return true 69 | }) 70 | return err 71 | } 72 | return nil 73 | } 74 | 75 | // SendData 向 TCP 设备发送数据 76 | func (s *TCPServer) SendData(device *model.Device, data interface{}, param ...string) error { 77 | connAny, ok := s.conns.Load(device.ClientID) 78 | if !ok { 79 | return fmt.Errorf("TCP 设备 %s 未找到", device.ClientID) 80 | } 81 | conn := connAny.(net.Conn) 82 | 83 | var encodedData []byte 84 | var err error 85 | 86 | if s.protocolHandler != nil { 87 | encodedData, err = s.protocolHandler.Encode(device, data, param...) 88 | if err != nil { 89 | return fmt.Errorf("编码数据失败: %v", err) 90 | } 91 | } else { 92 | encodedData = []byte(fmt.Sprintf("%v\n", data)) 93 | } 94 | 95 | _, err = conn.Write(encodedData) 96 | return err 97 | } 98 | 99 | // handleConnection 处理 TCP 设备连接 100 | func (s *TCPServer) handleConnection(ctx context.Context, conn net.Conn) { 101 | defer conn.Close() 102 | clientID := conn.RemoteAddr().String() 103 | device := s.handleConnect(clientID, conn) 104 | s.conns.Store(clientID, conn) 105 | defer func() { 106 | s.handleDisconnect(device) 107 | s.conns.Delete(clientID) 108 | }() 109 | 110 | // 创建缓冲读取器 111 | var reader = bufio.NewReader(conn) 112 | buffer := make([]byte, 1024) // 或其他适合的缓冲区大小 113 | 114 | for { 115 | select { 116 | case <-ctx.Done(): 117 | return 118 | default: 119 | if s.timeout > 0 { 120 | if err := conn.SetReadDeadline(time.Now().Add(s.timeout)); err != nil { 121 | glog.Debugf(context.Background(), "设置读取超时失败: %v\n", err) 122 | return 123 | } 124 | } 125 | 126 | var data []byte 127 | // 直接读取数据 128 | n, err := reader.Read(buffer) 129 | if err != nil { 130 | if err != io.EOF { 131 | glog.Debugf(context.Background(), "读取错误: %v\n", err) 132 | } 133 | continue 134 | } 135 | data = buffer[:n] 136 | fmt.Println(fmt.Sprintf("data: %x", data)) 137 | 138 | // 如果数据长度小于等于头部长度,则初始化协议处理器 139 | fmt.Println(fmt.Sprintf("data len: %d, header len: %d", n, s.packetConfig.HeaderLength)) 140 | if n <= s.packetConfig.HeaderLength { 141 | s.protocolHandler.Init(device, data) // 初始化协议处理器 142 | if device != nil { 143 | device.OnlineStatus = true 144 | device.LastActive = time.Now() // 更新设备最后活跃时间 145 | if device.DeviceKey != "" { 146 | vars.UpdateDeviceMap(device.DeviceKey, device) // 更新到全局设备列表 147 | } 148 | } 149 | } else { 150 | if s.packetConfig.Type != NoHandling { 151 | var err error 152 | data, err = s.readPacket(reader) 153 | if err != nil { 154 | if err != io.EOF { 155 | glog.Debugf(context.Background(), "读取错误: %v\n", err) 156 | } 157 | continue 158 | } 159 | } 160 | 161 | device.LastActive = time.Now() 162 | resData, err := s.handleReceiveData(device, data) 163 | if err != nil { 164 | glog.Debugf(context.Background(), "处理数据错误: %v\n", err) 165 | continue 166 | } 167 | 168 | if resData != nil { 169 | if err := s.SendData(device, resData); err != nil { 170 | glog.Debugf(context.Background(), "发送回复失败: %v\n", err) 171 | } 172 | } 173 | } 174 | } 175 | } 176 | } 177 | 178 | // readPacket 根据配置的粘包处理方式读取数据 179 | func (s *TCPServer) readPacket(reader io.Reader) ([]byte, error) { 180 | switch s.packetConfig.Type { 181 | case FixedLength: 182 | data := make([]byte, s.packetConfig.FixedLength) 183 | _, err := io.ReadFull(reader, data) 184 | return data, err 185 | case HeaderBodySeparate: 186 | headerBuf := make([]byte, s.packetConfig.HeaderLength) 187 | _, err := io.ReadFull(reader, headerBuf) 188 | if err != nil { 189 | return nil, err 190 | } 191 | bodyLength := binary.BigEndian.Uint32(headerBuf) 192 | data := make([]byte, bodyLength) 193 | _, err = io.ReadFull(reader, data) 194 | return data, err 195 | case Delimiter: 196 | return readUntilDelimiter(reader, s.packetConfig.Delimiter) 197 | default: 198 | return readUntilCRLF(reader) 199 | } 200 | } 201 | 202 | // readUntilDelimiter 读取数据直到遇到指定的分隔符 203 | func readUntilDelimiter(reader io.Reader, delimiter string) ([]byte, error) { 204 | var buffer bytes.Buffer 205 | delimiterBytes := []byte(delimiter) 206 | delimiterLength := len(delimiterBytes) 207 | 208 | for { 209 | b := make([]byte, 1) 210 | _, err := reader.Read(b) 211 | if err != nil { 212 | return nil, err 213 | } 214 | 215 | buffer.Write(b) 216 | 217 | if buffer.Len() >= delimiterLength { 218 | if bytes.Equal(buffer.Bytes()[buffer.Len()-delimiterLength:], delimiterBytes) { 219 | return buffer.Bytes(), nil 220 | } 221 | } 222 | } 223 | } 224 | 225 | // readUntilCRLF 读取数据直到遇到 226 | func readUntilCRLF(reader io.Reader) ([]byte, error) { 227 | var buffer bytes.Buffer 228 | for { 229 | b := make([]byte, 1) 230 | _, err := reader.Read(b) 231 | if err != nil { 232 | return nil, err 233 | } 234 | 235 | buffer.Write(b) 236 | 237 | if buffer.Len() >= 2 { 238 | if buffer.Bytes()[buffer.Len()-2] == '\r' && buffer.Bytes()[buffer.Len()-1] == '\n' { 239 | return buffer.Bytes(), nil 240 | } 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /network/udp.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/sagoo-cloud/iotgateway/model" 8 | "log" 9 | "net" 10 | "time" 11 | ) 12 | 13 | // UDPServer 结构体表示 UDP 服务器 14 | type UDPServer struct { 15 | *BaseServer 16 | conn *net.UDPConn 17 | } 18 | 19 | // NewUDPServer 创建一个新的 UDP 服务器实例 20 | func NewUDPServer(options ...Option) NetworkServer { 21 | return &UDPServer{ 22 | BaseServer: NewBaseServer(options...), 23 | } 24 | } 25 | 26 | // Start 启动 UDP 服务器 27 | func (s *UDPServer) Start(ctx context.Context, addr string) error { 28 | udpAddr, err := net.ResolveUDPAddr("udp", addr) 29 | if err != nil { 30 | return fmt.Errorf("解析 UDP 地址失败: %v", err) 31 | } 32 | 33 | s.conn, err = net.ListenUDP("udp", udpAddr) 34 | if err != nil { 35 | return fmt.Errorf("UDP 监听失败: %v", err) 36 | } 37 | 38 | go s.cleanupInactiveDevices(ctx) 39 | 40 | go func() { 41 | <-ctx.Done() 42 | s.Stop() 43 | }() 44 | 45 | buffer := make([]byte, 2048) 46 | for { 47 | select { 48 | case <-ctx.Done(): 49 | return nil 50 | default: 51 | if s.timeout > 0 { 52 | if err := s.conn.SetReadDeadline(time.Now().Add(s.timeout)); err != nil { 53 | log.Printf("设置 UDP 读取超时失败: %v\n", err) 54 | continue 55 | } 56 | } 57 | n, remoteAddr, err := s.conn.ReadFromUDP(buffer) 58 | if err != nil { 59 | if errors.Is(err, net.ErrClosed) { 60 | return nil // 正常关闭 61 | } 62 | log.Printf("读取 UDP 数据失败: %v", err) 63 | continue 64 | } 65 | 66 | clientID := remoteAddr.String() 67 | device, _ := s.devices.LoadOrStore(clientID, &model.Device{ClientID: clientID, LastActive: time.Now()}) 68 | device.(*model.Device).LastActive = time.Now() 69 | 70 | data := buffer[:n] 71 | resData, err := s.handleReceiveData(device.(*model.Device), data) 72 | if err != nil { 73 | log.Printf("处理数据错误: %v\n", err) 74 | continue 75 | } 76 | 77 | if resData != nil { 78 | if err := s.SendData(device.(*model.Device), resData); err != nil { 79 | log.Printf("发送回复失败: %v\n", err) 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | // Stop 停止 UDP 服务器 87 | func (s *UDPServer) Stop() error { 88 | if s.conn != nil { 89 | return s.conn.Close() 90 | } 91 | return nil 92 | } 93 | 94 | // SendData 向 UDP 设备发送数据 95 | func (s *UDPServer) SendData(device *model.Device, data interface{}, param ...string) error { 96 | udpAddr, err := net.ResolveUDPAddr("udp", device.ClientID) 97 | if err != nil { 98 | return fmt.Errorf("解析 UDP 地址失败: %v", err) 99 | } 100 | 101 | var encodedData []byte 102 | if s.protocolHandler != nil { 103 | encodedData, err = s.protocolHandler.Encode(nil, data, param...) 104 | if err != nil { 105 | return fmt.Errorf("编码数据失败: %v", err) 106 | } 107 | } else { 108 | encodedData = []byte(fmt.Sprintf("%v", data)) 109 | } 110 | 111 | _, err = s.conn.WriteToUDP(encodedData, udpAddr) 112 | return err 113 | } 114 | -------------------------------------------------------------------------------- /service.go: -------------------------------------------------------------------------------- 1 | package iotgateway 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | mqtt "github.com/eclipse/paho.mqtt.golang" 10 | "github.com/gogf/gf/v2/os/glog" 11 | "github.com/gogf/gf/v2/util/gconv" 12 | "github.com/gookit/event" 13 | "github.com/sagoo-cloud/iotgateway/lib" 14 | "github.com/sagoo-cloud/iotgateway/log" 15 | "github.com/sagoo-cloud/iotgateway/model" 16 | "github.com/sagoo-cloud/iotgateway/mqttProtocol" 17 | "github.com/sagoo-cloud/iotgateway/vars" 18 | ) 19 | 20 | // SubscribeServiceEvent 订阅平台的服务调用,需要在有新设备接入时调用 21 | func (gw *Gateway) SubscribeServiceEvent(deviceKey string) { 22 | if gw.MQTTClient == nil || !gw.MQTTClient.IsConnected() { 23 | log.Error("【IotGateway】SubscribeServiceEvent Error:Client has lost connection with the MQTT broker.") 24 | return 25 | } 26 | topic := fmt.Sprintf(serviceTopic, deviceKey) 27 | glog.Debugf(context.Background(), "%s 设备订阅了服务调用监听topic: %s", deviceKey, topic) 28 | token := gw.MQTTClient.Subscribe(topic, 1, onServiceMessage) 29 | if token.Error() != nil { 30 | glog.Debug(context.Background(), "subscribe error: ", token.Error()) 31 | } 32 | } 33 | 34 | // onServiceMessage 服务调用处理 35 | var onServiceMessage mqtt.MessageHandler = func(client mqtt.Client, msg mqtt.Message) { 36 | //忽略_reply结尾的topic 37 | if strings.HasSuffix(msg.Topic(), "_reply") { 38 | return 39 | } 40 | if msg != nil { 41 | defer func() { 42 | if r := recover(); r != nil { 43 | fmt.Println("Recovered in safeCall:", r) 44 | } 45 | }() 46 | //通过监听到的topic地址获取设备标识 47 | deviceKey := lib.GetTopicInfo("deviceKey", msg.Topic()) 48 | var data = mqttProtocol.ServiceCallRequest{} 49 | glog.Debug(context.Background(), "接收到服务下发的topic:", msg.Topic()) 50 | glog.Debug(context.Background(), "接收到服务下发的数据:", msg.Payload()) 51 | 52 | err := gconv.Scan(msg.Payload(), &data) 53 | if err != nil { 54 | glog.Debug(context.Background(), "解析服务功能数据出错: %s", err) 55 | return 56 | } 57 | 58 | //触发下发事件 59 | data.Params["DeviceKey"] = deviceKey 60 | 61 | method := strings.Split(data.Method, ".") 62 | var up model.UpMessage 63 | up.MessageID = data.Id 64 | up.SendTime = time.Now().UnixNano() / 1e9 65 | up.MethodName = method[2] 66 | up.Topic = msg.Topic() 67 | 68 | // ✅ 优化消息缓存存储,支持并发消息处理 69 | vars.UpdateUpMessageMap(deviceKey, up) 70 | 71 | // 验证缓存存储 72 | ra, ee := vars.GetUpMessageMap(deviceKey) 73 | log.Debug("==222===MessageHandler===========", ra, ee) 74 | 75 | // 在事件参数中添加消息ID,便于后续精确匹配 76 | data.Params["MessageID"] = data.Id 77 | event.MustFire(method[2], data.Params) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /set.go: -------------------------------------------------------------------------------- 1 | package iotgateway 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | mqtt "github.com/eclipse/paho.mqtt.golang" 10 | "github.com/gogf/gf/v2/os/glog" 11 | "github.com/gogf/gf/v2/util/gconv" 12 | "github.com/gookit/event" 13 | "github.com/sagoo-cloud/iotgateway/lib" 14 | "github.com/sagoo-cloud/iotgateway/log" 15 | "github.com/sagoo-cloud/iotgateway/model" 16 | "github.com/sagoo-cloud/iotgateway/mqttProtocol" 17 | "github.com/sagoo-cloud/iotgateway/vars" 18 | ) 19 | 20 | // SubscribeSetEvent 订阅平台的属性设置,需要在有新设备接入时调用 21 | func (gw *Gateway) SubscribeSetEvent(deviceKey string) { 22 | if gw.MQTTClient == nil || !gw.MQTTClient.IsConnected() { 23 | log.Error("【IotGateway】Client has lost connection with the MQTT broker.") 24 | return 25 | } 26 | topic := fmt.Sprintf(setTopic, deviceKey) 27 | glog.Debugf(context.Background(), "【IotGateway】%s 设备订阅了属性设置监听topic: %s", deviceKey, topic) 28 | token := gw.MQTTClient.Subscribe(topic, 1, onSetMessage) 29 | if token.Error() != nil { 30 | glog.Debug(context.Background(), "subscribe error: ", token.Error()) 31 | } 32 | } 33 | 34 | // onSetMessage 属性设置调用处理 35 | var onSetMessage mqtt.MessageHandler = func(client mqtt.Client, msg mqtt.Message) { 36 | if msg != nil { 37 | defer func() { 38 | if r := recover(); r != nil { 39 | fmt.Println("【IotGateway】Recovered in safeCall:", r) 40 | } 41 | }() 42 | ctx := context.Background() 43 | //通过监听到的topic地址获取设备标识 44 | deviceKey := lib.GetTopicInfo("deviceKey", msg.Topic()) 45 | var data = mqttProtocol.ServiceCallRequest{} 46 | glog.Debug(ctx, "【IotGateway】接收到属性设置下发的topic:", msg.Topic()) 47 | glog.Debug(ctx, "【IotGateway】接收收到属性设置下发的数据:", msg.Payload()) 48 | 49 | err := gconv.Scan(msg.Payload(), &data) 50 | if err != nil { 51 | glog.Debug(ctx, "【IotGateway】解析属性设置功能数据出错: %s", err) 52 | return 53 | } 54 | 55 | //触发下发事件 56 | data.Params["DeviceKey"] = deviceKey 57 | 58 | method := strings.Split(data.Method, ".") 59 | var up model.UpMessage 60 | up.MessageID = data.Id 61 | up.SendTime = time.Now().UnixNano() / 1e9 62 | up.MethodName = method[2] 63 | up.Topic = msg.Topic() 64 | 65 | // ✅ 优化消息缓存存储,支持并发消息处理 66 | vars.UpdateUpMessageMap(deviceKey, up) 67 | 68 | // 验证缓存存储(可选,用于调试) 69 | //ra, ee := vars.GetUpMessageMap(deviceKey) 70 | //glog.Debug(ctx, "【IotGateway】验证是否将接收到的属性设置数据进行了缓存:MessageHandler", ra, ee) 71 | 72 | // 在事件参数中添加消息ID,便于后续精确匹配 73 | data.Params["MessageID"] = data.Id 74 | event.MustFire(method[2], data.Params) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /vars/clientListMap.go: -------------------------------------------------------------------------------- 1 | package vars 2 | 3 | import "sync" 4 | 5 | var ClientList sync.Map // 存储客户端列表 6 | 7 | func SetClient(deviceKey, value string) { 8 | ClientList.Store(deviceKey, value) 9 | } 10 | 11 | func GetClient(deviceKey string) (string, bool) { 12 | value, ok := ClientList.Load(deviceKey) 13 | return value.(string), ok 14 | } 15 | -------------------------------------------------------------------------------- /vars/deviceListAllMap.go: -------------------------------------------------------------------------------- 1 | package vars 2 | 3 | import ( 4 | "errors" 5 | "github.com/sagoo-cloud/iotgateway/model" 6 | "sync" 7 | ) 8 | 9 | // 设备列表 10 | var deviceListAllMap sync.Map 11 | 12 | func UpdateDeviceMap(key string, device *model.Device) { 13 | deviceListAllMap.Store(key, device) 14 | } 15 | 16 | func GetDevice(key string) (res *model.Device, err error) { 17 | v, ok := deviceListAllMap.Load(key) 18 | if !ok { 19 | err = errors.New("not data") 20 | return 21 | } 22 | res = v.(*model.Device) 23 | return 24 | } 25 | 26 | // CountDevices 统计设备数量 27 | func CountDevices() int { 28 | count := 0 29 | deviceListAllMap.Range(func(key, value interface{}) bool { 30 | count++ 31 | return true 32 | }) 33 | return count 34 | } 35 | -------------------------------------------------------------------------------- /vars/deviceMessageMap.go: -------------------------------------------------------------------------------- 1 | package vars 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sync" 7 | "time" 8 | 9 | "github.com/sagoo-cloud/iotgateway/model" 10 | ) 11 | 12 | // 设备信息列表 - 保持原有变量名以确保向下兼容 13 | var deviceMessageMap sync.Map 14 | 15 | // 增强的消息缓存,包含过期时间 16 | type enhancedUpMessage struct { 17 | Message model.UpMessage `json:"message"` 18 | ExpireTime int64 `json:"expireTime"` 19 | CreateTime int64 `json:"createTime"` 20 | } 21 | 22 | // 复合键缓存,用于支持同一设备的多个并发消息 23 | var compositeKeyMessageMap sync.Map 24 | 25 | // 消息过期时间(默认30秒) 26 | const messageExpireTimeout = 30 * time.Second 27 | 28 | // 定期清理过期消息的定时器 29 | var cleanupTicker *time.Ticker 30 | var cleanupOnce sync.Once 31 | 32 | // 启动定期清理任务 33 | func startCleanupRoutine() { 34 | cleanupOnce.Do(func() { 35 | cleanupTicker = time.NewTicker(5 * time.Minute) // 每5分钟清理一次 36 | go func() { 37 | for { 38 | select { 39 | case <-cleanupTicker.C: 40 | cleanExpiredMessages() 41 | } 42 | } 43 | }() 44 | }) 45 | } 46 | 47 | // 清理过期消息 48 | func cleanExpiredMessages() { 49 | now := time.Now().Unix() 50 | // 清理复合键缓存中的过期消息 51 | compositeKeyMessageMap.Range(func(key, value interface{}) bool { 52 | if enhanced, ok := value.(enhancedUpMessage); ok { 53 | if enhanced.ExpireTime < now { 54 | compositeKeyMessageMap.Delete(key) 55 | } 56 | } 57 | return true 58 | }) 59 | 60 | // 清理原有缓存中的过期消息 61 | deviceMessageMap.Range(func(key, value interface{}) bool { 62 | if enhanced, ok := value.(enhancedUpMessage); ok { 63 | if enhanced.ExpireTime < now { 64 | deviceMessageMap.Delete(key) 65 | } 66 | } 67 | return true 68 | }) 69 | } 70 | 71 | // 生成复合键 72 | func generateCompositeKey(deviceKey, messageId string) string { 73 | return fmt.Sprintf("%s_%s", deviceKey, messageId) 74 | } 75 | 76 | // UpdateUpMessageMap 保持原有函数签名,内部优化实现 77 | func UpdateUpMessageMap(key string, device model.UpMessage) { 78 | // 启动清理任务 79 | startCleanupRoutine() 80 | 81 | now := time.Now() 82 | enhanced := enhancedUpMessage{ 83 | Message: device, 84 | ExpireTime: now.Add(messageExpireTimeout).Unix(), 85 | CreateTime: now.Unix(), 86 | } 87 | 88 | // 原有的按设备Key存储方式(保持向下兼容) 89 | deviceMessageMap.Store(key, enhanced) 90 | 91 | // 新增:按复合键存储,支持同一设备的多个并发消息 92 | if device.MessageID != "" { 93 | compositeKey := generateCompositeKey(key, device.MessageID) 94 | compositeKeyMessageMap.Store(compositeKey, enhanced) 95 | } 96 | } 97 | 98 | // GetUpMessageMap 保持原有函数签名,内部优化实现 99 | func GetUpMessageMap(key string) (res model.UpMessage, err error) { 100 | // 先尝试从原有缓存获取 101 | v, ok := deviceMessageMap.Load(key) 102 | if !ok { 103 | err = errors.New("not data") 104 | return 105 | } 106 | 107 | // 检查是否为增强类型 108 | if enhanced, ok := v.(enhancedUpMessage); ok { 109 | // 检查是否过期 110 | if enhanced.ExpireTime < time.Now().Unix() { 111 | deviceMessageMap.Delete(key) 112 | err = errors.New("message expired") 113 | return 114 | } 115 | res = enhanced.Message 116 | return 117 | } 118 | 119 | // 兼容旧数据格式 120 | if oldMsg, ok := v.(model.UpMessage); ok { 121 | res = oldMsg 122 | return 123 | } 124 | 125 | err = errors.New("invalid data format") 126 | return 127 | } 128 | 129 | // 新增:根据设备Key和消息ID获取消息(用于精确匹配) 130 | func GetUpMessageByCompositeKey(deviceKey, messageId string) (res model.UpMessage, err error) { 131 | compositeKey := generateCompositeKey(deviceKey, messageId) 132 | v, ok := compositeKeyMessageMap.Load(compositeKey) 133 | if !ok { 134 | // 如果复合键缓存中没有,尝试从原有缓存获取作为兼容 135 | return GetUpMessageMap(deviceKey) 136 | } 137 | 138 | if enhanced, ok := v.(enhancedUpMessage); ok { 139 | // 检查是否过期 140 | if enhanced.ExpireTime < time.Now().Unix() { 141 | compositeKeyMessageMap.Delete(compositeKey) 142 | err = errors.New("message expired") 143 | return 144 | } 145 | res = enhanced.Message 146 | return 147 | } 148 | 149 | err = errors.New("invalid data format") 150 | return 151 | } 152 | 153 | // DeleteFromUpMessageMap 保持原有函数签名,内部优化实现 154 | func DeleteFromUpMessageMap(key string) { 155 | // 删除原有缓存 156 | if v, ok := deviceMessageMap.LoadAndDelete(key); ok { 157 | // 如果是增强类型,同时删除复合键缓存 158 | if enhanced, ok := v.(enhancedUpMessage); ok { 159 | if enhanced.Message.MessageID != "" { 160 | compositeKey := generateCompositeKey(key, enhanced.Message.MessageID) 161 | compositeKeyMessageMap.Delete(compositeKey) 162 | } 163 | } 164 | } 165 | } 166 | 167 | // 新增:根据复合键删除消息 168 | func DeleteFromUpMessageMapByCompositeKey(deviceKey, messageId string) { 169 | compositeKey := generateCompositeKey(deviceKey, messageId) 170 | compositeKeyMessageMap.Delete(compositeKey) 171 | 172 | // 检查是否还有其他消息,如果没有则清理设备缓存 173 | hasOtherMessages := false 174 | compositeKeyMessageMap.Range(func(key, value interface{}) bool { 175 | if keyStr, ok := key.(string); ok { 176 | if len(keyStr) > len(deviceKey)+1 && keyStr[:len(deviceKey)+1] == deviceKey+"_" { 177 | hasOtherMessages = true 178 | return false // 停止遍历 179 | } 180 | } 181 | return true 182 | }) 183 | 184 | // 如果没有其他消息,清理设备缓存 185 | if !hasOtherMessages { 186 | deviceMessageMap.Delete(deviceKey) 187 | } 188 | } 189 | 190 | // 新增:清理指定设备的所有消息(设备离线时调用) 191 | func ClearDeviceMessages(deviceKey string) { 192 | // 清理设备缓存 193 | deviceMessageMap.Delete(deviceKey) 194 | 195 | // 清理所有相关的复合键缓存 196 | keysToDelete := make([]interface{}, 0) 197 | compositeKeyMessageMap.Range(func(key, value interface{}) bool { 198 | if keyStr, ok := key.(string); ok { 199 | if len(keyStr) > len(deviceKey)+1 && keyStr[:len(deviceKey)+1] == deviceKey+"_" { 200 | keysToDelete = append(keysToDelete, key) 201 | } 202 | } 203 | return true 204 | }) 205 | 206 | // 删除收集到的键 207 | for _, key := range keysToDelete { 208 | compositeKeyMessageMap.Delete(key) 209 | } 210 | } 211 | 212 | // 新增:获取缓存统计信息(用于监控和调试) 213 | func GetCacheStats() map[string]interface{} { 214 | deviceCount := 0 215 | compositeCount := 0 216 | expiredCount := 0 217 | now := time.Now().Unix() 218 | 219 | deviceMessageMap.Range(func(key, value interface{}) bool { 220 | deviceCount++ 221 | if enhanced, ok := value.(enhancedUpMessage); ok { 222 | if enhanced.ExpireTime < now { 223 | expiredCount++ 224 | } 225 | } 226 | return true 227 | }) 228 | 229 | compositeKeyMessageMap.Range(func(key, value interface{}) bool { 230 | compositeCount++ 231 | return true 232 | }) 233 | 234 | return map[string]interface{}{ 235 | "deviceCacheCount": deviceCount, 236 | "compositeCacheCount": compositeCount, 237 | "expiredCount": expiredCount, 238 | "lastCleanupTime": time.Now().Format("2006-01-02 15:04:05"), 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /vars/deviceMessageMap_test.go: -------------------------------------------------------------------------------- 1 | package vars 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/sagoo-cloud/iotgateway/model" 7 | ) 8 | 9 | func TestUpdateUpMessageMap(t *testing.T) { 10 | 11 | var device = model.UpMessage{ 12 | MessageID: "112233", 13 | SendTime: 1, 14 | MethodName: "testMethodName", 15 | Topic: "testTopic", 16 | } 17 | UpdateUpMessageMap("test123", device) 18 | gotRes, err := GetUpMessageMap("test123") 19 | if err != nil { 20 | t.Errorf("GetUpMessageMap() error = %v", err) 21 | return 22 | } 23 | t.Log("获取存入的数据:", gotRes) 24 | 25 | // 测试复合键功能 26 | gotResComposite, err := GetUpMessageByCompositeKey("test123", "112233") 27 | if err != nil { 28 | t.Errorf("GetUpMessageByCompositeKey() error = %v", err) 29 | return 30 | } 31 | t.Log("通过复合键获取存入的数据:", gotResComposite) 32 | 33 | // 测试缓存统计 34 | stats := GetCacheStats() 35 | t.Log("缓存统计信息:", stats) 36 | 37 | // 清理测试数据 38 | DeleteFromUpMessageMap("test123") 39 | } 40 | -------------------------------------------------------------------------------- /vars/vars.go: -------------------------------------------------------------------------------- 1 | package vars 2 | 3 | import "github.com/sagoo-cloud/iotgateway/conf" 4 | 5 | var GatewayServerConfig conf.GatewayServerConfig 6 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import "fmt" 4 | 5 | var ( 6 | BuildVersion string 7 | BuildTime string 8 | CommitID string 9 | ) 10 | 11 | func ShowLogo(buildVersion, buildTime, commitID string) { 12 | BuildVersion = buildVersion 13 | BuildTime = buildTime 14 | CommitID = commitID 15 | 16 | //版本号 17 | //fmt.Println(" _____ \n / ____| \n | (___ __ _ __ _ ___ ___ \n \\___ \\ / _` |/ _` |/ _ \\ / _ \\ \n ____) | (_| | (_| | (_) | (_) |\n |_____/ \\__,_|\\__, |\\___/ \\___/ \n __/ | \n |___/ ") 18 | fmt.Println("\n\x1b[32m_________________________________________________________\n __ __ __ ______\n / ) / / ) / \n----\\--------__----__----__----__-----/----/----/---/----\n \\ / ) / ) / ) / ) / / / / \n_(____/___(___(_(___/_(___/_(___/_ _/_ __(____/___/______\n / \n (_ / \x1b[0m\n") 19 | fmt.Println("Version :", buildVersion) 20 | fmt.Println("BuildTime :", buildTime) 21 | fmt.Println("CommitID :", commitID) 22 | fmt.Println("") 23 | } 24 | func GetVersion() string { 25 | if BuildVersion == "" { 26 | BuildVersion = "0.0" 27 | } 28 | return BuildVersion 29 | } 30 | func GetBuildTime() string { 31 | return BuildTime 32 | } 33 | func GetCommitID() string { 34 | return CommitID 35 | } 36 | --------------------------------------------------------------------------------