记录(一):go语言实现Bancnet协议数据发送模拟与读取
准备工作
- 虚拟机
- bacnet scan(本机安装)
- BACnet Device Simulator(虚拟机安装)
- vscode
- bacnet的go语言库:github.com/alexbeltran/gobacnet
执行工作
虚拟机中,用模拟软件新建虚拟设备
在本机执行bacnetScan
-
选中IP,然后点击小眼睛,搜索设备,此时发送whois请求,返回iam请求
-
选中一个设备,右键搜索点,此时应该实行的是readProperty请求,返回设备下的object对象
-
然后选中一个object,右键-》读对象属性,显示该对象的属性,其中presentValue是比较关注的属性。
这是在bacnetScan工具中,读取设备的流程,可以看到设备的一些信息,在simulator中的log可以看到,scan执行之后相互发送的报文,但是看起来有点混乱,推荐使用WireShark软件检测报文的交互,可以看到非常详细的报文解释,在程序中可以对照着scan发送的报文,来修改代码。
代码编程
- 引用前面提到的库,开始读取设备,注!!!要关掉本机的BacnetScan,因为会占端口,虚拟机和本机的防火墙都打开,入站规则,出战规则都加上!!!否则会读取不到设备信息。
package main
import (
"fmt"
"log"
"github.com/alexbeltran/gobacnet"
"github.com/alexbeltran/gobacnet/property"
"github.com/alexbeltran/gobacnet/types"
)
func main() {
cli, err := gobacnet.NewClient("VMware Network Adapter VMnet8", gobacnet.DefaultPort)
if err != nil {
fmt.Println("123")
}
defer cli.Close()
resp, err1 := cli.WhoIs(-1, -1)
if err1 != nil {
fmt.Println("读取设备有误")
}
fmt.Println("设备信息", resp)
for _, d := range resp {
dest := d
rp := types.ReadPropertyData{
Object: types.Object{
ID: types.ObjectID{
Type: 8, //types.DeviceType,
Instance: types.ObjectInstance(dest.ID.Instance),
},
Properties: []types.Property{
types.Property{
Type: property.ObjectList,
ArrayIndex: gobacnet.ArrayAll,
},
},
},
}
out, err := cli.ReadProperty(dest, rp)
if err != nil {
log.Fatal(err)
return
}
fmt.Println(out)
//读数据
ids, ok := out.Object.Properties[0].Data.([]interface{})
if !ok {
fmt.Println("Unable to get object list")
continue
}
rpm := types.ReadMultipleProperty{}
rpm.Objects = make([]types.Object, len(ids))
for i, raw_id := range ids {
id, ok := raw_id.(types.ObjectID)
if !ok {
log.Println("Unable to read object id %v", raw_id)
return
}
rpm.Objects[i].ID = id
rpm.Objects[i].Properties = []types.Property{
types.Property{
Type: property.ObjectName,
ArrayIndex: gobacnet.ArrayAll,
},
types.Property{
Type: property.PresentValue,
ArrayIndex: gobacnet.ArrayAll,
},
}
x, err := cli.ReadMultiProperty(dest, rpm)
if err != nil {
log.Println(err)
} else {
fmt.Println(x)
}
}
}
}
- 使用库源代码的例子,会读取不到很多信息,通过wireShark会发现,请求报文有异常,解包也有问题,所以对照报文进行修改自己的代码,发送正确的报文请求,就能获取信息。
坑记
- 建立连接的时候,不知道填什么接口,所以需要通过代码调试,根据代码来填写。
- 发送whois请求,接受iam时,解包错误,修改listen.go的handmsg函数,添加a := npdu.Source解出iam的源地址网络号,然后将a地址与源代码中iam解包的地址结合。这样后续的readproperty函数和readmultiproperty函数就可以使用返回对象中的地址属性来给请求的目的地址赋值。
func (c *Client) handleMsg(src *net.UDPAddr, b []byte) {
var header bactype.BVLC
var npdu bactype.NPDU
var apdu bactype.APDU
dec := encoding.NewDecoder(b)
err := dec.BVLC(&header)
if err != nil {
c.log.Error(err)
return
}
if header.Function == bactype.BacFuncBroadcast || header.Function == bactype.BacFuncUnicast || header.Function == bactype.BacFuncForwardedNPDU {
// Remove the header information
b = b[mtuHeaderLength:]
err = dec.NPDU(&npdu)
a := npdu.Source
if err != nil {
return
}
if npdu.IsNetworkLayerMessage {
c.log.Debug("Ignored Network Layer Message")
return
}
// We want to keep the APDU intact so we will get a snapshot before decoding
// further
send := dec.Bytes()
err = dec.APDU(&apdu)
if err != nil {
return
}
switch apdu.DataType {
case bactype.UnconfirmedServiceRequest:
if apdu.UnconfirmedService == bactype.ServiceUnconfirmedIAm {
c.log.Debug("Received IAm Message")
dec = encoding.NewDecoder(apdu.RawData)
var iam bactype.IAm
err = dec.IAm(&iam)
// For whatever reason, the IP section won't be populated until
// we set the type.
src.IP = src.IP.To4()
iam.Addr = bactype.UDPToAddress(src)
if a != nil {
iam.Addr.Net = a.Net
iam.Addr.Len = a.Len
iam.Addr.Adr = a.Adr
} else {
c.log.Errorf("npdu.Source is nil")
return //不管默认设备
}
//iam.Addr = *a //改为iam解码的地址
if err != nil {
c.log.Error(err)
return
}
c.utsm.Publish(int(iam.ID.Instance), iam)
} else if apdu.UnconfirmedService == bactype.ServiceUnconfirmedWhoIs {
dec := encoding.NewDecoder(apdu.RawData)
var low, high int32
dec.WhoIs(&low, &high)
// For now we are going to ignore who is request.
//log.WithFields(log.Fields{"low": low, "high": high}).Debug("WHO IS Request")
} else {
c.log.Errorf("Unconfirmed: %d %v", apdu.UnconfirmedService, apdu.RawData)
}
case bactype.ComplexAck:
c.log.Debug("Received Complex Ack")
err := c.tsm.Send(int(apdu.InvokeId), send)
if err != nil {
return
}
case bactype.ConfirmedServiceRequest:
c.log.Debug("Received Confirmed Service Request")
err := c.tsm.Send(int(apdu.InvokeId), send)
if err != nil {
return
}
case bactype.Error:
err := fmt.Errorf("Error Class %d Code %d", apdu.Error.Class, apdu.Error.Code)
err = c.tsm.Send(int(apdu.InvokeId), err)
if err != nil {
c.log.Debug("unable to send error to %d: %v", apdu.InvokeId, err)
}
default:
// Ignore it
//log.WithFields(log.Fields{"raw": b}).Debug("An ignored packet went through")
}
}
if header.Function == bactype.BacFuncForwardedNPDU {
// Right now we are ignoring the NPDU data that is stored in the packet. Eventually
// we will need to check it for any additional information we can gleam.
// NDPU has source
b = b[forwardHeaderLength:]
c.log.Debug("Ignored NDPU Forwarded")
}
}
- 发送请求时,源代码的报文是包含源地址的,但是与scan的请求报文不一致,所以请求报文不填源地址。比如whois中npdu里的source直接注释掉,这样发出去请求就能返回设备信息,读属性和读多属性也是,有source就不行,没有就能成功读到,不知道为啥。
func (c *Client) WhoIs(low, high int) ([]types.Device, error) {
dest := types.UDPToAddress(&net.UDPAddr{
IP: c.broadcastAddress,
Port: DefaultPort,
})
// src, _ := c.localAddress()
// ip, _, _ := net.ParseCIDR(c.myAddress)
// src := types.UDPToAddress(&net.UDPAddr{
// IP: ip,
// Port: c.port,
// })
dest.SetBroadcast(true)
enc := encoding.NewEncoder()
npdu := types.NPDU{
Version: types.ProtocolVersion,
Destination: &dest,
// Source: &src,
IsNetworkLayerMessage: false,
// We are not expecting a direct reply from a single destination
ExpectingReply: false,
Priority: types.Normal,
HopCount: types.DefaultHopCount,
}
enc.NPDU(npdu)
err := enc.WhoIs(int32(low), int32(high))
if err != nil {
return nil, err
}
// Subscribe to any changes in the the range. If it is a broadcast,
var start, end int
if low == -1 || high == -1 {
start = 0
end = maxInt
} else {
start = low
end = high
}
// Run in parallel
errChan := make(chan error)
go func() {
_, err = c.send(dest, enc.Bytes())
errChan <- err
}()
values, err := c.utsm.Subscribe(start, end)
if err != nil {
return nil, err
}
err = <-errChan
if err != nil {
return nil, err
}
// Weed out values that are not important such as non object type
// and that are not
uniqueMap := make(map[types.ObjectInstance]types.Device)
uniqueList := make([]types.Device, len(uniqueMap))
for _, v := range values {
r, ok := v.(types.IAm)
// Skip non I AM responses
if !ok {
continue
}
//声明一个地址
// addrsrc := types.Address{}
// addrsrc = r.Addr
// addrsrc.Mac = dest.Mac
// addrsrc.MacLen = uint8(len(dest.Mac))
// Check to see if we are in the map before inserting
if _, ok := uniqueMap[r.ID.Instance]; !ok {
dev := types.Device{
Addr: r.Addr,
ID: r.ID,
MaxApdu: r.MaxApdu,
Segmentation: r.Segmentation,
Vendor: r.Vendor,
}
uniqueMap[r.ID.Instance] = types.Device(dev)
uniqueList = append(uniqueList, dev)
}
}
return uniqueList, err
}