| /* |
| * 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 ( |
| "context" |
| "fmt" |
| "net" |
| "net/url" |
| "strconv" |
| "strings" |
| "sync" |
| "time" |
| |
| "github.com/IBM/netaddr" |
| "github.com/libp2p/go-reuseport" |
| "github.com/pkg/errors" |
| "github.com/rs/zerolog" |
| |
| apiModel "github.com/apache/plc4x/plc4go/pkg/api/model" |
| driverModel "github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model" |
| spiModel "github.com/apache/plc4x/plc4go/spi/model" |
| "github.com/apache/plc4x/plc4go/spi/options" |
| "github.com/apache/plc4x/plc4go/spi/utils" |
| ) |
| |
| type Discoverer struct { |
| wg sync.WaitGroup // use to track spawned go routines |
| |
| passLogToModel bool |
| log zerolog.Logger |
| } |
| |
| func NewDiscoverer(_options ...options.WithOption) *Discoverer { |
| passLoggerToModel, _ := options.ExtractPassLoggerToModel(_options...) |
| customLogger := options.ExtractCustomLoggerOrDefaultToGlobal(_options...) |
| return &Discoverer{ |
| passLogToModel: passLoggerToModel, |
| log: customLogger, |
| } |
| } |
| |
| func (d *Discoverer) Discover(ctx context.Context, callback func(event apiModel.PlcDiscoveryItem), discoveryOptions ...options.WithDiscoveryOption) error { |
| // TODO: handle ctx |
| interfaces, err := extractInterfaces(discoveryOptions) |
| if err != nil { |
| return errors.Wrap(err, "error extracting interfaces") |
| } |
| |
| specificOptions, err := extractProtocolSpecificOptions(discoveryOptions) |
| if err != nil { |
| return errors.Wrap(err, "error extracting protocol specific options") |
| } |
| |
| communicationChannels, err := d.buildupCommunicationChannels(ctx, interfaces, specificOptions.bacNetPort) |
| if err != nil { |
| return errors.Wrap(err, "error building communication channels") |
| } |
| |
| // TODO: make adjustable |
| ctx, cancelFunc := context.WithTimeout(ctx, time.Second*60) |
| defer func() { |
| cancelFunc() |
| }() |
| incomingBVLCChannel, err := d.broadcastAndDiscover(ctx, communicationChannels, specificOptions) |
| if err != nil { |
| return errors.Wrap(err, "error broadcasting and discovering") |
| } |
| d.handleIncomingBVLCs(ctx, callback, incomingBVLCChannel) |
| // TODO: make adjustable |
| time.Sleep(time.Second * 60) |
| for _, channel := range communicationChannels { |
| _ = channel.Close() |
| } |
| return nil |
| } |
| |
| func (d *Discoverer) broadcastAndDiscover(ctx context.Context, communicationChannels []communicationChannel, specificOptions *protocolSpecificOptions) (chan receivedBvlcMessage, error) { |
| incomingBVLCChannel := make(chan receivedBvlcMessage) |
| for _, communicationChannelInstance := range communicationChannels { |
| if err := ctx.Err(); err != nil { |
| return incomingBVLCChannel, err |
| } |
| // Prepare the discovery packet data |
| { |
| var lowLimit driverModel.BACnetContextTagUnsignedInteger |
| var highLimit driverModel.BACnetContextTagUnsignedInteger |
| if whoIsOptions := specificOptions.whoIsOptions; whoIsOptions != nil && whoIsOptions.limits != nil { |
| lowLimit = driverModel.CreateBACnetContextTagUnsignedInteger(0, whoIsOptions.limits.low) |
| highLimit = driverModel.CreateBACnetContextTagUnsignedInteger(1, whoIsOptions.limits.high) |
| } |
| requestWhoIs := driverModel.NewBACnetUnconfirmedServiceRequestWhoIs(lowLimit, highLimit) |
| apdu := driverModel.NewAPDUUnconfirmedRequest(requestWhoIs) |
| |
| 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) |
| bvlc := driverModel.NewBVLCOriginalUnicastNPDU(npdu) |
| |
| // Send the search request. |
| theBytes, err := bvlc.Serialize() |
| if err != nil { |
| return nil, err |
| } |
| if _, err := communicationChannelInstance.broadcastConnection.WriteTo(theBytes, communicationChannelInstance.broadcastConnection.LocalAddr()); err != nil { |
| d.log.Debug().Err(err).Msg("Error sending broadcast") |
| } |
| } |
| if whoHasOptions := specificOptions.whoHasOptions; whoHasOptions != nil { |
| var lowLimit driverModel.BACnetContextTagUnsignedInteger |
| var highLimit driverModel.BACnetContextTagUnsignedInteger |
| if limits := whoHasOptions.limits; limits != nil { |
| lowLimit = driverModel.CreateBACnetContextTagUnsignedInteger(0, limits.deviceInstanceRangeLow) |
| highLimit = driverModel.CreateBACnetContextTagUnsignedInteger(1, limits.deviceInstanceRangeHigh) |
| } |
| var object driverModel.BACnetUnconfirmedServiceRequestWhoHasObject |
| if identifier := whoHasOptions.object.identifier; identifier != nil { |
| var objectType uint16 |
| objectTypeByName, ok := driverModel.BACnetObjectTypeByName(identifier.type_) |
| if ok { |
| parseUint, err := strconv.ParseUint(identifier.type_, 10, 16) |
| if err != nil { |
| return nil, err |
| } |
| objectType = uint16(parseUint) |
| } else { |
| objectType = uint16(objectTypeByName) |
| } |
| objectIdentifier := driverModel.CreateBACnetContextTagObjectIdentifier(2, objectType, uint32(identifier.instance)) |
| object = driverModel.NewBACnetUnconfirmedServiceRequestWhoHasObjectIdentifier(objectIdentifier.GetHeader(), objectIdentifier) |
| } else if name := whoHasOptions.object.name; name != nil { |
| characterString := driverModel.CreateBACnetContextTagCharacterString(3, driverModel.BACnetCharacterEncoding_ISO_10646, *name) |
| object = driverModel.NewBACnetUnconfirmedServiceRequestWhoHasObjectName(characterString.GetHeader(), characterString) |
| } else { |
| panic("Invalid state") |
| } |
| requestWhoHas := driverModel.NewBACnetUnconfirmedServiceRequestWhoHas(lowLimit, highLimit, object) |
| apdu := driverModel.NewAPDUUnconfirmedRequest(requestWhoHas) |
| |
| 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) |
| bvlc := driverModel.NewBVLCOriginalUnicastNPDU(npdu) |
| |
| // Send the search request. |
| theBytes, err := bvlc.Serialize() |
| if err != nil { |
| return nil, err |
| } |
| if _, err := communicationChannelInstance.broadcastConnection.WriteTo(theBytes, communicationChannelInstance.broadcastConnection.LocalAddr()); err != nil { |
| d.log.Debug().Err(err).Msg("Error sending broadcast") |
| } |
| } |
| |
| d.wg.Go(func() { |
| for { |
| if err := ctx.Err(); err != nil { |
| d.log.Debug().Err(err).Msg("ending") |
| return |
| } |
| blockingReadChan := make(chan bool) |
| d.wg.Go(func() { |
| buf := make([]byte, 4096) |
| n, addr, err := communicationChannelInstance.unicastConnection.ReadFrom(buf) |
| if err != nil { |
| d.log.Debug().Err(err).Msg("Ending unicast receive") |
| blockingReadChan <- false |
| return |
| } |
| d.log.Debug().Stringer("addr", addr).Msg("Received broadcast bvlc") |
| ctxForModel := options.GetLoggerContextForModel(ctx, d.log, options.WithPassLoggerToModel(d.passLogToModel)) |
| incomingBvlc, err := driverModel.BVLCParse[driverModel.BVLC](ctxForModel, buf[:n]) |
| if err != nil { |
| d.log.Warn().Err(err).Msg("Could not parse bvlc") |
| blockingReadChan <- true |
| return |
| } |
| incomingBVLCChannel <- receivedBvlcMessage{incomingBvlc, addr} |
| blockingReadChan <- true |
| }) |
| select { |
| case ok := <-blockingReadChan: |
| if !ok { |
| d.log.Debug().Msg("Ending unicast reading") |
| return |
| } |
| d.log.Trace().Msg("Received something unicast") |
| case <-ctx.Done(): |
| d.log.Debug().Err(ctx.Err()).Msg("Ending unicast receive") |
| return |
| } |
| } |
| }) |
| |
| d.wg.Go(func() { |
| for { |
| if err := ctx.Err(); err != nil { |
| d.log.Debug().Err(err).Msg("ending") |
| return |
| } |
| blockingReadChan := make(chan bool) |
| d.wg.Go(func() { |
| buf := make([]byte, 4096) |
| n, addr, err := communicationChannelInstance.broadcastConnection.ReadFrom(buf) |
| if err != nil { |
| d.log.Debug().Err(err).Msg("Ending broadcast receive") |
| blockingReadChan <- false |
| return |
| } |
| d.log.Debug().Stringer("addr", addr).Msg("Received broadcast bvlc") |
| ctxForModel := options.GetLoggerContextForModel(ctx, d.log, options.WithPassLoggerToModel(d.passLogToModel)) |
| incomingBvlc, err := driverModel.BVLCParse[driverModel.BVLC](ctxForModel, buf[:n]) |
| if err != nil { |
| d.log.Warn().Err(err).Msg("Could not parse bvlc") |
| blockingReadChan <- true |
| } |
| incomingBVLCChannel <- receivedBvlcMessage{incomingBvlc, addr} |
| blockingReadChan <- true |
| }) |
| select { |
| case ok := <-blockingReadChan: |
| if !ok { |
| d.log.Debug().Msg("Ending broadcast reading") |
| return |
| } |
| d.log.Trace().Msg("Received something broadcast") |
| case <-ctx.Done(): |
| d.log.Debug().Err(ctx.Err()).Msg("Ending broadcast receive") |
| return |
| } |
| } |
| }) |
| } |
| return incomingBVLCChannel, nil |
| } |
| |
| func (d *Discoverer) handleIncomingBVLCs(ctx context.Context, callback func(event apiModel.PlcDiscoveryItem), incomingBVLCChannel chan receivedBvlcMessage) { |
| for { |
| if err := ctx.Err(); err != nil { |
| // TODO: maybe we log something, but maybe it is fine |
| return |
| } |
| select { |
| case receivedBvlc := <-incomingBVLCChannel: |
| var npdu driverModel.NPDU |
| if bvlc, ok := receivedBvlc.bvlc.(interface{ GetNpdu() driverModel.NPDU }); ok { |
| npdu = bvlc.GetNpdu() |
| } |
| _ = npdu |
| if apdu := npdu.GetApdu(); apdu == nil { |
| nlm := npdu.GetNlm() |
| d.log.Debug().Stringer("nlm", nlm).Msg("Got nlm") |
| continue |
| } |
| apdu := npdu.GetApdu() |
| if _, ok := apdu.(driverModel.APDUConfirmedRequest); ok { |
| d.log.Debug().Stringer("apdu", apdu).Msg("Got apdu") |
| continue |
| } |
| apduUnconfirmedRequest := apdu.(driverModel.APDUUnconfirmedRequest) |
| serviceRequest := apduUnconfirmedRequest.GetServiceRequest() |
| switch serviceRequest := serviceRequest.(type) { |
| case driverModel.BACnetUnconfirmedServiceRequestIAm: |
| iAm := serviceRequest |
| remoteUrl, err := url.Parse("udp://" + receivedBvlc.addr.String()) |
| if err != nil { |
| d.log.Debug().Err(err).Msg("Error parsing url") |
| } |
| discoveryEvent := spiModel.NewDefaultPlcDiscoveryItem( |
| "bacnet-ip", |
| "udp", |
| *remoteUrl, |
| nil, |
| fmt.Sprintf("device %v:%v", iAm.GetDeviceIdentifier().GetObjectType(), iAm.GetDeviceIdentifier().GetInstanceNumber()), |
| nil, |
| ) |
| |
| // Pass the event back to the callback |
| callback(discoveryEvent) |
| case driverModel.BACnetUnconfirmedServiceRequestIHave: |
| iHave := serviceRequest |
| remoteUrl, err := url.Parse("udp://" + receivedBvlc.addr.String()) |
| if err != nil { |
| d.log.Debug().Err(err).Msg("Error parsing url") |
| } |
| discoveryEvent := spiModel.NewDefaultPlcDiscoveryItem( |
| "bacnet-ip", |
| "udp", |
| *remoteUrl, |
| nil, |
| fmt.Sprintf("device %v:%v with %v:%v and %v", iHave.GetDeviceIdentifier().GetObjectType(), iHave.GetDeviceIdentifier().GetInstanceNumber(), iHave.GetObjectIdentifier().GetObjectType(), iHave.GetObjectIdentifier().GetInstanceNumber(), iHave.GetObjectName().GetValue()), |
| nil, |
| ) |
| |
| // Pass the event back to the callback |
| callback(discoveryEvent) |
| } |
| case <-ctx.Done(): |
| d.log.Debug().Err(ctx.Err()).Msg("Ending unicast receive") |
| return |
| } |
| } |
| } |
| |
| func (d *Discoverer) buildupCommunicationChannels(ctx context.Context, interfaces []net.Interface, bacNetPort int) (communicationChannels []communicationChannel, err error) { |
| // Iterate over all network devices of this system. |
| for _, networkInterface := range interfaces { |
| if err := ctx.Err(); err != nil { |
| return nil, err |
| } |
| unicastInterfaceAddress, err := networkInterface.Addrs() |
| if err != nil { |
| return nil, errors.Wrapf(err, "Error getting Addresses for %v", networkInterface) |
| } |
| // Iterate over all addresses the current interface has configured |
| for _, unicastAddress := range unicastInterfaceAddress { |
| if err := ctx.Err(); err != nil { |
| return nil, err |
| } |
| 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 { |
| // TODO: for now we only support ipv4 (reuse doesn't like v6 address strings atm) |
| continue |
| 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 { |
| // TODO: for now we only support ipv4 (reuse doesn't like v6 address strings atm) |
| continue |
| ipAddr = addr.IP.To16() |
| } |
| default: |
| continue |
| } |
| |
| if !ipAddr.IsGlobalUnicast() { |
| continue |
| } |
| |
| // Handle undirected |
| unicastConnection, err := reuseport.ListenPacket("udp4", fmt.Sprintf("%v:%d", ipAddr, bacNetPort)) |
| if err != nil { |
| d.log.Debug().Err(err).Msg("Error building unicast Port") |
| continue |
| } |
| |
| _, cidr, _ := net.ParseCIDR(unicastAddress.String()) |
| broadcastAddr := netaddr.BroadcastAddr(cidr) |
| // Handle undirected |
| broadcastConnection, err := reuseport.ListenPacket("udp4", fmt.Sprintf("%v:%d", broadcastAddr, bacNetPort)) |
| if err != nil { |
| if err := unicastConnection.Close(); err != nil { |
| d.log.Debug().Err(err).Msg("Error closing transport instance") |
| } |
| d.log.Debug().Err(err).Msg("Error building broadcast Port") |
| continue |
| } |
| communicationChannels = append(communicationChannels, communicationChannel{ |
| networkInterface: networkInterface, |
| unicastConnection: unicastConnection, |
| broadcastConnection: broadcastConnection, |
| log: d.log, |
| }) |
| } |
| } |
| return |
| } |
| |
| func (d *Discoverer) Close() error { |
| defer utils.StopWarn(d.log)() |
| d.log.Trace().Msg("Waiting for goroutines to stop") |
| d.wg.Wait() |
| return nil |
| } |
| |
| type receivedBvlcMessage struct { |
| bvlc driverModel.BVLC |
| addr net.Addr |
| } |
| |
| type communicationChannel struct { |
| networkInterface net.Interface |
| unicastConnection net.PacketConn |
| broadcastConnection net.PacketConn |
| log zerolog.Logger |
| } |
| |
| func (c communicationChannel) Close() error { |
| defer utils.StopWarn(c.log)() |
| _ = c.unicastConnection.Close() |
| _ = c.broadcastConnection.Close() |
| return nil |
| } |
| |
| func extractInterfaces(discoveryOptions []options.WithDiscoveryOption) ([]net.Interface, error) { |
| allInterfaces, err := net.Interfaces() |
| if err != nil { |
| return nil, 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 |
| } |
| return interfaces, nil |
| } |
| |
| type protocolSpecificOptions struct { |
| bacNetPort int |
| whoIsOptions *struct { |
| limits *struct { |
| low uint |
| high uint |
| } |
| } |
| whoHasOptions *struct { |
| limits *struct { |
| deviceInstanceRangeLow uint |
| deviceInstanceRangeHigh uint |
| } |
| object struct { |
| identifier *struct { |
| type_ string |
| instance uint |
| } |
| name *string |
| } |
| } |
| } |
| |
| func bacNetPort(port int) option { |
| return func(specificOptions *protocolSpecificOptions) error { |
| specificOptions.bacNetPort = port |
| return nil |
| } |
| } |
| |
| func whoIsLimits(whoIsLowLimit, whoIsHighLimit uint) option { |
| return func(specificOptions *protocolSpecificOptions) error { |
| specificOptions.whoIsOptions = &struct { |
| limits *struct { |
| low uint |
| high uint |
| } |
| }{&struct { |
| low uint |
| high uint |
| }{whoIsLowLimit, whoIsHighLimit}} |
| return nil |
| } |
| } |
| |
| func whoHasOption() option { |
| return func(specificOptions *protocolSpecificOptions) error { |
| specificOptions.whoHasOptions = &struct { |
| limits *struct { |
| deviceInstanceRangeLow uint |
| deviceInstanceRangeHigh uint |
| } |
| object struct { |
| identifier *struct { |
| type_ string |
| instance uint |
| } |
| name *string |
| } |
| }{} |
| return nil |
| } |
| } |
| |
| func whoHasLimits(whoHasDeviceInstanceRangeLowLimit, whoHasDeviceInstanceRangeHighLimit uint) option { |
| return func(specificOptions *protocolSpecificOptions) error { |
| if specificOptions.whoHasOptions == nil { |
| panic("we should have set this before") |
| } |
| specificOptions.whoHasOptions.limits = &struct { |
| deviceInstanceRangeLow uint |
| deviceInstanceRangeHigh uint |
| }{whoHasDeviceInstanceRangeLowLimit, whoHasDeviceInstanceRangeHighLimit} |
| return nil |
| } |
| } |
| |
| func whoHasObjectIdentifier(objectIdentifierType string, objectIdentifierInstance uint) option { |
| return func(specificOptions *protocolSpecificOptions) error { |
| if specificOptions.whoHasOptions == nil { |
| panic("we should have set this before") |
| } |
| specificOptions.whoHasOptions.object.identifier = &struct { |
| type_ string |
| instance uint |
| }{objectIdentifierType, objectIdentifierInstance} |
| return nil |
| } |
| } |
| |
| func whoHasObjectName(objectName string) option { |
| return func(specificOptions *protocolSpecificOptions) error { |
| if specificOptions.whoHasOptions == nil { |
| panic("we should have set this before") |
| } |
| specificOptions.whoHasOptions.object.name = &objectName |
| return nil |
| } |
| } |
| |
| func newProtocolSpecificOptions(options ...option) (*protocolSpecificOptions, error) { |
| var specificOptions protocolSpecificOptions |
| for _, _option := range options { |
| if parseErr := _option(&specificOptions); parseErr != nil { |
| return nil, parseErr |
| } |
| } |
| return &specificOptions, nil |
| } |
| |
| type option func(specificOptions *protocolSpecificOptions) error |
| |
| func extractProtocolSpecificOptions(discoveryOptions []options.WithDiscoveryOption) (*protocolSpecificOptions, error) { |
| var collectedOptions []option |
| filteredOptionMap := make(map[string][]any) |
| for _, protocolSpecificOption := range options.FilterDiscoveryOptionProtocolSpecific(discoveryOptions) { |
| key := protocolSpecificOption.GetKey() |
| value := protocolSpecificOption.GetValue() |
| if _, ok := filteredOptionMap[key]; !ok { |
| filteredOptionMap[key] = make([]any, 0) |
| } |
| filteredOptionMap[key] = append(filteredOptionMap[key], value) |
| } |
| keyDependencies := map[string][]struct { |
| key string |
| mustBePresent bool |
| }{ |
| "who-is-low-limit": {{"who-is-high-limit", true}}, |
| "who-is-high-limit": {{"who-is-low-limit", true}}, |
| "who-has-device-instance-range-low-limit": {{"who-has-device-instance-range-high-limit", true}, {"who-has-object*", true}}, |
| "who-has-device-instance-range-high-limit": {{"who-has-device-instance-range-low-limit", true}, {"who-has-object*", true}}, |
| "who-has-object-identifier-type": {{"who-has-object-identifier-instance", true}, {"who-has-object-name", false}}, |
| "who-has-object-identifier-instance": {{"who-has-object-identifier-type", true}, {"who-has-object-name", false}}, |
| "who-has-object-name": {{"who-has-object-identifier-instance", false}, {"who-has-object-identifier-type", false}}, |
| } |
| for key, value := range keyDependencies { |
| if _, ok := filteredOptionMap[key]; ok { |
| for _, otherKey := range value { |
| if strings.HasSuffix(otherKey.key, "*") { |
| prefix := strings.TrimSuffix(otherKey.key, "*") |
| mustBePresent := otherKey.mustBePresent |
| var found bool |
| for key := range filteredOptionMap { |
| found = found || strings.HasPrefix(key, prefix) |
| } |
| if mustBePresent && !found { |
| return nil, errors.Errorf("When %s is set one of %s must also be set", key, otherKey.key) |
| } else if !mustBePresent && found { |
| return nil, errors.Errorf("When %s is set none of %s must be set", key, otherKey.key) |
| } |
| } else if _, otherOk := filteredOptionMap[otherKey.key]; otherOk && !otherKey.mustBePresent { |
| return nil, errors.Errorf("When %s is set %s must not be set", key, otherKey.key) |
| } else if !otherOk && otherKey.mustBePresent { |
| return nil, errors.Errorf("When %s is set %s must be set too", key, otherKey.key) |
| } |
| } |
| } |
| } |
| if _, ok := filteredOptionMap["bacnet-port"]; ok { |
| parsedInt, err := OneInt(filteredOptionMap, "bacnet-port") |
| if err != nil { |
| return nil, err |
| } |
| collectedOptions = append(collectedOptions, bacNetPort(parsedInt)) |
| } else { |
| collectedOptions = append(collectedOptions, bacNetPort(47808)) |
| } |
| |
| if whoIsLow, whoIsHigh, ok, err := func() (whoIsLowLimit uint, whoIsHighLimit uint, ok bool, err error) { |
| if _, limitPresent := filteredOptionMap["who-is-low-limit"]; !limitPresent { |
| return |
| } |
| ok = true |
| whoIsLowLimit, err = OneUint(filteredOptionMap, "who-is-low-limit") |
| whoIsHighLimit, err = OneUint(filteredOptionMap, "who-is-high-limit") |
| return |
| }(); ok { |
| collectedOptions = append(collectedOptions, whoIsLimits(whoIsLow, whoIsHigh)) |
| } else if err != nil { |
| return nil, err |
| } |
| for key := range filteredOptionMap { |
| if strings.HasPrefix(key, "who-has-object") { |
| collectedOptions = append(collectedOptions, whoHasOption()) |
| break |
| } |
| } |
| if whoHasDeviceInstanceRangeLowLimit, whoHasDeviceInstanceRangeHighLimit, ok, err := func() (whoIsLowLimit uint, whoIsHighLimit uint, ok bool, err error) { |
| if _, limitPresent := filteredOptionMap["who-has-device-instance-range-low-limit"]; !limitPresent { |
| return |
| } |
| ok = true |
| whoIsLowLimit, err = OneUint(filteredOptionMap, "who-has-device-instance-range-low-limit") |
| whoIsHighLimit, err = OneUint(filteredOptionMap, "who-has-device-instance-range-high-limit") |
| return |
| }(); ok { |
| collectedOptions = append(collectedOptions, whoHasLimits(whoHasDeviceInstanceRangeLowLimit, whoHasDeviceInstanceRangeHighLimit)) |
| } else if err != nil { |
| return nil, err |
| } |
| |
| if whoHasObjectIdentifierType, objectIdentifierInstance, ok, err := func() (whoHasObjectIdentifierType string, whoHasObjectIdentifierInstance uint, ok bool, err error) { |
| if _, limitPresent := filteredOptionMap["who-has-object-identifier-type"]; !limitPresent { |
| return |
| } |
| ok = true |
| whoHasObjectIdentifierType, err = OneString(filteredOptionMap, "who-has-object-identifier-type") |
| whoHasObjectIdentifierInstance, err = OneUint(filteredOptionMap, "who-has-object-identifier-instance") |
| return |
| }(); ok { |
| collectedOptions = append(collectedOptions, whoHasObjectIdentifier(whoHasObjectIdentifierType, objectIdentifierInstance)) |
| } else if err != nil { |
| return nil, err |
| } |
| |
| if _, ok := filteredOptionMap["who-has-object-name"]; ok { |
| if name, err := OneString(filteredOptionMap, "who-has-object-name"); err != nil { |
| return nil, err |
| } else { |
| collectedOptions = append(collectedOptions, whoHasObjectName(name)) |
| } |
| } |
| return newProtocolSpecificOptions(collectedOptions...) |
| } |
| |
| func OneInt(filteredOptionMap map[string][]any, key string) (int, error) { |
| value, err := One(filteredOptionMap, key) |
| if err != nil { |
| return 0, err |
| } |
| parsedInt, err := strconv.ParseInt(fmt.Sprintf("%v", value), 10, 32) |
| if err != nil { |
| return 0, errors.Wrap(err, "Error parsing option bacnet-port") |
| } |
| return int(parsedInt), nil |
| } |
| |
| func OneUint(filteredOptionMap map[string][]any, key string) (uint, error) { |
| value, err := One(filteredOptionMap, key) |
| if err != nil { |
| return 0, err |
| } |
| parsedInt, err := strconv.ParseUint(fmt.Sprintf("%v", value), 10, 32) |
| if err != nil { |
| return 0, errors.Wrap(err, "Error parsing option bacnet-port") |
| } |
| return uint(parsedInt), nil |
| } |
| func OneString(filteredOptionMap map[string][]any, key string) (string, error) { |
| value, err := One(filteredOptionMap, key) |
| if err != nil { |
| return "", err |
| } |
| return fmt.Sprintf("%v", value), nil |
| } |
| |
| func One(filteredOptionMap map[string][]any, key string) (any, error) { |
| values := filteredOptionMap[key] |
| if len(values) != 1 { |
| return nil, errors.Errorf("%s expects only one value", key) |
| } |
| return values[0], nil |
| } |