This is an automated email from the ASF dual-hosted git repository.
sruehl pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/plc4x.git
The following commit(s) were added to refs/heads/develop by this push:
new 8e5f347013 feat(plc4go/bacnetip): routed BACnet addressing via
RemoteNetwork/RemoteAddress connection options
8e5f347013 is described below
commit 8e5f34701387a9803d363a24db63e55a4d6a2267
Author: Sebastian Rühl <[email protected]>
AuthorDate: Thu Jul 23 16:20:06 2026 +0200
feat(plc4go/bacnetip): routed BACnet addressing via
RemoteNetwork/RemoteAddress connection options
A connection whose target device sits behind a BACnet router (ASHRAE 135
clause 6) can now reach it: connect to the ROUTER's IP and set
RemoteNetwork=<dnet>&RemoteAddress=<ip:port|0xhex>. Every outgoing NPDU
(Reader, Writer, Subscriber, segment acks) then carries the destination
specifier (DNET/DLEN/DADR, hop count 255) so the router forwards it onto
the target's network; replies match on invoke id as before.
The discovery side mirrors the same vocabulary: a routed I-Am's NPDU
source specifier (SNET/SADR) is surfaced on the PlcDiscoveryItem as
RemoteNetwork/RemoteAddress options — copy-paste ready for the routed
connection URL, with 6-octet SADRs rendered as B/IP <ip>:<port> and other
datalink MACs as 0x hex.
This wires the first slice of the Phase 6 routed-addressing plan; the
DeviceInfoCache/StaticDevices scaffolding stays available for multi-device
connections later.
---
plc4go/internal/bacnetip/Configuration.go | 15 +++
plc4go/internal/bacnetip/Configuration_plc4xgen.go | 8 ++
plc4go/internal/bacnetip/Connection.go | 12 ++
plc4go/internal/bacnetip/Discoverer.go | 9 +-
plc4go/internal/bacnetip/Framing.go | 127 +++++++++++++++++++--
plc4go/internal/bacnetip/Framing_test.go | 122 +++++++++++++++++++-
.../internal/bacnetip/IdleWriteRoundtrip_test.go | 2 +-
plc4go/internal/bacnetip/MessageCodec_test.go | 2 +-
plc4go/internal/bacnetip/ReadRoundtrip_test.go | 2 +-
plc4go/internal/bacnetip/Reader.go | 6 +-
.../internal/bacnetip/ReaderResultDelivery_test.go | 2 +-
plc4go/internal/bacnetip/ReaderSegmentation.go | 2 +-
plc4go/internal/bacnetip/Subscriber.go | 2 +-
plc4go/internal/bacnetip/WriteRoundtrip_test.go | 6 +-
plc4go/internal/bacnetip/Writer.go | 5 +-
15 files changed, 293 insertions(+), 29 deletions(-)
diff --git a/plc4go/internal/bacnetip/Configuration.go
b/plc4go/internal/bacnetip/Configuration.go
index dfe282339f..424e212ef9 100644
--- a/plc4go/internal/bacnetip/Configuration.go
+++ b/plc4go/internal/bacnetip/Configuration.go
@@ -88,6 +88,21 @@ type Configuration struct {
// WhoIs (e.g. routed devices on a network with no BBMD).
Comma-separated entries
// of the form "<deviceId>@<network>:<host>:<port>".
StaticDevices string
+
+ // RemoteNetwork is the BACnet network number of this connection's
target device
+ // when it sits behind a BACnet router (ASHRAE 135 clause 6). The
connection's
+ // transport host is then the ROUTER's IP; every outgoing NPDU carries a
+ // destination specifier (DNET=RemoteNetwork, DADR=RemoteAddress, hop
count 255)
+ // so the router forwards it onto that network, and replies arrive with
the
+ // device's source specifier mirrored back. 0 (default) means the
target is on
+ // the local segment and NPDUs stay specifier-free.
+ RemoteNetwork uint16
+
+ // RemoteAddress is the target device's MAC address on RemoteNetwork,
required
+ // when RemoteNetwork is set. For BACnet/IP-to-BACnet/IP routing use
+ // "<ip>:<port>" (encoded as the 6-byte B/IP DADR); for other datalinks
a hex
+ // string ("0x0C") supplies the raw MAC octets.
+ RemoteAddress string
}
// ParseFromOptions populates a Configuration from the connection-URL query
options,
diff --git a/plc4go/internal/bacnetip/Configuration_plc4xgen.go
b/plc4go/internal/bacnetip/Configuration_plc4xgen.go
index 32a880cb31..4866962b39 100644
--- a/plc4go/internal/bacnetip/Configuration_plc4xgen.go
+++ b/plc4go/internal/bacnetip/Configuration_plc4xgen.go
@@ -106,6 +106,14 @@ func (d *Configuration) SerializeWithWriteBuffer(ctx
context.Context, writeBuffe
if err := writeBuffer.WriteString("staticDevices",
uint32(len(d.StaticDevices)*8), d.StaticDevices); err != nil {
return err
}
+
+ if err := writeBuffer.WriteUint16("remoteNetwork", 16,
d.RemoteNetwork); err != nil {
+ return err
+ }
+
+ if err := writeBuffer.WriteString("remoteAddress",
uint32(len(d.RemoteAddress)*8), d.RemoteAddress); err != nil {
+ return err
+ }
if err := writeBuffer.PopContext("Configuration"); err != nil {
return err
}
diff --git a/plc4go/internal/bacnetip/Connection.go
b/plc4go/internal/bacnetip/Connection.go
index 227e30ae6c..881d3a00c2 100644
--- a/plc4go/internal/bacnetip/Connection.go
+++ b/plc4go/internal/bacnetip/Connection.go
@@ -47,6 +47,7 @@ type Connection struct {
invokeIdGenerator InvokeIdGenerator
messageCodec spi.MessageCodec
configuration Configuration
+ routedDest *routedDestination // non-nil when the target device
is behind a BACnet router
driverContext DriverContext
subscribers []*Subscriber
subscribersMu sync.Mutex
@@ -72,11 +73,20 @@ func NewConnection(messageCodec spi.MessageCodec,
tagHandler spi.PlcTagHandler,
customLogger.Warn().Err(err).Msg("invalid driver options;
falling back to defaults")
configuration = createDefaultConfiguration()
}
+ routedDest, err := routedDestinationFromConfiguration(configuration)
+ if err != nil {
+ // Fail closed on the routing options: silently ignoring them
would
+ // unicast routed requests at the router without a destination
+ // specifier, which the router (correctly) cannot forward.
+ customLogger.Error().Err(err).Msg("invalid routed-destination
options; connection will address the local segment only")
+ routedDest = nil
+ }
connection := &Connection{
invokeIdGenerator: InvokeIdGenerator{currentInvokeId: 0},
messageCodec: messageCodec,
configuration: configuration,
driverContext: NewDriverContext(configuration),
+ routedDest: routedDest,
tm: tm,
log: customLogger,
_options: _options,
@@ -214,6 +224,7 @@ func (c *Connection) ReadRequestBuilder()
apiModel.PlcReadRequestBuilder {
&c.invokeIdGenerator,
c.messageCodec,
c.tm,
+ c.routedDest,
append(c._options, options.WithCustomLogger(c.log))...,
),
)
@@ -228,6 +239,7 @@ func (c *Connection) WriteRequestBuilder()
apiModel.PlcWriteRequestBuilder {
c.messageCodec,
c.tm,
c.driverContext,
+ c.routedDest,
append(c._options, options.WithCustomLogger(c.log))...,
),
)
diff --git a/plc4go/internal/bacnetip/Discoverer.go
b/plc4go/internal/bacnetip/Discoverer.go
index d7616c56c5..e62c829fbe 100644
--- a/plc4go/internal/bacnetip/Discoverer.go
+++ b/plc4go/internal/bacnetip/Discoverer.go
@@ -344,11 +344,18 @@ func (d *Discoverer) handleIncomingBVLCs(ctx
context.Context, callback func(even
if err != nil {
d.log.Debug().Err(err).Msg("Error
parsing url")
}
+ // A routed I-Am carries the device's origin as
an NPDU source
+ // specifier (SNET/SADR, ASHRAE 135 clause
6.2.4). Surface it as
+ // ready-to-use connection options (the same
RemoteNetwork /
+ // RemoteAddress keys the connection URL
accepts) so consumers can
+ // reach the device through the relaying
router: transport URL =
+ // the router (the frame's UDP source), options
= the routed hop.
+ discoveryOptions := routedOriginOptions(npdu)
discoveryEvent :=
spiModel.NewDefaultPlcDiscoveryItem(
"bacnet-ip",
"udp",
*remoteUrl,
- nil,
+ discoveryOptions,
fmt.Sprintf("device %v:%v",
iAm.GetDeviceIdentifier().GetObjectType(),
iAm.GetDeviceIdentifier().GetInstanceNumber()),
nil,
)
diff --git a/plc4go/internal/bacnetip/Framing.go
b/plc4go/internal/bacnetip/Framing.go
index de7124c8fb..7829050dbb 100644
--- a/plc4go/internal/bacnetip/Framing.go
+++ b/plc4go/internal/bacnetip/Framing.go
@@ -20,36 +20,143 @@
package bacnetip
import (
+ "encoding/binary"
+ "encoding/hex"
+ "fmt"
+ "net/netip"
+ "strconv"
+ "strings"
+
+ "github.com/apache/plc4x/plc4go/spi/errors"
+
"github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model"
)
+// routedDestination is the NPDU destination specifier for a connection whose
+// target device sits behind a BACnet router (Configuration.RemoteNetwork /
+// RemoteAddress): DNET is the device's network number, DADR its MAC on that
+// network. nil means local-segment addressing (no specifier), the default.
+type routedDestination struct {
+ dnet uint16
+ dadr []uint8
+}
+
+// routedDestinationHopCount is the initial hop count of freshly originated
+// routed NPDUs (ASHRAE 135 clause 6.2.2 recommends starting at 255).
+const routedDestinationHopCount uint8 = 255
+
+// parseRemoteAddress turns Configuration.RemoteAddress into DADR octets:
+// "<ip>:<port>" becomes the 6-byte B/IP address (Annex J), and a "0x…" hex
+// string supplies raw MAC octets for non-IP datalinks.
+func parseRemoteAddress(remoteAddress string) ([]uint8, error) {
+ if s := strings.TrimPrefix(remoteAddress, "0x"); s != remoteAddress {
+ dadr, err := hex.DecodeString(s)
+ if err != nil {
+ return nil, errors.Wrapf(err, "RemoteAddress %q is not
valid hex", remoteAddress)
+ }
+ if len(dadr) == 0 {
+ return nil, errors.Errorf("RemoteAddress %q decodes to
zero octets", remoteAddress)
+ }
+ return dadr, nil
+ }
+ ap, err := netip.ParseAddrPort(remoteAddress)
+ if err != nil {
+ return nil, errors.Wrapf(err, "RemoteAddress %q is neither
<ip>:<port> nor 0x-prefixed hex", remoteAddress)
+ }
+ if !ap.Addr().Is4() {
+ return nil, errors.Errorf("RemoteAddress %q must be IPv4 for a
B/IP DADR", remoteAddress)
+ }
+ ip4 := ap.Addr().As4()
+ dadr := make([]uint8, 6)
+ copy(dadr[0:4], ip4[:])
+ binary.BigEndian.PutUint16(dadr[4:6], ap.Port())
+ return dadr, nil
+}
+
+// routedDestinationFromConfiguration derives the connection-scoped destination
+// specifier, or nil for local-segment connections. RemoteAddress without
+// RemoteNetwork (and vice versa) is a configuration error.
+func routedDestinationFromConfiguration(configuration Configuration)
(*routedDestination, error) {
+ if configuration.RemoteNetwork == 0 && configuration.RemoteAddress ==
"" {
+ return nil, nil
+ }
+ if configuration.RemoteNetwork == 0 || configuration.RemoteAddress ==
"" {
+ return nil, errors.New("RemoteNetwork and RemoteAddress must be
set together")
+ }
+ dadr, err := parseRemoteAddress(configuration.RemoteAddress)
+ if err != nil {
+ return nil, err
+ }
+ return &routedDestination{dnet: configuration.RemoteNetwork, dadr:
dadr}, nil
+}
+
// wrapAPDU encapsulates an APDU in the BACnet/IP NPDU + BVLC layers expected
// by MessageCodec.Send (which type-asserts to model.BVLC). Without this
// wrapping the codec panics on the cast and the request silently dies.
//
// expectingReply is set for confirmed requests; unconfirmed requests pass
-// false. The NPDU is intentionally local-only (no DNET/SNET) because routed
-// addressing happens at the Tag layer in Phase 6.
-func wrapAPDU(apdu model.APDU, expectingReply bool) model.BVLC {
+// false. With dest == nil the NPDU is local-only (no DNET/SNET); a non-nil
+// dest emits the destination specifier (DNET/DLEN/DADR + hop count) so the
+// first router on the connection's segment forwards the request onto the
+// target's network (ASHRAE 135 clause 6).
+func wrapAPDU(apdu model.APDU, expectingReply bool, dest *routedDestination)
model.BVLC {
control := model.NewNPDUControl(
- false, // messageTypeFieldPresent
- false, // destinationSpecified
- false, // sourceSpecified
+ false, // messageTypeFieldPresent
+ dest != nil, // destinationSpecified
+ false, // sourceSpecified
expectingReply,
model.NPDUNetworkPriority_NORMAL_MESSAGE,
)
+ var destNet *uint16
+ var destLen *uint8
+ var destAddr []uint8
+ var hopCount *uint8
+ if dest != nil {
+ dnet := dest.dnet
+ destNet = &dnet
+ dlen := uint8(len(dest.dadr))
+ destLen = &dlen
+ destAddr = dest.dadr
+ hops := routedDestinationHopCount
+ hopCount = &hops
+ }
npdu := model.NewNPDU(
1, // protocolVersionNumber
control,
- nil, // destinationNetworkAddress
- nil, // destinationLength
- nil, // destinationAddress
+ destNet,
+ destLen,
+ destAddr,
nil, // sourceNetworkAddress
nil, // sourceLength
nil, // sourceAddress
- nil, // hopCount
+ hopCount,
nil, // nlm
apdu,
)
return model.NewBVLCOriginalUnicastNPDU(npdu)
}
+
+// routedOriginOptions extracts a routed frame's NPDU source specifier as
+// connection options (RemoteNetwork/RemoteAddress — the keys the connection
+// URL accepts), or nil for local frames. A 6-octet SADR is rendered as the
+// B/IP "<ip>:<port>" form; other datalink MACs as 0x-prefixed hex.
+func routedOriginOptions(npdu model.NPDU) map[string][]string {
+ if npdu == nil || npdu.GetControl() == nil ||
!npdu.GetControl().GetSourceSpecified() {
+ return nil
+ }
+ snet := npdu.GetSourceNetworkAddress()
+ sadr := npdu.GetSourceAddress()
+ if snet == nil || len(sadr) == 0 {
+ return nil
+ }
+ var remoteAddress string
+ if len(sadr) == 6 {
+ remoteAddress = fmt.Sprintf("%d.%d.%d.%d:%d", sadr[0], sadr[1],
sadr[2], sadr[3], binary.BigEndian.Uint16(sadr[4:6]))
+ } else {
+ remoteAddress = "0x" + hex.EncodeToString(sadr)
+ }
+ return map[string][]string{
+ "RemoteNetwork": {strconv.FormatUint(uint64(*snet), 10)},
+ "RemoteAddress": {remoteAddress},
+ }
+}
diff --git a/plc4go/internal/bacnetip/Framing_test.go
b/plc4go/internal/bacnetip/Framing_test.go
index 007f962890..df22d6733f 100644
--- a/plc4go/internal/bacnetip/Framing_test.go
+++ b/plc4go/internal/bacnetip/Framing_test.go
@@ -20,6 +20,7 @@
package bacnetip
import (
+ "context"
"testing"
"github.com/stretchr/testify/assert"
@@ -35,7 +36,7 @@ func newWhoIsAPDU(t *testing.T) readWriteModel.APDU {
}
func TestWrapAPDU_ProducesBVLCOriginalUnicastNPDU(t *testing.T) {
- bvlc := wrapAPDU(newWhoIsAPDU(t), false)
+ bvlc := wrapAPDU(newWhoIsAPDU(t), false, nil)
require.NotNil(t, bvlc)
// MessageCodec.Send type-asserts to BVLCOriginalUnicastNPDU on the
// send-path; wrapAPDU must produce exactly that type.
@@ -47,7 +48,7 @@ func TestWrapAPDU_NPDUProtocolVersionIs1(t *testing.T) {
// BACnet stacks reject NPDUs with a wrong protocol version. Pin it
// to 1 (the only spec-valid value) so an accidental change shows up
// as a test failure rather than a wire-protocol incompatibility.
- bvlc := wrapAPDU(newWhoIsAPDU(t),
false).(readWriteModel.BVLCOriginalUnicastNPDU)
+ bvlc := wrapAPDU(newWhoIsAPDU(t), false,
nil).(readWriteModel.BVLCOriginalUnicastNPDU)
assert.Equal(t, uint8(1), bvlc.GetNpdu().GetProtocolVersionNumber())
}
@@ -61,7 +62,7 @@ func TestWrapAPDU_ExpectingReplyPropagatesToControl(t
*testing.T) {
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
- bvlc := wrapAPDU(newWhoIsAPDU(t),
tc.expectingReply).(readWriteModel.BVLCOriginalUnicastNPDU)
+ bvlc := wrapAPDU(newWhoIsAPDU(t), tc.expectingReply,
nil).(readWriteModel.BVLCOriginalUnicastNPDU)
control := bvlc.GetNpdu().GetControl()
assert.Equal(t, tc.expectingReply,
control.GetExpectingReply(),
"NPDU control.expectingReply must reflect the
wrapAPDU argument")
@@ -73,7 +74,7 @@ func TestWrapAPDU_LocalScope_NoRouting(t *testing.T) {
// We only support local (same-network) addressing in Phase 6's tag
layer.
// The NPDU control fields for routing must all be off so
bacpypes3/Niagara
// don't interpret the message as routed.
- bvlc := wrapAPDU(newWhoIsAPDU(t),
true).(readWriteModel.BVLCOriginalUnicastNPDU)
+ bvlc := wrapAPDU(newWhoIsAPDU(t), true,
nil).(readWriteModel.BVLCOriginalUnicastNPDU)
control := bvlc.GetNpdu().GetControl()
assert.False(t, control.GetMessageTypeFieldPresent())
assert.False(t, control.GetDestinationSpecified())
@@ -93,7 +94,7 @@ func TestWrapAPDU_LocalScope_NoRouting(t *testing.T) {
func TestWrapAPDU_PreservesAPDU(t *testing.T) {
apdu := newWhoIsAPDU(t)
- bvlc := wrapAPDU(apdu, false).(readWriteModel.BVLCOriginalUnicastNPDU)
+ bvlc := wrapAPDU(apdu, false,
nil).(readWriteModel.BVLCOriginalUnicastNPDU)
// Same APDU identity should be reachable through the wrapper —
// MessageCodec.Receive parses BVLC → NPDU → APDU and Reader/Writer
// match expectations by walking that chain.
@@ -104,7 +105,7 @@ func TestWrapAPDU_SerializesToValidBVLC(t *testing.T) {
// End-to-end sanity: the wrapped message round-trips through the
// model serializer. Catches accidental nil-required-field changes
// in wrapAPDU that would only show up at runtime under Send().
- bvlc := wrapAPDU(newWhoIsAPDU(t), false)
+ bvlc := wrapAPDU(newWhoIsAPDU(t), false, nil)
raw, err := bvlc.Serialize()
require.NoError(t, err, "wrapAPDU output must serialize")
// First byte is BVLC type 0x81; second is function 0x0a
(OriginalUnicastNPDU).
@@ -112,3 +113,112 @@ func TestWrapAPDU_SerializesToValidBVLC(t *testing.T) {
assert.Equal(t, byte(0x81), raw[0], "BVLC magic byte")
assert.Equal(t, byte(0x0a), raw[1], "BVLC function =
OriginalUnicastNPDU")
}
+
+// TestWrapAPDU_RoutedDestination pins the routed framing (ASHRAE 135 clause
+// 6): a connection whose target sits behind a BACnet router emits a
+// destination specifier (DNET/DLEN/DADR) with a fresh hop count, and the
+// frame survives a serialize/parse round trip.
+func TestWrapAPDU_RoutedDestination(t *testing.T) {
+ dest := &routedDestination{dnet: 3001, dadr: []uint8{192, 168, 102, 20,
0xBA, 0xC0}}
+ bvlc := wrapAPDU(newWhoIsAPDU(t), false,
dest).(readWriteModel.BVLCOriginalUnicastNPDU)
+ npdu := bvlc.GetNpdu()
+
+ assert.True(t, npdu.GetControl().GetDestinationSpecified(),
"control.destinationSpecified")
+ assert.False(t, npdu.GetControl().GetSourceSpecified(), "source must
stay absent on originated frames")
+ require.NotNil(t, npdu.GetDestinationNetworkAddress())
+ assert.Equal(t, uint16(3001), *npdu.GetDestinationNetworkAddress())
+ require.NotNil(t, npdu.GetDestinationLength())
+ assert.Equal(t, uint8(6), *npdu.GetDestinationLength())
+ assert.Equal(t, []uint8{192, 168, 102, 20, 0xBA, 0xC0},
npdu.GetDestinationAddress())
+ require.NotNil(t, npdu.GetHopCount())
+ assert.Equal(t, routedDestinationHopCount, *npdu.GetHopCount())
+
+ // Round trip: the routed header must reparse byte-identically.
+ data, err := bvlc.Serialize()
+ require.NoError(t, err)
+ reparsed, err :=
readWriteModel.BVLCParse[readWriteModel.BVLC](context.Background(), data)
+ require.NoError(t, err)
+ renpdu := reparsed.(readWriteModel.BVLCOriginalUnicastNPDU).GetNpdu()
+ require.NotNil(t, renpdu.GetDestinationNetworkAddress())
+ assert.Equal(t, uint16(3001), *renpdu.GetDestinationNetworkAddress())
+ assert.Equal(t, []uint8{192, 168, 102, 20, 0xBA, 0xC0},
renpdu.GetDestinationAddress())
+}
+
+// TestParseRemoteAddress covers the two DADR syntaxes and rejects garbage.
+func TestParseRemoteAddress(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ want []uint8
+ wantErr bool
+ }{
+ {"bacnet-ip", "192.168.102.20:47808", []uint8{192, 168, 102,
20, 0xBA, 0xC0}, false},
+ {"hex mac", "0x0C", []uint8{0x0C}, false},
+ {"hex multi-octet", "0x00fF", []uint8{0x00, 0xFF}, false},
+ {"ipv6 rejected", "[::1]:47808", nil, true},
+ {"empty hex", "0x", nil, true},
+ {"garbage", "not-an-address", nil, true},
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ got, err := parseRemoteAddress(tc.input)
+ if tc.wantErr {
+ assert.Error(t, err)
+ return
+ }
+ require.NoError(t, err)
+ assert.Equal(t, tc.want, got)
+ })
+ }
+}
+
+// TestRoutedDestinationFromConfiguration pins the option pairing rules.
+func TestRoutedDestinationFromConfiguration(t *testing.T) {
+ dest, err := routedDestinationFromConfiguration(Configuration{})
+ require.NoError(t, err)
+ assert.Nil(t, dest, "no options -> local addressing")
+
+ dest, err =
routedDestinationFromConfiguration(Configuration{RemoteNetwork: 3001,
RemoteAddress: "192.168.102.20:47808"})
+ require.NoError(t, err)
+ require.NotNil(t, dest)
+ assert.Equal(t, uint16(3001), dest.dnet)
+ assert.Len(t, dest.dadr, 6)
+
+ _, err =
routedDestinationFromConfiguration(Configuration{RemoteNetwork: 3001})
+ assert.Error(t, err, "network without address must fail")
+ _, err =
routedDestinationFromConfiguration(Configuration{RemoteAddress:
"192.168.102.20:47808"})
+ assert.Error(t, err, "address without network must fail")
+}
+
+// TestRoutedOriginOptions pins the discovery-side mirror of the routed
+// options: a routed I-Am's SNET/SADR comes back as the same
+// RemoteNetwork/RemoteAddress keys the connection URL accepts.
+func TestRoutedOriginOptions(t *testing.T) {
+ mkNPDU := func(src bool, snet uint16, sadr []uint8) readWriteModel.NPDU
{
+ control := readWriteModel.NewNPDUControl(false, false, src,
false, readWriteModel.NPDUNetworkPriority_NORMAL_MESSAGE)
+ var snetP *uint16
+ var slenP *uint8
+ if src {
+ snetP = &snet
+ slen := uint8(len(sadr))
+ slenP = &slen
+ }
+ return readWriteModel.NewNPDU(1, control, nil, nil, nil, snetP,
slenP, sadr, nil, nil, newWhoIsAPDU(t))
+ }
+
+ assert.Nil(t, routedOriginOptions(mkNPDU(false, 0, nil)), "local frame
-> no options")
+
+ opts := routedOriginOptions(mkNPDU(true, 3001, []uint8{192, 168, 102,
20, 0xBA, 0xC0}))
+ require.NotNil(t, opts)
+ assert.Equal(t, []string{"3001"}, opts["RemoteNetwork"])
+ assert.Equal(t, []string{"192.168.102.20:47808"}, opts["RemoteAddress"])
+
+ opts = routedOriginOptions(mkNPDU(true, 5, []uint8{0x0C}))
+ require.NotNil(t, opts)
+ assert.Equal(t, []string{"0x0c"}, opts["RemoteAddress"], "non-B/IP MAC
renders as hex")
+
+ // Round trip: the discovery options must parse back into the same DADR.
+ dadr, err := parseRemoteAddress(opts["RemoteAddress"][0])
+ require.NoError(t, err)
+ assert.Equal(t, []uint8{0x0C}, dadr)
+}
diff --git a/plc4go/internal/bacnetip/IdleWriteRoundtrip_test.go
b/plc4go/internal/bacnetip/IdleWriteRoundtrip_test.go
index 07f8d8007a..b9283ff78f 100644
--- a/plc4go/internal/bacnetip/IdleWriteRoundtrip_test.go
+++ b/plc4go/internal/bacnetip/IdleWriteRoundtrip_test.go
@@ -94,7 +94,7 @@ func (d *delayedWriteDevice) serve() {
if choice ==
model.BACnetConfirmedServiceChoice_READ_PROPERTY {
resp = buildReadAckFor(invokeId)
} else {
- resp =
wrapAPDU(model.NewAPDUSimpleAck(invokeId, choice), false)
+ resp =
wrapAPDU(model.NewAPDUSimpleAck(invokeId, choice), false, nil)
}
theBytes, err := resp.Serialize()
if err != nil {
diff --git a/plc4go/internal/bacnetip/MessageCodec_test.go
b/plc4go/internal/bacnetip/MessageCodec_test.go
index e55bb67d34..35a1857fef 100644
--- a/plc4go/internal/bacnetip/MessageCodec_test.go
+++ b/plc4go/internal/bacnetip/MessageCodec_test.go
@@ -144,7 +144,7 @@ func makeWhoIsBVLC(t *testing.T) readWriteModel.BVLC {
t.Helper()
whoIs := readWriteModel.NewBACnetUnconfirmedServiceRequestWhoIs(nil,
nil)
apdu := readWriteModel.NewAPDUUnconfirmedRequest(whoIs)
- return wrapAPDU(apdu, false)
+ return wrapAPDU(apdu, false, nil)
}
func TestMessageCodec_Send_SerializesBVLCToTransport(t *testing.T) {
diff --git a/plc4go/internal/bacnetip/ReadRoundtrip_test.go
b/plc4go/internal/bacnetip/ReadRoundtrip_test.go
index bf2332cbc7..f7612bd2f0 100644
--- a/plc4go/internal/bacnetip/ReadRoundtrip_test.go
+++ b/plc4go/internal/bacnetip/ReadRoundtrip_test.go
@@ -135,7 +135,7 @@ func (d *fakeBacnetDevice) buildReadPropertyAck(invokeId
uint8) model.BVLC {
constructedDataFromTag(model.CreateBACnetApplicationTagReal(23.5)),
)
apdu := model.NewAPDUComplexAck(false, false, invokeId, nil, nil,
serviceAck, nil, nil)
- return wrapAPDU(apdu, false)
+ return wrapAPDU(apdu, false, nil)
}
func (d *fakeBacnetDevice) stop() {
diff --git a/plc4go/internal/bacnetip/Reader.go
b/plc4go/internal/bacnetip/Reader.go
index 5487ca8e46..654160301d 100644
--- a/plc4go/internal/bacnetip/Reader.go
+++ b/plc4go/internal/bacnetip/Reader.go
@@ -42,6 +42,7 @@ type Reader struct {
invokeIdGenerator *InvokeIdGenerator
messageCodec spi.MessageCodec
tm transactions.RequestTransactionManager
+ routedDest *routedDestination // nil for local-segment
connections
maxSegmentsAccepted readWriteModel.MaxSegmentsAccepted
maxApduLengthAccepted readWriteModel.MaxApduLengthAccepted
@@ -51,12 +52,13 @@ type Reader struct {
log zerolog.Logger
}
-func NewReader(invokeIdGenerator *InvokeIdGenerator, messageCodec
spi.MessageCodec, tm transactions.RequestTransactionManager, _options
...options.WithOption) *Reader {
+func NewReader(invokeIdGenerator *InvokeIdGenerator, messageCodec
spi.MessageCodec, tm transactions.RequestTransactionManager, routedDest
*routedDestination, _options ...options.WithOption) *Reader {
customLogger :=
options.ExtractCustomLoggerOrDefaultToGlobal(_options...)
return &Reader{
invokeIdGenerator: invokeIdGenerator,
messageCodec: messageCodec,
tm: tm,
+ routedDest: routedDest,
maxSegmentsAccepted:
readWriteModel.MaxSegmentsAccepted_MORE_THAN_64_SEGMENTS,
maxApduLengthAccepted:
readWriteModel.MaxApduLengthAccepted_NUM_OCTETS_1476,
@@ -144,7 +146,7 @@ func (m *Reader) Read(ctx context.Context, readRequest
apiModel.PlcReadRequest)
context.AfterFunc(transactionContext, cancel)
// Send the over the wire
m.log.Trace().Msg("Send ")
- if err := m.messageCodec.SendRequest(ctx, "read",
wrapAPDU(apdu, true), func(message spi.Message) bool {
+ if err := m.messageCodec.SendRequest(ctx, "read",
wrapAPDU(apdu, true, m.routedDest), func(message spi.Message) bool {
bvlc, ok := message.(readWriteModel.BVLC)
if !ok {
m.log.Debug().Type("bvlc",
bvlc).Msg("Received strange type")
diff --git a/plc4go/internal/bacnetip/ReaderResultDelivery_test.go
b/plc4go/internal/bacnetip/ReaderResultDelivery_test.go
index b576771da2..a6a74918dc 100644
--- a/plc4go/internal/bacnetip/ReaderResultDelivery_test.go
+++ b/plc4go/internal/bacnetip/ReaderResultDelivery_test.go
@@ -76,7 +76,7 @@ func (c *captureCodec) GetDefaultIncomingMessageChannel()
chan spi.Message { ret
func TestReader_lateErrorHandlerAfterFailedSendMustNotBlock(t *testing.T) {
codec := newCaptureCodec(errors.New("send failed: broken pipe"))
tm := transactions.NewRequestTransactionManager(1)
- reader := NewReader(&InvokeIdGenerator{}, codec, tm)
+ reader := NewReader(&InvokeIdGenerator{}, codec, tm, nil)
objType := readWriteModel.BACnetObjectType_ANALOG_INPUT
propId := readWriteModel.BACnetPropertyIdentifier_PRESENT_VALUE
diff --git a/plc4go/internal/bacnetip/ReaderSegmentation.go
b/plc4go/internal/bacnetip/ReaderSegmentation.go
index e77ea331a1..c3a2ee375c 100644
--- a/plc4go/internal/bacnetip/ReaderSegmentation.go
+++ b/plc4go/internal/bacnetip/ReaderSegmentation.go
@@ -149,7 +149,7 @@ func (m *Reader) sendSegmentAck(ctx context.Context, ack
readWriteModel.APDUSegm
if ack == nil {
return nil
}
- return m.messageCodec.Send(ctx, "segmentAck", wrapAPDU(ack, false))
+ return m.messageCodec.Send(ctx, "segmentAck", wrapAPDU(ack, false,
m.routedDest))
}
// apduFromMessage safely extracts the APDU from a received BVLC message,
diff --git a/plc4go/internal/bacnetip/Subscriber.go
b/plc4go/internal/bacnetip/Subscriber.go
index ee7ff04c47..9bc7715a9e 100644
--- a/plc4go/internal/bacnetip/Subscriber.go
+++ b/plc4go/internal/bacnetip/Subscriber.go
@@ -192,7 +192,7 @@ func (m *Subscriber) sendSubscribeCOV(ctx context.Context,
handle *SubscriptionH
// SimpleAck back through the codec. Phase 5 will replace this with a
real
// transaction-manager-backed retry loop honoring ApduTimeoutMs/Retries.
done := make(chan apiModel.PlcResponseCode, 1)
- err := m.connection.messageCodec.SendRequest(ctx, "subscribe-cov",
wrapAPDU(apdu, true),
+ err := m.connection.messageCodec.SendRequest(ctx, "subscribe-cov",
wrapAPDU(apdu, true, m.connection.routedDest),
func(message spi.Message) bool {
return m.acceptsResponse(message, invokeId)
},
diff --git a/plc4go/internal/bacnetip/WriteRoundtrip_test.go
b/plc4go/internal/bacnetip/WriteRoundtrip_test.go
index 07c4828a3d..1448062fef 100644
--- a/plc4go/internal/bacnetip/WriteRoundtrip_test.go
+++ b/plc4go/internal/bacnetip/WriteRoundtrip_test.go
@@ -146,7 +146,7 @@ func (d *fakeWriteDevice) extractInvokeIdAndChoice(data
[]byte) (uint8, model.BA
func (d *fakeWriteDevice) buildSimpleAck(invokeId uint8, choice
model.BACnetConfirmedServiceChoice) model.BVLC {
apdu := model.NewAPDUSimpleAck(invokeId, choice)
- return wrapAPDU(apdu, false)
+ return wrapAPDU(apdu, false, nil)
}
// buildReadAckFor mirrors fakeBacnetDevice.buildReadPropertyAck: a
ReadProperty
@@ -160,13 +160,13 @@ func buildReadAckFor(invokeId uint8) model.BVLC {
constructedDataFromTag(model.CreateBACnetApplicationTagReal(23.5)),
)
apdu := model.NewAPDUComplexAck(false, false, invokeId, nil, nil,
serviceAck, nil, nil)
- return wrapAPDU(apdu, false)
+ return wrapAPDU(apdu, false, nil)
}
func (d *fakeWriteDevice) buildErrorReply(invokeId uint8, choice
model.BACnetConfirmedServiceChoice) model.BVLC {
base := buildErrorAPDU(model.ErrorClass_PROPERTY,
model.ErrorCode_WRITE_ACCESS_DENIED)
apdu := model.NewAPDUError(invokeId, choice, base.GetError())
- return wrapAPDU(apdu, false)
+ return wrapAPDU(apdu, false, nil)
}
func (d *fakeWriteDevice) stop() {
diff --git a/plc4go/internal/bacnetip/Writer.go
b/plc4go/internal/bacnetip/Writer.go
index 703304d485..a054e66aa7 100644
--- a/plc4go/internal/bacnetip/Writer.go
+++ b/plc4go/internal/bacnetip/Writer.go
@@ -44,6 +44,7 @@ type Writer struct {
messageCodec spi.MessageCodec
tm transactions.RequestTransactionManager
driverContext DriverContext
+ routedDest *routedDestination // nil for local-segment
connections
wg sync.WaitGroup
@@ -55,6 +56,7 @@ func NewWriter(
messageCodec spi.MessageCodec,
tm transactions.RequestTransactionManager,
driverContext DriverContext,
+ routedDest *routedDestination,
_options ...options.WithOption,
) *Writer {
customLogger :=
options.ExtractCustomLoggerOrDefaultToGlobal(_options...)
@@ -63,6 +65,7 @@ func NewWriter(
messageCodec: messageCodec,
tm: tm,
driverContext: driverContext,
+ routedDest: routedDest,
log: customLogger,
}
}
@@ -108,7 +111,7 @@ func (m *Writer) Write(ctx context.Context, writeRequest
apiModel.PlcWriteReques
ctx, cancel := context.WithCancel(ctx)
context.AfterFunc(transactionContext, cancel)
- err := m.messageCodec.SendRequest(ctx, "write",
wrapAPDU(apdu, true), func(message spi.Message) bool {
+ err := m.messageCodec.SendRequest(ctx, "write",
wrapAPDU(apdu, true, m.routedDest), func(message spi.Message) bool {
return m.acceptsResponse(message, invokeId)
}, func(message spi.Message) error {
bvlc := message.(readWriteModel.BVLC)