# 边缘计算通用解决方案
**Repository Path**: coffeeLVeris/edge-computing
## Basic Information
- **Project Name**: 边缘计算通用解决方案
- **Description**: 提供经过验证的边缘计算处理框架,开放核心源代码以供二开,支持客户自主品牌产品开发
- **Primary Language**: Go
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 0
- **Created**: 2025-05-25
- **Last Updated**: 2025-06-25
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
**更详细说明请查看官方文档:[开放式边缘计算解决方案文档](https://www.yuque.com/edge-nexus)**
我们的定位
**「技术方案供应商」而非产品竞争者**
- [x] 提供经过验证的边缘计算处理框架
- [x] 开放核心源代码以供二开(非完整产品代码)
- [x] 支持客户自主品牌产品开发
方案优势对比表
本方案使用Go语言开发,有编程经验工程师基本一周可入门二开,比起从头用传统C研发,节省上百万人力成本不成问题。我们曾用传统方案开发同类产品,用5~6人团队研发近一年,用本方案只需要1~2人,不到一个月即可适配完成。
| **对比维度** | **传统C语言方案** | **本方案(Go语言)** | **客户价值** |
| --- | --- | --- | --- |
| **开发效率** | 需手动管理内存/指针,开发周期长(3-6个月) | **Go语言自动内存管理,标准库丰富,开发周期缩短60%+** | 快速迭代,抢占市场先机 |
| **代码可维护性** | 复杂指针操作导致调试困难,新人上手慢 | **Go语法简洁,代码可读性强,团队协作效率提升3倍** | 降低技术债务,长期省心 |
| **协议扩展性** | 新增协议需重写底层驱动,开发成本高 | **Go模块化设计,已有Modbus/OPC UA等插件,新协议开发时间减少80%** | 灵活适配各类设备 |
| **并发性能** | 需自行实现线程池/锁机制,风险高 | **原生goroutine轻量级并发,单节点可处理10,000+并发连接** | 高吞吐量,稳定可靠 |
| **云原生支持** | 需额外集成中间件,兼容性差 | **原生支持HTTP/JSON/gRPC,与K8s/Docker生态无缝对接** | 轻松上云,未来无忧 |
**Go语言核心优势**:
✔ **开发速度比C快5倍**(实测同等功能代码量减少70%)
✔ **丰富的扩展包**,更易获取和管理的三方包,更易获取和管理
✔ **跨平台编译**,同一套代码可部署到ARM/x86/边缘设备
适合三类合作伙伴
1. **硬件制造商**
- 快速获得工业网关软件能力
- 可自由选择硬件方案(我们提供BSP适配指南)
2. **行业解决方案商**
- 专注行业应用开发(我们处理底层架构)
- 支持私有协议定制(提供协议开发工具包)
3. **大型企业IT部门**
- 避免被单一供应商绑定
- 获得持续迭代的技术支持
## **主要功能**
+ **边缘侧数据采集:**
- 在设备侧即可完成轮询采集设备数据,同时将设备数据转换成标准、统一的JSON格式,不仅减轻远程平台的轮询采集压力,而且减轻远程平台数据转换压力,远程平台只需专注于应用;内置公式计算引擎,支持将设备数据进行运算,减轻远程平台运算压力;
+ **边缘侧协议转换:**
- 在设备侧即可完成数据采集、协议转换和数据解析,更加实时高效,减轻远程平台的通信压力;可扩展各种标准通信协议,如ModbusRTU、ModbusTCP、DLT645-1997/07、CJ/T188、西门子PLC、BACNet、EC104、OPCUA等;
+ **多协议、多通道上报:**
- 支持多种协议上报(MQTT、HTTP、自定义TCP/UDP等),支持同一时刻上报数据到不同的平台;支持定时上报和变化上报,即保证设备数据的实时性,又降低了流量消耗;支持远程平台命令下发,接收命令后进行解析和验证,在转发给设备;
+ **边缘侧场景联动:**
- 在设备测即可完成多种设备的联动控制,无需将设备数据上传至远程平台,联动控制速度更快、更稳定;
+ **远程维护、远程升级:**
- 内置Webserver,通过浏览器即可完成各项功能配置,上手简单、使用方便,减少运维成本,提高产品使用体验;
+ **设备远程上下载**:
- 提供VPN扩展服务,可实现网络设备的远程调试、固件上下载等功能。
## 技术架构

## 快速体验
**PS:本技术方案支持跨平台编译,以下仅提供各OS典型版本,如果需要特殊版本的测试包,请联系我们!**
### 测试包
[windows-x64.zip](https://www.yuque.com/attachments/yuque/0/2025/zip/227204/1746966263936-efce7830-3acc-468c-85c5-79290d003a2f.zip):针对windows-x64位系统。
[linux-amd64.zip](https://www.yuque.com/attachments/yuque/0/2025/zip/227204/1746973959139-2146bfba-0f36-48db-a1dc-4b747b8c4ae4.zip):针对linux-x64位系统。
### 测试方法
1. 将软件包解压到相应的设备中,直接运行其中的可执行程序。以windows系统为例,运行目录中的`windows-x64.exe`。

2. 运行后可看到log信息,提示`gin初始化端口成功`就表示正常运行了,因暂未配置VPN服务器和公有云,所以会有些错误提示,不用管。

3. 在浏览器地址栏输入运行程序的设备地址,即可看到配置页面。默认端口是`8080`,这个可以从配置文件中修改。

4. 首次登录需要输入用户名和密码,默认是`admin/admin`。登录成功后即可看到一些默认配置。


5. 点到从机设备,默认配置中自带一个modbusTCP设备的采集示例。点击`编辑`即可看到相关配置。这里的通讯接口是`tcpc1`。


6. 可以修改`tcp1`的配置,默认配置是采集IP为`127.0.0.1`,端口为`502`的modbus从机设备,这里推荐大家直接安装`Modbus Slave`软件来进行测试。可以根据自己的情况来修改接口的配置。


7. 点击从机`点表`可以看到当前配置的采集数据点配置,默认增加了两个保持寄存器的采集。


8. 如果`Modbus Slave`软件配置运行成功,再次刷新页面即可看到设备由离线状态变为在线状态,此时点击`监控`选项即可看到最新的采集值。



9. 设备数据是实时上报的,网页现在开发的还不是很完善,监控每次需要点一下才能看到最新的值。
10. 如果需要测试新的从机,可以在从机页面添加自己的从机配置。选择好从机协议、通信端口即可。


11. 页面上所有提供的配置都可进行修改测试,因提供的web前端仅是demo案例,难免有bug。但是能保证后端代码是经过完整测试的。真实商用产品需要基于现有的前端代码进行二开或者自行开发。
12. 所有配置型都存储在`selfpara`目录的`json`文件中。

### 演示视频
**windows平台**
[点击链接查看](https://www.yuque.com/docs/219022204#b24NW)
**linux平台**
[点击链接查看](https://www.yuque.com/docs/219022204#GJtLq)
**mips平台(openwrt)**
[点击链接查看](https://www.yuque.com/docs/219022204#miLzF)
## 源码结构
```plain
├── config //设备配置文件
│ ├── config.ini //包含常规配置,比如日志级别,内置网页端口号等
│ ├── device
│ │ └── devType.json //描述内置网页支持的协议
│ └── protocol //描述支持的各协议的寄存器类型
│ ├── modbusTCP.json
│ └── siemensS7.json
├── device //设备层核心源码
│ ├── cloudManage.go
│ ├── collectInterface.go //设备采集主要实现文件
│ ├── commInterface //通信接口
│ │ ├── commInterface.go
│ │ ├── commIoIn.go
│ │ ├── commIoOut.go
│ │ ├── commSerial.go
│ │ ├── commTcpClient.go
│ │ └── commTcpServer.go
│ ├── commManage.go //通信接口管理
│ ├── deviceNode.go
│ ├── deviceTSL.go //设备物模型的管理
│ └── eventBus
├── go.mod
├── go.sum
├── httpServer //内置网页实现
│ ├── contorl
│ ├── middleware
│ ├── model
│ └── router.go
├── log
│ └── gogate.log
├── main.go
├── Makefile
├── protocol //各自协议
│ ├── proHaasTcp.go
│ ├── proInterface.go
│ ├── proModbusTcp.go
│ └── proSiemensS7.go
├── report //上报转发层实现
│ ├── mqttAliyun
│ ├── mqttJiJian
│ └── reportService.go
├── selfpara //运行实时配置文件
│ ├── collInterface.json
│ ├── commIoInInterface.json
│ ├── commIoOutInterface.json
│ ├── commSerialInterface.json
│ ├── commTcpClientInterface.json
│ ├── commTcpServerInterface.json
│ ├── deviceTSLParam.json
│ ├── reportServiceParamListAliyun.json
│ ├── reportServiceParamListJiJian.json
├── setting //设置接口封装
│ ├── backup.go
│ ├── cmd.go
│ ├── conf.go
│ ├── csvUtils.go
│ ├── loger.go
│ ├── mlua.go
│ ├── network.go
│ ├── ntp.go
│ ├── ntp_test.go
│ ├── recover.go
│ ├── serial.go
│ ├── system.go
│ ├── update.go
│ ├── vpn.go
│ └── zap.go
├── utils //组件封装
│ ├── dataUtils.go
│ ├── fileUtils.go
│ ├── nvUtils.go
│ └── testnv.go
└── webroot //内置页面打包
├── index.html
└── static
```
## 子协议源码示例
+ 这里以`modbusTCP`的实现为例。
```go
package protocol
import (
"encoding/binary"
"fmt"
modbus "github.com/thinkgos/gomodbus/v2"
"gogate/device/commInterface"
"gogate/setting"
"math"
"strconv"
)
type ProModbusTcpTemplate struct {
Name string //协议名
Sid int //从机地址
//UseComIf bool //是否使用标准通信接口
comIf commInterface.CommunicationInterface //通信接口
isConnect bool
client modbus.Client
}
func (c *ProModbusTcpTemplate) Construct() {
c.Name = "modbusTCP"
c.Sid = -1
c.comIf = nil
//c.UseComIf = false
c.isConnect = false
}
func (c *ProModbusTcpTemplate) Init(inf commInterface.CommunicationInterface) bool {
c.comIf = inf
if inf.GetType() != commInterface.CommTypeTcpClient {
setting.ZAPS.Errorf("err comm type")
}
para := inf.GetParam().TcpCParam
setting.ZAPS.Debugf("protocol [%s] ip=[%s]", c.Name, para.IP)
addr := fmt.Sprintf("%s:%s", para.IP, para.Port)
p := modbus.NewTCPClientProvider(addr)
c.client = modbus.NewClient(p)
c.client.LogMode(false)
return true
}
func (c *ProModbusTcpTemplate) Connect() bool {
err := c.client.Connect()
if err != nil {
setting.ZAPS.Errorf("client connect err")
c.isConnect = false
return false
}
c.isConnect = true
return true
}
func (c *ProModbusTcpTemplate) IsConnect() bool {
return c.isConnect
}
func (c *ProModbusTcpTemplate) GetName() string {
return c.Name
}
const (
RegTypeCoilStatus int = iota //线圈状态
RegTypeInputStatus //离散输入状态
RegTypeHoldingReg //保持寄存器
RegTypeInputReg //输入寄存器
)
const (
DataTypeINT16 int = iota //整型
DataTypeUINT16
//DataTypeUBCD16
DataTypeINT32
DataTypeUINT32
//DataTypeUBCD32
DataTypeINT64
DataTypeUINT64
//DataTypeUBCD64
DataTypeFLOAT //浮点
//DataTypeDOUBLE
DataTypeBIT //位
)
var regTypeMap = map[string]int{"线圈状态": RegTypeCoilStatus, "离散输入状态": RegTypeInputStatus, "保持寄存器": RegTypeHoldingReg, "输入寄存器": RegTypeInputReg}
var dataTypeMap = map[string]int{"INT16": DataTypeINT16, "BIT": DataTypeBIT, "FLOAT": DataTypeFLOAT, "UINT64": DataTypeUINT64, "INT64": DataTypeINT64, "UINT32": DataTypeUINT32, "INT32": DataTypeINT32, "UINT16": DataTypeUINT16}
func (c *ProModbusTcpTemplate) GetNode(node *NodeProperty) int {
//setting.ZAPS.Debugf("%s get node %s", c.Name, node.ID)
//先检查连接情况,如果没有连接,先尝试连接
if !c.isConnect {
ret := c.Connect()
//如果还连接不成功,不再查询
if !ret {
return -1
}
}
nodeRegType := regTypeMap[node.RegType]
nodeDataType := dataTypeMap[node.DataType]
if nodeRegType == RegTypeCoilStatus {
switch nodeDataType {
case DataTypeBIT:
value, err := c.client.ReadCoils(byte(c.Sid), uint16(node.RegAddr), 1)
if err != nil {
setting.ZAPS.Errorf("read Coil err")
return -1
}
node.Value = value[0]
setting.ZAPS.Debugf("read data = %d", node.Value)
default:
{
setting.ZAPS.Errorf("data type err")
return -1
}
}
} else if nodeRegType == RegTypeInputStatus {
switch nodeDataType {
case DataTypeBIT:
value, err := c.client.ReadDiscreteInputs(byte(c.Sid), uint16(node.RegAddr), 1)
if err != nil {
setting.ZAPS.Errorf("read Discrete err")
return -1
}
node.Value = value[0]
setting.ZAPS.Debugf("read data = %d", node.Value)
default:
{
setting.ZAPS.Errorf("data type err")
return -1
}
}
} else if nodeRegType == RegTypeHoldingReg {
switch nodeDataType {
case DataTypeINT16:
fallthrough
case DataTypeUINT16:
value, err := c.client.ReadHoldingRegisters(byte(c.Sid), uint16(node.RegAddr), 1)
if err != nil {
setting.ZAPS.Errorf("read hold err")
return -1
}
node.Value = value[0]
//setting.ZAPS.Debugf("read data = %d", node.Value)
case DataTypeINT32:
fallthrough
case DataTypeUINT32:
value, err := c.client.ReadHoldingRegistersBytes(byte(c.Sid), uint16(node.RegAddr), 2)
if err != nil {
setting.ZAPS.Errorf("read hold err")
return -1
}
node.Value = binary.BigEndian.Uint32(value)
//node.Value = *(*uint32)(unsafe.Pointer(&value))
setting.ZAPS.Debugf("read data = %d", node.Value)
case DataTypeFLOAT:
value, err := c.client.ReadHoldingRegistersBytes(byte(c.Sid), uint16(node.RegAddr), 2)
if err != nil {
setting.ZAPS.Errorf("read hold err")
return -1
}
bit := binary.BigEndian.Uint32(value)
node.Value = math.Float32frombits(bit)
//node.Value = *(*float32)(unsafe.Pointer(&value))
setting.ZAPS.Debugf("read data = %f", node.Value)
case DataTypeINT64:
fallthrough
case DataTypeUINT64:
value, err := c.client.ReadHoldingRegistersBytes(byte(c.Sid), uint16(node.RegAddr), 4)
if err != nil {
setting.ZAPS.Errorf("read hold err")
return -1
}
node.Value = binary.BigEndian.Uint64(value)
//node.Value = *(*uint64)(unsafe.Pointer(&value))
setting.ZAPS.Debugf("read data = %d", node.Value)
default:
{
setting.ZAPS.Errorf("data type err")
return -1
}
}
} else if nodeRegType == RegTypeInputReg {
switch nodeDataType {
case DataTypeINT16:
fallthrough
case DataTypeUINT16:
value, err := c.client.ReadInputRegisters(byte(c.Sid), uint16(node.RegAddr), 1)
if err != nil {
setting.ZAPS.Errorf("read Input err")
return -1
}
node.Value = value
setting.ZAPS.Debugf("read data = %d", node.Value)
case DataTypeINT32:
fallthrough
case DataTypeUINT32:
value, err := c.client.ReadInputRegistersBytes(byte(c.Sid), uint16(node.RegAddr), 2)
if err != nil {
setting.ZAPS.Errorf("read Input err")
return -1
}
node.Value = binary.BigEndian.Uint32(value)
//node.Value = *(*uint32)(unsafe.Pointer(&value))
setting.ZAPS.Debugf("read data = %d", node.Value)
case DataTypeFLOAT:
value, err := c.client.ReadInputRegistersBytes(byte(c.Sid), uint16(node.RegAddr), 2)
if err != nil {
setting.ZAPS.Errorf("read Input err")
return -1
}
bit := binary.BigEndian.Uint32(value)
node.Value = math.Float32frombits(bit)
//node.Value = *(*float32)(unsafe.Pointer(&value))
setting.ZAPS.Debugf("read data = %f", node.Value)
case DataTypeINT64:
fallthrough
case DataTypeUINT64:
value, err := c.client.ReadInputRegistersBytes(byte(c.Sid), uint16(node.RegAddr), 4)
if err != nil {
setting.ZAPS.Errorf("read Input err")
return -1
}
node.Value = binary.BigEndian.Uint64(value)
//node.Value = *(*uint64)(unsafe.Pointer(&value))
setting.ZAPS.Debugf("read data = %d", node.Value)
default:
{
setting.ZAPS.Errorf("data type err")
return -1
}
}
} else {
setting.ZAPS.Errorf("%s get node %s type err", c.Name, node.ID)
return -1
}
return 0
}
func (c *ProModbusTcpTemplate) SetNode(node *NodeProperty) int {
setting.ZAPS.Debugf("%s set node %s", c.Name, node.ID)
var array []byte
nodeRegType := regTypeMap[node.RegType]
nodeDataType := dataTypeMap[node.DataType]
if nodeRegType == RegTypeCoilStatus {
switch nodeDataType {
case DataTypeBIT:
val, err := strconv.ParseBool(fmt.Sprint(node.SetValue.(string)))
if err != nil {
setting.ZAPS.Errorf("write coil err, node value change bool err")
return -1
}
err = c.client.WriteSingleCoil(byte(c.Sid), uint16(node.RegAddr), val)
if err != nil {
setting.ZAPS.Errorf("write coil err")
return -1
}
default:
{
setting.ZAPS.Errorf("data type err")
return -1
}
}
} else if nodeRegType == RegTypeHoldingReg {
switch nodeDataType {
case DataTypeFLOAT:
val, err := strconv.ParseFloat(node.SetValue.(string), 16)
if err != nil {
setting.ZAPS.Errorf("data is invalid")
return -1
}
bit := math.Float32bits(float32(val))
binary.BigEndian.PutUint32(array, bit)
err = c.client.WriteMultipleRegistersBytes(byte(c.Sid), uint16(node.RegAddr), 2, array)
if err != nil {
setting.ZAPS.Errorf("write Reg err")
return -1
}
case DataTypeINT16:
fallthrough
case DataTypeUINT16:
val, err := strconv.ParseUint(node.SetValue.(string), 10, 16)
if err != nil {
setting.ZAPS.Errorf("data is invalid")
return -1
}
err = c.client.WriteSingleRegister(byte(c.Sid), uint16(node.RegAddr), uint16(val))
if err != nil {
setting.ZAPS.Errorf("write Reg err")
return -1
}
case DataTypeINT32:
fallthrough
case DataTypeUINT32:
val, err := strconv.ParseUint(node.SetValue.(string), 10, 16)
if err != nil {
setting.ZAPS.Errorf("data is invalid")
return -1
}
binary.BigEndian.PutUint32(array, uint32(val))
err = c.client.WriteMultipleRegistersBytes(byte(c.Sid), uint16(node.RegAddr), 2, array)
if err != nil {
setting.ZAPS.Errorf("write Reg err")
return -1
}
case DataTypeINT64:
fallthrough
case DataTypeUINT64:
val, err := strconv.ParseUint(node.SetValue.(string), 10, 16)
if err != nil {
setting.ZAPS.Errorf("data is invalid")
return -1
}
binary.BigEndian.PutUint64(array, val)
err = c.client.WriteMultipleRegistersBytes(byte(c.Sid), uint16(node.RegAddr), 4, array)
if err != nil {
setting.ZAPS.Errorf("write Reg err")
return -1
}
default:
{
setting.ZAPS.Errorf("data type err")
return -1
}
}
} else {
setting.ZAPS.Debugf("%s set node %s type err", c.Name, node.ID)
return -1
}
return 0
}
func (c *ProModbusTcpTemplate) GetSid() int {
return c.Sid
}
func (c *ProModbusTcpTemplate) SetSid(id int) {
c.Sid = id
}
func (c *ProModbusTcpTemplate) GetComInf() commInterface.CommunicationInterface {
return c.comIf
}
```
## 子协议配置示例
+ 以`modbusTCP`为例,在`/config/protocol`目录中增加子协议寄存器配置文件,这个文件主要提供相应协议增加寄存器时的寄存器的配置项,由后台程序返回给前端页面。

```json
{
"ProType": "modbusTCP",
"TypeVal": [
"线圈寄存器",
"离散输入寄存器",
"保持寄存器",
"输入寄存器"
],
"Properties": [
{
"RegType": "线圈寄存器",
"AddrLimit": [
0,
65535
],
"DataType": [
"BIT"
]
},
{
"RegType": "离散输入寄存器",
"AddrLimit": [
0,
65535
],
"DataType": [
"BIT"
]
},
{
"RegType": "保持寄存器",
"AddrLimit": [
0,
65535
],
"DataType": [
"INT16",
"INT32",
"FLOAT"
]
},
{
"RegType": "输入寄存器",
"AddrLimit": [
0,
65535
],
"DataType": [
"INT16",
"INT32",
"FLOAT"
]
}
]
}
```
+ 在`devicedevType.json`中添加子协议的配置,这个主要是在内置网页添加从机时候,提供从机接口选择配置项使用,这里以西门子协议为例。

```json
[
{
"value": "Modbus",
"label": "Modbus",
"children": [
{
"label": "TCP",
"value": "modbusTCP",
"ComType": "TcpClient",
"HasSid": true
},
{
"label": "RTU",
"value": "modbusRTU",
"ComType": "LocalSerial",
"HasSid": true
}
]
},
{
"value": "西门子",
"label": "西门子",
"children": [
{
"value": "S7200",
"label": "S7200",
"children": [
{
"label": "S7(网口)",
"value": "ppiTCP",
"ComType": "TcpClient",
"HasSid": false
},
{
"label": "PPI(串口)",
"value": "ppiRTU",
"ComType": "LocalSerial",
"HasSid": true
}
]
},
{
"value": "S7300",
"label": "S7300",
"children": [
{
"label": "S7(网口)",
"value": "mpiTCP",
"ComType": "TcpClient",
"HasSid": false
},
{
"label": "PPI(串口)",
"value": "mpiRTU",
"ComType": "LocalSerial",
"HasSid": true
}
]
}
]
},
{
"value": "Haas",
"label": "Haas",
"children": [
{
"label": "TCP",
"value": "haasTCP",
"ComType": "TcpClient",
"HasSid": true
}
]
}
]
```
大部分网关核心功能已经完成,客户获取源码后更多的是增加私有协议和定制自己的业务逻辑。以下对几处主要的二次开发功能点进行说明:
## 私有协议
仿照[源码概览](https://www.yuque.com/edge-nexus/poxy3m/xurwmyiu1aqqakmt?singleDoc#)章节的`modbustcp`协议的例子,首先增加子协议的源码实现,这里主要实现`GetNode`、`SetNode`两个核心框架抽象的接口以及`协议的寄存器定义`即可。然后提供子协议的配置文件即可完成对接。在工业物联网(IIoT)和工业自动化领域,Go语言已经积累丰富的协议库支持,所以在子协议对接开发上效率会非常高,[参考go工业协议支持](https://www.yuque.com/edge-nexus/poxy3m/gf0cx5g2e8tqug1w?singleDoc#%20《二开说明》)。
## 内置网页
源码中的内置网页是基于Vue开发的,客户可直接使用进行二次开发,也可以让专门前端工程师重新定制。
## VPN服务
如果客户产品要实现远程上下载服务,那么只需要提供一台公有云服务器即可,我们可协助客户搭建VPN服务器。设备端只要配置一下VPN服务器的地址即可使用。无需额外的开发。
## 转发服务
源码中提供了阿里云以及一个简单的通用MQTT极简云对接示例,客户可根据这个示例实现任意的转发服务。客户只需要订阅内部总线上的各类主题即可方便的获取到设备的各种状态信息,比如上线/离线、数据点变化、点表修改等。