Skip to content

Commit

Permalink
feat(ads): ADS Auto-Discovery
Browse files Browse the repository at this point in the history
- Got the auto-discovery for ADS working fully
  • Loading branch information
chrisdutz committed Nov 13, 2022
1 parent 1e4e1d3 commit 71b7977
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 150 deletions.
Expand Up @@ -17,17 +17,14 @@
* under the License.
*/

package ads
package main

import (
"testing"

plc4go "github.com/apache/plc4x/plc4go/pkg/api"
"github.com/apache/plc4x/plc4go/pkg/api/drivers"
)

func TestBrowserManual(t *testing.T) {
t.Skip("manual test")
func main() {
driverManager := plc4go.NewPlcDriverManager()
drivers.RegisterAdsDriver(driverManager)
connectionChan := driverManager.GetConnection("ads:tcp://192.168.23.20?sourceAmsNetId=192.168.23.200.1.1&sourceAmsPort=65534&targetAmsNetId=192.168.23.20.1.1&targetAmsPort=851")
Expand Down
Expand Up @@ -17,20 +17,19 @@
* under the License.
*/

package ads
package main

import (
"context"
"testing"
"time"

apiModel "github.com/apache/plc4x/plc4go/pkg/api/model"
"github.com/apache/plc4x/plc4go/internal/ads"
"github.com/apache/plc4x/plc4go/pkg/api/model"
)

func TestDiscovererManual(t *testing.T) {
t.Skip("manual test")
discoverer := NewDiscoverer()
discoverer.Discover(context.Background(), func(event apiModel.PlcDiscoveryItem) {
func main() {
discoverer := ads.NewDiscoverer()
discoverer.Discover(context.Background(), func(event model.PlcDiscoveryItem) {
print(event)
})
time.Sleep(time.Second * 5)
Expand Down
303 changes: 165 additions & 138 deletions plc4go/internal/ads/Discoverer.go
Expand Up @@ -39,6 +39,13 @@ import (
"github.com/rs/zerolog/log"
)

type discovery struct {
interf net.Interface
localAddress net.IP
broadcastAddress net.IP
socket *net.UDPConn
}

type Discoverer struct {
messageCodec spi.MessageCodec
}
Expand All @@ -48,109 +55,6 @@ func NewDiscoverer() *Discoverer {
}

func (d *Discoverer) Discover(ctx context.Context, callback func(event apiModel.PlcDiscoveryItem), discoveryOptions ...options.WithDiscoveryOption) error {

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Set up a listening socket on all devices for processing the responses to any search requests

// Open a listening udp socket for the incoming responses
responseAddr, err := net.ResolveUDPAddr("udp4", fmt.Sprintf(":%d", model.AdsDiscoveryConstants_ADSDISCOVERYUDPDEFAULTPORT))
if err != nil {
panic(err)
}
socket, err := net.ListenUDP("udp4", responseAddr)
if err != nil {
panic(err)
}
defer socket.Close()

// Start a worker to receive responses
go func() {
buf := make([]byte, 1024)
for {
length, fromAddr, err := socket.ReadFromUDP(buf)
if length == 0 {
continue
}
discoveryResponse, err := model.AdsDiscoveryParse(buf[0:length])
if err != nil {
log.Error().Err(err).Str("src-ip", fromAddr.String()).Msg("error decoding response")
continue
}

if !(discoveryResponse.GetRequestId() != 0 ||
discoveryResponse.GetPortNumber() != model.AdsPortNumbers_SYSTEM_SERVICE ||
discoveryResponse.GetOperation() != model.Operation_DISCOVERY_RESPONSE) {
continue
}

remoteAmsNetId := discoveryResponse.GetAmsNetId()
var hostNameBlock model.AdsDiscoveryBlockHostName
//var osDataBlock model.AdsDiscoveryBlockOsData
var versionBlock model.AdsDiscoveryBlockVersion
var fingerprintBlock model.AdsDiscoveryBlockFingerprint
for _, block := range discoveryResponse.GetBlocks() {
switch block.GetBlockType() {
case model.AdsDiscoveryBlockType_HOST_NAME:
hostNameBlock = block.(model.AdsDiscoveryBlockHostName)
/* case model.AdsDiscoveryBlockType_OS_DATA:
osDataBlock = block.(model.AdsDiscoveryBlockOsData)*/
case model.AdsDiscoveryBlockType_VERSION:
versionBlock = block.(model.AdsDiscoveryBlockVersion)
case model.AdsDiscoveryBlockType_FINGERPRINT:
fingerprintBlock = block.(model.AdsDiscoveryBlockFingerprint)
}
}

if hostNameBlock == nil {
continue
}

opts := make(map[string][]string)
// opts["sourceAmsNetId"] = []string{localIpV4Address.String() + ".1.1"}
opts["sourceAmsPort"] = []string{"65534"}
opts["targetAmsNetId"] = []string{strconv.Itoa(int(remoteAmsNetId.GetOctet1())) + "." +
strconv.Itoa(int(remoteAmsNetId.GetOctet2())) + "." +
strconv.Itoa(int(remoteAmsNetId.GetOctet3())) + "." +
strconv.Itoa(int(remoteAmsNetId.GetOctet4())) + "." +
strconv.Itoa(int(remoteAmsNetId.GetOctet5())) + "." +
strconv.Itoa(int(remoteAmsNetId.GetOctet6()))}
// TODO: Check if this is legit, or if we can get the information from somewhere.
opts["targetAmsPort"] = []string{"851"}

attributes := make(map[string]values.PlcValue)
attributes["hostName"] = values2.NewPlcSTRING(hostNameBlock.GetHostName().GetText())
if versionBlock != nil {
versionData := versionBlock.GetVersionData()
patchVersion := (int(versionData[3])&0xFF)<<8 | (int(versionData[2]) & 0xFF)
attributes["twinCatVersion"] = values2.NewPlcSTRING(fmt.Sprintf("%d.%d.%d", int(versionData[0])&0xFF, int(versionData[1])&0xFF, patchVersion))
}
if fingerprintBlock != nil {
attributes["fingerprint"] = values2.NewPlcSTRING(string(fingerprintBlock.GetData()))
}
// TODO: Find out how to handle the OS Data

// Add an entry to the results.
remoteAddress, err2 := url.Parse("udp://" + strconv.Itoa(int(remoteAmsNetId.GetOctet1())) + "." +
strconv.Itoa(int(remoteAmsNetId.GetOctet2())) + "." +
strconv.Itoa(int(remoteAmsNetId.GetOctet3())) + "." +
strconv.Itoa(int(remoteAmsNetId.GetOctet4())) + ":" +
strconv.Itoa(int(driverModel.AdsConstants_ADSTCPDEFAULTPORT)))
if err2 == nil {
plcDiscoveryItem := &internalModel.DefaultPlcDiscoveryItem{
ProtocolCode: "ads",
TransportCode: "tcp",
TransportUrl: *remoteAddress,
Options: opts,
Name: hostNameBlock.GetHostName().GetText(),
Attributes: attributes,
}

// Pass the event back to the callback
callback(plcDiscoveryItem)
}
}
}()

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Find out which interfaces to use for sending out search requests

Expand All @@ -177,10 +81,8 @@ func (d *Discoverer) Discover(ctx context.Context, callback func(event apiModel.
interfaces = allInterfaces
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Send out search requests on all selected interfaces

// Iterate over all network devices of this system.
// Iterate over all selected network devices and filter out all the devices with IPv4 configured
var discoveryItems []*discovery
for _, interf := range interfaces {
addrs, err := interf.Addrs()
if err != nil {
Expand Down Expand Up @@ -212,42 +114,167 @@ func (d *Discoverer) Discover(ctx context.Context, callback func(event apiModel.
broadcastAddress := make(net.IP, len(ipv4Addr))
binary.BigEndian.PutUint32(broadcastAddress, binary.BigEndian.Uint32(ipv4Addr)|^binary.BigEndian.Uint32(net.IP(addr.(*net.IPNet).Mask).To4()))

// Prepare the discovery packet data
// Create the discovery request message for this device.
amsNetId := model.NewAmsNetId(ipv4Addr[0], ipv4Addr[1], ipv4Addr[2], ipv4Addr[3], uint8(1), uint8(1))
discoveryRequestMessage := model.NewAdsDiscovery(0, model.Operation_DISCOVERY_REQUEST, amsNetId, model.AdsPortNumbers_SYSTEM_SERVICE, []model.AdsDiscoveryBlock{})
// Add the item to the list.
discoveryItems = append(discoveryItems, &discovery{
interf: interf,
localAddress: ipv4Addr,
broadcastAddress: broadcastAddress,
socket: nil,
})
}
}

// Serialize the message
bytes, err := discoveryRequestMessage.Serialize()
if err != nil {
log.Error().Err(err).Str("broadcast-ip", broadcastAddress.String()).Msg("Error serialising broadcast search packet")
continue
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Set up a listening socket on all devices for processing the responses to any search requests

// Create a not-connected UDP connection to the broadcast address
requestAddr, err := net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:%d", broadcastAddress.String(), model.AdsDiscoveryConstants_ADSDISCOVERYUDPDEFAULTPORT))
if err != nil {
log.Error().Err(err).Str("broadcast-ip", broadcastAddress.String()).Msg("Error resolving target socket for broadcast search")
continue
// Open a listening udp socket for each of the discoveryItems
for _, discoveryItem := range discoveryItems {
responseAddr, err := net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:%d", discoveryItem.localAddress, model.AdsDiscoveryConstants_ADSDISCOVERYUDPDEFAULTPORT))
if err != nil {
panic(err)
}
socket, err := net.ListenUDP("udp4", responseAddr)
if err != nil {
panic(err)
}
discoveryItem.socket = socket

// Start a worker to receive responses
go func(discoveryItem *discovery) {
buf := make([]byte, 1024)
for {
length, fromAddr, err := socket.ReadFromUDP(buf)
if length == 0 {
continue
}
discoveryResponse, err := model.AdsDiscoveryParse(buf[0:length])
if err != nil {
log.Error().Err(err).Str("src-ip", fromAddr.String()).Msg("error decoding response")
continue
}

if discoveryResponse.GetRequestId() != 0 ||
discoveryResponse.GetPortNumber() != model.AdsPortNumbers_SYSTEM_SERVICE ||
discoveryResponse.GetOperation() != model.Operation_DISCOVERY_RESPONSE {
continue
}

remoteAmsNetId := discoveryResponse.GetAmsNetId()
var hostNameBlock model.AdsDiscoveryBlockHostName
//var osDataBlock model.AdsDiscoveryBlockOsData
var versionBlock model.AdsDiscoveryBlockVersion
var fingerprintBlock model.AdsDiscoveryBlockFingerprint
for _, block := range discoveryResponse.GetBlocks() {
switch block.GetBlockType() {
case model.AdsDiscoveryBlockType_HOST_NAME:
hostNameBlock = block.(model.AdsDiscoveryBlockHostName)
/* case model.AdsDiscoveryBlockType_OS_DATA:
osDataBlock = block.(model.AdsDiscoveryBlockOsData)*/
case model.AdsDiscoveryBlockType_VERSION:
versionBlock = block.(model.AdsDiscoveryBlockVersion)
case model.AdsDiscoveryBlockType_FINGERPRINT:
fingerprintBlock = block.(model.AdsDiscoveryBlockFingerprint)
}
}

if hostNameBlock == nil {
continue
}

opts := make(map[string][]string)
opts["sourceAmsNetId"] = []string{discoveryItem.localAddress.String() + ".1.1"}
opts["sourceAmsPort"] = []string{"65534"}
opts["targetAmsNetId"] = []string{strconv.Itoa(int(remoteAmsNetId.GetOctet1())) + "." +
strconv.Itoa(int(remoteAmsNetId.GetOctet2())) + "." +
strconv.Itoa(int(remoteAmsNetId.GetOctet3())) + "." +
strconv.Itoa(int(remoteAmsNetId.GetOctet4())) + "." +
strconv.Itoa(int(remoteAmsNetId.GetOctet5())) + "." +
strconv.Itoa(int(remoteAmsNetId.GetOctet6()))}
// TODO: Check if this is legit, or if we can get the information from somewhere.
opts["targetAmsPort"] = []string{"851"}

attributes := make(map[string]values.PlcValue)
attributes["hostName"] = values2.NewPlcSTRING(hostNameBlock.GetHostName().GetText())
if versionBlock != nil {
versionData := versionBlock.GetVersionData()
patchVersion := (int(versionData[3])&0xFF)<<8 | (int(versionData[2]) & 0xFF)
attributes["twinCatVersion"] = values2.NewPlcSTRING(fmt.Sprintf("%d.%d.%d", int(versionData[0])&0xFF, int(versionData[1])&0xFF, patchVersion))
}
if fingerprintBlock != nil {
attributes["fingerprint"] = values2.NewPlcSTRING(string(fingerprintBlock.GetData()))
}
// TODO: Find out how to handle the OS Data

// Add an entry to the results.
remoteAddress, err2 := url.Parse("udp://" + strconv.Itoa(int(remoteAmsNetId.GetOctet1())) + "." +
strconv.Itoa(int(remoteAmsNetId.GetOctet2())) + "." +
strconv.Itoa(int(remoteAmsNetId.GetOctet3())) + "." +
strconv.Itoa(int(remoteAmsNetId.GetOctet4())) + ":" +
strconv.Itoa(int(driverModel.AdsConstants_ADSTCPDEFAULTPORT)))
if err2 == nil {
plcDiscoveryItem := &internalModel.DefaultPlcDiscoveryItem{
ProtocolCode: "ads",
TransportCode: "tcp",
TransportUrl: *remoteAddress,
Options: opts,
Name: hostNameBlock.GetHostName().GetText(),
Attributes: attributes,
}

// Pass the event back to the callback
callback(plcDiscoveryItem)
}
}
/*localAddr, err := net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:%d", ipv4Addr.String(), model.AdsDiscoveryConstants_ADSDISCOVERYUDPDEFAULTPORT))
if err != nil {
log.Error().Err(err).Str("local-ip", ipv4Addr.String()).Msg("Error resolving local address for broadcast search")
continue
}(discoveryItem)
}
defer func() {
for _, discoveryItem := range discoveryItems {
if discoveryItem.socket != nil {
discoveryItem.socket.Close()
}
udp, err := net.DialUDP("udp4", localAddr, requestAddr)
if err != nil {
log.Error().Err(err).Str("local-ip", ipv4Addr.String()).Str("broadcast-ip", broadcastAddress.String()).
Msg("Error creating sending udp socket for broadcast search")
continue
}*/
}
}()

// Send out the message.
_, err = socket.WriteTo(bytes, requestAddr)
if err != nil {
log.Error().Err(err).Str("broadcast-ip", broadcastAddress.String()).Msg("Error sending request for broadcast search")
continue
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Send out search requests on all selected interfaces

// Iterate over all network devices of this system.
for _, discoveryItem := range discoveryItems {
// Prepare the discovery packet data
// Create the discovery request message for this device.
amsNetId := model.NewAmsNetId(discoveryItem.localAddress[0], discoveryItem.localAddress[1], discoveryItem.localAddress[2], discoveryItem.localAddress[3], uint8(1), uint8(1))
discoveryRequestMessage := model.NewAdsDiscovery(0, model.Operation_DISCOVERY_REQUEST, amsNetId, model.AdsPortNumbers_SYSTEM_SERVICE, []model.AdsDiscoveryBlock{})

// Serialize the message
bytes, err := discoveryRequestMessage.Serialize()
if err != nil {
log.Error().Err(err).Str("broadcast-ip", discoveryItem.broadcastAddress.String()).Msg("Error serialising broadcast search packet")
continue
}

// Create a not-connected UDP connection to the broadcast address
requestAddr, err := net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:%d", discoveryItem.broadcastAddress.String(), model.AdsDiscoveryConstants_ADSDISCOVERYUDPDEFAULTPORT))
if err != nil {
log.Error().Err(err).Str("broadcast-ip", discoveryItem.broadcastAddress.String()).Msg("Error resolving target socket for broadcast search")
continue
}
/*localAddr, err := net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:%d", ipv4Addr.String(), model.AdsDiscoveryConstants_ADSDISCOVERYUDPDEFAULTPORT))
if err != nil {
log.Error().Err(err).Str("local-ip", ipv4Addr.String()).Msg("Error resolving local address for broadcast search")
continue
}
udp, err := net.DialUDP("udp4", localAddr, requestAddr)
if err != nil {
log.Error().Err(err).Str("local-ip", ipv4Addr.String()).Str("broadcast-ip", broadcastAddress.String()).
Msg("Error creating sending udp socket for broadcast search")
continue
}*/

// Send out the message.
_, err = discoveryItem.socket.WriteTo(bytes, requestAddr)
if err != nil {
log.Error().Err(err).Str("broadcast-ip", discoveryItem.broadcastAddress.String()).Msg("Error sending request for broadcast search")
continue
}
}

Expand Down

0 comments on commit 71b7977

Please sign in to comment.