以下摘自百度百科
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来实现的。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)