开源!Go+Wails+Vue3 手搓一个 PLC 实时监控桌面工具

写在前面

工业自动化领域,监控 PLC 点位数据是绕不开的需求。商业方案有西门子博图(TIA Portal)的 Watch Table、各类 SCADA 系统,但要么臃肿、要么昂贵、要么闭源。

最近用 Go + Wails v2 + Vue 3 搓了一个开源桌面工具 PLC-Monitor,实现了类似博图 Watch Table 的核心功能------多连接管理、实时点位监控、趋势曲线、强制写入。本文将分享整个项目的技术架构和实现细节。

项目已开源在 Gitee:gitee.com/shenjoy/plc...


一、技术选型:为什么是 Wails?

桌面监控工具需要满足几个要求:

  • 原生性能,低延迟轮询 PLC
  • 跨平台(Windows + Linux 工控机都跑)
  • UI 现代,不要 MFC/Winform 那种老古董

对比了几个方案:

方案 优点 缺点
Electron 生态丰富,UI 好 内存占用大,打包体积大
C# WPF .NET 生态 跨平台麻烦
Qt (C++) 成熟稳定 开发效率低
Wails v2 Go 后端 + 前端 Web 技术 生态相对年轻

Wails v2 是 Go 写的桌面框架------后端用 Go 直接调 PLC 协议栈,前端用 Vue/React 写界面,原生 WebView 渲染,打包体积只有十几 MB。对工控场景来说:后端性能要硬,前端交互要活,Wails 正好卡在这个点上。


二、项目架构

bash 复制代码
plc-monitor/
├── main.go                       # 应用入口
├── app.go                        # Wails 应用结构体 (IPC 绑定)
├── pkg/
│   ├── driver/                   # 驱动抽象层
│   │   ├── driver.go             # Driver 接口定义
│   │   ├── s7/s7.go              # Siemens S7 驱动
│   │   └── modbus/modbus.go      # Modbus TCP 驱动
│   ├── engine/                   # 轮询引擎
│   │   ├── engine.go             # 核心:轮询 + 变化检测 + 历史存储
│   │   └── engine_test.go        # 单元测试
│   └── store/                    # 配置持久化
└── frontend/
    ├── src/
    │   ├── App.vue               # 主布局
    │   ├── components/
    │   │   ├── connection/       # 连接管理组件
    │   │   ├── watchtable/       # 监控表组件
    │   │   └── trendchart/       # 趋势图组件 (ECharts)
    │   └── composables/api.ts    # Wails IPC 桥接

架构分层示意:

scss 复制代码
┌─────────────────────────────────────┐
│           Vue 3 Frontend             │
│  ConnectionList │ WatchTable │ Chart │
├─────────────────────────────────────┤
│         Wails IPC (Events + Bind)    │
├─────────────────────────────────────┤
│     app.go (业务编排 + 事件日志)       │
├─────────────────────────────────────┤
│   PollingEngine (轮询 + 变化检测)      │
├──────────────┬──────────────────────┤
│  S7 Driver   │   Modbus Driver      │
├──────────────┴──────────────────────┤
│          PLC 设备 (物理/仿真)          │
└─────────────────────────────────────┘

三、关键设计:驱动抽象层

这是项目最核心的抽象。工控协议百花齐放------S7、Modbus、OPC UA、MC Protocol......但不管什么协议,本质都是:连接、读、写。

pkg/driver/driver.go 中定义接口:

go 复制代码
type Driver interface {
    Connect(connector Connector) error
    Disconnect() error
    ReadTag(tag Tag) (interface{}, error)
    WriteTag(tag Tag, value interface{}) error
    IsConnected() bool
}

在此基础上,还抽象了一个可选的 BatchReader 接口:

go 复制代码
type BatchReader interface {
    ReadBatch(tags []BatchTag) []BatchResult
}

为什么拆成两个接口?因为不是所有协议都支持批量读取(Modbus 通常逐点读,S7 可以读连续内存块然后自己解析)。用接口组合代替继承,避免了强迫 Modbus 驱动实现一个空壳批量方法。

驱动工厂与注册机制

驱动采用注册式工厂模式 ,新增驱动只需在 init() 中注册:

go 复制代码
// pkg/driver/factory.go
func NewDriver(protocol string) (Driver, error) {
    // 从注册表查找
}

// pkg/driver/s7/s7.go
func init() {
    driver.Register("s7", func() driver.Driver { return &S7Driver{} })
}

// pkg/driver/modbus/modbus.go
func init() {
    driver.Register("modbus-tcp", func() driver.Driver { return &ModbusDriver{} })
}

main.go 中通过 blank import 注册:

go 复制代码
import (
    _ "plc-monitor/pkg/driver/modbus"
    _ "plc-monitor/pkg/driver/s7"
)

扩展新协议只需要新建一个包,实现 Driver 接口,init() 里注册------零侵入


四、S7 批量读取优化

S7 驱动实现了 BatchReader 接口,这是性能提升的关键。

逐点读取 vs 批量读取

假设监控表有 20 个 DB 块点位:

  • 逐点读取:20 次 TCP 请求,往返延迟 = 20 × RTT
  • 批量读取:1 次 TCP 请求,读取连续内存块后本地解析

实现思路

将一批点位按内存区域分组(DB 块、M 区、E 区、A 区),每组发起一次连续读,再从返回的字节数组中按偏移和类型解析出各点位值:

go 复制代码
func (d *S7Driver) ReadBatch(tags []driver.BatchTag) []driver.BatchResult {
    // 1. 按内存区域分组
    groups := groupByArea(tags)

    // 2. 每组读取连续内存块
    for area, group := range groups {
        buffer := make([]byte, areaSize(group))
        err := d.client.AGReadDB(area.DB, area.Start, len(buffer), buffer)

        // 3. 从缓冲中解析每个点位
        for _, tag := range group {
            value := parseFromBuffer(buffer, tag)
            results = append(results, ...)
        }
    }
    return results
}

地址格式支持

地址类型 格式 示例 说明
DB 位 DB{n}.DBX{a}.{b} DB1.DBX0.0 DB1 的第 0 字节的第 0 位
DB 字节 DB{n}.DBB{a} DB1.DBB0 DB1 的第 0 字节
DB 字 DB{n}.DBW{a} DB1.DBW2 DB1 的第 2 字节开始的字(16 位)
DB 双字 DB{n}.DBD{a} DB1.DBD4 DB1 的第 4 字节开始的双字(32 位)
中间寄存器 M{a}.{b} M0.0, MW20 Merker/中间寄存器
输入 E{a}.{b} / I{a}.{b} E0.0 物理输入
输出 A{a}.{b} / Q{a}.{b} A0.0 物理输出

数据类型支持

bool, byte, word, int, dword, dint, real, string


五、核心引擎:轮询 + 变化检测 + 历史存储

轮询引擎

PollingEngine 是 Go 的并发经典模式------每个连接一个 goroutine,通过 context.Context 统一控制生命周期:

go 复制代码
func (e *PollingEngine) startPolling(state *connectionState) {
    ctx, cancel := context.WithCancel(context.Background())
    state.cancel = cancel

    go e.pollLoop(ctx, state)
}

func (e *PollingEngine) pollLoop(ctx context.Context, state *connectionState) {
    ticker := time.NewTicker(500 * time.Millisecond)
    defer ticker.Stop()

    for {
        select {
        case <-ctx.Done():
            return
        case <-ticker.C:
            e.pollOnce(connID, drv, tags, ...)
        }
    }
}

停止轮询只需要 cancel(),goroutine 自动退出。干净利落。

变化检测

每个点位缓存上一次的值,轮询时做比较,只把变化的数据推向前端:

go 复制代码
func valuesDiffer(a, b interface{}) bool {
    switch va := a.(type) {
    case bool:
        return va != b.(bool)
    case float64:
        return va != b.(float64)
    // ...
    }
}

前端接收到变化后,对应的单元格高亮闪烁 2 秒,视觉效果参考博图 Watch Table。

历史数据环形缓冲区

趋势图需要历史数据,直接全量存内存不现实。用环形缓冲区(Ring Buffer),每个 tag 保留最近 1000 个点:

go 复制代码
type HistoryStore struct {
    data    map[string][]HistoryPoint
    maxSize int
}

func (h *HistoryStore) Add(tagID string, point HistoryPoint) {
    entries := h.data[tagID]
    if len(entries) >= h.maxSize {
        entries = entries[1:]  // 移除最旧
    }
    h.data[tagID] = append(entries, point)
}

简单、够用、零依赖。

断线检测

连续 3 次轮询全部失败,判定为断线:

go 复制代码
if allFailed {
    state.failCount++
} else {
    state.failCount = 0
}
if state.failCount >= 3 {
    emitFunc("connection-status", "disconnected", ...)
}

六、前端实现

前端用 Vue 3 Composition API + TypeScript,没有引入 Pinia 或 Vuex------状态足够简单,refcomputed 就搞定。

界面布局

Wails IPC 通信

Wails 自动将 Go 方法暴露给前端,通过 window.go.main.App.xxx() 调用:

typescript 复制代码
export const api = {
    getConnections: (): Promise<ConnectionDTO[]> =>
        window.go.main.App.GetConnections(),

    addTag: (connId: string, name: string, address: string, dataType: string) =>
        window.go.main.App.AddTag(connId, name, address, dataType),

    startMonitor: () => window.go.main.App.StartMonitor(),
    // ...
}

实时数据推送走 Wails 的 Events 机制------后端 runtime.EventsEmit(),前端 EventsOn() 监听:

typescript 复制代码
events.onDataUpdate((update) => {
    for (const snap of update.snapshots) {
        values.value.set(snap.tagId, snap)
    }
    values.value = new Map(values.value)  // 触发响应式
})

趋势图的性能优化

趋势图用 ECharts 6 实时展示,这里有一个关键优化------鼠标悬停时暂停更新

typescript 复制代码
let hovered = false
let dirty = false

chart.on('mouseover', () => { hovered = true })
chart.on('mouseout', () => {
    hovered = false
    if (dirty) flushToChart()  // 移开时一次性刷新
})

function addDataPoint(idx, timestamp, value) {
    dataStore[idx] = [...dataStore[idx], [timestamp, value]]
    if (hovered) {
        dirty = true  // 悬停中:标记脏数据,不触发重绘
        return
    }
    flushToChart()
}

不做这个优化的话,鼠标悬停看数据点时,图表每 500ms 重绘一次,Tooltip 会疯狂闪烁,体验极差。

CSV 批量导入

工控工程师习惯用 Excel 排点位表,CSV 导入直接打通这个工作流:

go 复制代码
name,address,dataType
温度,DB1.0,uint16
压力,DB1.2,uint16
启停,DB1.DBX0.0,bool
转速,DB1.DBD4,float32

后端解析 CSV 后自动加入监控表,几十个点位一次搞定。


七、使用流程

整个流程围绕工控工程师的真实操作路径设计,上手成本很低。

运行效果:

复制代码
wails dev

Wails 会自动打开桌面窗口(1280×800),也可以在浏览器访问 http://localhost:34115 调试。


八、快速体验

前置条件

  • Go 1.21+
  • Node.js 18+
  • Wails v2 CLI
bash 复制代码
go install github.com/wailsapp/wails/v2/cmd/wails@latest

克隆并运行

bash 复制代码
git clone https://gitee.com/shenjoy/plc-monitor.git
cd plc-monitor
wails dev

打包构建

bash 复制代码
wails build

产物在 build/bin/ 目录。


九、总结与下一步

这个项目展示了 Go + Wails + Vue 3 在工业桌面工具领域的可行性------Go 后端直接对接 PLC 协议栈,前端用现代 Web 技术做交互,打包成十几 MB 的原生应用,即开即用。

目前支持

  • Siemens S7 协议(含批量读取优化)
  • Modbus TCP 协议
  • 实时轮询 + 变化高亮
  • 趋势曲线(ECharts 时间序列)
  • 强制写入 / CSV 批量导入
  • 事件日志 / 配置持久化

技术栈一览

技术
桌面框架 Wails v2
后端语言 Go
前端框架 Vue 3 + TypeScript
图表库 ECharts 6
S7 协议 gos7
Modbus 协议 goburrow/modbus
构建工具 Vite

计划中

  • OPC UA 协议支持
  • 报警规则引擎
  • 数据导出(CSV/Excel)
  • Web 远程监控模式
  • Docker 部署版本

开源地址: gitee.com/shenjoy/plc...

欢迎 Star、Fork、提 Issue。工控开源生态需要大家一起建设。

相关推荐
止语Lab2 小时前
为什么你的 Go TCP server P99 延迟这么高
go
Andy Dennis8 小时前
nsq学习记录
消息队列·go·nsq
韦胖漫谈IT10 小时前
选语言不是站队,是选适合问题的工具
java·python·ai·rust·go·技术落地
喵个咪21 小时前
GoWind Toolkit Go后端代码生成 完整全流程实战
后端·go·orm
夜悊1 天前
Go网络编程的学习代码示例:客户端/服务端(C/S)模型
go
审判长烧鸡1 天前
【AI问答】GO代码循环返值
go
捧 花1 天前
Eino框架记忆功能实现指南
go·agent·eino
Java陈序员1 天前
主流数据库通吃!一款开源实用的数据库备份管理工具!
react.js·postgresql·go
云浪1 天前
搞懂 Go WaitGroup:一篇文章彻底理解并发等待机制
后端·go