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 IPrack = 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 mainimport ("fmt""time""github.com/robinson/gos7"
)func main() {const (ipAddr = "192.168.10.230" //PLC IPrack = 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 Statuss7CpuStatusUnknown = 0s7CpuStatusRun = 8s7CpuStatusStop = 4
0为未知状态,8为运行状态,4为停止状态。
读取数据
gos7读取数据的方式均为使用PLC对象的AGReadDB()方法,读取所有需要读取的数据的字节,然后再做解析。gos7已经内置了解析数据的方法,当然如果了解数据的存储结构的话,也可以进行手动解析。
PLC的数据如下图所示:
gos7内置方法解析
gos7提供了一个gos7.Helper的结构体(相当于类),里面集成了多种PLC数据类型的解析和转换的方法。此处根据我们需要读取的五种数据,我们使用对应的方法进行数据解析。不过对于bool、int和real类型,都是可以使用GetValueAt方法进行数据解析的。
需要注意的是,gos7内置方法是可以解析WString类型的,中文也是可以解析的,所以无需另外再自己写解析函数。
代码如下:
package mainimport ("fmt""time""github.com/robinson/gos7"
)type PlcData struct {boolValue boolintValue uint16realValue float32stringValue stringwstringValue string
}func main() {const (ipAddr = "192.168.10.230"rack = 0slot = 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 PlcDatadata.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 mainimport ("encoding/binary""fmt""math""time""github.com/robinson/gos7""golang.org/x/text/encoding/unicode""golang.org/x/text/transform"
)type PlcData struct {boolValue boolintValue uint16realValue float32stringValue stringwstringValue string
}func main() {const (ipAddr = "192.168.10.230"rack = 0slot = 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//手动解析Booldata.boolValue = ByteToBool(buffer[0])[0]//手动解析Intdata.intValue = binary.BigEndian.Uint16(buffer[2:4])//手动解析Realdata.realValue = math.Float32frombits(binary.BigEndian.Uint32(buffer[4:8]))//手动解析StringstringPos := 8data.stringValue = string(buffer[stringPos+2 : stringPos+2+int(buffer[stringPos+1])])//手动解析WStringwstringPos := 264endPos := 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]boolfor i := 0; i < 8; i++ {res[i] = data&1 == 1data = 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 mainimport ("time""github.com/robinson/gos7"
)func main() {const (ipAddr = "192.168.10.230" //PLC IPrack = 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 = 254if maxLen < slen {maxLen = slen}var helper gos7.Helperhelper.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 IDs7areape = 0x81 //process inputss7areapa = 0x82 //process outputss7areamk = 0x83 //Merkerss7areadb = 0x84 //DBs7areact = 0x1C //counterss7areatm = 0x1D //timers// Word Lengths7wlbit = 0x01 //Bit (inside a word)s7wlbyte = 0x02 //Byte (8 bit)s7wlChar = 0x03s7wlword = 0x04 //Word (16 bit)s7wlint = 0x05s7wldword = 0x06 //Double Word (32 bit)s7wldint = 0x07s7wlreal = 0x08 //Real (32 bit float)s7wlcounter = 0x1C //Counter (16 bit)s7wltimer = 0x1D //Timer (16 bit)
代码如下:
package mainimport ("time""github.com/robinson/gos7"
)func main() {const (ipAddr = "192.168.10.230" //PLC IPrack = 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)//获取批量写入的DataItemdatas := []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 = 254if maxLen < slen {maxLen = slen}var helper gos7.Helperhelper.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来实现的。
Go使用gos7实现西门子PLC通讯相关推荐
- PC与西门子PLC通讯免费软件Libnodave
软件介绍:PC与西门子PLC通讯免费软件Libnodave Introduction: Libnodave is a free communication library for Siemens S7 ...
- 上位机与PLC 通讯源码 上位机与三菱PLC,西门子PLC通讯 同时一起通讯,单独控制,三菱采用官方MX 通讯,支持三菱FX系列,
上位机与PLC 通讯源码 上位机与三菱PLC,西门子PLC通讯 同时一起通讯,单独控制,三菱采用官方MX 通讯,支持三菱FX系列,A系列,Q系列,L系列,R系列,全系系列,各种串口和各种网口通讯,, ...
- 上位机与PLC 通讯源码 上位机与三菱PLC,西门子PLC通讯
上位机与PLC 通讯源码 上位机与三菱PLC,西门子PLC通讯 同时一起通讯,单独控制,三菱采用官方MX 通讯,支持三菱FX系列,A系列,Q系列,L系列,R系列,全系系列,各种串口和各种网口通讯,, ...
- 上位机与PLC 通讯源码DEMO 上位机与三菱PLC,西门子PLC通讯 同时一起通讯,单独控制
上位机与PLC 通讯源码DEMO 上位机与三菱PLC,西门子PLC通讯 同时一起通讯,单独控制,三菱采用官方MX 通讯,支持三菱FX系列,A系列,Q系列,L系列,R系列,全系系列,各种串口和各种网口通 ...
- python与西门子PLC通讯
python与西门子PLC通讯 安装Python-snap7 win+R打开"运行",输入cmd,确定后,进入DOS命令行终端,输入下面的命令: pip install pytho ...
- C#与西门子plc通讯上位机 c#软件 工控软件
C#与西门子plc通讯上位机 c#软件 工控软件 1.该程可以实现CSharp与西门子全系列plc(200,200smart,300,1200,1500)的以太网s7通讯,通讯传输快稳定. 2.该程序 ...
- jetson nano上编译与使用西门子PLC通讯库snap7
文章目录 一.西门子snap7介绍 二.西门子S7通讯介绍 三.jetson nano编译snap7库 四.Qt Cmake导入snap7库 五.snap7主要函数说明 1.与PLC建立连接 2.读写 ...
- 威纶和s7200通讯线_PLC通讯,西门子plc通讯知识汇总学习
(一)西门子 200 plc 使用 MPI 协议与组态王进行通讯时需要哪些设置? 1)在运行组态王的机器上需要安装西门子公司提供的 STEP7 Microwin 3.2 的编程软件,我们的驱动需要调用 ...
- c#与汇川机器人通讯_C#与西门子PLC通讯
S7Net 如下图,ip="192.168.0.5". 如下图,rack为0,slot为1. public Plc(CpuType cpu, string ip, short ra ...
- C#中使用S7.net与西门子PLC通讯
最近因为工作的原因用到了西门子PLC,在使用过程中一直在思考上位机和PLC的通讯问题,后来上网查了一下,找到了一个专门针对S7开发的一个.net库–<S7netPlus>,PLC通讯方法比 ...
最新文章
- 【AIX 命令学习】创建逻辑卷!
- py文件转exe时包含paramiko模块出错解决方法
- 按汇总分组/多维数据集
- Linux C 函数指针应用---回调函数
- [No0000E0]批量打开当前路径下的文件
- PHP书写规范 PHP Coding Standard
- 「陶哲軒實分析」 習題 3.5.1
- 【亲测】Ripro子主题美化X系列主题(夏系列)-开源未加密
- 百度文库复制内容,留作笔记
- 2招PDF去密码,秒学秒懂秒用上!
- 数据结构实验报告:顺序表基本操作的实现
- 网络安全学习笔记-入侵检测系统IDS
- 机器学习基石1 学习笔记
- seo软件优化工具软件-免费seo软件优化工具以及免费排名软件下载
- [网鼎杯 2018]Fakebook
- 导出DNS服务器上的记录
- 解决python.exe 无法找到程序入口,无法定位程序输入点
- Unity3d搭建HTTP弱联网的服务器搭建及客户端编写(一)之java服务器
- fastadmin 使用switch 点击修改无反应 提示“未更新任何行”
- CSS:focus-visible伪类真的太6了!
热门文章
- MMO 游戏中使用多核
- 计算机正在获取ip,电脑频繁显示网络电缆已拔出和正在获取IP怎么办
- python面向对象2--综合案例:烤地瓜、搬家具、单继承、多继承
- 苹果手机锁屏后无线重新连接服务器,iphone11锁屏自动断开wifi怎么办 苹果11手机热点自动断开解决方法...
- 计算机毕业设计HTML+CSS+JavaScript仿大型购物商城(1页)
- bouncing results问题
- python lncrna_一文解决TCGA任意肿瘤的差异lncRNA,miRNA,mRNA
- 华为2022年度伙伴奖项正式揭晓!恒驰信息荣获华为云GrowCloud优秀解决方案提供商奖
- RAP2 详细部署、操作指南
- V831基础-摄像头使用