diff --git a/plc4go/internal/bacnetip/CommunicationsModule.go b/plc4go/internal/bacnetip/CommunicationsModule.go index 2b13a79352d..b3e0c6ba0e9 100644 --- a/plc4go/internal/bacnetip/CommunicationsModule.go +++ b/plc4go/internal/bacnetip/CommunicationsModule.go @@ -25,6 +25,7 @@ import ( "strconv" "strings" + "github.com/apache/plc4x/plc4go/internal/bacnetip/globals" "github.com/apache/plc4x/plc4go/spi" "github.com/apache/plc4x/plc4go/spi/utils" @@ -151,12 +152,18 @@ func (p *__PCI) GetLengthInBits(ctx context.Context) uint16 { func (p *__PCI) String() string { pduUserDataString := "" - if p.pduUserData != nil { + if p.pduUserData != nil && globals.ExtendedPDUOutput { pduUserDataString = p.pduUserData.String() if strings.Contains(pduUserDataString, "\n") { pduUserDataString = "\n" + pduUserDataString + "\n" } pduUserDataString = "pduUserData: " + pduUserDataString + " ," + } else if p.pduUserData != nil { + if bytes, err := p.pduUserData.Serialize(); err != nil { + pduUserDataString = "pduUserData: " + err.Error() + " ," + } else { + pduUserDataString = "pduUserData: " + Btox(bytes, ".") + " ," + } } return fmt.Sprintf("__PCI{%spduSource: %s, pduDestination: %s}", pduUserDataString, p.pduSource, p.pduDestination) } diff --git a/plc4go/internal/bacnetip/PDU.go b/plc4go/internal/bacnetip/PDU.go index cb3551d3b7c..4b1e4eb0c00 100644 --- a/plc4go/internal/bacnetip/PDU.go +++ b/plc4go/internal/bacnetip/PDU.go @@ -799,6 +799,22 @@ func uint32ToIpv4(number uint32) net.IP { return ipv4 } +// PackIpAddr Given an IP address tuple like ('1.2.3.4', 47808) return the six-octet string +// useful for a BACnet address. +func PackIpAddr(addrTuple *AddressTuple[string, uint16]) (octetString []byte) { + addr, port := addrTuple.Left, addrTuple.Right + octetString = append(net.ParseIP(addr).To4(), uint16ToPort(port)...) + return +} + +// UnpackIpAddr Given a six-octet BACnet address, return an IP address tuple. +func UnpackIpAddr(addr []byte) (addrTuple *AddressTuple[string, uint16]) { + ip := ipv4ToUint32(addr[:4]) + port := portToUint16(addr[4:]) + + return &AddressTuple[string, uint16]{uint32ToIpv4(ip).String(), port} +} + func NewLocalStation(localLog zerolog.Logger, addr any, route *Address) (*Address, error) { l := &Address{ log: localLog, diff --git a/plc4go/internal/bacnetip/bvll.go b/plc4go/internal/bacnetip/bvll.go index b664af20677..bac477bcf60 100644 --- a/plc4go/internal/bacnetip/bvll.go +++ b/plc4go/internal/bacnetip/bvll.go @@ -325,6 +325,7 @@ func (w *WriteBroadcastDistributionTable) produceBvlciBDT(entries []readWriteMod } return } + func (w *WriteBroadcastDistributionTable) Encode(bvlpdu Arg) error { switch bvlpdu := bvlpdu.(type) { case BVLPDU: @@ -419,6 +420,7 @@ func (w *ReadBroadcastDistributionTable) String() string { type ReadBroadcastDistributionTableAck struct { *_BVLPDU + bvlciBDT []*Address } @@ -514,27 +516,108 @@ func (w *ReadBroadcastDistributionTableAck) String() string { return fmt.Sprintf("ReadBroadcastDistributionTableAck{%v, bvlciBDT: %v}", w._BVLPDU, w.bvlciBDT) } -// TODO: finish type ForwardedNPDU struct { *_BVLPDU + + bvlciAddress *Address } var _ BVLPDU = (*ForwardedNPDU)(nil) -func NewForwardedNPDU() (BVLPDU, error) { +func NewForwardedNPDU(pdu PDU, opts ...func(*ForwardedNPDU)) (*ForwardedNPDU, error) { b := &ForwardedNPDU{} - b._BVLPDU = NewBVLPDU(nil).(*_BVLPDU) + for _, opt := range opts { + opt(b) + } + switch npdu := pdu.(type) { + case readWriteModel.NPDUExactly: + b._BVLPDU = NewBVLPDU(readWriteModel.NewBVLCForwardedNPDU(b.produceInnerNPDU(npdu))).(*_BVLPDU) + case nil: + b._BVLPDU = NewBVLPDU(nil).(*_BVLPDU) + default: + // TODO: re-encode seems expensive... check if there is a better option (e.g. only do it on the message bridge) + data := pdu.GetPduData() + parse, err := readWriteModel.NPDUParse(context.Background(), data, uint16(len(data))) + if err != nil { + return nil, errors.Wrap(err, "error re-encoding") + } + b._BVLPDU = NewBVLPDU(readWriteModel.NewBVLCForwardedNPDU(b.produceInnerNPDU(parse))).(*_BVLPDU) + } return b, nil } -func (b *ForwardedNPDU) Encode(pdu Arg) error { - // TODO: finish - return nil +func WithForwardedNPDUAddress(addr *Address) func(*ForwardedNPDU) { + return func(b *ForwardedNPDU) { + b.bvlciAddress = addr + } } -func (b *ForwardedNPDU) Decode(pdu Arg) error { - // TODO: finish - return nil +func (w *ForwardedNPDU) GetBvlciAddress() *Address { + return w.bvlciAddress +} + +func (w *ForwardedNPDU) produceInnerNPDU(inNpdu readWriteModel.NPDU) (ip []uint8, port uint16, npdu readWriteModel.NPDU, bvlcPayloadLength uint16) { + ip = w.bvlciAddress.AddrAddress[:4] + port = uint16(47808) + if w.bvlciAddress.AddrPort != nil { + port = *w.bvlciAddress.AddrPort + } + npdu = inNpdu + return +} + +func (w *ForwardedNPDU) Encode(bvlpdu Arg) error { + switch bvlpdu := bvlpdu.(type) { + case BVLPDU: + if err := bvlpdu.Update(w); err != nil { + return errors.Wrap(err, "error updating BVLPDU") + } + + // encode the addrress + bvlpdu.PutData(w.bvlciAddress.AddrAddress...) + + // encode the rest of the data + bvlpdu.PutData(w.GetPduData()...) + + bvlpdu.setBVLC(w.bvlc) + return nil + default: + return errors.Errorf("invalid BVLPDU type %T", bvlpdu) + } +} + +func (w *ForwardedNPDU) Decode(bvlpdu Arg) error { + switch bvlpdu := bvlpdu.(type) { + case BVLPDU: + if err := w.Update(bvlpdu); err != nil { + return errors.Wrap(err, "error updating BVLPDU") + } + switch pduUserData := bvlpdu.GetPDUUserData().(type) { + case readWriteModel.BVLCForwardedNPDUExactly: + switch bvlc := pduUserData.(type) { + case readWriteModel.BVLCForwardedNPDU: + addr := bvlc.GetIp() + port := bvlc.GetPort() + var portArray = make([]byte, 2) + binary.BigEndian.PutUint16(portArray, port) + var err error + address, err := NewAddress(zerolog.Nop(), append(addr, portArray...)) + if err != nil { + return errors.Wrap(err, "error creating address") + } + w.bvlciAddress = address + + w.setBVLC(bvlc) + } + } + return nil + default: + return errors.Errorf("invalid BVLPDU type %T", bvlpdu) + } +} + +func (w *ForwardedNPDU) String() string { + return fmt.Sprintf("ForwardedNPDU{%v, bvlciAddress: %v}", w._BVLPDU, w.bvlciAddress) } // TODO: finish @@ -844,7 +927,7 @@ func init() { return v }, 0x04: func() interface{ Decode(Arg) error } { - v, _ := NewForwardedNPDU() + v, _ := NewForwardedNPDU(nil) return v }, 0x05: func() interface{ Decode(Arg) error } { diff --git a/plc4go/internal/bacnetip/comp.go b/plc4go/internal/bacnetip/comp.go index fd37ac093fe..a35d64cc027 100644 --- a/plc4go/internal/bacnetip/comp.go +++ b/plc4go/internal/bacnetip/comp.go @@ -64,6 +64,10 @@ func (a Args) Get0MultiplexServer() *_MultiplexServer { func (a Args) String() string { r := "" for i, ea := range a { + switch tea := ea.(type) { + case []byte: + ea = Btox(tea, ".") + } r += fmt.Sprintf("%d: %v, ", i, ea) } if r != "" { @@ -93,12 +97,16 @@ func NewKWArgs(kw ...any) KWArgs { func (k KWArgs) String() string { r := "" for kk, ea := range k { + switch tea := ea.(type) { + case []byte: + ea = Btox(tea, ".") + } r += fmt.Sprintf("%s=%v, ", kk, ea) } if r != "" { r = r[:len(r)-2] } - return r + return "{" + r + "}" } type KnownKey string diff --git a/plc4go/internal/bacnetip/tests/state_machine.go b/plc4go/internal/bacnetip/tests/state_machine.go index 8fe2c04cda2..77b5b93d696 100644 --- a/plc4go/internal/bacnetip/tests/state_machine.go +++ b/plc4go/internal/bacnetip/tests/state_machine.go @@ -299,7 +299,11 @@ func MatchPdu(localLog zerolog.Logger, pdu bacnetip.PDU, pduType any, pduAttrs m return a.Equals(b) }) case bacnetip.KWBvlciAddress: - panic("implement me") + nni, ok := pdu.(*bacnetip.ForwardedNPDU) + if !ok { + return false + } + return nni.GetBvlciAddress().Equals(attrValue) case bacnetip.KWFdAddress: panic("implement me") case bacnetip.KWFdTTL: diff --git a/plc4go/internal/bacnetip/tests/test_bvll/test_codec_test.go b/plc4go/internal/bacnetip/tests/test_bvll/test_codec_test.go index a937a00ede0..e82f94e584c 100644 --- a/plc4go/internal/bacnetip/tests/test_bvll/test_codec_test.go +++ b/plc4go/internal/bacnetip/tests/test_bvll/test_codec_test.go @@ -63,6 +63,14 @@ func ReadBroadcastDistributionTableAck(bdt ...*bacnetip.Address) *bacnetip.ReadB return readBroadcastDistributionTable } +func ForwardNPDU(addr *bacnetip.Address, pduBytes []byte) *bacnetip.ForwardedNPDU { + npdu, err := bacnetip.NewForwardedNPDU(bacnetip.NewPDU(&bacnetip.MessageBridge{Bytes: pduBytes}), bacnetip.WithForwardedNPDUAddress(addr)) + if err != nil { + panic(err) + } + return npdu +} + type TestAnnexJCodecSuite struct { suite.Suite @@ -279,6 +287,42 @@ func (suite *TestAnnexJCodecSuite) TestReadBroadcastDistributionTableAck() { err = suite.Confirmation(bacnetip.NewArgs((*bacnetip.ReadBroadcastDistributionTableAck)(nil)), bacnetip.NewKWArgs(bacnetip.KWBvlciBDT, []*bacnetip.Address{addr})) } +func (suite *TestAnnexJCodecSuite) TestForwardNPDU() { + // Read an empty TableAck + addr, err := bacnetip.NewAddress(zerolog.Nop(), "192.168.0.1") + xpdu, err := bacnetip.Xtob( + // "deadbeef", // forwarded PDU // TODO: this is not a ndpu so we just exploded with that. We use the iartn for that for now + // TODO: this below is from us as upstream message is not parsable + "01.80" + // version, network layer message + "01 0001 0002 0003", // message type and network list + ) + suite.Require().NoError(err) + pduBytes, err := bacnetip.Xtob("81.04.0013" + // bvlci // TODO: length was 0e before + "c0.a8.00.01.ba.c0" + // original source address + // "deadbeef", // forwarded PDU // TODO: this is not a ndpu so we just exploded with that. We use the iartn for that for now + // TODO: this below is from us as upstream message is not parsable + "01.80" + // version, network layer message + "01 0001 0002 0003", // message type and network list + ) + suite.Require().NoError(err) + { // Parse with plc4x parser to validate + parse, err := readWriteModel.BVLCParse(testutils.TestContext(suite.T()), pduBytes) + suite.Assert().NoError(err) + if parse != nil { + suite.T().Log("\n" + parse.String()) + } + } + + err = suite.Request(bacnetip.NewArgs(ForwardNPDU(addr, xpdu)), bacnetip.NoKWArgs) + suite.Assert().NoError(err) + err = suite.Indication(bacnetip.NoArgs, bacnetip.NewKWArgs(bacnetip.KWPDUData, pduBytes)) + suite.Assert().NoError(err) + + err = suite.Response(bacnetip.NewArgs(bacnetip.NewPDU(&bacnetip.MessageBridge{Bytes: pduBytes})), bacnetip.NoKWArgs) + suite.Assert().NoError(err) + err = suite.Confirmation(bacnetip.NewArgs((*bacnetip.ForwardedNPDU)(nil)), bacnetip.NewKWArgs(bacnetip.KWBvlciAddress, addr, bacnetip.KWPDUData, xpdu)) +} + func TestAnnexJCodec(t *testing.T) { suite.Run(t, new(TestAnnexJCodecSuite)) }