Skip to content

Commit

Permalink
feat(plc4go/bacnet): initial implementation of discovery
Browse files Browse the repository at this point in the history
  • Loading branch information
sruehl committed Jun 21, 2022
1 parent fdb52e5 commit 644d5c9
Show file tree
Hide file tree
Showing 10 changed files with 348 additions and 76 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package main

import (
"github.com/apache/plc4x/plc4go/pkg/api"
"github.com/apache/plc4x/plc4go/pkg/api/drivers"
"github.com/apache/plc4x/plc4go/pkg/api/logging"
"github.com/apache/plc4x/plc4go/pkg/api/model"
"github.com/rs/zerolog/log"
"os"
"time"
)

// TODO: not yet finished... much c&p from knx atm
func main() {
// Set logging to INFO
logging.DebugLevel()

driverManager := plc4go.NewPlcDriverManager()
drivers.RegisterBacnetDriver(driverManager)

var connectionStrings []string
if len(os.Args) < 2 {
// Try to auto-find bacnet devices via broadcast-message discovery
if err := driverManager.Discover(func(event model.PlcDiscoveryEvent) {
connStr := event.GetProtocolCode() + "://" + event.GetTransportUrl().Host
log.Info().Str("connection string", connStr).Msg("Found Bacnet Gateway")

connectionStrings = append(connectionStrings, connStr)
},
plc4go.WithDiscoveryOptionProtocolSpecific("who-is-low-limit", 0),
plc4go.WithDiscoveryOptionProtocolSpecific("who-is-high-limit", "255"),
); err != nil {
panic(err)
}
// Wait for 5 seconds for incoming responses
time.Sleep(time.Second * 5)
} else {
connStr := "bacnet-ip://" + os.Args[1] + ":47808"
log.Info().Str("connection string", connStr).Msg("Using manually provided bacnet gateway")
connectionStrings = append(connectionStrings, connStr)
}

for _, connStr := range connectionStrings {
log.Info().Str("connection string", connStr).Msg("Connecting")
crc := driverManager.GetConnection(connStr)

// Wait for the driver to connect (or not)
connectionResult := <-crc
if connectionResult.GetErr() != nil {
log.Error().Msgf("error connecting to PLC: %s", connectionResult.GetErr().Error())
return
}
log.Info().Str("connection string", connStr).Msg("Connected")
connection := connectionResult.GetConnection()
defer connection.BlockingClose()
}
}
5 changes: 1 addition & 4 deletions plc4go/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ module github.com/apache/plc4x/plc4go
go 1.16

require (
github.com/IBM/netaddr v1.5.0
github.com/ajankovic/xdiff v0.0.1
github.com/elastic/go-licenser v0.4.0 // indirect
github.com/google/gopacket v1.1.19
github.com/icza/bitio v1.1.0
github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4
Expand All @@ -36,9 +36,6 @@ require (
github.com/spf13/viper v1.12.0
github.com/stretchr/testify v1.7.2
github.com/subchen/go-xmldom v1.1.2
github.com/tebeka/go2xunit v1.4.10 // indirect
github.com/viney-shih/go-lock v1.1.1
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect
golang.org/x/tools v0.1.11 // indirect
gotest.tools/gotestsum v1.8.1 // indirect
)
38 changes: 3 additions & 35 deletions plc4go/go.sum

Large diffs are not rendered by default.

224 changes: 224 additions & 0 deletions plc4go/internal/bacnetip/Discoverer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package bacnetip

import (
"fmt"
"net"
"net/url"
"strconv"
"time"

"github.com/IBM/netaddr"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"

"github.com/apache/plc4x/plc4go/internal/spi"
internalModel "github.com/apache/plc4x/plc4go/internal/spi/model"
"github.com/apache/plc4x/plc4go/internal/spi/options"
"github.com/apache/plc4x/plc4go/internal/spi/transports"
"github.com/apache/plc4x/plc4go/internal/spi/transports/udp"
apiModel "github.com/apache/plc4x/plc4go/pkg/api/model"
driverModel "github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model"
)

type Discoverer struct {
messageCodec spi.MessageCodec
}

func NewDiscoverer() *Discoverer {
return &Discoverer{}
}

func (d *Discoverer) Discover(callback func(event apiModel.PlcDiscoveryEvent), discoveryOptions ...options.WithDiscoveryOption) error {
udpTransport := udp.NewTransport()

allInterfaces, err := net.Interfaces()
if err != nil {
return err
}

// If no device is explicitly selected via option, simply use all of them
// However if a discovery option is present to select a device by name, only
// add those devices matching any of the given names.
var interfaces []net.Interface
deviceNames := options.FilterDiscoveryOptionsDeviceName(discoveryOptions)
if len(deviceNames) > 0 {
for _, curInterface := range allInterfaces {
for _, deviceNameOption := range deviceNames {
if curInterface.Name == deviceNameOption.GetDeviceName() {
interfaces = append(interfaces, curInterface)
break
}
}
}
} else {
interfaces = allInterfaces
}

var whoIsLowLimit *uint
var whoIsHighLimit *uint
for _, protocolSpecificOption := range options.FilterDiscoveryOptionProtocolSpecific(discoveryOptions) {
switch protocolSpecificOption.GetKey() {
case "who-is-low-limit":
whoIsLowLimitParsed, err := strconv.ParseUint(fmt.Sprintf("%v", protocolSpecificOption.GetValue()), 10, 8)
if err != nil {
return errors.Wrap(err, "Error parsing option")
}
whoIsLowLimitParsedUint := uint(whoIsLowLimitParsed)
whoIsLowLimit = &whoIsLowLimitParsedUint
case "who-is-high-limit":
whoIsHighLimitParsed, err := strconv.ParseUint(fmt.Sprintf("%v", protocolSpecificOption.GetValue()), 10, 8)
if err != nil {
return errors.Wrap(err, "Error parsing option")
}
whoIsHighLimitParsedUint := uint(whoIsHighLimitParsed)
whoIsHighLimit = &whoIsHighLimitParsedUint
}
}
if whoIsLowLimit != nil && whoIsHighLimit == nil || whoIsLowLimit == nil && whoIsHighLimit != nil {
return errors.Errorf("who-is high-limit must be specified together")
}

var tranportInstances []transports.TransportInstance
// Iterate over all network devices of this system.
for _, networkInterface := range interfaces {
unicastInterfaceAddress, err := networkInterface.Addrs()
if err != nil {
return errors.Wrapf(err, "Error getting Addresses for %v", networkInterface)
}
// Iterate over all addresses the current interface has configured
for _, unicastAddress := range unicastInterfaceAddress {
var ipAddr net.IP
switch addr := unicastAddress.(type) {
// If the device is configured to communicate with a subnet
case *net.IPNet:
ipAddr = addr.IP.To4()
if ipAddr == nil {
ipAddr = addr.IP.To16()
}

// If the device is configured for a point-to-point connection
case *net.IPAddr:
ipAddr = addr.IP.To4()
if ipAddr == nil {
ipAddr = addr.IP.To16()
}
default:
continue
}

if !ipAddr.IsGlobalUnicast() {
continue
}
_, cidr, _ := net.ParseCIDR(unicastAddress.String())
broadcastAddr := netaddr.BroadcastAddr(cidr)
udpAddr := &net.UDPAddr{IP: broadcastAddr, Port: 0xBAC0}
connectionUrl, err := url.Parse(fmt.Sprintf("udp://%s", udpAddr))
if err != nil {
log.Debug().Err(err).Msg("error parsing url")
continue
}
localAddr := &net.UDPAddr{IP: ipAddr, Port: 0xBAC0}
transportInstance, err :=
udpTransport.CreateTransportInstanceForLocalAddress(*connectionUrl, nil, localAddr)
if err != nil {
log.Debug().Err(err).Msg("error creating transport instance")
return err
}
tranportInstances = append(tranportInstances, transportInstance)
}
}

for _, transportInstance := range tranportInstances {
// Create a codec for sending and receiving messages.
codec := NewMessageCodec(transportInstance)
// Explicitly start the worker
if err := codec.Connect(); err != nil {
return errors.Wrap(err, "Error connecting")
}

// Cast to the UDP transport instance so we can access information on the local port.
udpTransportInstance, ok := transportInstance.(*udp.TransportInstance)
if !ok {
return errors.New("couldn't cast transport instance to UDP transport instance")
}
_ = udpTransportInstance

// Prepare the discovery packet data
var lowLimit driverModel.BACnetContextTagUnsignedInteger
if whoIsLowLimit != nil {
lowLimit = driverModel.CreateBACnetContextTagUnsignedInteger(0, *whoIsLowLimit)
}
var highLimit driverModel.BACnetContextTagUnsignedInteger
if whoIsHighLimit != nil {
highLimit = driverModel.CreateBACnetContextTagUnsignedInteger(1, *whoIsHighLimit)
}
requestWhoIs := driverModel.NewBACnetUnconfirmedServiceRequestWhoIs(lowLimit, highLimit, 0)
apdu := driverModel.NewAPDUUnconfirmedRequest(requestWhoIs, 0)

control := driverModel.NewNPDUControl(false, false, false, false, driverModel.NPDUNetworkPriority_NORMAL_MESSAGE)
npdu := driverModel.NewNPDU(1, control, nil, nil, nil, nil, nil, nil, nil, nil, apdu, 0)
bvlc := driverModel.NewBVLCOriginalUnicastNPDU(npdu, 0)

// Send the search request.
err = codec.Send(bvlc)
go func() {
// Keep on reading responses till the timeout is done.
// TODO: Make this configurable
timeout := time.NewTimer(time.Second * 1)
timeout.Stop()
for start := time.Now(); time.Since(start) < time.Second*5; {
timeout.Reset(time.Second * 1)
select {
case message := <-codec.GetDefaultIncomingMessageChannel():
{
if !timeout.Stop() {
<-timeout.C
}
_ = message
deviceName := "todo"
remoteUrl, err := url.Parse("udp://todo")
if err != nil {
log.Debug().Err(err).Msg("Error parsing url")
continue
}
discoveryEvent := &internalModel.DefaultPlcDiscoveryEvent{
ProtocolCode: "bacnet-ip",
TransportCode: "udp",
TransportUrl: *remoteUrl,
Options: nil,
Name: deviceName,
}
// Pass the event back to the callback
callback(discoveryEvent)
}
continue
case <-timeout.C:
{
timeout.Stop()
continue
}
}
}
}()
}
return nil
}
10 changes: 10 additions & 0 deletions plc4go/internal/bacnetip/Driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ package bacnetip

import (
_default "github.com/apache/plc4x/plc4go/internal/spi/default"
"github.com/apache/plc4x/plc4go/internal/spi/options"
"github.com/apache/plc4x/plc4go/internal/spi/transports"
"github.com/apache/plc4x/plc4go/pkg/api"
apiModel "github.com/apache/plc4x/plc4go/pkg/api/model"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"net/url"
Expand Down Expand Up @@ -75,3 +77,11 @@ func (m *Driver) GetConnection(transportUrl url.URL, transports map[string]trans
log.Debug().Msg("created connection, connecting now")
return connection.Connect()
}

func (m *Driver) SupportsDiscovery() bool {
return true
}

func (m *Driver) Discover(callback func(event apiModel.PlcDiscoveryEvent), discoveryOptions ...options.WithDiscoveryOption) error {
return NewDiscoverer().Discover(callback, discoveryOptions...)
}
2 changes: 1 addition & 1 deletion plc4go/internal/knxnetip/Discoverer.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ func (d *Discoverer) Discover(callback func(event apiModel.PlcDiscoveryEvent), d
if !timeout.Stop() {
<-timeout.C
}
searchResponse := driverModel.CastSearchResponse(message)
searchResponse := message.(driverModel.SearchResponse)
if searchResponse != nil {
addr := searchResponse.GetHpaiControlEndpoint().GetIpAddress().GetAddr()
remoteUrl, err := url.Parse(fmt.Sprintf("udp://%d.%d.%d.%d:%d",
Expand Down
Loading

0 comments on commit 644d5c9

Please sign in to comment.