Go使用gos7实现西门子PLC通讯

Go简介

以下摘自百度百科

Go(又称 Golang)是 Google 的 Robert Griesemer,Rob Pike 及 Ken Thompson 开发的一种静态强类型、编译型语言。Go 语言语法与 C 相近,但功能上有:内存安全,GC(垃圾回收),结构形态及 CSP-style并发计算。

Go是一门小而精的编程语言,没有过多复杂的语法,却有着极高的性能,特别适合高并发的应用场景。Go的语法与C相近,同时也有与Python和JS等语言相似的简明性,使得拥有其它语言基础的的人能极快上手Go。即便没有基础,也无需花费太多的时间,因为Go本身就不是一门复杂且难以学习的语言。Go语言的通道等特性的存在,使得Go天生就支持高并发,这是Go最大的优点之一。


gos7简介

gos7是国外一名开发工程师根据Snap7,开发的Go专用的西门子PLC通讯库。它采用的依旧是S7通讯协议,使用TCP/IP通讯,传输指定的S7通讯报文,以实现数据的读取与写入。较之前两篇文章中介绍过的python-snap7和S7.NetPlus,gos7对PLC与编程语言之间的数据类型转换的支持更好,虽然依然有一点小bug,但是依旧是一个非常优秀的通讯库。

GitHub地址:GitHub - robinson/gos7: Implementation of Siemens S7 protocol in golang

目前并未找到gos7的介绍文档,很多地方需要结合源码查看并进行使用。

本文源码已上传至GitHub,项目地址如下:

https://github.com/XMNHCAS/GoS7Demo 


安装gos7

Go安装第三方库与Python类似,可以自动安装也可以手动安装。此处我们使用自动安装。

先创建一个文件夹,用以收纳我们的项目文件。然后输入以下命令,创建go.mod文件。

go mod init 项目名称

项目名称可以根据喜好自行修改。此处示例使用的是VS Code编辑器,使用其它的编辑器的操作也基本一致。效果如下图所示。 

然后输入以下指令,自动安装gos7。

go get github.com/robinson/gos7

 运行结果如下:

可以看到我们的项目目录下多了一个go.sum文件,这里就安装完成了。再在目录下新建一个src文件夹,用以存放我们的代码文件。需要注意的是,Go在一个根目录下只允许存在一个main函数,所以需要多个main函数的情况下,我们就需要在src文件夹下创建多个文件夹用以存放。


创建连接

如果手头没有PLC,可以参考我写的这篇文章:C#使用S7NetPlus以及PLCSIM Advanced V3.0实现西门子PLC仿真通讯

使用PLCSIM Advanced可以仿真出PLC来进行通讯测试

gos7依旧是使用S7通讯实现数据的传输,PLC充当的是服务端的角色,所以首先需要为我们的程序先创建TCP客户端,以实现与PLC之间的连接。

	const (
		ipAddr = "192.168.10.230" //PLC IP
		rack   = 0                // PLC机架号
		slot   = 1                // PLC插槽号
	)
	//PLC tcp连接客户端
	handler := gos7.NewTCPClientHandler(ipAddr, rack, slot)
	//连接及读取超时
	handler.Timeout = 200 * time.Second
	//关闭连接超时
	handler.IdleTimeout = 200 * time.Second
	//打开连接
	handler.Connect()
	//函数退出时关闭连接
	defer handler.Close()

完成TCP客户端的创建并连接之后,可以使用 gos7.NewClient()创建PLC的对象,用以获取PLC的信息或者读写数据。

    //获取PLC对象
    client := gos7.NewClient(handler)
    
    //获取PLC运行状态
    client.PLCGetStatus()

完整代码如下:

package main

import (
	"fmt"
	"time"

	"github.com/robinson/gos7"
)

func main() {
	const (
		ipAddr = "192.168.10.230" //PLC IP
		rack   = 0                // PLC机架号
		slot   = 1                // PLC插槽号
	)
	//PLC tcp连接客户端
	handler := gos7.NewTCPClientHandler(ipAddr, rack, slot)
	//连接及读取超时
	handler.Timeout = 200 * time.Second
	//关闭连接超时
	handler.IdleTimeout = 200 * time.Second
	//打开连接
	handler.Connect()
	//函数退出时关闭连接
	defer handler.Close()

	//获取PLC对象
	client := gos7.NewClient(handler)

	//输出PLC运行状态
	fmt.Println(client.PLCGetStatus())
}

运行结果如下:

 

client.PLCGetStatus()会返回两个结果,第一个是PLC的运行状态码,第二个是错误信息。运行之后它返回了8和nil,表示状态码为8,无错误。查看源码的client.go文件,可以看到状态码对应如下:

	// PLC Status
	s7CpuStatusUnknown = 0
	s7CpuStatusRun     = 8
	s7CpuStatusStop    = 4

 0为未知状态,8为运行状态,4为停止状态。


读取数据

gos7读取数据的方式均为使用PLC对象的AGReadDB()方法,读取所有需要读取的数据的字节,然后再做解析。gos7已经内置了解析数据的方法,当然如果了解数据的存储结构的话,也可以进行手动解析。

PLC的数据如下图所示:

gos7内置方法解析

gos7提供了一个gos7.Helper的结构体(相当于类),里面集成了多种PLC数据类型的解析和转换的方法。此处根据我们需要读取的五种数据,我们使用对应的方法进行数据解析。不过对于bool、int和real类型,都是可以使用GetValueAt方法进行数据解析的。

需要注意的是,gos7内置方法是可以解析WString类型的,中文也是可以解析的,所以无需另外再自己写解析函数。

代码如下:

package main

import (
	"fmt"
	"time"

	"github.com/robinson/gos7"
)

type PlcData struct {
	boolValue    bool
	intValue     uint16
	realValue    float32
	stringValue  string
	wstringValue string
}

func main() {
	const (
		ipAddr = "192.168.10.230"
		rack   = 0
		slot   = 1
	)

	//PLC tcp连接客户端
	handler := gos7.NewTCPClientHandler(ipAddr, rack, slot)
	//连接及读取超时
	handler.Timeout = 200 * time.Second
	//关闭连接超时
	handler.IdleTimeout = 200 * time.Second
	//打开连接
	handler.Connect()
	//函数退出时关闭连接
	defer handler.Close()

	//获取PLC对象
	client := gos7.NewClient(handler)

	//DB号
	address := 10
	//起始地址
	start := 0
	//读取字节数
	size := 776
	//读写字节缓存区
	buffer := make([]byte, size)

	//读取字节
	client.AGReadDB(address, start, size, buffer)

	//gos7解析数据类
	var helper gos7.Helper

	//gos7内置方法解析数据
	var data PlcData
	data.boolValue = helper.GetBoolAt(buffer[0], 0)
	helper.GetValueAt(buffer[2:4], 0, &data.intValue)
	data.realValue = helper.GetRealAt(buffer[4:8], 0)
	data.stringValue = helper.GetStringAt(buffer[8:264], 0)
	data.wstringValue = helper.GetWStringAt(buffer[264:], 0)

	//输出数据
	fmt.Println(data)
}

运行结果如下:

可以看到数据被成功读取并正确解析。

手动解析 

在理解PLC的数据存储方式的情况下,可以不依靠gos7提供的方法,而使用Go语言的标准函数进行数据解析。

bool:通过循环位操作来判断字节中的每一位的值,根据大小端的对应的规则,转换成bool数组后即可获取bool的值。

int:通过binary.BigEndian.Uint16()方法,可以将值的字节数组转换为对应的uint16。此处是默认int值为正数,所以使用uint16,即16位无符号整型。如果值可能为负数,则需要修改此方法。

real:通过binary.BigEndian.Uint32()获取其32位整型形式,然后再通过math.Float32frombits()转换为正确的浮点型值。

string:由于PLC中的string为ASCII编码,第一个字节为该变量的最大字符数,第二个字节为该变量的当前字符数,故根据第二个字节的值对字节数组进行切片,获取字符所在的字节数组,然后直接使用string()方法做类型转换,即可获取正确的string的值。

wstring:原理与string一致,但是wstring是双字节存储,所以最大字符数为第一二个字节,变量的当前字符数是第三四个字节,所以需要解析第二个字节,获取字符数,然后再从第五个字节开始进行字节数组切片,获取字符串的值。在PLC中,wstring的编码格式是16位的大端Unicode,Go是无法直接将该编码格式的字节数组转换为正确的字符串的,所以需要使用transform.Bytes(unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewDecoder(), 字符的字节数组)方法进行一次解码,然后再将转码之后的结果使用string()方法做类型转换,这样就可以得到正确的结果了。

package main

import (
	"encoding/binary"
	"fmt"
	"math"
	"time"

	"github.com/robinson/gos7"
	"golang.org/x/text/encoding/unicode"
	"golang.org/x/text/transform"
)

type PlcData struct {
	boolValue    bool
	intValue     uint16
	realValue    float32
	stringValue  string
	wstringValue string
}

func main() {
	const (
		ipAddr = "192.168.10.230"
		rack   = 0
		slot   = 1
	)

	//PLC tcp连接客户端
	handler := gos7.NewTCPClientHandler(ipAddr, rack, slot)
	//连接及读取超时
	handler.Timeout = 200 * time.Second
	//关闭连接超时
	handler.IdleTimeout = 200 * time.Second
	//打开连接
	handler.Connect()
	//函数退出时关闭连接
	defer handler.Close()

	//获取PLC对象
	client := gos7.NewClient(handler)
	//DB号
	address := 10
	//起始地址
	start := 0
	//读取字节数
	size := 776
	//读写字节缓存区
	buffer := make([]byte, size)
	//读取字节
	client.AGReadDB(address, start, size, buffer)

	var data PlcData

	//手动解析Bool
	data.boolValue = ByteToBool(buffer[0])[0]

	//手动解析Int
	data.intValue = binary.BigEndian.Uint16(buffer[2:4])

	//手动解析Real
	data.realValue = math.Float32frombits(binary.BigEndian.Uint32(buffer[4:8]))

	//手动解析String
	stringPos := 8
	data.stringValue = string(buffer[stringPos+2 : stringPos+2+int(buffer[stringPos+1])])

	//手动解析WString
	wstringPos := 264
	endPos := binary.BigEndian.Uint16(buffer[wstringPos+2 : wstringPos+4])
	res, _, _ := transform.Bytes(unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewDecoder(), buffer[wstringPos+4:wstringPos+4+int(endPos)*2])
	data.wstringValue = string(res)

	//输出数据
	fmt.Println(data)

}

//字节转bool数组(大端)
func ByteToBool(data byte) [8]bool {
	var res [8]bool
	for i := 0; i < 8; i++ {
		res[i] = data&1 == 1
		data = data >> 1
	}
	return res
}

运行结果如下:


写入数据

gos7提供了逐个写入和批量写入的方法,可以根据具体的应用场景选择需要使用的方法。我们在做读取的时候,实际上是生成了请求报文,发送给PLC后,PLC根据我们发送的报文返回结果报文,最后我们得到的就是包含了需要读取的数据的字节数组。而当我们写入的时候,其实就是需要把我们的数据按照PLC指定的规则生成报文,发送过去。gos7也提供了生成写入数据的字节数组的方法。

逐个写入 

使用gos7.Helper的对应的Set方法,可以获取指定数据类型的字节数组,然后通过PLC对象的AGWriteDB()传入对应的值并生成写入报文,发送给PLC。

gos7写入WString存在一个小bug,因为一个中文字符在Go中是占用三个字节的,而在其它的地方的基本都是只占用两个字节,在PLC中也是两个字节。而gos7在计算字符数的时候,用的是len()函数,它返回的字符的字节数,也就是说如果我们传入的是"中文"两个字,它计算出来的字符数却是6个。当我们用gos7的SetWStringAt生成的字节数组,把数据写入PLC之后,PLC会发现字符数是6,但实际上只传了两个,这时PLC就会在传入的字符后面补"$0000",来进行字符数的补全。

因为这个问题,WString我们需要自己重新写方法来生成值,即代码最下面的SetWStringAt方法。

具体代码如下:

package main

import (
	"time"

	"github.com/robinson/gos7"
)

func main() {
	const (
		ipAddr = "192.168.10.230" //PLC IP
		rack   = 0                // PLC机架号
		slot   = 1                // PLC插槽号
	)
	//PLC tcp连接客户端
	handler := gos7.NewTCPClientHandler(ipAddr, rack, slot)
	//连接及读取超时
	handler.Timeout = 200 * time.Second
	//关闭连接超时
	handler.IdleTimeout = 200 * time.Second
	//打开连接
	handler.Connect()
	//函数退出时关闭连接
	defer handler.Close()

	//获取PLC对象
	client := gos7.NewClient(handler)

	//gos7解析数据类
	var helper gos7.Helper

	//DB号
	dbNo := 10
	//起始地址
	startAdr := 0

	//写入数据的字节二位数组
	buffers := [][]byte{
		make([]byte, 2),
		make([]byte, 2),
		make([]byte, 4),
		make([]byte, 256),
		make([]byte, 512),
	}

	//生成需要写入的变量的数组
	helper.SetBoolAt(buffers[0][0], 0, false)
	helper.SetValueAt(buffers[1], 0, uint16(100))
	helper.SetRealAt(buffers[2], 0, float32(66.6))
	helper.SetStringAt(buffers[3], 0, 254, "Hello Go")
	SetWStringAt(buffers[4], 0, "中文")

	//循环数据,逐个写入
	for _, v := range buffers {
		client.AGWriteDB(dbNo, startAdr, len(v), v)
		startAdr += len(v)
	}
}

//获取WString的报文
func SetWStringAt(buffer []byte, pos int, value string) []byte {
	chars := []rune(value)
	slen := len(chars)
	var maxLen int = 254
	if maxLen < slen {
		maxLen = slen
	}
	var helper gos7.Helper
	helper.SetValueAt(buffer, pos+0, int16(maxLen))
	helper.SetValueAt(buffer, pos+2, int16(slen))
	for i, c := range chars {
		if i >= maxLen {
			return buffer
		}
		helper.SetValueAt(buffer, pos+4+i*2, uint16(c))
	}
	return buffer
}

运行结果如下:

可以看到,数据被成功写入。 

批量写入 

gos7中提供了批量写入的方式。逐个写入会为每一次写入生成一条对应的写入请求报文,而批量写入则是会将需要写入的所有数据放到一条报文中,一次性发送。

批量写入主要使用S7DataItem类。其中Area为数据类型;WordLen为字长,基本默认为0x02即可;DBNumber为DB号;Start为起始地址;Amount为实际数据字节数;Data为需要写入的数据的字节数组。

由源码可以得知Area和WordLen的对应标识码:

	// Area ID
	s7areape = 0x81 //process inputs
	s7areapa = 0x82 //process outputs
	s7areamk = 0x83 //Merkers
	s7areadb = 0x84 //DB
	s7areact = 0x1C //counters
	s7areatm = 0x1D //timers

	// Word Length
	s7wlbit     = 0x01 //Bit (inside a word)
	s7wlbyte    = 0x02 //Byte (8 bit)
	s7wlChar    = 0x03
	s7wlword    = 0x04 //Word (16 bit)
	s7wlint     = 0x05
	s7wldword   = 0x06 //Double Word (32 bit)
	s7wldint    = 0x07
	s7wlreal    = 0x08 //Real (32 bit float)
	s7wlcounter = 0x1C //Counter (16 bit)
	s7wltimer   = 0x1D //Timer (16 bit)

代码如下: 

package main

import (
	"time"

	"github.com/robinson/gos7"
)

func main() {
	const (
		ipAddr = "192.168.10.230" //PLC IP
		rack   = 0                // PLC机架号
		slot   = 1                // PLC插槽号
	)
	//PLC tcp连接客户端
	handler := gos7.NewTCPClientHandler(ipAddr, rack, slot)
	//连接及读取超时
	handler.Timeout = 200 * time.Second
	//关闭连接超时
	handler.IdleTimeout = 200 * time.Second
	//打开连接
	handler.Connect()
	//函数退出时关闭连接
	defer handler.Close()

	//获取PLC对象
	client := gos7.NewClient(handler)

	//gos7解析数据类
	var helper gos7.Helper

	//写入数据的字节二位数组
	buffers := [][]byte{
		make([]byte, 2),
		make([]byte, 2),
		make([]byte, 4),
		make([]byte, 256),
		make([]byte, 512),
	}

	//需要写入的字符串
	stringValue := "Hello World"
	wstringValue := "中国"

	//生成需要写入的变量的数组
	buffers[0][0] = helper.SetBoolAt(buffers[0][0], 0, true)
	helper.SetValueAt(buffers[1], 0, uint16(66))
	helper.SetRealAt(buffers[2], 0, float32(33.33))
	helper.SetStringAt(buffers[3], 0, 254, stringValue)
	SetWStringAt(buffers[4], 0, wstringValue)

	//获取批量写入的DataItem
	datas := []gos7.S7DataItem{
		{
			Area:     0x84,
			WordLen:  0x02,
			DBNumber: 10,
			Start:    0,
			Amount:   1,
			Data:     buffers[0],
		},
		{
			Area:     0x84,
			WordLen:  0x02,
			DBNumber: 10,
			Start:    2,
			Amount:   2,
			Data:     buffers[1],
		},
		{
			Area:     0x84,
			WordLen:  0x02,
			DBNumber: 10,
			Start:    4,
			Amount:   4,
			Data:     buffers[2],
		},
		{
			Area:     0x84,
			WordLen:  0x02,
			DBNumber: 10,
			Start:    8,
			Amount:   len([]rune(stringValue)) + 2,
			Data:     buffers[3],
		},
		{
			Area:     0x84,
			WordLen:  0x02,
			DBNumber: 10,
			Start:    264,
			Amount:   len([]rune(wstringValue))*2 + 4,
			Data:     buffers[4],
		},
	}

	//批量写入数据
	client.AGWriteMulti(datas, len(datas))
}

//获取WString的报文
func SetWStringAt(buffer []byte, pos int, value string) []byte {
	chars := []rune(value)
	slen := len(chars)
	var maxLen int = 254
	if maxLen < slen {
		maxLen = slen
	}
	var helper gos7.Helper
	helper.SetValueAt(buffer, pos+0, int16(maxLen))
	helper.SetValueAt(buffer, pos+2, int16(slen))
	for i, c := range chars {
		if i >= maxLen {
			return buffer
		}
		helper.SetValueAt(buffer, pos+4+i*2, uint16(c))
	}
	return buffer
}

运行结果如下: 


结尾

本文介绍了Go通过gos7实现西门子PLC的通讯。由于gos7使用者并不多,而且该项目发布时间也不是很长,所以它依然存在一些bug,希望作者后续会有更新,并发布完整的使用文档。

Go是一门优秀的后端语言,在一些小型的数据采集项目中,服务器直连PLC的情况下,是可以考虑使用Go来实现的。

猜你喜欢

转载自blog.csdn.net/XUMENGCAS/article/details/123801285