You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@plc4x.apache.org by sr...@apache.org on 2024/08/15 15:53:08 UTC
(plc4x) branch develop updated: test(plc4go/bacnet): add tests (initial sync)
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 5aac795b2c test(plc4go/bacnet): add tests (initial sync)
5aac795b2c is described below
commit 5aac795b2c87d769910400380154f576af9d0192
Author: Sebastian Rühl <sr...@apache.org>
AuthorDate: Thu Aug 15 17:53:02 2024 +0200
test(plc4go/bacnet): add tests (initial sync)
---
plc4go/internal/bacnetip/tests/state_machine.go | 1616 ++++++++++++++++++++
.../test_max_apdu_length_accepted_test.go | 47 +
.../tests/test_base_types/test_name_value_test.go | 22 +
.../internal/bacnetip/tests/test_bvll/helpers.go | 293 ++++
.../bacnetip/tests/test_bvll/test_simple_test.go | 215 +++
.../bacnetip/tests/test_pdu/test_address_test.go | 649 ++++++++
.../bacnetip/tests/test_pdu/test_pci_test.go | 22 +
.../bacnetip/tests/test_pdu/test_pdu_test.go | 22 +
.../bacnetip/tests/test_primitive_data/todo | 0
.../tests/test_segmentation/test_1_test.go | 558 +++++++
.../bacnetip/tests/test_service/helpers.go | 492 ++++++
.../tests/test_service/test_cov_av_test.go | 22 +
.../tests/test_service/test_cov_bv_test.go | 22 +
.../tests/test_service/test_cov_pc_test.go | 22 +
.../bacnetip/tests/test_service/test_cov_test.go | 66 +
.../tests/test_service/test_device_2_test.go | 22 +
.../tests/test_service/test_device_test.go | 22 +
.../bacnetip/tests/test_service/test_file_test.go | 22 +
.../tests/test_service/test_object_test.go | 22 +
.../test_client_state_machine_test.go | 63 +
.../test_server_state_machine_test.go | 63 +
.../test_service_access_point_test.go | 247 +++
.../test_utilities/test_state_machine_test.go | 666 ++++++++
.../tests/test_utilities/test_time_machine_test.go | 306 ++++
.../tests/test_vlan/test_ipnetwork_test.go | 475 ++++++
.../bacnetip/tests/test_vlan/test_network_test.go | 348 +++++
plc4go/internal/bacnetip/tests/time_machine.go | 236 +++
plc4go/internal/bacnetip/tests/trapped_classes.go | 569 +++++++
plc4go/internal/bacnetip/tests/util.go | 67 +
29 files changed, 7196 insertions(+)
diff --git a/plc4go/internal/bacnetip/tests/state_machine.go b/plc4go/internal/bacnetip/tests/state_machine.go
new file mode 100644
index 0000000000..bbfc0db13a
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/state_machine.go
@@ -0,0 +1,1616 @@
+/*
+ * 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 tests
+
+import (
+ "bytes"
+ "fmt"
+ "reflect"
+ "slices"
+ "time"
+
+ "github.com/apache/plc4x/plc4go/internal/bacnetip"
+
+ "github.com/pkg/errors"
+ "github.com/rs/zerolog"
+)
+
+// Transition Instances of this class are transitions betweeen getStates of a state machine.
+type Transition struct {
+ nextState State
+}
+
+func (t Transition) String() string {
+ return fmt.Sprintf("Transition{nextState: %s}", t.nextState)
+}
+
+type SendTransition struct {
+ Transition
+ pdu bacnetip.PDU
+}
+
+func (t SendTransition) String() string {
+ return fmt.Sprintf("SendTransition{Transition: %s, pdu: %s}", t.Transition, t.pdu)
+}
+
+type criteria struct {
+ pduType any
+ pduAttrs map[string]any
+}
+
+func (c criteria) String() string {
+ return fmt.Sprintf("criteria{%s, %v}", c.pduType, c.pduAttrs)
+}
+
+type ReceiveTransition struct {
+ Transition
+ criteria criteria
+}
+
+func (t ReceiveTransition) String() string {
+ return fmt.Sprintf("ReceiveTransition{Transition: %s, criteria: %s}", t.Transition, t.criteria)
+}
+
+type EventTransition struct {
+ Transition
+ eventId string
+}
+
+func (t EventTransition) String() string {
+ return fmt.Sprintf("EventTransition{Transition: %s, eventId: %s}", t.Transition, t.eventId)
+}
+
+type TimeoutTransition struct {
+ Transition
+ timeout time.Time
+}
+
+func (t TimeoutTransition) String() string {
+ return fmt.Sprintf("TimeoutTransition{Transition: %s, timeout: %s}", t.Transition, t.timeout)
+}
+
+type fnargs struct {
+ fn func(args bacnetip.Args, kwargs bacnetip.KWArgs) error
+ args bacnetip.Args
+ kwargs bacnetip.KWArgs
+}
+
+func (f fnargs) String() string {
+ return fmt.Sprintf("fnargs{fn: %t, args: %s, kwargs: %s}", f.fn == nil, f.args, f.kwargs)
+}
+
+type CallTransition struct {
+ Transition
+ fnargs fnargs
+}
+
+func (t CallTransition) String() string {
+ return fmt.Sprintf("CallTransition{Transition: %s, fnargs: %s}", t.Transition, t.fnargs)
+}
+
+func MatchPdu(localLog zerolog.Logger, pdu bacnetip.PDU, pduType any, pduAttrs map[string]any) bool {
+ // check the type
+ if pduType != nil && fmt.Sprintf("%T", pdu) != fmt.Sprintf("%T", pduType) {
+ localLog.Debug().Msg("failed match, wrong type")
+ return false
+ }
+ for attrName, attrValue := range pduAttrs {
+ switch attrName {
+ case "pduSource":
+ if !pdu.GetPDUSource().Equals(attrValue) {
+ localLog.Debug().Msg("source doesn't match")
+ return false
+ }
+ case "pduDestination":
+ if !pdu.GetPDUDestination().Equals(attrValue) {
+ localLog.Debug().Msg("destination doesn't match")
+ return false
+ }
+ case "x": // only used in test cases
+ return bytes.Equal(pdu.(interface{ X() []byte }).X(), attrValue.([]byte))
+ case "y": // only used in test cases
+ return false
+ case "a": // only used in test cases
+ a := pdu.(interface{ A() int }).A()
+ if a == 0 {
+ return false
+ }
+ return a == attrValue.(int)
+ case "b": // only used in test cases
+ b := pdu.(interface{ B() int }).B()
+ if b == 0 {
+ return false
+ }
+ return b == attrValue.(int)
+ case "pduData":
+ return reflect.DeepEqual(pdu.GetMessage(), attrValue)
+ default:
+ panic("implement " + attrName)
+ }
+ }
+ // TODO: implement
+ /*
+ #
+ # check for matching attribute values
+ for attr_name, attr_value in pdu_attrs.items():
+ if not hasattr(pdu, attr_name):
+ if _debug: matchbacnetip.PDU._debug(" - failed match, missing attr: %r", attr_name)
+ return False
+ if getattr(pdu, attr_name) != attr_value:
+ if _debug: stateMachine._debug(" - failed match, attr value: %r, %r", attr_name, attr_value)
+ return False
+ */
+ localLog.Trace().Msg("successful match")
+ return true
+}
+
+type TimeoutTask struct {
+ *bacnetip.OneShotTask
+
+ fn func(args bacnetip.Args, kwargs bacnetip.KWArgs) error
+ args bacnetip.Args
+ kwargs bacnetip.KWArgs
+}
+
+func NewTimeoutTask(fn func(args bacnetip.Args, kwargs bacnetip.KWArgs) error, args bacnetip.Args, kwargs bacnetip.KWArgs, when *time.Time) *TimeoutTask {
+ task := &TimeoutTask{
+ fn: fn,
+ args: args,
+ kwargs: kwargs,
+ }
+ task.OneShotTask = bacnetip.NewOneShotTask(task, when)
+ return task
+}
+
+func (t *TimeoutTask) ProcessTask() error {
+ return t.fn(t.args, t.kwargs)
+}
+
+func (t *TimeoutTask) String() string {
+ return fmt.Sprintf("TimeoutTask(%v, fn: %t, args: %s, kwargs: %s)", t.Task, t.fn != nil, t.args, t.kwargs)
+}
+
+type StateInterceptor interface {
+ BeforeSend(pdu bacnetip.PDU)
+ AfterSend(pdu bacnetip.PDU)
+ BeforeReceive(pdu bacnetip.PDU)
+ AfterReceive(pdu bacnetip.PDU)
+ UnexpectedReceive(pdu bacnetip.PDU)
+}
+
+type State interface {
+ fmt.Stringer
+
+ Send(pdu bacnetip.PDU, nextState State) State
+ Receive(pduType any, pduAttrs map[string]any) State
+ Reset()
+ Fail(docstring string) State
+ Success(docstring string) State
+ ExitState()
+ EnterState()
+ EventSet(eventId string)
+ Timeout(duration time.Duration, nextState State) State
+ WaitEvent(eventId string, nextState State) State
+ SetEvent(eventId string) State
+ Doc(docstring string) State
+ DocString() string
+ Call(fn func(args bacnetip.Args, kwargs bacnetip.KWArgs) error, args bacnetip.Args, kwargs bacnetip.KWArgs) State
+
+ getStateMachine() StateMachine
+ setStateMachine(StateMachine)
+ getDocString() string
+ IsSuccessState() bool
+ IsFailState() bool
+ getSendTransitions() []SendTransition
+ getReceiveTransitions() []ReceiveTransition
+ getSetEventTransitions() []EventTransition
+ getClearEventTransitions() []EventTransition
+ getWaitEventTransitions() []EventTransition
+ getTimeoutTransition() *TimeoutTransition
+ getCallTransition() *CallTransition
+ getInterceptor() StateInterceptor
+
+ Equals(other State) bool
+}
+
+type state struct {
+ interceptor StateInterceptor
+
+ stateMachine StateMachine
+ docString string
+ isSuccessState bool
+ isFailState bool
+ sendTransitions []SendTransition
+ receiveTransitions []ReceiveTransition
+ setEventTransitions []EventTransition
+ clearEventTransitions []EventTransition
+ waitEventTransitions []EventTransition
+ timeoutTransition *TimeoutTransition
+ callTransition *CallTransition
+
+ log zerolog.Logger
+}
+
+func (s *state) Equals(other State) bool {
+ if s == other {
+ return true
+ }
+ return false
+}
+
+func (s *state) getStateMachine() StateMachine {
+ return s.stateMachine
+}
+
+func (s *state) setStateMachine(machine StateMachine) {
+ s.stateMachine = machine
+}
+
+func (s *state) getDocString() string {
+ return s.docString
+}
+
+func (s *state) getSendTransitions() []SendTransition {
+ return s.sendTransitions
+}
+
+func (s *state) getReceiveTransitions() []ReceiveTransition {
+ return s.receiveTransitions
+}
+
+func (s *state) getSetEventTransitions() []EventTransition {
+ return s.setEventTransitions
+}
+
+func (s *state) getClearEventTransitions() []EventTransition {
+ return s.clearEventTransitions
+}
+
+func (s *state) getWaitEventTransitions() []EventTransition {
+ return s.waitEventTransitions
+}
+
+func (s *state) getTimeoutTransition() *TimeoutTransition {
+ return s.timeoutTransition
+}
+
+func (s *state) getCallTransition() *CallTransition {
+ return s.callTransition
+}
+
+func (s *state) getInterceptor() StateInterceptor {
+ return s.interceptor
+}
+
+func NewState(localLog zerolog.Logger, stateMachine StateMachine, docString string, opts ...func(state *state)) State {
+ s := &state{
+ stateMachine: stateMachine,
+ docString: docString,
+
+ log: localLog.With().Str("docString", docString).Logger(),
+ }
+ for _, opt := range opts {
+ opt(s)
+ }
+ if s.interceptor == nil {
+ s.interceptor = s
+ }
+ return s
+}
+
+func WithStateStateInterceptor(interceptor StateInterceptor) func(state *state) {
+ return func(state *state) {
+ state.interceptor = interceptor
+ }
+}
+
+func (s *state) String() string {
+ if s == nil {
+ return "<nil>(*state)"
+ }
+ return fmt.Sprintf("state(doc: %s, successState: %t, isFailState: %t)", s.docString, s.isSuccessState, s.isFailState)
+}
+
+// Reset Override this method in a derived class if the state maintains counters or other information. Called when the
+//
+// associated state machine is Reset.
+func (s *state) Reset() {
+ s.log.Trace().Msg("Reset")
+}
+
+// Doc Change the documentation string (label) for the state. The state
+//
+// is returned for method chaining.
+func (s *state) Doc(docString string) State {
+ s.log.Debug().Str("docString", docString).Msg("Doc")
+ s.docString = docString
+ return s
+}
+
+func (s *state) DocString() string {
+ return s.docString
+}
+
+// Success Mark a state as a successful final state. The state is returned for method chaining. docString: an optional
+//
+// label for the state
+func (s *state) Success(docString string) State {
+ s.log.Debug().Str("docString", docString).Msg("Success")
+ if s.isSuccessState {
+ panic("already a Success state")
+ }
+ if s.isFailState {
+ panic("already a Fail state")
+ }
+
+ s.isSuccessState = true
+
+ if docString != "" {
+ s.docString = docString
+ } else if s.docString == "" {
+ s.docString = "Success"
+ }
+ return s
+}
+
+func (s *state) IsSuccessState() bool {
+ return s.isSuccessState
+}
+
+// Fail Mark a state as a failure final state. The state is returned for method chaining. docString: an optional
+//
+// label for the state
+func (s *state) Fail(docString string) State {
+ s.log.Debug().Str("docString", docString).Msg("Fail")
+ if s.isSuccessState {
+ panic("already a Success state")
+ }
+ if s.isFailState {
+ panic("already a Fail state")
+ }
+
+ s.isFailState = true
+
+ if docString != "" {
+ s.docString = docString
+ } else if s.docString == "" {
+ s.docString = "Fail"
+ }
+ return s
+}
+
+func (s *state) IsFailState() bool {
+ return s.isFailState
+}
+
+// EnterState Called when the state machine is entering the state.
+func (s *state) EnterState() {
+ s.log.Debug().Msg("EnterState")
+ if s.timeoutTransition != nil {
+ s.log.Debug().Time("timeout", s.timeoutTransition.timeout).Msg("waiting")
+ s.stateMachine.getStateTimeoutTask().InstallTask(bacnetip.InstallTaskOptions{When: &s.timeoutTransition.timeout})
+ } else {
+ s.log.Trace().Msg("no timeout")
+ }
+}
+
+// ExitState Called when the state machine is existing the state.
+func (s *state) ExitState() {
+ s.log.Debug().Msg("ExitState")
+ if s.timeoutTransition != nil {
+ s.log.Trace().Msg("canceling timeout")
+ s.stateMachine.getStateTimeoutTask().SuspendTask()
+ }
+}
+
+// Send Create a SendTransition from this state to another, possibly new, state. The next state is returned for method
+//
+// chaining. pdu tPDU to send nextState state to transition to after sending
+func (s *state) Send(pdu bacnetip.PDU, nextState State) State {
+ s.log.Debug().Stringer("pdu", pdu).Msg("Send")
+ if nextState == nil {
+ nextState = s.stateMachine.NewState("")
+ s.log.Debug().Stringer("nextState", nextState).Msg("new nextState")
+ } else if !slices.ContainsFunc(s.stateMachine.getStates(), nextState.Equals) {
+ panic("off the rails")
+ }
+
+ s.sendTransitions = append(s.sendTransitions, SendTransition{
+ Transition: Transition{nextState: nextState},
+ pdu: pdu,
+ })
+ return nextState
+}
+
+// BeforeSend Called before each tPDU about to be sent.
+func (s *state) BeforeSend(pdu bacnetip.PDU) {
+ s.stateMachine.BeforeSend(pdu)
+}
+
+// AfterSend Called after each tPDU about to be sent.
+func (s *state) AfterSend(pdu bacnetip.PDU) {
+ s.stateMachine.AfterSend(pdu)
+}
+
+// Receive Create a ReceiveTransition from this state to another, possibly new,
+//
+// state. The next state is returned for method chaining.
+//
+// criteria tPDU to match
+// next_state destination state after a successful match
+func (s *state) Receive(pduType any, pduAttrs map[string]any) State {
+ s.log.Debug().Interface("pduType", pduType).Interface("pduAttrs", pduAttrs).Msg("Receive")
+ var nextState State
+ if _nextState, ok := pduAttrs["next_state"]; ok {
+ nextState = _nextState.(State)
+ s.log.Debug().Stringer("nextState", nextState).Msg("nextState")
+ }
+
+ if nextState == nil {
+ nextState = s.stateMachine.NewState("")
+ s.log.Debug().Stringer("nextState", nextState).Msg("nextState")
+ } else if !slices.ContainsFunc(s.stateMachine.getStates(), nextState.Equals) {
+ panic("off the rails")
+ }
+
+ // add this to the list of transitions
+ criteria := criteria{
+ pduType: pduType,
+ pduAttrs: pduAttrs,
+ }
+ s.log.Debug().Interface("criteria", criteria).Msg("criteria")
+ s.receiveTransitions = append(s.receiveTransitions, ReceiveTransition{
+ Transition: Transition{nextState: nextState},
+ criteria: criteria,
+ })
+
+ return nextState
+}
+
+// BeforeReceive Called with each tPDU received before matching.
+func (s *state) BeforeReceive(pdu bacnetip.PDU) {
+ s.stateMachine.BeforeReceive(pdu)
+}
+
+// AfterReceive Called with tPDU received after match.
+func (s *state) AfterReceive(pdu bacnetip.PDU) {
+ s.stateMachine.AfterReceive(pdu)
+}
+
+// Ignore Create a ReceiveTransition from this state to itself, if match is successful the effect is to Ignore the tPDU.
+//
+// criteria tPDU to match
+func (s *state) Ignore(pduType any, pduAttrs map[string]any) State {
+ s.log.Debug().Interface("pduType", pduType).Interface("pduAttrs", pduAttrs).Msg("Ignore")
+ s.receiveTransitions = append(s.receiveTransitions, ReceiveTransition{
+ Transition: Transition{},
+ criteria: criteria{
+ pduType: pduType,
+ pduAttrs: pduAttrs,
+ },
+ })
+
+ return s
+}
+
+// UnexpectedReceive Called with tPDU that did not match. Unless this is trapped by the state, the default behaviour is
+//
+// to fail.
+func (s *state) UnexpectedReceive(pdu bacnetip.PDU) {
+ s.log.Debug().Stringer("pdu", pdu).Msg("UnexpectedReceive")
+ s.stateMachine.UnexpectedReceive(pdu)
+}
+
+// SetEvent Create an EventTransition for this state that sets an event. The current state is returned for method
+//
+// chaining. event_id event identifier
+func (s *state) SetEvent(eventId string) State {
+ s.log.Debug().Str("eventId", eventId).Msg("SetEvent")
+ s.setEventTransitions = append(s.setEventTransitions, EventTransition{
+ Transition: Transition{},
+ eventId: eventId,
+ })
+ return s
+}
+
+// EventSet Called with the event that was set.
+func (s *state) EventSet(eventId string) {
+ // Nothing
+}
+
+// ClearEvent Create an EventTransition for this state that clears an event. The current state is returned for method
+//
+// chaining. event_id event identifier
+func (s *state) ClearEvent(eventId string) State {
+ s.log.Debug().Str("eventId", eventId).Msg("ClearEvent")
+ s.clearEventTransitions = append(s.clearEventTransitions, EventTransition{
+ Transition: Transition{},
+ eventId: eventId,
+ })
+ return s
+}
+
+// WaitEvent Create an EventTransition from this state to another, possibly new, state. The next state is returned for
+//
+// method chaining. pdu tPDU to send next_state state to transition to after sending
+func (s *state) WaitEvent(eventId string, nextState State) State {
+ s.log.Debug().Str("eventId", eventId).Msg("WaitEvent")
+ if nextState == nil {
+ nextState = s.stateMachine.NewState("")
+ s.log.Debug().Stringer("nextState", nextState).Msg("nextState")
+ } else if !slices.ContainsFunc(s.stateMachine.getStates(), nextState.Equals) {
+ panic("off the rails")
+ }
+
+ s.waitEventTransitions = append(s.waitEventTransitions, EventTransition{
+ Transition: Transition{
+ nextState: nextState,
+ },
+ eventId: eventId,
+ })
+ return nextState
+}
+
+// Timeout Create a TimeoutTransition from this state to another, possibly new,
+//
+// state. There can only be one timeout transition per state. The next
+// state is returned for method chaining.
+//
+// delay the amount of time to wait for a matching tPDU
+// next_state destination state after timeout
+func (s *state) Timeout(delay time.Duration, nextState State) State {
+ s.log.Debug().Dur("delay", delay).Stringer("nextState", nextState).Msg("Timeout")
+ if s.timeoutTransition != nil {
+ panic("state already has a timeout")
+ }
+
+ if nextState == nil {
+ nextState = s.stateMachine.NewState("")
+ s.log.Debug().Stringer("nextState", nextState).Msg("nextState")
+ } else if !slices.ContainsFunc(s.stateMachine.getStates(), nextState.Equals) {
+ panic("off the rails")
+ }
+
+ now := bacnetip.GetTaskManagerTime()
+
+ s.timeoutTransition = &TimeoutTransition{
+ Transition: Transition{nextState: nextState},
+ timeout: now.Add(delay),
+ }
+ return nextState
+}
+
+// Call Create a CallTransition from this state to another, possibly new, state. The next state is returned for method
+//
+// chaining. criteria tPDU to match next_state destination state after a successful match
+func (s *state) Call(fn func(args bacnetip.Args, kwargs bacnetip.KWArgs) error, args bacnetip.Args, kwargs bacnetip.KWArgs) State {
+ s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Call")
+ if s.callTransition != nil {
+ panic("state already has a 'Call' per state")
+ }
+ var nextState State
+ if _nextState, ok := kwargs["next_state"]; ok {
+ nextState = _nextState.(State)
+ s.log.Debug().Stringer("nextState", nextState).Msg("nextState")
+ delete(kwargs, "next_state")
+ }
+
+ if nextState == nil {
+ nextState = s.stateMachine.NewState("")
+ s.log.Debug().Stringer("nextState", nextState).Msg("nextState")
+ } else if !slices.ContainsFunc(s.stateMachine.getStates(), nextState.Equals) {
+ panic("off the rails")
+ }
+
+ s.callTransition = &CallTransition{
+ Transition: Transition{nextState: nextState},
+ fnargs: fnargs{
+ fn: fn,
+ args: args,
+ kwargs: kwargs,
+ },
+ }
+ return nextState
+}
+
+type StateMachineRequirements interface {
+ Send(args bacnetip.Args, kwargs bacnetip.KWArgs) error
+}
+
+// StateMachine A state machine consisting of states. Every state machine has a start
+//
+// state where the state machine begins when it is started. It also has
+// an *unexpected receive* fail state where the state machine goes if
+// there is an unexpected (unmatched) tPDU received.
+type StateMachine interface {
+ fmt.Stringer
+
+ NewState(string) State
+ UnexpectedReceive(pdu bacnetip.PDU)
+ BeforeSend(pdu bacnetip.PDU)
+ Send(args bacnetip.Args, kwargs bacnetip.KWArgs) error
+ AfterSend(pdu bacnetip.PDU)
+ BeforeReceive(pdu bacnetip.PDU)
+ Receive(args bacnetip.Args, kwargs bacnetip.KWArgs) error
+ AfterReceive(pdu bacnetip.PDU)
+ EventSet(id string)
+ Run() error
+ Reset()
+ GetTransactionLog() []string
+ GetCurrentState() State
+ GetUnexpectedReceiveState() State
+ GetStartState() State
+ IsRunning() bool
+ IsSuccessState() bool
+ IsFailState() bool
+
+ getStateTimeoutTask() *TimeoutTask
+ getStates() []State
+ getCurrentState() State
+ setMachineGroup(machineGroup *StateMachineGroup)
+ getMachineGroup() *StateMachineGroup
+ halt()
+}
+
+type stateMachine struct {
+ StateMachineRequirements
+
+ interceptor StateInterceptor
+ stateDecorator func(state State) State
+
+ states []State
+ name string
+ machineGroup *StateMachineGroup
+ stateSubStruct any
+ startState State
+ unexpectedReceiveState State
+ transitionQueue chan bacnetip.PDU
+ stateTimeoutTask *TimeoutTask
+ timeout time.Duration
+ timeoutState State
+ stateMachineTimeout *time.Time
+ timeoutTask *TimeoutTask
+ running bool
+ startupFlag bool
+ isSuccessState *bool
+ isFailState *bool
+ stateTransitioning int
+ currentState State
+ transactionLog []string
+
+ log zerolog.Logger
+}
+
+// NewStateMachine creates a new state machine. Make sure to call the init function (Init must be called after a new (can't be done in constructor as initialization is then not yet finished))
+func NewStateMachine(localLog zerolog.Logger, stateMachineRequirements StateMachineRequirements, opts ...func(machine *stateMachine)) (sm StateMachine, init func()) {
+ s := &stateMachine{
+ StateMachineRequirements: stateMachineRequirements,
+
+ log: localLog,
+ }
+ for _, opt := range opts {
+ opt(s)
+ }
+ if s.name != "" {
+ s.log = s.log.With().Str("name", s.name).Logger()
+ }
+ if s.stateDecorator == nil {
+ s.stateDecorator = func(state State) State {
+ return state
+ }
+ }
+ return s, func() {
+ s.Reset()
+
+ if s.startState != nil {
+ if s.startState.getStateMachine() != nil {
+ panic("start state already bound to a machine")
+ }
+ s.states = append(s.states, s.startState)
+ s.startState.setStateMachine(s)
+ } else {
+ s.startState = s.NewState("start")
+ s.startState = s.stateDecorator(s.startState)
+ }
+
+ if s.unexpectedReceiveState != nil {
+ if s.unexpectedReceiveState.getStateMachine() != nil {
+ panic("start state already bound to a machine")
+ }
+ s.states = append(s.states, s.unexpectedReceiveState)
+ s.unexpectedReceiveState.setStateMachine(s)
+ } else {
+ s.unexpectedReceiveState = s.NewState("unexpected receive").Fail("")
+ s.unexpectedReceiveState = s.stateDecorator(s.unexpectedReceiveState)
+ }
+
+ s.transitionQueue = make(chan bacnetip.PDU, 100)
+
+ s.stateTimeoutTask = NewTimeoutTask(s.StateTimeout, bacnetip.NoArgs, bacnetip.NoKWArgs, nil)
+
+ if s.timeout != 0 {
+ s.timeoutState = s.NewState("state machine timeout").Fail("")
+ s.timeoutTask = NewTimeoutTask(s.StateMachineTimeout, bacnetip.NoArgs, bacnetip.NoKWArgs, s.stateMachineTimeout)
+ }
+ }
+}
+
+func WithStateMachineName(name string) func(stateMachine *stateMachine) {
+ return func(stateMachine *stateMachine) {
+ stateMachine.name = name
+ }
+}
+
+func WithStateMachineStateInterceptor(interceptor StateInterceptor) func(stateMachine *stateMachine) {
+ return func(stateMachine *stateMachine) {
+ stateMachine.interceptor = interceptor
+ }
+}
+
+func WithStateMachineTimeout(timeout time.Duration) func(stateMachine *stateMachine) {
+ return func(stateMachine *stateMachine) {
+ stateMachine.timeout = timeout
+ }
+}
+
+func WithStateMachineStartState(startState State) func(stateMachine *stateMachine) {
+ return func(stateMachine *stateMachine) {
+ stateMachine.startState = startState
+ }
+}
+
+func WithStateMachineUnexpectedReceiveState(unexpectedReceiveState State) func(stateMachine *stateMachine) {
+ return func(stateMachine *stateMachine) {
+ stateMachine.unexpectedReceiveState = unexpectedReceiveState
+ }
+}
+
+func WithStateMachineMachineGroup(machineGroup *StateMachineGroup) func(stateMachine *stateMachine) {
+ return func(stateMachine *stateMachine) {
+ stateMachine.machineGroup = machineGroup
+ }
+}
+
+func WithStateMachineStateDecorator(stateDecorator func(state State) State) func(stateMachine *stateMachine) {
+ return func(stateMachine *stateMachine) {
+ stateMachine.stateDecorator = stateDecorator
+ }
+}
+
+func (s *stateMachine) getStateTimeoutTask() *TimeoutTask {
+ return s.stateTimeoutTask
+}
+
+func (s *stateMachine) getStates() []State {
+ return s.states
+}
+
+func (s *stateMachine) getMachineGroup() *StateMachineGroup {
+ return s.machineGroup
+}
+
+func (s *stateMachine) getCurrentState() State {
+ return s.currentState
+}
+
+func (s *stateMachine) setMachineGroup(machineGroup *StateMachineGroup) {
+ s.machineGroup = machineGroup
+}
+
+func (s *stateMachine) GetTransactionLog() []string {
+ return s.transactionLog
+}
+
+func (s *stateMachine) GetCurrentState() State {
+ return s.currentState
+}
+
+func (s *stateMachine) GetStartState() State {
+ return s.startState
+}
+
+func (s *stateMachine) String() string {
+ return fmt.Sprintf("stateMachine(name=%s)", s.name)
+}
+
+func (s *stateMachine) NewState(docString string) State {
+ s.log.Trace().Str("docString", docString).Msg("NewState")
+ _state := NewState(s.log, s, docString, WithStateStateInterceptor(s.interceptor))
+ _state = s.stateDecorator(_state)
+ s.states = append(s.states, _state)
+ return _state
+}
+
+func (s *stateMachine) Reset() {
+ s.log.Trace().Msg("Reset")
+ // make sure we're not running
+ if s.running {
+ panic("state machine is running")
+ }
+
+ // flags for remembering Success or fail
+ s.isSuccessState = nil
+ s.isFailState = nil
+
+ // no current state, empty transaction log
+ s.currentState = nil
+ s.transactionLog = make([]string, 0)
+
+ // we are not starting up
+ s.startupFlag = false
+
+ // give all the getStates a chance to reset
+ for _, state := range s.states {
+ state.Reset()
+ }
+}
+
+func (s *stateMachine) Run() error {
+ s.log.Trace().Msg("Run")
+ if s.running {
+ panic("state machine is running")
+ }
+ if s.currentState != nil {
+ panic("not running but has a current state")
+ }
+
+ if s.timeoutTask != nil {
+ s.log.Debug().Msg("schedule runtime limit")
+ s.timeoutTask.InstallTask(bacnetip.InstallTaskOptions{Delta: &s.timeout})
+ }
+
+ // we are starting up
+ s.startupFlag = true
+
+ // go to the start state
+ if err := s.gotoState(s.startState); err != nil {
+ return errors.Wrap(err, "error going to start state")
+ }
+
+ // startup complete
+ s.startupFlag = false
+
+ // if it is part of a group, let the group know
+ if s.machineGroup != nil {
+ s.machineGroup.Started(s)
+
+ // if it is stopped already, let the group know
+ if !s.running {
+ s.machineGroup.Stopped(s)
+ }
+ }
+ return nil
+}
+
+// Called when the state machine should no longer be running.
+func (s *stateMachine) halt() {
+ s.log.Trace().Msg("Halt")
+ // make sure we're running
+ if !s.running {
+ panic("state machine is not running")
+ }
+
+ // cancel the timeout task
+ if s.timeoutTask != nil {
+ s.log.Debug().Msg("cancel runtime limit")
+ s.timeoutTask.SuspendTask()
+ }
+
+ close(s.transitionQueue)
+
+ // no longer running
+ s.running = false
+}
+
+// success Called when the state machine has successfully completed.
+func (s *stateMachine) success() {
+ s.log.Trace().Msg("Success")
+ isSuccessState := true
+ s.isSuccessState = &isSuccessState
+}
+
+// success Called when the state machine has successfully completed.
+func (s *stateMachine) fail() {
+ s.log.Trace().Msg("Fail")
+ isFailState := true
+ s.isFailState = &isFailState
+}
+
+func (s *stateMachine) gotoState(state State) error {
+ s.log.Debug().Stringer("state", state).Msg("gotoState")
+ //where do you think you're going?
+ if !slices.ContainsFunc(s.states, state.Equals) {
+ return errors.New("off the rails")
+ }
+
+ s.stateTransitioning += 1
+
+ if s.currentState != nil {
+ s.currentState.ExitState()
+ } else if state == s.startState {
+ // starting up
+ s.running = true
+ } else {
+ return errors.New("start at the start state")
+ }
+
+ s.currentState = state
+ currentState := state
+
+ currentState.EnterState()
+ s.log.Trace().Msg("state entered")
+
+ if s.machineGroup != nil {
+ for _, transition := range currentState.getSetEventTransitions() {
+ s.log.Debug().Str("eventId", transition.eventId).Msg("setting event")
+ s.machineGroup.SetEvent(transition.eventId)
+ }
+
+ for _, transition := range currentState.getClearEventTransitions() {
+ s.log.Debug().Str("eventId", transition.eventId).Msg("clearing event")
+ s.machineGroup.ClearEvent(transition.eventId)
+ }
+ }
+
+ if currentState.IsSuccessState() {
+ s.log.Trace().Msg("Success state")
+ s.stateTransitioning -= 1
+
+ s.halt()
+ s.success()
+
+ if s.machineGroup != nil && s.startupFlag {
+ s.machineGroup.Stopped(s)
+ }
+
+ return nil
+ }
+
+ if currentState.IsFailState() {
+ s.log.Trace().Msg("Fail state")
+ s.stateTransitioning -= 1
+
+ s.halt()
+ s.fail()
+
+ if s.machineGroup != nil && s.startupFlag {
+ s.machineGroup.Stopped(s)
+ }
+
+ return nil
+ }
+
+ var nextState State
+
+ if s.machineGroup != nil {
+ didBreak := false
+ for _, transition := range currentState.getWaitEventTransitions() {
+ s.log.Debug().Str("eventID", transition.eventId).Msg("waiting event")
+ if _, ok := s.machineGroup.events[transition.eventId]; ok {
+ nextState = transition.nextState
+ s.log.Debug().Stringer("nextState", nextState).Msg("nextState")
+ if nextState != currentState {
+ didBreak = true
+ break
+ }
+ }
+ }
+ if !didBreak {
+ s.log.Trace().Msg("no events already set")
+ }
+ } else {
+ s.log.Trace().Msg("not part of a group")
+ }
+
+ if callTransition := currentState.getCallTransition(); callTransition != nil {
+ s.log.Debug().Interface("callTransition", callTransition).Msg("calling transition")
+ f := callTransition.fnargs
+ fn, args, kwargs := f.fn, f.args, f.kwargs
+ if err := fn(args, kwargs); err != nil {
+ var assertionError AssertionError
+ if !errors.As(err, &assertionError) {
+ return err
+ }
+ s.log.Trace().Err(err).Msg("called exception")
+ s.stateTransitioning -= 1
+
+ s.halt()
+ s.fail()
+
+ if s.machineGroup != nil && !s.startupFlag {
+ s.machineGroup.Stopped(s)
+ }
+
+ return nil
+ } else {
+ s.log.Trace().Msg("called, exception")
+ }
+
+ nextState = callTransition.nextState
+ } else {
+ s.log.Trace().Msg("no calls")
+ }
+
+ if nextState == nil {
+ for _, transition := range currentState.getSendTransitions() {
+ s.log.Debug().Stringer("transition", transition).Msg("sending transition")
+ currentState.getInterceptor().BeforeSend(transition.pdu)
+ if err := s.StateMachineRequirements.Send(bacnetip.NewArgs(transition.pdu), bacnetip.NewKWArgs()); err != nil {
+ return errors.Wrap(err, "failed to send")
+ }
+ currentState.getInterceptor().AfterSend(transition.pdu)
+
+ nextState = transition.nextState
+ s.log.Debug().Stringer("nextState", nextState).Msg("nextState")
+
+ if nextState != currentState {
+ break
+ }
+ }
+ }
+
+ if nextState == nil {
+ s.log.Trace().Msg("nowhere to go")
+ } else if nextState == s.currentState {
+ s.log.Trace().Msg("going nowhere")
+ } else {
+ s.log.Trace().Msg("going")
+ if err := s.gotoState(nextState); err != nil {
+ return errors.Wrap(err, "error in recursion")
+ }
+ }
+
+ s.stateTransitioning -= 1
+
+ if s.stateTransitioning == 0 {
+ queueRead:
+ for s.running {
+ select {
+ case pdu := <-s.transitionQueue:
+ if err := s.Receive(bacnetip.NewArgs(pdu), bacnetip.NewKWArgs()); err != nil {
+ return errors.Wrap(err, "failed to receive")
+ }
+ default:
+ break queueRead
+ }
+ }
+ }
+ return nil
+}
+
+func (s *stateMachine) BeforeSend(pdu bacnetip.PDU) {
+ s.transactionLog = append(s.transactionLog, fmt.Sprintf("<<<%v", pdu))
+}
+
+func (s *stateMachine) Send(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ panic("not implemented")
+}
+
+func (s *stateMachine) AfterSend(pdu bacnetip.PDU) {
+}
+
+func (s *stateMachine) BeforeReceive(pdu bacnetip.PDU) {
+ s.transactionLog = append(s.transactionLog, fmt.Sprintf(">>>%v", pdu))
+}
+
+func (s *stateMachine) Receive(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ s.log.Trace().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Receive")
+ pdu := args.Get0PDU()
+ if s.currentState == nil || s.stateTransitioning != 0 {
+ s.log.Trace().Msg("queue for later")
+ s.transitionQueue <- pdu
+ return nil
+ }
+
+ if !s.running {
+ s.log.Trace().Msg("already completed")
+ return nil
+ }
+
+ currentState := s.currentState
+ s.log.Debug().Stringer("currentState", currentState).Msg("current_state")
+
+ currentState.getInterceptor().BeforeReceive(pdu)
+
+ var nextState State
+ matchFound := false
+ for _, transition := range currentState.getReceiveTransitions() {
+ if s.MatchPDU(pdu, transition.criteria) {
+ s.log.Trace().Msg("match found")
+ matchFound = true
+
+ currentState.getInterceptor().AfterReceive(pdu)
+
+ nextState = transition.nextState
+ s.log.Debug().Stringer("nextState", nextState).Msg("nextState")
+
+ if nextState != currentState {
+ break
+ }
+ } else {
+ s.log.Trace().Msg("no matches")
+ }
+ }
+
+ if !matchFound {
+ currentState.getInterceptor().UnexpectedReceive(pdu)
+ } else if nextState != currentState {
+ if err := s.gotoState(nextState); err != nil {
+ return errors.Wrap(err, "error going to state")
+ }
+ }
+ return nil
+}
+
+func (s *stateMachine) AfterReceive(pdu bacnetip.PDU) {
+ s.log.Trace().Stringer("pdu", pdu).Msg("AfterReceive")
+}
+
+func (s *stateMachine) UnexpectedReceive(pdu bacnetip.PDU) {
+ s.log.Trace().Stringer("pdu", pdu).Msg("UnexpectedReceive")
+ s.log.Trace().Stringer("currentState", s.currentState).Msg("currentState")
+ if err := s.gotoState(s.unexpectedReceiveState); err != nil {
+ s.log.Error().Err(err).Msg("error going to unexpected state")
+ }
+}
+
+func (s *stateMachine) GetUnexpectedReceiveState() State {
+ return s.unexpectedReceiveState
+}
+
+func (s *stateMachine) EventSet(eventId string) {
+ s.log.Debug().Str("eventId", eventId).Msg("EventSet")
+ if !s.running {
+ s.log.Trace().Msg("not running")
+ return
+ }
+
+ if s.stateTransitioning == 1 {
+ s.log.Trace().Msg("transitioning")
+ return
+ }
+ if s.currentState == nil {
+ panic("current state is nil")
+ }
+ currentState := s.currentState
+
+ var nextState State
+ matchFound := false
+ for _, transition := range currentState.getWaitEventTransitions() {
+ if transition.eventId == eventId {
+ s.log.Trace().Msg("match found")
+ matchFound = true
+
+ currentState.EventSet(eventId)
+
+ nextState = transition.nextState
+ s.log.Debug().Stringer("nextState", nextState).Msg("nextState")
+
+ if nextState != currentState {
+ break
+ }
+ }
+ }
+ if len(currentState.getWaitEventTransitions()) == 0 {
+ s.log.Trace().Msg("going nowhere")
+ }
+
+ if matchFound && nextState != currentState {
+ s.log.Trace().Msg("going")
+ if err := s.gotoState(nextState); err != nil {
+ s.log.Error().Err(err).Msg("failed to go to next state")
+ }
+ }
+}
+
+func (s *stateMachine) StateTimeout(_ bacnetip.Args, _ bacnetip.KWArgs) error {
+ s.log.Trace().Msg("StateTimeout")
+ if !s.running {
+ return errors.New("state machine is not running")
+ }
+ if s.currentState.getTimeoutTransition() == nil {
+ return errors.New("state timeout, but timeout transition is nil")
+ }
+ if err := s.gotoState(s.currentState.getTimeoutTransition().nextState); err != nil {
+ return errors.Wrap(err, "failed to go to next state")
+ }
+ return nil
+}
+
+func (s *stateMachine) StateMachineTimeout(_ bacnetip.Args, _ bacnetip.KWArgs) error {
+ s.log.Trace().Msg("StateMachineTimeout")
+ if !s.running {
+ return errors.New("state machine is not running")
+ }
+ if err := s.gotoState(s.timeoutState); err != nil {
+ return errors.Wrap(err, "failed to go to next state")
+ }
+ return nil
+}
+
+func (s *stateMachine) MatchPDU(pdu bacnetip.PDU, criteria criteria) bool {
+ s.log.Debug().Stringer("pdu", pdu).Stringer("criteria", criteria).Msg("MatchPDU")
+ return MatchPdu(s.log, pdu, criteria.pduType, criteria.pduAttrs)
+}
+
+func (s *stateMachine) IsRunning() bool {
+ return s.running
+}
+
+func (s *stateMachine) IsSuccessState() bool {
+ if s.isSuccessState == nil {
+ return false
+ }
+ return *s.isSuccessState
+}
+
+func (s *stateMachine) IsFailState() bool {
+ if s.isFailState == nil {
+ return false
+ }
+ return *s.isFailState
+}
+
+// StateMachineGroup A state machine group is a collection of state machines that are all
+//
+// started and stopped together. There are methods available to derived
+// classes that are called when all of the machines in the group have
+// completed, either all successfully or at least one has failed.
+//
+// .. note:: When creating a group of state machines, add the ones that
+// are expecting to receive one or more tPDU's first before the ones
+// that send tPDU's. They will be started first, and be ready for the
+// tPDU that might be sent.
+type StateMachineGroup struct {
+ stateMachines []StateMachine
+ isSuccessState bool
+ isFailState bool
+ events map[string]struct{}
+ startupFlag bool
+ isRunning bool
+
+ log zerolog.Logger
+}
+
+func NewStateMachineGroup(localLog zerolog.Logger) *StateMachineGroup {
+ return &StateMachineGroup{
+ events: map[string]struct{}{},
+ log: localLog,
+ }
+}
+
+// Append Add a state machine to the end of the list of state machines
+func (s *StateMachineGroup) Append(machine StateMachine) {
+ s.log.Debug().Stringer("stateMachine", machine).Msg("Append")
+ if machine.getMachineGroup() != nil {
+ panic("state machine group already contains this machine")
+ }
+
+ machine.setMachineGroup(s)
+
+ s.stateMachines = append(s.stateMachines, machine)
+}
+
+// Remove a state machine from the list of state machines.
+func (s *StateMachineGroup) Remove(machine StateMachine) {
+ s.log.Debug().Stringer("stateMachine", machine).Msg("Remove")
+ if machine.getMachineGroup() != s {
+ panic("state machine is not a member of this group")
+ }
+
+ machine.setMachineGroup(nil)
+ for i, stateMachine := range s.stateMachines {
+ if stateMachine == machine {
+ s.stateMachines = append(s.stateMachines[:i], s.stateMachines[i+1:]...)
+ break
+ }
+ }
+}
+
+// Reset resets all the machines in the group.
+func (s *StateMachineGroup) Reset() {
+ s.log.Trace().Msg("Reset")
+ for _, stateMachine := range s.stateMachines {
+ s.log.Debug().Stringer("stateMachine", stateMachine).Msg("Resetting")
+ stateMachine.Reset()
+ }
+
+ s.isSuccessState = false
+ s.isFailState = false
+
+ s.events = make(map[string]struct{})
+}
+
+// SetEvent save an event as 'set' and pass it to the state machines to see
+//
+// if they are in a state that is waiting for the event.
+func (s *StateMachineGroup) SetEvent(id string) {
+ s.log.Trace().Str("eventId", id).Msg("SetEvent")
+ s.events[id] = struct{}{}
+
+ for _, machine := range s.stateMachines {
+ s.log.Debug().Stringer("stateMachine", machine).Msg("Setting")
+ machine.EventSet(id)
+ }
+}
+
+// ClearEvent Remove an event from the set of elements that are 'set'.
+func (s *StateMachineGroup) ClearEvent(id string) {
+ s.log.Trace().Str("eventId", id).Msg("ClearEvent")
+ delete(s.events, id)
+}
+
+// Run Runs all the machines in the group.
+func (s *StateMachineGroup) Run() error {
+ s.log.Trace().Msg("Run")
+ s.startupFlag = true
+ s.isRunning = true
+
+ for _, machine := range s.stateMachines {
+ s.log.Debug().Stringer("stateMachine", machine).Msg("starting")
+ if err := machine.Run(); err != nil {
+ return errors.Wrap(err, "failed to start machine")
+ }
+ }
+
+ s.startupFlag = false
+ s.log.Trace().Msg("all started")
+
+ allSuccess, someFailed := s.CheckForSuccess()
+ if allSuccess {
+ s.Success()
+ } else if someFailed {
+ s.Fail()
+ } else {
+ s.log.Trace().Msg("some still running")
+ }
+ return nil
+}
+
+// Started Called by a state machine in the group when it has completed its
+//
+// transition into its starting state.
+func (s *StateMachineGroup) Started(machine *stateMachine) {
+ s.log.Debug().Stringer("stateMachine", machine).Msg("started")
+}
+
+// Stopped Called by a state machine after it has halted and its Success()
+//
+// or fail() method has been called.
+func (s *StateMachineGroup) Stopped(machine *stateMachine) {
+ s.log.Debug().Stringer("stateMachine", machine).Msg("stopped")
+ if s.startupFlag {
+ s.log.Trace().Msg("still starting up")
+ return
+ }
+
+ allSuccess, someFailed := s.CheckForSuccess()
+ if allSuccess {
+ s.Success()
+ } else if someFailed {
+ s.Fail()
+ } else {
+ s.log.Trace().Msg("some still running")
+ }
+}
+
+// CheckForSuccess Called after all of the machines have started, and each time a
+//
+// machine has stopped, to see if the entire group should be considered
+// a Success or fail.
+func (s *StateMachineGroup) CheckForSuccess() (allSuccess bool, someFailed bool) {
+ s.log.Trace().Msg("CheckForSuccess")
+ allSuccess = true
+ someFailed = false
+
+ for _, machine := range s.stateMachines {
+ if machine.IsRunning() {
+ s.log.Trace().Stringer("machine", machine).Msg("running")
+ allSuccess = false
+ someFailed = false
+ break
+ }
+
+ if machine.getCurrentState() == nil {
+ s.log.Trace().Stringer("machine", machine).Msg("not started")
+ allSuccess = false
+ someFailed = false
+ break
+ }
+
+ allSuccess = allSuccess && machine.getCurrentState().IsSuccessState()
+ someFailed = someFailed || machine.getCurrentState().IsFailState()
+ }
+ s.log.Debug().Bool("allSuccess", allSuccess).Msg("allSuccess")
+ s.log.Debug().Bool("someFailed", allSuccess).Msg("someFailed")
+ return
+}
+
+// Halt halts all of the running machines in the group.
+func (s *StateMachineGroup) Halt() {
+ s.log.Trace().Msg("Halt")
+ for _, machine := range s.stateMachines {
+ if machine.IsRunning() {
+ machine.halt()
+ }
+ }
+}
+
+// Success Called when all of the machines in the group have halted and they
+//
+// are all in a 'Success' final state.
+func (s *StateMachineGroup) Success() {
+ s.log.Trace().Msg("Success")
+ s.isRunning = false
+ s.isSuccessState = true
+}
+
+// Fail Called when all of the machines in the group have halted and at
+//
+// at least one of them is in a 'fail' final state.
+func (s *StateMachineGroup) Fail() {
+ s.log.Trace().Msg("Fail")
+ s.isRunning = false
+ s.isFailState = true
+}
+
+func (s *StateMachineGroup) GetStateMachines() []StateMachine {
+ return s.stateMachines
+}
+
+func (s *StateMachineGroup) IsRunning() bool {
+ return s.isRunning
+}
+
+func (s *StateMachineGroup) IsSuccessState() bool {
+ return s.isSuccessState
+}
+
+func (s *StateMachineGroup) IsFailState() bool {
+ return s.isFailState
+}
+
+// ClientStateMachine An instance of this class sits at the top of a stack. tPDU's that the
+//
+// state machine sends are sent down the stack and tPDU's coming up the
+// stack are fed as received tPDU's.
+type ClientStateMachine struct {
+ *bacnetip.Client
+ StateMachine
+
+ name string
+
+ log zerolog.Logger
+}
+
+func NewClientStateMachine(localLog zerolog.Logger, opts ...func(*ClientStateMachine)) (*ClientStateMachine, error) {
+ c := &ClientStateMachine{
+ log: localLog,
+ }
+ for _, opt := range opts {
+ opt(c)
+ }
+ var err error
+ c.Client, err = bacnetip.NewClient(localLog, c)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating client")
+ }
+ var init func()
+ c.StateMachine, init = NewStateMachine(localLog, c, WithStateMachineName(c.name))
+ init()
+ return c, nil
+}
+
+func WithClientStateMachineName(name string) func(*ClientStateMachine) {
+ return func(c *ClientStateMachine) {
+ c.name = name
+ }
+}
+
+func (s *ClientStateMachine) String() string {
+ return fmt.Sprintf("ClientStateMachine{Client: %v, StateMachine: %v}", s.Client, s.StateMachine)
+}
+
+func (s *ClientStateMachine) Send(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ s.log.Trace().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Send")
+ return s.Request(args, kwargs)
+}
+
+func (s *ClientStateMachine) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ s.log.Trace().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Confirmation")
+ return s.Receive(args, kwargs)
+}
+
+type ServerStateMachine struct {
+ *bacnetip.Server
+ StateMachine
+
+ name string
+
+ log zerolog.Logger
+}
+
+func NewServerStateMachine(localLog zerolog.Logger, opts ...func(*ServerStateMachine)) (*ServerStateMachine, error) {
+ c := &ServerStateMachine{
+ log: localLog,
+ }
+ for _, opt := range opts {
+ opt(c)
+ }
+ var err error
+ c.Server, err = bacnetip.NewServer(localLog, c)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating Server")
+ }
+ var init func()
+ c.StateMachine, init = NewStateMachine(localLog, c, WithStateMachineName(c.name))
+ init()
+ return c, nil
+}
+
+func WithServerStateMachineName(name string) func(*ServerStateMachine) {
+ return func(s *ServerStateMachine) {
+ s.name = name
+ }
+}
+
+func (s *ServerStateMachine) String() string {
+ return fmt.Sprintf("ServerStateMachine(TBD...)") // TODO: fill some info here
+}
+
+func (s *ServerStateMachine) Send(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ s.log.Trace().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Send")
+ return s.Response(args, kwargs)
+}
+
+func (s *ServerStateMachine) Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ s.log.Trace().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Indication")
+ return s.Receive(args, kwargs)
+}
+
+type TrafficLog struct {
+ traffic []struct {
+ time.Time
+ bacnetip.Args
+ }
+}
+
+// Call Capture the current time and the arguments.
+func (t *TrafficLog) Call(args bacnetip.Args) {
+ t.traffic = append(t.traffic, struct {
+ time.Time
+ bacnetip.Args
+ }{Time: time.Now(), Args: args})
+}
+
+// Dump the traffic, pass the correct handler like SomeClass._debug
+func (t *TrafficLog) Dump(handlerFn func(format string, args bacnetip.Args)) {
+ if t == nil {
+ return
+ }
+ for _, args := range t.traffic {
+ argFormat := " %6.3f:"
+ for _, arg := range args.Args[1:] {
+ _ = arg
+ argFormat += " %v"
+ }
+ handlerFn(argFormat, args.Args)
+ }
+}
diff --git a/plc4go/internal/bacnetip/tests/test_apdu/test_max_apdu_length_accepted_test.go b/plc4go/internal/bacnetip/tests/test_apdu/test_max_apdu_length_accepted_test.go
new file mode 100644
index 0000000000..704af458ea
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/test_apdu/test_max_apdu_length_accepted_test.go
@@ -0,0 +1,47 @@
+/*
+ * 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 test_apdu
+
+import (
+ "context"
+ "testing"
+
+ "github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestMaxApduLengthAcceptedEncode(t *testing.T) {
+ t.Skip("Plc4x doesn't normalise at model level")
+ apdu := model.NewAPDU(50)
+ assert.Equal(t, 0, 50, apdu.ApduLength)
+}
+
+func TestMaxApduLengthAcceptedDecode(t *testing.T) {
+ t.Skip("Plc4x doesn't normalise at model level")
+ apdu := model.NewAPDU(0)
+ serialize, err := apdu.Serialize()
+ require.NoError(t, err)
+ apduParse, err := model.APDUParse(context.Background(), serialize, 0)
+ require.NoError(t, err)
+ // TODO: no way to access the length
+ _ = apduParse
+}
diff --git a/plc4go/internal/bacnetip/tests/test_base_types/test_name_value_test.go b/plc4go/internal/bacnetip/tests/test_base_types/test_name_value_test.go
new file mode 100644
index 0000000000..78b3f753f8
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/test_base_types/test_name_value_test.go
@@ -0,0 +1,22 @@
+/*
+ * 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 test_base_types
+
+// TODO:
diff --git a/plc4go/internal/bacnetip/tests/test_bvll/helpers.go b/plc4go/internal/bacnetip/tests/test_bvll/helpers.go
new file mode 100644
index 0000000000..c793f5b3e7
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/test_bvll/helpers.go
@@ -0,0 +1,293 @@
+/*
+ * 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 test_bvll
+
+import (
+ "fmt"
+
+ "github.com/apache/plc4x/plc4go/internal/bacnetip"
+ "github.com/apache/plc4x/plc4go/internal/bacnetip/tests"
+
+ "github.com/pkg/errors"
+ "github.com/rs/zerolog"
+)
+
+type _NetworkServiceElement struct {
+ *bacnetip.NetworkServiceElement
+}
+
+func new_NetworkServiceElement(localLog zerolog.Logger) (*_NetworkServiceElement, error) {
+ i := &_NetworkServiceElement{}
+
+ // This class turns off the deferred startup function call that broadcasts
+ // I-Am-Router-To-Network and Network-Number-Is messages.
+ var err error
+ i.NetworkServiceElement, err = bacnetip.NewNetworkServiceElement(localLog, nil, true)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating network service element")
+ }
+ return i, nil
+}
+
+type FauxMultiplexer struct {
+ *bacnetip.Client
+ *bacnetip.Server
+
+ address *bacnetip.Address
+ unicastTuple *bacnetip.AddressTuple[string, uint16]
+ broadcastTuple *bacnetip.AddressTuple[string, uint16]
+
+ node *bacnetip.IPNode
+
+ log zerolog.Logger
+}
+
+func NewFauxMultiplexer(localLog zerolog.Logger, addr *bacnetip.Address, network *bacnetip.IPNetwork) (*FauxMultiplexer, error) {
+ f := &FauxMultiplexer{
+ address: addr,
+ log: localLog,
+ }
+ var err error
+ f.Client, err = bacnetip.NewClient(localLog, f)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating client")
+ }
+ f.Server, err = bacnetip.NewServer(localLog, f)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating server")
+ }
+
+ // get the unicast and broadcast tuples
+ f.unicastTuple = addr.AddrTuple
+ f.broadcastTuple = addr.AddrBroadcastTuple
+
+ // make an internal node and bind to it, this takes the place of
+ // both the direct port and broadcast port of the real UDPMultiplexer
+ f.node, err = bacnetip.NewIPNode(localLog, addr, network)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating ip node")
+ }
+ if err := bacnetip.Bind(localLog, f, f.node); err != nil {
+ return nil, errors.Wrap(err, "error binding")
+ }
+ return f, nil
+}
+
+func (s *FauxMultiplexer) String() string {
+ return fmt.Sprintf("FauxMultiplexer(TBD...)") // TODO: fill some info here
+}
+
+func (s *FauxMultiplexer) Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ s.log.Debug().Stringer("Args", args).Stringer("KWArgs", kwargs).Msg("Indication")
+
+ pdu := args.Get0PDU()
+
+ var dest *bacnetip.Address
+ // check for a broadcast message
+ if pdu.GetPDUDestination().AddrType == bacnetip.LOCAL_BROADCAST_ADDRESS {
+ var err error
+ dest, err = bacnetip.NewAddress(s.log, s.broadcastTuple)
+ if err != nil {
+ return errors.Wrap(err, "error creating address")
+ }
+ s.log.Debug().Stringer("dest", dest).Msg("Requesting local broadcast")
+ } else if pdu.GetPDUDestination().AddrType == bacnetip.LOCAL_STATION_ADDRESS {
+ var err error
+ dest, err = bacnetip.NewAddress(s.log, pdu.GetPDUDestination().AddrAddress)
+ if err != nil {
+ return errors.Wrap(err, "error creating address")
+ }
+ s.log.Debug().Stringer("dest", dest).Msg("Requesting local station")
+ } else {
+ return errors.New("unknown destination type")
+ }
+
+ unicast, err := bacnetip.NewAddress(s.log, s.unicastTuple)
+ if err != nil {
+ return errors.Wrap(err, "error creating address")
+ }
+ return s.Request(bacnetip.NewArgs(bacnetip.NewPDUFromPDU(pdu, bacnetip.WithPDUSource(unicast), bacnetip.WithPDUDestination(dest))), bacnetip.NoKWArgs)
+}
+
+func (s *FauxMultiplexer) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ s.log.Debug().Stringer("Args", args).Stringer("KWArgs", kwargs).Msg("Indication")
+ pdu := args.Get0PDU()
+
+ // the PDU source and destination are tuples, convert them to Address instances
+ src := pdu.GetPDUSource()
+
+ broadcast, err := bacnetip.NewAddress(s.log, s.broadcastTuple)
+ if err != nil {
+ return errors.Wrap(err, "error creating address")
+ }
+ var dest *bacnetip.Address
+ // see if the destination was our broadcast address
+ if pdu.GetPDUDestination().Equals(broadcast) {
+ dest = bacnetip.NewLocalBroadcast(nil)
+ } else {
+ dest, err = bacnetip.NewAddress(s.log, pdu.GetPDUDestination().AddrAddress)
+ if err != nil {
+ return errors.Wrap(err, "error creating address")
+ }
+ }
+
+ return s.Response(bacnetip.NewArgs(bacnetip.NewPDUFromPDU(pdu, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest))), bacnetip.NoKWArgs)
+}
+
+type SnifferStateMachine struct {
+ *tests.ClientStateMachine
+
+ name string
+ address *bacnetip.Address
+ annexj *bacnetip.AnnexJCodec
+ mux *FauxMultiplexer
+
+ log zerolog.Logger
+}
+
+func NewSnifferStateMachine(localLog zerolog.Logger, address string, vlan *bacnetip.IPNetwork) (*SnifferStateMachine, error) {
+ s := &SnifferStateMachine{
+ log: localLog,
+ }
+ machine, err := tests.NewClientStateMachine(localLog)
+ if err != nil {
+ return nil, errors.Wrap(err, "error building client state machine")
+ }
+ s.ClientStateMachine = machine
+
+ // save the name and address
+ s.name = address
+ s.address, err = bacnetip.NewAddress(localLog, address)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating address")
+ }
+
+ // BACnet/IP interpreter
+ s.annexj, err = bacnetip.NewAnnexJCodec(localLog)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating annexj")
+ }
+
+ // fake multiplexer has a VLAN node in it
+ s.mux, err = NewFauxMultiplexer(localLog, s.address, vlan)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating faux multiplexer")
+ }
+
+ // might receive all packets and allow spoofing
+ s.mux.node.SetPromiscuous(true)
+ s.mux.node.SetSpoofing(true)
+
+ // bind the stack together
+ if err := bacnetip.Bind(localLog, s, s.annexj, s.mux); err != nil {
+ return nil, errors.Wrap(err, "error binding")
+ }
+
+ return s, nil
+}
+
+type BIPStateMachine struct {
+ *tests.ClientStateMachine
+}
+
+// TODO: implement BIPStateMachine
+
+type BIPSimpleStateMachine struct {
+ *tests.ClientStateMachine
+ name string
+
+ address *bacnetip.Address
+
+ bip *bacnetip.BIPSimple
+ annexj *bacnetip.AnnexJCodec
+ mux *FauxMultiplexer
+
+ log zerolog.Logger
+}
+
+func NewBIPSimpleStateMachine(name string, localLog zerolog.Logger, netstring string, vlan *bacnetip.IPNetwork) (*BIPSimpleStateMachine, error) {
+ address, err := bacnetip.NewAddress(localLog, netstring)
+ if err != nil {
+ return nil, errors.Wrap(err, "error building address")
+ }
+ stateMachine := &BIPSimpleStateMachine{
+ // save the name and address
+ name: netstring,
+ address: address,
+ log: localLog,
+ }
+ clientStateMachine, err := tests.NewClientStateMachine(localLog, tests.WithClientStateMachineName(name))
+ if err != nil {
+ return nil, errors.Wrap(err, "error building client state machine")
+ }
+ stateMachine.ClientStateMachine = clientStateMachine
+
+ // BACnet/IP interpreter
+ stateMachine.bip, err = bacnetip.NewBIPSimple(localLog)
+ if err != nil {
+ return nil, errors.Wrap(err, "error building bip simple")
+ }
+ stateMachine.annexj, err = bacnetip.NewAnnexJCodec(localLog)
+ if err != nil {
+ return nil, errors.Wrap(err, "error building annexj codec")
+ }
+
+ // fake multiplexer has a VLAN node in it
+ stateMachine.mux, err = NewFauxMultiplexer(localLog, stateMachine.address, vlan)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating faux")
+ }
+
+ // bind the stack together
+ if err := bacnetip.Bind(localLog, stateMachine, stateMachine.bip, stateMachine.annexj, stateMachine.mux); err != nil {
+ return nil, errors.Wrap(err, "error binding")
+ }
+
+ return stateMachine, nil
+}
+
+type BIPForeignStateMachine struct {
+ *tests.ClientStateMachine
+}
+
+type BIPBBMDStateMachine struct {
+ *tests.ClientStateMachine
+}
+
+type BIPSimpleNode struct {
+}
+
+type BIPBBMDNode struct {
+}
+
+type TestDeviceObject struct {
+ *bacnetip.LocalDeviceObject
+}
+
+type BIPSimpleApplicationLayerStateMachine struct {
+ *bacnetip.ApplicationServiceElement
+ *tests.ClientStateMachine
+}
+
+type BIPBBMDApplication struct {
+ *bacnetip.Application
+ *bacnetip.WhoIsIAmServices
+ *bacnetip.ReadWritePropertyServices
+}
diff --git a/plc4go/internal/bacnetip/tests/test_bvll/test_simple_test.go b/plc4go/internal/bacnetip/tests/test_bvll/test_simple_test.go
new file mode 100644
index 0000000000..6b237ffae0
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/test_bvll/test_simple_test.go
@@ -0,0 +1,215 @@
+/*
+ * 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 test_bvll
+
+import (
+ "testing"
+ "time"
+
+ "github.com/apache/plc4x/plc4go/internal/bacnetip"
+ "github.com/apache/plc4x/plc4go/internal/bacnetip/tests"
+ "github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model"
+ "github.com/apache/plc4x/plc4go/spi/testutils"
+
+ "github.com/rs/zerolog"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type TNetwork struct {
+ *tests.StateMachineGroup
+ vlan *bacnetip.IPNetwork
+ td *BIPSimpleStateMachine
+ iut *BIPSimpleStateMachine
+ sniffer *SnifferStateMachine
+
+ t *testing.T
+
+ log zerolog.Logger
+}
+
+func NewTNetwork(t *testing.T) *TNetwork {
+ localLog := testutils.ProduceTestingLogger(t)
+ tn := &TNetwork{
+ t: t,
+ log: localLog,
+ }
+ tn.StateMachineGroup = tests.NewStateMachineGroup(localLog)
+
+ tests.NewGlobalTimeMachine(localLog) // TODO: this is really stupid because of concurrency...
+ // reset the time machine
+ tests.ResetTimeMachine(tests.StartTime)
+ localLog.Trace().Msg("time machine reset")
+
+ // make a little LAN
+ tn.vlan = bacnetip.NewIPNetwork(localLog)
+
+ // Test devices
+ var err error
+ tn.td, err = NewBIPSimpleStateMachine("td", localLog, "192.168.4.1/24", tn.vlan)
+ require.NoError(t, err)
+ tn.Append(tn.td)
+
+ // implementation under test
+ tn.iut, err = NewBIPSimpleStateMachine("iut", localLog, "192.168.4.2/24", tn.vlan)
+ require.NoError(t, err)
+ tn.Append(tn.iut)
+
+ // sniffer node
+ tn.sniffer, err = NewSnifferStateMachine(localLog, "192.168.4.254/24", tn.vlan)
+ require.NoError(t, err)
+ tn.Append(tn.sniffer)
+ return tn
+}
+
+func (t *TNetwork) Run(timeLimit time.Duration) {
+ if timeLimit == 0 {
+ timeLimit = 60 * time.Second
+ }
+ t.log.Debug().Dur("time_limit", timeLimit).Msg("run")
+
+ // run the group
+ err := t.StateMachineGroup.Run()
+ require.NoError(t.t, err)
+
+ // run it some time
+ tests.RunTimeMachine(t.log, timeLimit, time.Time{})
+ t.log.Trace().Msg("time machine finished")
+ for _, machine := range t.StateMachineGroup.GetStateMachines() {
+ t.log.Debug().Stringer("machine", machine).Msg("Machine:")
+ for _, s := range machine.GetTransactionLog() {
+ t.log.Debug().Str("logEntry", s).Msg("logEntry")
+ }
+ }
+
+ // check for success
+ success, failed := t.CheckForSuccess()
+ assert.True(t.t, success)
+ assert.False(t.t, failed)
+}
+
+func TestSimple(t *testing.T) {
+ t.Run("test_idle", func(t *testing.T) { //Test an idle network, nothing happens is success.
+ tests.LockGlobalTimeMachine(t)
+ tnet := NewTNetwork(t)
+
+ // all start state are successful
+ tnet.td.GetStartState().Success("")
+ tnet.iut.GetStartState().Success("")
+ tnet.sniffer.GetStartState().Success("")
+
+ // run the group
+ tnet.Run(0)
+ })
+ t.Run("test_unicast", func(t *testing.T) { //Test a unicast message from TD to IUT.
+ t.Skip("not ready yet") // TODO: figure out why it is failing
+ tests.LockGlobalTimeMachine(t)
+ tnet := NewTNetwork(t)
+
+ //make a PDU from node 1 to node 2
+ pduData, err := bacnetip.Xtob("dead.beef")
+ require.NoError(t, err)
+ apdu := model.NewAPDUUnknown(0, pduData, 0)
+ control := model.NewNPDUControl(false, true, true, false, model.NPDUNetworkPriority_CRITICAL_EQUIPMENT_MESSAGE)
+ sourceAddr := tnet.td.address
+ destAddr := tnet.iut.address
+ npdu := model.NewNPDU(0,
+ control,
+ destAddr.AddrNet,
+ destAddr.AddrLen,
+ destAddr.AddrAddress,
+ sourceAddr.AddrNet,
+ sourceAddr.AddrLen,
+ sourceAddr.AddrAddress,
+ nil,
+ nil,
+ apdu,
+ 0)
+ t.Logf("pdu: \n%v", npdu)
+
+ // test device sends it, iut gets it
+ pdu := bacnetip.NewPDU(npdu)
+ pdu.SetPDUSource(tnet.td.address)
+ pdu.SetPDUDestination(tnet.iut.address)
+ tnet.td.GetStartState().Send(pdu, nil).Success("")
+ tnet.iut.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{
+ "pduSource": tnet.td.address,
+ }).Success("")
+
+ // sniffer sees message on the wire
+ tnet.sniffer.GetStartState().Receive(bacnetip.NewPDU(npdu), map[string]any{
+ "pduSource": tnet.td.address.AddrTuple,
+ "pduDestination": tnet.iut.address.AddrTuple,
+ "pduData": pduData,
+ },
+ ).Timeout(1.0*time.Millisecond, nil).Success("")
+
+ // run the group
+ tnet.Run(0)
+ })
+ t.Run("test_broadcast", func(t *testing.T) { //Test a broadcast message from TD to IUT.
+ t.Skip("not ready yet") // TODO: figure out why it is failing
+ tests.LockGlobalTimeMachine(t)
+ tnet := NewTNetwork(t)
+
+ //make a PDU from node 1 to node 2
+ pduData, err := bacnetip.Xtob("dead.beef")
+ require.NoError(t, err)
+ apdu := model.NewAPDUUnknown(0, pduData, 0)
+ control := model.NewNPDUControl(false, true, true, false, model.NPDUNetworkPriority_CRITICAL_EQUIPMENT_MESSAGE)
+ sourceAddr := tnet.td.address
+ destAddr := bacnetip.NewLocalBroadcast(nil)
+ {
+ // TODO: why is this uncommented upstream
+ destAddr, err = bacnetip.NewAddress(testutils.ProduceTestingLogger(t), "192.168.4.255")
+ require.NoError(t, err)
+ }
+ npdu := model.NewNPDU(0,
+ control,
+ destAddr.AddrNet,
+ destAddr.AddrLen,
+ destAddr.AddrAddress,
+ sourceAddr.AddrNet,
+ sourceAddr.AddrLen,
+ sourceAddr.AddrAddress,
+ nil,
+ nil,
+ apdu,
+ 0)
+ t.Logf("pdu: \n%v", npdu)
+
+ // test device sends it, iut gets it
+ tnet.td.GetStartState().Send(bacnetip.NewPDU(npdu, bacnetip.WithPDUSource(tnet.td.address), bacnetip.WithPDUDestination(bacnetip.NewLocalBroadcast(nil))), nil).Success("")
+ tnet.iut.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{
+ "pduSource": tnet.td.address,
+ }).Success("")
+
+ // sniffer sees message on the wire
+ tnet.sniffer.GetStartState().Receive(bacnetip.NewPDU(npdu), map[string]any{
+ "pduSource": tnet.td.address.AddrTuple,
+ //"pduDestination": tnet.iut.address.AddrTuple,
+ "pduData": pduData,
+ },
+ ).Timeout(1.0*time.Second, nil).Success("")
+
+ // run the group
+ tnet.Run(0)
+ })
+}
diff --git a/plc4go/internal/bacnetip/tests/test_pdu/test_address_test.go b/plc4go/internal/bacnetip/tests/test_pdu/test_address_test.go
new file mode 100644
index 0000000000..f112bd5fe4
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/test_pdu/test_address_test.go
@@ -0,0 +1,649 @@
+/*
+ * 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 test_pdu
+
+import (
+ "encoding/hex"
+ "testing"
+
+ "github.com/apache/plc4x/plc4go/internal/bacnetip"
+ "github.com/apache/plc4x/plc4go/spi/testutils"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// Assert that the type, network, length, and address are what
+//
+// they should be. Note that the address parameter is a hex string
+// that will be converted to bytes for comparison.
+//
+// :param addr: the address to match
+// :param t: the address type
+// :param n: the network number
+// :param l: the address length
+// :param a: the address expressed as hex bytes
+func matchAddress(_t *testing.T, addr *bacnetip.Address, t bacnetip.AddressType, n *uint16, l *uint8, a string) {
+ _t.Helper()
+ assert.Equal(_t, addr.AddrType, t)
+ assert.Equal(_t, addr.AddrNet, n)
+ assert.Equal(_t, addr.AddrLen, l)
+ if a == "" {
+ assert.Nil(_t, addr.AddrAddress)
+ } else {
+ decodeString, err := hex.DecodeString(a)
+ require.NoError(_t, err)
+ assert.Equal(_t, addr.AddrAddress, decodeString)
+ }
+}
+
+func init() { // TODO: maybe put in a setupsuite
+ bacnetip.Settings.RouteAware = true
+}
+
+func TestAddress(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // null address
+ testAddr, err := bacnetip.NewAddress(testingLogger)
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.NULL_ADDRESS, nil, nil, "")
+}
+
+func TestAddressInt(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // test integer local station
+ testAddr, err := bacnetip.NewAddress(testingLogger, 1)
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(1), "01")
+ assert.Equal(t, "1", testAddr.String())
+
+ testAddr, err = bacnetip.NewAddress(testingLogger, 254)
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(1), "fe")
+ assert.Equal(t, "254", testAddr.String())
+
+ // Test bad integer
+ _, err = bacnetip.NewAddress(testingLogger, -1)
+ assert.Error(t, err)
+
+ _, err = bacnetip.NewAddress(testingLogger, 256)
+ assert.Error(t, err)
+}
+
+func TestAddressIpv4Str(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // test IPv4 local station address
+ testAddr, err := bacnetip.NewAddress(testingLogger, "1.2.3.4")
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(6), "01020304BAC0")
+ assert.Equal(t, "1.2.3.4", testAddr.String())
+
+ // test IPv4 local station address with non-standard port
+ testAddr, err = bacnetip.NewAddress(testingLogger, "1.2.3.4:47809")
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(6), "01020304BAC1")
+ assert.Equal(t, "1.2.3.4:47809", testAddr.String())
+
+ // test IPv4 local station address with unrecognized port
+ testAddr, err = bacnetip.NewAddress(testingLogger, "1.2.3.4:47999")
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(6), "01020304bb7f")
+ assert.Equal(t, "0x01020304bb7f", testAddr.String())
+}
+
+func TestAddressEthStr(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // test IPv4 local station address
+ testAddr, err := bacnetip.NewAddress(testingLogger, "01:02:03:04:05:06")
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(6), "010203040506")
+ assert.Equal(t, "0x010203040506", testAddr.String())
+}
+
+func TestAddressLocalStationStr(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // test integer local station
+ testAddr, err := bacnetip.NewAddress(testingLogger, "1")
+ require.NoError(t, err)
+ assert.Equal(t, "1", testAddr.String())
+
+ testAddr, err = bacnetip.NewAddress(testingLogger, "254")
+ require.NoError(t, err)
+ assert.Equal(t, "254", testAddr.String())
+
+ // Test bad integer
+ _, err = bacnetip.NewAddress(testingLogger, 256)
+ assert.Error(t, err)
+
+ // test modern hex string
+ testAddr, err = bacnetip.NewAddress(testingLogger, "0x01")
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(1), "01")
+ assert.Equal(t, "1", testAddr.String())
+
+ testAddr, err = bacnetip.NewAddress(testingLogger, "0x0102")
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(2), "0102")
+ assert.Equal(t, "0x0102", testAddr.String())
+
+ // test old school hex string
+ testAddr, err = bacnetip.NewAddress(testingLogger, "X'01'")
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(1), "01")
+ assert.Equal(t, "1", testAddr.String())
+
+ testAddr, err = bacnetip.NewAddress(testingLogger, "X'0102'")
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(2), "0102")
+ assert.Equal(t, "0x0102", testAddr.String())
+}
+
+func TestAddressLocalBroadcastStr(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // test IPv4 local station address
+ testAddr, err := bacnetip.NewAddress(testingLogger, "*")
+ require.NoError(t, err)
+ assert.Equal(t, "*", testAddr.String())
+}
+
+func TestAddressRemoteBroadcastStr(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // test IPv4 local station address
+ testAddr, err := bacnetip.NewAddress(testingLogger, "1:*")
+ require.NoError(t, err)
+ assert.Equal(t, "1:*", testAddr.String())
+}
+
+func TestAddressRemoteStationStr(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // test IPv4 local station address
+ testAddr, err := bacnetip.NewAddress(testingLogger, "1:2")
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(1), "02")
+ assert.Equal(t, "1:2", testAddr.String())
+
+ testAddr, err = bacnetip.NewAddress(testingLogger, "1:254")
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(1), "fe")
+ assert.Equal(t, "1:254", testAddr.String())
+
+ // test bad network and mode
+ _, err = bacnetip.NewAddress(testingLogger, "65536:2")
+ assert.Error(t, err)
+ _, err = bacnetip.NewAddress(testingLogger, "1:256")
+ assert.Error(t, err)
+
+ // test moder hex string
+ testAddr, err = bacnetip.NewAddress(testingLogger, "1:0x02")
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(1), "02")
+ assert.Equal(t, "1:2", testAddr.String())
+
+ // test bad network
+ _, err = bacnetip.NewAddress(testingLogger, "65536:0x02")
+ assert.Error(t, err)
+
+ testAddr, err = bacnetip.NewAddress(testingLogger, "1:0x0203")
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(2), "0203")
+ assert.Equal(t, "1:0x0203", testAddr.String())
+
+ // test old school hex
+ testAddr, err = bacnetip.NewAddress(testingLogger, "1:X'02'")
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(1), "02")
+ assert.Equal(t, "1:2", testAddr.String())
+
+ testAddr, err = bacnetip.NewAddress(testingLogger, "1:X'0203'")
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(2), "0203")
+ assert.Equal(t, "1:0x0203", testAddr.String())
+
+ _, err = bacnetip.NewAddress(testingLogger, "65536:X'02'")
+ assert.Error(t, err)
+}
+
+func TestAddressGlobalBroadcastStr(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // test IPv4 local station address
+ testAddr, err := bacnetip.NewAddress(testingLogger, "*:*")
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.GLOBAL_BROADCAST_ADDRESS, nil, nil, "")
+ assert.Equal(t, "*:*", testAddr.String())
+}
+
+func TestLocalStation(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // one Parameter
+ _, err := bacnetip.NewLocalStation(testingLogger, nil, nil)
+ require.Error(t, err)
+
+ // test integer
+ testAddr, err := bacnetip.NewLocalStation(testingLogger, 1, nil)
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(1), "01")
+ assert.Equal(t, "1", testAddr.String())
+
+ testAddr, err = bacnetip.NewLocalStation(testingLogger, 254, nil)
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(1), "fe")
+ assert.Equal(t, "254", testAddr.String())
+
+ // Test bad integer
+ _, err = bacnetip.NewLocalStation(testingLogger, -1, nil)
+ require.Error(t, err)
+ _, err = bacnetip.NewLocalStation(testingLogger, 256, nil)
+ require.Error(t, err)
+
+ // Test bytes
+ xtob, err := bacnetip.Xtob("01")
+ require.NoError(t, err)
+ testAddr, err = bacnetip.NewLocalStation(testingLogger, xtob, nil)
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(1), "01")
+ assert.Equal(t, "1", testAddr.String())
+ xtob, err = bacnetip.Xtob("fe")
+ require.NoError(t, err)
+ testAddr, err = bacnetip.NewLocalStation(testingLogger, xtob, nil)
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(1), "fe")
+ assert.Equal(t, "254", testAddr.String())
+
+ // multi-byte strings are hex encoded
+ xtob, err = bacnetip.Xtob("0102")
+ require.NoError(t, err)
+ testAddr, err = bacnetip.NewLocalStation(testingLogger, xtob, nil)
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(2), "0102")
+ assert.Equal(t, "0x0102", testAddr.String())
+
+ xtob, err = bacnetip.Xtob("010203")
+ require.NoError(t, err)
+ testAddr, err = bacnetip.NewLocalStation(testingLogger, xtob, nil)
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(3), "010203")
+ assert.Equal(t, "0x010203", testAddr.String())
+
+ // match and IP address
+ xtob, err = bacnetip.Xtob("01020304bac0")
+ require.NoError(t, err)
+ testAddr, err = bacnetip.NewLocalStation(testingLogger, xtob, nil)
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.LOCAL_STATION_ADDRESS, nil, l(6), "01020304bac0")
+ assert.Equal(t, "1.2.3.4", testAddr.String())
+}
+
+func TestRemoteStation(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // two Parameters, correct types
+ _, err := bacnetip.NewRemoteStation(testingLogger, nil, nil, nil)
+ require.Error(t, err)
+
+ // test bad network
+ _, err = bacnetip.NewRemoteStation(testingLogger, nil, -11, nil)
+ require.Error(t, err)
+}
+
+func TestRemoteStationInts(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ net := func(i uint16) *uint16 {
+ return &i
+ }
+
+ // testInteger
+ testAddr, err := bacnetip.NewRemoteStation(testingLogger, net(1), 1, nil)
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(1), "01")
+ assert.Equal(t, "1:1", testAddr.String())
+
+ testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), 254, nil)
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(1), "fe")
+ assert.Equal(t, "1:254", testAddr.String())
+
+ // test station address
+ _, err = bacnetip.NewRemoteStation(testingLogger, nil, -1, nil)
+ require.Error(t, err)
+ _, err = bacnetip.NewRemoteStation(testingLogger, nil, 256, nil)
+ require.Error(t, err)
+}
+
+func TestRemoteStationBytes(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ net := func(i uint16) *uint16 {
+ return &i
+ }
+
+ // multi-byte strings are hex encoded
+ xtob, err := bacnetip.Xtob("0102")
+ require.NoError(t, err)
+ testAddr, err := bacnetip.NewRemoteStation(testingLogger, net(1), xtob, nil)
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(2), "0102")
+ assert.Equal(t, "1:0x0102", testAddr.String())
+
+ xtob, err = bacnetip.Xtob("010203")
+ require.NoError(t, err)
+ testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), xtob, nil)
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(3), "010203")
+ assert.Equal(t, "1:0x010203", testAddr.String())
+
+ // match with IPv4 address
+ xtob, err = bacnetip.Xtob("01020304bac0")
+ require.NoError(t, err)
+ testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), xtob, nil)
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(6), "01020304bac0")
+ assert.Equal(t, "1:1.2.3.4", testAddr.String())
+
+ xtob, err = bacnetip.Xtob("01020304bac1")
+ require.NoError(t, err)
+ testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), xtob, nil)
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(6), "01020304bac1")
+ assert.Equal(t, "1:1.2.3.4:47809", testAddr.String())
+}
+
+func TestRemoteStationIntsRouted(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ net := func(i uint16) *uint16 {
+ return &i
+ }
+
+ Address := func(a string) *bacnetip.Address {
+ address, err := bacnetip.NewAddress(testingLogger, a)
+ require.NoError(t, err)
+ return address
+ }
+
+ // testInteger
+ testAddr, err := bacnetip.NewRemoteStation(testingLogger, net(1), 1, Address("1.2.3.4"))
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(1), "01")
+ assert.Equal(t, "1:1@1.2.3.4", testAddr.String())
+
+ testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), 254, Address("1.2.3.4"))
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(1), "fe")
+ assert.Equal(t, "1:254@1.2.3.4", testAddr.String())
+
+ testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), 254, Address("1.2.3.4:47809"))
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(1), "fe")
+ assert.Equal(t, "1:254@1.2.3.4:47809", testAddr.String())
+
+ testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), 254, Address("0x01020304BAC0"))
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(1), "fe")
+ assert.Equal(t, "1:254@1.2.3.4", testAddr.String())
+
+ testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), 254, Address("0x01020304BAC1"))
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(1), "fe")
+ assert.Equal(t, "1:254@1.2.3.4:47809", testAddr.String())
+
+ // test station address
+ _, err = bacnetip.NewRemoteStation(testingLogger, nil, -1, nil)
+ require.Error(t, err)
+ _, err = bacnetip.NewRemoteStation(testingLogger, nil, 256, nil)
+ require.Error(t, err)
+}
+
+func TestRemoteStationBytesRouted(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ net := func(i uint16) *uint16 {
+ return &i
+ }
+
+ Address := func(a string) *bacnetip.Address {
+ address, err := bacnetip.NewAddress(testingLogger, a)
+ require.NoError(t, err)
+ return address
+ }
+
+ // multi-byte strings are hex encoded
+ xtob, err := bacnetip.Xtob("0102")
+ require.NoError(t, err)
+ testAddr, err := bacnetip.NewRemoteStation(testingLogger, net(1), xtob, Address("1.2.3.4"))
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(2), "0102")
+ assert.Equal(t, "1:0x0102@1.2.3.4", testAddr.String())
+
+ xtob, err = bacnetip.Xtob("010203")
+ require.NoError(t, err)
+ testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), xtob, Address("1.2.3.4"))
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(3), "010203")
+ assert.Equal(t, "1:0x010203@1.2.3.4", testAddr.String())
+
+ xtob, err = bacnetip.Xtob("010203")
+ require.NoError(t, err)
+ testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), xtob, Address("1.2.3.4:47809"))
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(3), "010203")
+ assert.Equal(t, "1:0x010203@1.2.3.4:47809", testAddr.String())
+
+ xtob, err = bacnetip.Xtob("010203")
+ require.NoError(t, err)
+ testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), xtob, Address("0x01020304BAC0"))
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(3), "010203")
+ assert.Equal(t, "1:0x010203@1.2.3.4", testAddr.String())
+
+ xtob, err = bacnetip.Xtob("010203")
+ require.NoError(t, err)
+ testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), xtob, Address("0x01020304BAC1"))
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(3), "010203")
+ assert.Equal(t, "1:0x010203@1.2.3.4:47809", testAddr.String())
+
+ // match with an IPv4 address
+ xtob, err = bacnetip.Xtob("01020304bac0")
+ require.NoError(t, err)
+ testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), xtob, Address("1.2.3.4"))
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(6), "01020304bac0")
+ assert.Equal(t, "1:1.2.3.4@1.2.3.4", testAddr.String())
+
+ xtob, err = bacnetip.Xtob("01020304bac0")
+ require.NoError(t, err)
+ testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), xtob, Address("1.2.3.4:47809"))
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(6), "01020304bac0")
+ assert.Equal(t, "1:1.2.3.4@1.2.3.4:47809", testAddr.String())
+
+ xtob, err = bacnetip.Xtob("01020304bac0")
+ require.NoError(t, err)
+ testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), xtob, Address("0x01020304BAC0"))
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(6), "01020304bac0")
+ assert.Equal(t, "1:1.2.3.4@1.2.3.4", testAddr.String())
+
+ xtob, err = bacnetip.Xtob("01020304bac0")
+ require.NoError(t, err)
+ testAddr, err = bacnetip.NewRemoteStation(testingLogger, net(1), xtob, Address("0x01020304BAC1"))
+ require.NoError(t, err)
+ matchAddress(t, testAddr, bacnetip.REMOTE_STATION_ADDRESS, n(1), l(6), "01020304bac0")
+ assert.Equal(t, "1:1.2.3.4@1.2.3.4:47809", testAddr.String())
+}
+
+func TestLocalBroadcast(t *testing.T) {
+ testAddr := bacnetip.NewLocalBroadcast(nil)
+ matchAddress(t, testAddr, bacnetip.LOCAL_BROADCAST_ADDRESS, nil, nil, "")
+ assert.Equal(t, "*", testAddr.String())
+}
+
+func TestLocalBroadcastRouted(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+ Address := func(a string) *bacnetip.Address {
+ address, err := bacnetip.NewAddress(testingLogger, a)
+ require.NoError(t, err)
+ return address
+ }
+
+ testAddr := bacnetip.NewLocalBroadcast(Address("1.2.3.4"))
+ matchAddress(t, testAddr, bacnetip.LOCAL_BROADCAST_ADDRESS, nil, nil, "")
+ assert.Equal(t, "*@1.2.3.4", testAddr.String())
+}
+
+func TestRemoteBroadcast(t *testing.T) {
+ testAddr := bacnetip.NewRemoteBroadcast(1, nil)
+ matchAddress(t, testAddr, bacnetip.REMOTE_BROADCAST_ADDRESS, n(1), nil, "")
+ assert.Equal(t, "1:*", testAddr.String())
+}
+
+func TestRemoteBroadcastRouted(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+ Address := func(a string) *bacnetip.Address {
+ address, err := bacnetip.NewAddress(testingLogger, a)
+ require.NoError(t, err)
+ return address
+ }
+
+ testAddr := bacnetip.NewRemoteBroadcast(1, Address("1.2.3.4"))
+ matchAddress(t, testAddr, bacnetip.REMOTE_BROADCAST_ADDRESS, n(1), nil, "")
+ assert.Equal(t, "1:*@1.2.3.4", testAddr.String())
+}
+
+func TestGlobalBroadcast(t *testing.T) {
+ testAddr := bacnetip.NewGlobalBroadcast(nil)
+ matchAddress(t, testAddr, bacnetip.GLOBAL_BROADCAST_ADDRESS, nil, nil, "")
+ assert.Equal(t, "*:*", testAddr.String())
+}
+
+func TestGlobalBroadcastRouted(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+ Address := func(a string) *bacnetip.Address {
+ address, err := bacnetip.NewAddress(testingLogger, a)
+ require.NoError(t, err)
+ return address
+ }
+
+ testAddr := bacnetip.NewGlobalBroadcast(Address("1.2.3.4"))
+ matchAddress(t, testAddr, bacnetip.GLOBAL_BROADCAST_ADDRESS, nil, nil, "")
+ assert.Equal(t, "*:*@1.2.3.4", testAddr.String())
+
+ testAddr = bacnetip.NewGlobalBroadcast(Address("1.2.3.4:47809"))
+ matchAddress(t, testAddr, bacnetip.GLOBAL_BROADCAST_ADDRESS, nil, nil, "")
+ assert.Equal(t, "*:*@1.2.3.4:47809", testAddr.String())
+}
+
+func TestAddressEquality(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+ Address := func(a any) *bacnetip.Address {
+ address, err := bacnetip.NewAddress(testingLogger, a)
+ require.NoError(t, err)
+ return address
+ }
+ LocalStation := func(addr any) *bacnetip.Address {
+ station, err := bacnetip.NewLocalStation(testingLogger, addr, nil)
+ require.NoError(t, err)
+ return station
+ }
+ RemoteStation := func(net uint16, addr any) *bacnetip.Address {
+ station, err := bacnetip.NewRemoteStation(testingLogger, &net, addr, nil)
+ require.NoError(t, err)
+ return station
+ }
+ LocalBroadcast := func() *bacnetip.Address {
+ broadcast := bacnetip.NewLocalBroadcast(nil)
+ return broadcast
+ }
+ RemoteBroadcast := func(net uint16) *bacnetip.Address {
+ broadcast := bacnetip.NewRemoteBroadcast(net, nil)
+ return broadcast
+ }
+ GlobalBroadcast := func() *bacnetip.Address {
+ broadcast := bacnetip.NewGlobalBroadcast(nil)
+ return broadcast
+ }
+
+ assert.True(t, Address(1).Equals(LocalStation(1)))
+ assert.True(t, Address("2").Equals(LocalStation(2)))
+ assert.True(t, Address("*").Equals(LocalBroadcast()))
+ assert.True(t, Address("3:4").Equals(RemoteStation(3, 4)))
+ assert.True(t, Address("5:*").Equals(RemoteBroadcast(5)))
+ assert.True(t, Address("*:*").Equals(GlobalBroadcast()))
+
+}
+
+func TestAddressEqualityRouted(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+ Address := func(a any) *bacnetip.Address {
+ address, err := bacnetip.NewAddress(testingLogger, a)
+ require.NoError(t, err)
+ return address
+ }
+ RemoteStation := func(net uint16, addr any, route *bacnetip.Address) *bacnetip.Address {
+ station, err := bacnetip.NewRemoteStation(testingLogger, &net, addr, route)
+ require.NoError(t, err)
+ return station
+ }
+ RemoteBroadcast := func(net uint16, route *bacnetip.Address) *bacnetip.Address {
+ broadcast := bacnetip.NewRemoteBroadcast(net, route)
+ return broadcast
+ }
+ GlobalBroadcast := func(route *bacnetip.Address) *bacnetip.Address {
+ broadcast := bacnetip.NewGlobalBroadcast(route)
+ return broadcast
+ }
+
+ assert.True(t, Address("3:4@6.7.8.9").Equals(RemoteStation(3, 4, Address("6.7.8.9"))))
+ assert.True(t, Address("3:4@0x06070809BAC0").Equals(RemoteStation(3, 4, Address("6.7.8.9"))))
+
+ assert.True(t, Address("3:4@6.7.8.9:47809").Equals(RemoteStation(3, 4, Address("6.7.8.9:47809"))))
+ assert.True(t, Address("3:4@0x06070809BAC1").Equals(RemoteStation(3, 4, Address("6.7.8.9:47809"))))
+
+ assert.True(t, Address("5:*@6.7.8.9").Equals(RemoteBroadcast(5, Address("6.7.8.9"))))
+ assert.True(t, Address("5:*@0x06070809BAC0").Equals(RemoteBroadcast(5, Address("6.7.8.9"))))
+
+ assert.True(t, Address("5:*@6.7.8.9:47809").Equals(RemoteBroadcast(5, Address("6.7.8.9:47809"))))
+ assert.True(t, Address("5:*@0x06070809BAC1").Equals(RemoteBroadcast(5, Address("6.7.8.9:47809"))))
+
+ assert.True(t, Address("*:*@6.7.8.9").Equals(GlobalBroadcast(Address("6.7.8.9"))))
+ assert.True(t, Address("*:*@0x06070809BAC0").Equals(GlobalBroadcast(Address("6.7.8.9"))))
+
+ assert.True(t, Address("*:*@6.7.8.9:47809").Equals(GlobalBroadcast(Address("6.7.8.9:47809"))))
+ assert.True(t, Address("*:*@0x06070809BAC1").Equals(GlobalBroadcast(Address("6.7.8.9:47809"))))
+}
+
+func n(n uint16) *uint16 {
+ return &n
+}
+
+func l(l uint8) *uint8 {
+ return &l
+}
diff --git a/plc4go/internal/bacnetip/tests/test_pdu/test_pci_test.go b/plc4go/internal/bacnetip/tests/test_pdu/test_pci_test.go
new file mode 100644
index 0000000000..cb4ce6131c
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/test_pdu/test_pci_test.go
@@ -0,0 +1,22 @@
+/*
+ * 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 test_pdu
+
+// TODO
diff --git a/plc4go/internal/bacnetip/tests/test_pdu/test_pdu_test.go b/plc4go/internal/bacnetip/tests/test_pdu/test_pdu_test.go
new file mode 100644
index 0000000000..def57c62a0
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/test_pdu/test_pdu_test.go
@@ -0,0 +1,22 @@
+/*
+ * 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 test_pdu
+
+// TODO:
diff --git a/plc4go/internal/bacnetip/tests/test_primitive_data/todo b/plc4go/internal/bacnetip/tests/test_primitive_data/todo
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/plc4go/internal/bacnetip/tests/test_segmentation/test_1_test.go b/plc4go/internal/bacnetip/tests/test_segmentation/test_1_test.go
new file mode 100644
index 0000000000..b333fcc609
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/test_segmentation/test_1_test.go
@@ -0,0 +1,558 @@
+/*
+ * 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 test_segmentation
+
+import (
+ "github.com/apache/plc4x/plc4go/internal/bacnetip/tests"
+ "testing"
+ "time"
+
+ "github.com/apache/plc4x/plc4go/internal/bacnetip"
+ "github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model"
+ "github.com/apache/plc4x/plc4go/spi/testutils"
+ "github.com/apache/plc4x/plc4go/spi/utils"
+
+ "github.com/pkg/errors"
+ "github.com/rs/zerolog"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// This struct turns off the deferred startup function call that broadcasts I-Am-Router-To-Network and Network-Number-Is
+//
+// messages.
+type _NetworkServiceElement struct {
+ *bacnetip.NetworkServiceElement
+}
+
+func new_NetworkServiceElement(localLog zerolog.Logger) (*_NetworkServiceElement, error) {
+ n := &_NetworkServiceElement{}
+ var err error
+ n.NetworkServiceElement, err = bacnetip.NewNetworkServiceElement(localLog, nil, true)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating network service element")
+ }
+ return n, nil
+}
+
+type ApplicationNetworkRequirements interface {
+ Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error
+ Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error
+}
+
+type ApplicationNetwork struct {
+ ApplicationNetworkRequirements
+ *tests.StateMachineGroup
+
+ trafficLog *tests.TrafficLog
+ vlan *bacnetip.Network
+ sniffer *SnifferNode
+ td *ApplicationStateMachine
+ iut *ApplicationStateMachine
+
+ log zerolog.Logger
+}
+
+func NewApplicationNetwork(localLog zerolog.Logger, applicationNetworkRequirements ApplicationNetworkRequirements, tdDeviceObject, iutDeviceObject *bacnetip.LocalDeviceObject) (*ApplicationNetwork, error) {
+ a := &ApplicationNetwork{
+ ApplicationNetworkRequirements: applicationNetworkRequirements,
+ log: localLog,
+ }
+ a.StateMachineGroup = tests.NewStateMachineGroup(localLog)
+
+ // Reset the time machine
+ tests.ResetTimeMachine(time.Time{})
+
+ // Create a traffic log
+ a.trafficLog = new(tests.TrafficLog)
+
+ // make a little LAN
+ a.vlan = bacnetip.NewNetwork(a.log, bacnetip.WithNetworkBroadcastAddress(bacnetip.NewLocalBroadcast(nil)), bacnetip.WithNetworkTrafficLogger(a.trafficLog))
+
+ // sniffer
+ var err error
+ a.sniffer, err = NewSnifferNode(a.log, a.vlan)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating sniffer node")
+ }
+
+ // test device
+ a.td, err = NewApplicationStateMachine(a.log, tdDeviceObject, a.vlan, a)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating application state machine")
+ }
+ a.Append(a.td)
+
+ // implementation under test
+ a.iut, err = NewApplicationStateMachine(a.log, iutDeviceObject, a.vlan, a)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating application state machine")
+ }
+ a.Append(a.iut)
+
+ return a, nil
+}
+
+func (a *ApplicationNetwork) Run(timeLimit time.Duration) error {
+ if timeLimit == 0 {
+ timeLimit = 60 * time.Second
+ }
+ a.log.Debug().Dur("timeLimit", timeLimit).Msg("run")
+
+ // Run the group
+ err := a.StateMachineGroup.Run()
+ if err != nil {
+ return errors.Wrap(err, "error running state machine group")
+ }
+ a.log.Trace().Msg("group running")
+
+ // run it for some time
+ tests.RunTimeMachine(a.log, timeLimit, time.Time{})
+ if a.log.Debug().Enabled() {
+ a.log.Debug().Msg("time machine finished")
+ for _, machine := range a.GetStateMachines() {
+ a.log.Debug().Stringer("machine", machine).Msg("machine")
+ for _, entry := range machine.GetTransactionLog() {
+ a.log.Debug().Str("entry", entry).Msg("transaction log entry")
+ }
+ }
+
+ a.trafficLog.Dump(a._debug)
+ }
+
+ // check for success
+ success, failed := a.CheckForSuccess()
+ if !success {
+ return errors.New("application network did not succeed")
+ }
+ _ = failed
+
+ return nil
+}
+
+func (a *ApplicationNetwork) _debug(format string, args bacnetip.Args) {
+ a.log.Debug().Msgf(format, args)
+}
+
+type SnifferNode struct {
+ *bacnetip.Client
+
+ name string
+ address *bacnetip.Address
+ node *bacnetip.Node
+
+ log zerolog.Logger
+}
+
+func NewSnifferNode(localLog zerolog.Logger, vlan *bacnetip.Network) (*SnifferNode, error) {
+ s := &SnifferNode{
+ name: "sniffer",
+ log: localLog,
+ }
+ s.address, _ = bacnetip.NewAddress(localLog)
+ var err error
+ s.Client, err = bacnetip.NewClient(localLog, s)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating client")
+ }
+
+ // create a promiscuous node, added to the network
+ s.node, err = bacnetip.NewNode(localLog, s.address, vlan, bacnetip.WithNodePromiscuous(true))
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating node")
+ }
+ s.log.Debug().Stringer("node", s.node).Msg("node")
+
+ // bind the node
+ err = bacnetip.Bind(s.log, s, s.node)
+ if err != nil {
+ return nil, errors.Wrap(err, "error binding node")
+ }
+ return s, nil
+}
+
+func (s *SnifferNode) Request(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("request")
+ return errors.New("sniffers don't request")
+}
+
+func (s *SnifferNode) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("confirmation")
+ pdu := args.Get0PDU()
+
+ // it's and NPDU
+ npdu := pdu.GetMessage().(model.NPDU)
+
+ // filter out network layer traffic if there is any, probably not
+ if nlm := npdu.GetNlm(); nlm != nil {
+ s.log.Debug().Stringer("nlm", nlm).Msg("network message")
+ return nil
+ }
+
+ // decode as generic APDU
+ apdu := npdu.GetApdu()
+
+ // TODO: not sure what to do here
+ /*
+ # "lift" the source and destination address
+ if npdu.npduSADR:
+ apdu.pduSource = npdu.npduSADR
+ else:
+ apdu.pduSource = npdu.pduSource
+ if npdu.npduDADR:
+ apdu.pduDestination = npdu.npduDADR
+ else:
+ apdu.pduDestination = npdu.pduDestination
+ */
+ _ = apdu
+
+ // TODO: print etc
+
+ return nil
+}
+
+type ApplicationStateMachineRequirements interface {
+ Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error
+}
+
+type ApplicationStateMachine struct {
+ ApplicationStateMachineRequirements
+ *bacnetip.Application
+ tests.StateMachine
+
+ address *bacnetip.Address
+ asap *bacnetip.ApplicationServiceAccessPoint
+ smap *bacnetip.StateMachineAccessPoint
+ nsap *bacnetip.NetworkServiceAccessPoint
+ nse *_NetworkServiceElement
+ node *bacnetip.Node
+
+ confirmedPrivateResult string
+
+ log zerolog.Logger
+}
+
+func NewApplicationStateMachine(localLog zerolog.Logger, localDevice *bacnetip.LocalDeviceObject, vlan *bacnetip.Network, applicationStateMachineRequirements ApplicationStateMachineRequirements) (*ApplicationStateMachine, error) {
+ a := &ApplicationStateMachine{
+ ApplicationStateMachineRequirements: applicationStateMachineRequirements,
+ log: localLog,
+ }
+
+ // build and address and save it
+ _, instance := bacnetip.ObjectIdentifierStringToTuple(localDevice.ObjectIdentifier)
+ var err error
+ a.address, err = bacnetip.NewAddress(a.log, instance)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating address")
+ }
+ a.log.Debug().Stringer("address", a.address).Msg("address")
+
+ // continue with initialization
+ a.Application, err = bacnetip.NewApplication(a.log, localDevice)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating application io controller")
+ }
+ var init func()
+ a.StateMachine, init = tests.NewStateMachine(a.log, a, tests.WithStateMachineName(localDevice.ObjectName))
+ init()
+
+ // include a application decoder
+ a.asap, err = bacnetip.NewApplicationServiceAccessPoint(a.log)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating application service access point")
+ }
+
+ // pass the device object to the state machine access point so it
+ // can know if it should support segmentation
+
+ // the segmentation state machines need access to the same device
+ // information cache as the application
+ deviceInfoCache := a.GetDeviceInfoCache()
+ a.smap, err = bacnetip.NewStateMachineAccessPoint(a.log, localDevice, bacnetip.WithStateMachineAccessPointDeviceInfoCache(deviceInfoCache))
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating state machine access point")
+ }
+
+ // a network service access point will be needed
+ a.nsap, err = bacnetip.NewNetworkServiceAccessPoint(a.log)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating network service access point")
+ }
+
+ // give the NSAP a generic network layer service element
+ a.nse, err = new_NetworkServiceElement(a.log)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating network service element")
+ }
+ err = bacnetip.Bind(a.log, a.nse, a.nsap)
+ if err != nil {
+ return nil, errors.Wrap(err, "error binding network service element")
+ }
+
+ // bind the top layers
+ err = bacnetip.Bind(a.log, a, a.asap, a.smap, a.nsap)
+ if err != nil {
+ return nil, errors.Wrap(err, "error binding top layers")
+ }
+
+ // create a node, added to the network
+ a.node, err = bacnetip.NewNode(a.log, a.address, vlan)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating node")
+ }
+
+ // bind the network service to the node, no network number
+ err = a.nsap.Bind(a.node, nil, nil)
+ if err != nil {
+ return nil, errors.Wrap(err, "error binding nsap")
+ }
+ return a, nil
+}
+
+func (a *ApplicationStateMachine) String() string {
+ return "ApplicationStateMachine" //TODO:
+}
+
+func (a *ApplicationStateMachine) Send(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ a.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Send")
+
+ // send the apdu down the stack
+ return a.Request(args, kwargs)
+}
+
+func (a *ApplicationStateMachine) Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ a.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Indication")
+
+ // let the state machine know the request was received
+ err := a.Receive(args, bacnetip.NoKWArgs)
+ if err != nil {
+ return errors.Wrap(err, "error receiving indication")
+ }
+
+ // allow the application to process it
+ return a.ApplicationStateMachineRequirements.Indication(args, kwargs)
+}
+
+func (a *ApplicationStateMachine) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ a.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Confirmation")
+
+ // forward the confirmation to the state machine
+ return a.Receive(args, kwargs)
+}
+
+// TODO
+func (a *ApplicationStateMachine) doConfirmedPrivateTransferRequest(_ struct{}) {
+ // TODO: who calls that?
+}
+
+type TODOWHATTODOWITHTHAT struct {
+}
+
+func (T TODOWHATTODOWITHTHAT) Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ //TODO implement me
+ panic("implement me")
+}
+
+func (T TODOWHATTODOWITHTHAT) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ //TODO implement me
+ panic("implement me")
+}
+
+func SegmentationTest(t *testing.T, prefix string, cLen, sLen int) {
+ tests.LockGlobalTimeMachine(t)
+ testingLogger := testutils.ProduceTestingLogger(t)
+ tests.NewGlobalTimeMachine(testingLogger)
+
+ // client device object
+ octets206 := model.MaxApduLengthAccepted_NUM_OCTETS_206
+ segmentation := model.BACnetSegmentation_SEGMENTED_BOTH
+ maxSegmentsAccepted := model.MaxSegmentsAccepted_NUM_SEGMENTS_04
+ tdDeviceObject := &bacnetip.LocalDeviceObject{
+ ObjectName: "td",
+ ObjectIdentifier: "device:10",
+ MaximumApduLengthAccepted: &octets206,
+ SegmentationSupported: &segmentation,
+ MaxSegmentsAccepted: &maxSegmentsAccepted,
+ VendorIdentifier: 999,
+ }
+
+ // server device object
+ maxSegmentsAccepted = model.MaxSegmentsAccepted_NUM_SEGMENTS_64
+ iutDeviceObject := &bacnetip.LocalDeviceObject{
+ ObjectName: "td",
+ ObjectIdentifier: "device:10",
+ MaximumApduLengthAccepted: &octets206,
+ SegmentationSupported: &segmentation,
+ MaxSegmentsAccepted: &maxSegmentsAccepted,
+ VendorIdentifier: 999,
+ }
+
+ // create a network
+ anet, err := NewApplicationNetwork(testingLogger, new(TODOWHATTODOWITHTHAT), tdDeviceObject, iutDeviceObject)
+ require.NoError(t, err)
+
+ // tell the device info cache of the client about the server
+ if false {
+ // TODO: what
+ //anet.td.GetDeviceInfoCache().GetDeviceInfo(anet.iut.address.String())
+ // # update the rest of the values
+ // iut_device_info.maxApduLengthAccepted = iut_device_object.maxApduLengthAccepted
+ // iut_device_info.segmentationSupported = iut_device_object.segmentationSupported
+ // iut_device_info.vendorID = iut_device_object.vendorIdentifier
+ // iut_device_info.maxSegmentsAccepted = iut_device_object.maxSegmentsAccepted
+ // iut_device_info.maxNpduLength = s_ndpu_len
+ }
+
+ // tell the device info cache of the server device about the client
+ if false {
+ // TODO: what
+ //anet.iut.GetDeviceInfoCache().GetDeviceInfo(anet.td.address.String())
+ // # update the rest of the values
+ // td_device_info.maxApduLengthAccepted = td_device_object.maxApduLengthAccepted
+ // td_device_info.segmentationSupported = td_device_object.segmentationSupported
+ // td_device_info.vendorID = td_device_object.vendorIdentifier
+ // td_device_info.maxSegmentsAccepted = td_device_object.maxSegmentsAccepted
+ // td_device_info.maxNpduLength = c_ndpu_len
+ }
+
+ // build a request string
+ var requestString string
+ if cLen != 0 {
+ requestString = utils.RandomString(cLen)
+ }
+
+ if sLen != 0 {
+ anet.iut.confirmedPrivateResult = utils.RandomString(sLen)
+ }
+
+ wrapRequestString := func() (
+ numberOfDataElements model.BACnetApplicationTagUnsignedInteger,
+ data []model.BACnetConstructedDataElement,
+ openingTag model.BACnetOpeningTag,
+ peekedTagHeader model.BACnetTagHeader,
+ closingTag model.BACnetClosingTag,
+ tagNumber uint8,
+ arrayIndexArgument model.BACnetTagPayloadUnsignedInteger,
+ ) {
+ numberOfDataElements = model.CreateBACnetApplicationTagUnsignedInteger(1)
+ elementArgCreator := func() (
+ peekedTagHeader model.BACnetTagHeader,
+ applicationTag model.BACnetApplicationTag,
+ contextTag model.BACnetContextTag,
+ constructedData model.BACnetConstructedData,
+ objectTypeArgument model.BACnetObjectType,
+ propertyIdentifierArgument model.BACnetPropertyIdentifier,
+ arrayIndexArgument model.BACnetTagPayloadUnsignedInteger,
+ ) {
+ peekedTagHeader = model.CreateBACnetTagHeaderBalanced(false, 0, 0)
+ applicationTag = model.CreateBACnetApplicationTagCharacterString(model.BACnetCharacterEncoding_ISO_8859_1, requestString)
+ return
+ }
+ data = []model.BACnetConstructedDataElement{
+ model.NewBACnetConstructedDataElement(elementArgCreator()),
+ }
+ openingTag = model.CreateBACnetOpeningTag(2)
+ peekedTagHeader = model.CreateBACnetTagHeaderBalanced(false, 2, 2)
+ closingTag = model.CreateBACnetClosingTag(2)
+ return
+ }
+
+ cpt := model.NewBACnetConfirmedServiceRequestConfirmedPrivateTransfer(
+ model.CreateBACnetVendorIdContextTagged(0, 999),
+ model.CreateBACnetContextTagUnsignedInteger(1, 1),
+ model.NewBACnetConstructedDataUnspecified(wrapRequestString()),
+ 0,
+ )
+
+ apdu := model.NewAPDUConfirmedRequest(
+ false,
+ false,
+ true,
+ maxSegmentsAccepted,
+ model.MaxApduLengthAccepted_NUM_OCTETS_1024,
+ 0,
+ nil,
+ nil,
+ cpt,
+ nil,
+ nil,
+ 0)
+
+ rq := bacnetip.NewPDU(apdu)
+
+ var trq model.BACnetServiceAckConfirmedPrivateTransfer
+ // send the request, get it acked
+ anet.td.GetStartState().Doc(prefix+"-0").
+ Send(rq, nil).Doc(prefix+"-1").
+ Receive(trq, nil).Doc(prefix + "-2").
+ Success("")
+
+ // no IUT application layer matching
+ anet.iut.GetStartState().Success("")
+
+ // run the group
+ err = anet.Run(9)
+ assert.NoError(t, err)
+}
+
+func Test1(t *testing.T) {
+ t.Skip("not ready yet") // TODO: figure out why it is failing
+ SegmentationTest(t, "7-1", 0, 0)
+}
+
+func Test2(t *testing.T) {
+ t.Skip("not ready yet") // TODO: figure out why it is failing
+ SegmentationTest(t, "7-2", 10, 0)
+}
+
+func Test3(t *testing.T) {
+ t.Skip("not ready yet") // TODO: figure out why it is failing
+ SegmentationTest(t, "7-3", 100, 0)
+}
+
+func Test4(t *testing.T) {
+ t.Skip("not ready yet") // TODO: figure out why it is failing
+ SegmentationTest(t, "7-4", 200, 0)
+}
+
+func Test5(t *testing.T) {
+ t.Skip("not ready yet") // TODO: figure out why it is failing
+ SegmentationTest(t, "7-5", 0, 10)
+}
+
+func Test6(t *testing.T) {
+ t.Skip("not ready yet") // TODO: figure out why it is failing
+ SegmentationTest(t, "7-6", 0, 200)
+}
+
+func Test7(t *testing.T) {
+ t.Skip("not ready yet") // TODO: figure out why it is failing
+ SegmentationTest(t, "7-7", 300, 0)
+}
+
+func Test8(t *testing.T) {
+ t.Skip("not ready yet") // TODO: figure out why it is failing
+ SegmentationTest(t, "7-8", 300, 300)
+}
+
+func Test9(t *testing.T) {
+ t.Skip("not ready yet") // TODO: figure out why it is failing
+ SegmentationTest(t, "7-9", 600, 600)
+}
diff --git a/plc4go/internal/bacnetip/tests/test_service/helpers.go b/plc4go/internal/bacnetip/tests/test_service/helpers.go
new file mode 100644
index 0000000000..997c11987c
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/test_service/helpers.go
@@ -0,0 +1,492 @@
+/*
+ * 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 test_service
+
+import (
+ "time"
+
+ "github.com/apache/plc4x/plc4go/internal/bacnetip"
+ "github.com/apache/plc4x/plc4go/internal/bacnetip/tests"
+ "github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model"
+
+ "github.com/pkg/errors"
+ "github.com/rs/zerolog"
+)
+
+// This struct turns off the deferred startup function call that broadcasts I-Am-Router-To-Network and Network-Number-Is
+//
+// messages.
+type _NetworkServiceElement struct {
+ *bacnetip.NetworkServiceElement
+}
+
+func new_NetworkServiceElement(localLog zerolog.Logger) (*_NetworkServiceElement, error) {
+ n := &_NetworkServiceElement{}
+ var err error
+ n.NetworkServiceElement, err = bacnetip.NewNetworkServiceElement(localLog, nil, true)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating network service element")
+ }
+ return n, nil
+}
+
+type ApplicationNetworkRequirements interface {
+ Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error
+ Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error
+}
+
+type ApplicationNetwork struct {
+ ApplicationNetworkRequirements
+ *tests.StateMachineGroup
+
+ trafficLog *tests.TrafficLog
+ vlan *bacnetip.Network
+ tdDeviceObject *bacnetip.LocalDeviceObject
+ td *ApplicationStateMachine
+ iutDeviceObject *bacnetip.LocalDeviceObject
+ iut *ApplicationStateMachine
+
+ log zerolog.Logger
+}
+
+func NewApplicationNetwork(localLog zerolog.Logger, applicationNetworkRequirements ApplicationNetworkRequirements) (*ApplicationNetwork, error) {
+ a := &ApplicationNetwork{
+ ApplicationNetworkRequirements: applicationNetworkRequirements,
+ log: localLog,
+ }
+ a.StateMachineGroup = tests.NewStateMachineGroup(localLog)
+
+ // Reset the time machine
+ tests.ResetTimeMachine(time.Time{})
+
+ // Create a traffic log
+ a.trafficLog = new(tests.TrafficLog)
+
+ // make a little LAN
+ a.vlan = bacnetip.NewNetwork(localLog, bacnetip.WithNetworkBroadcastAddress(bacnetip.NewLocalBroadcast(nil)), bacnetip.WithNetworkTrafficLogger(a.trafficLog))
+
+ // test device object
+ octets1024 := model.MaxApduLengthAccepted_NUM_OCTETS_1024
+ segmentation := model.BACnetSegmentation_NO_SEGMENTATION
+ a.tdDeviceObject = &bacnetip.LocalDeviceObject{
+ ObjectName: "td",
+ ObjectIdentifier: "device:10",
+ MaximumApduLengthAccepted: &octets1024,
+ SegmentationSupported: &segmentation,
+ VendorIdentifier: 999,
+ }
+
+ // test device
+ var err error
+ a.td, err = NewApplicationStateMachine(localLog, a.tdDeviceObject, a.vlan, a)
+ if err != nil {
+ return nil, errors.Wrap(err, "error building application state machine")
+ }
+ a.Append(a.td)
+
+ // implementation under test device object
+ octets1024 = model.MaxApduLengthAccepted_NUM_OCTETS_1024
+ segmentation = model.BACnetSegmentation_NO_SEGMENTATION
+ a.iutDeviceObject = &bacnetip.LocalDeviceObject{
+ ObjectName: "iut",
+ ObjectIdentifier: "device:20",
+ MaximumApduLengthAccepted: &octets1024,
+ SegmentationSupported: &segmentation,
+ VendorIdentifier: 999,
+ }
+
+ // implementation under test
+ a.iut, err = NewApplicationStateMachine(localLog, a.iutDeviceObject, a.vlan, a)
+ if err != nil {
+ return nil, errors.Wrap(err, "error building application state machine")
+ }
+ a.Append(a.iut)
+
+ return a, nil
+}
+
+func (a *ApplicationNetwork) Run(timeLimit time.Duration) error {
+ if timeLimit == 0 {
+ timeLimit = 60 * time.Second
+ }
+ a.log.Debug().Dur("timeLimit", timeLimit).Msg("run")
+
+ // Run the group
+ err := a.StateMachineGroup.Run()
+ if err != nil {
+ return errors.Wrap(err, "error running state machine group")
+ }
+ a.log.Trace().Msg("group running")
+
+ // run it for some time
+ tests.RunTimeMachine(a.log, timeLimit, time.Time{})
+ if a.log.Debug().Enabled() {
+ a.log.Debug().Msg("time machine finished")
+ for _, machine := range a.GetStateMachines() {
+ a.log.Debug().Stringer("machine", machine).Msg("machine")
+ for _, entry := range machine.GetTransactionLog() {
+ a.log.Debug().Str("entry", entry).Msg("transaction log entry")
+ }
+ }
+
+ a.trafficLog.Dump(a._debug)
+ }
+
+ // check for success
+ success, failed := a.CheckForSuccess()
+ if !success {
+ return errors.New("application network did not succeed")
+ }
+ _ = failed
+
+ return nil
+}
+
+func (a *ApplicationNetwork) _debug(format string, args bacnetip.Args) {
+ a.log.Debug().Msgf(format, args)
+}
+
+type SnifferNode struct {
+ *bacnetip.Client
+
+ name string
+ address *bacnetip.Address
+ node *bacnetip.Node
+
+ log zerolog.Logger
+}
+
+func NewSnifferNode(localLog zerolog.Logger, vlan *bacnetip.Network) (*SnifferNode, error) {
+ s := &SnifferNode{
+ name: "sniffer",
+ log: localLog,
+ }
+ s.address, _ = bacnetip.NewAddress(localLog)
+ var err error
+ s.Client, err = bacnetip.NewClient(localLog, s)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating client")
+ }
+
+ // create a promiscuous node, added to the network
+ s.node, err = bacnetip.NewNode(localLog, s.address, vlan, bacnetip.WithNodePromiscuous(true))
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating node")
+ }
+ s.log.Debug().Stringer("node", s.node).Msg("node")
+
+ // bind the node
+ err = bacnetip.Bind(s.log, s, s.node)
+ if err != nil {
+ return nil, errors.Wrap(err, "error binding node")
+ }
+ return s, nil
+}
+
+func (s *SnifferNode) Request(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("request")
+ return errors.New("sniffers don't request")
+}
+
+func (s *SnifferNode) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("confirmation")
+ pdu := args.Get0PDU()
+
+ // it's and NPDU
+ npdu := pdu.GetMessage().(model.NPDU)
+
+ // filter out network layer traffic if there is any, probably not
+ if nlm := npdu.GetNlm(); nlm != nil {
+ s.log.Debug().Stringer("nlm", nlm).Msg("network message")
+ return nil
+ }
+
+ // decode as generic APDU
+ apdu := npdu.GetApdu()
+
+ // TODO: not sure what to do here
+ /*
+ # "lift" the source and destination address
+ if npdu.npduSADR:
+ apdu.pduSource = npdu.npduSADR
+ else:
+ apdu.pduSource = npdu.pduSource
+ if npdu.npduDADR:
+ apdu.pduDestination = npdu.npduDADR
+ else:
+ apdu.pduDestination = npdu.pduDestination
+ */
+ _ = apdu
+
+ // TODO: print etc
+
+ return nil
+}
+
+type SnifferStateMachine struct {
+ *bacnetip.Client
+ tests.StateMachine
+
+ name string
+ address *bacnetip.Address
+ node *bacnetip.Node
+
+ log zerolog.Logger
+}
+
+func NewSnifferStateMachine(localLog zerolog.Logger, vlan *bacnetip.Network) (*SnifferStateMachine, error) {
+ s := &SnifferStateMachine{
+ name: "sniffer",
+ log: localLog,
+ }
+ s.address, _ = bacnetip.NewAddress(localLog)
+
+ // continue with initialization
+ var err error
+ s.Client, err = bacnetip.NewClient(localLog, s)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating client")
+ }
+ var init func()
+ s.StateMachine, init = tests.NewStateMachine(localLog, s)
+ init()
+
+ // create a promiscuous node, added to the network
+ s.node, err = bacnetip.NewNode(localLog, s.address, vlan, bacnetip.WithNodePromiscuous(true))
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating node")
+ }
+ s.log.Debug().Stringer("node", s.node).Msg("node")
+
+ // bind the node
+ err = bacnetip.Bind(s.log, s, s.node)
+ if err != nil {
+ return nil, errors.Wrap(err, "error binding node")
+ }
+ return s, nil
+}
+
+func (s *SnifferStateMachine) Sends(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("request")
+ return errors.New("sniffers don't send")
+}
+
+func (s *SnifferStateMachine) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("confirmation")
+ pdu := args.Get0PDU()
+
+ // it's and NPDU
+ npdu := pdu.GetMessage().(model.NPDU)
+
+ // filter out network layer traffic if there is any, probably not
+ if nlm := npdu.GetNlm(); nlm != nil {
+ s.log.Debug().Stringer("nlm", nlm).Msg("network message")
+ return nil
+ }
+
+ // decode as generic APDU
+ apdu := npdu.GetApdu()
+
+ // TODO: not sure what to do here
+ /*
+ # "lift" the source and destination address
+ if npdu.npduSADR:
+ apdu.pduSource = npdu.npduSADR
+ else:
+ apdu.pduSource = npdu.pduSource
+ if npdu.npduDADR:
+ apdu.pduDestination = npdu.npduDADR
+ else:
+ apdu.pduDestination = npdu.pduDestination
+ */
+ _ = apdu
+
+ // TODO: print etc
+
+ // pass to the state machine
+ return s.Receive(args, bacnetip.NoKWArgs)
+}
+
+func (s *SnifferStateMachine) String() string {
+ return "SnifferStateMachine" //TODO
+}
+
+type ApplicationStateMachineRequirements interface {
+ Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error
+ Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error
+}
+
+type ApplicationStateMachine struct {
+ ApplicationStateMachineRequirements
+ *bacnetip.ApplicationIOController
+ tests.StateMachine
+
+ address *bacnetip.Address
+ asap *bacnetip.ApplicationServiceAccessPoint
+ smap *bacnetip.StateMachineAccessPoint
+ nsap *bacnetip.NetworkServiceAccessPoint
+ nse *_NetworkServiceElement
+ node *bacnetip.Node
+
+ log zerolog.Logger
+}
+
+func NewApplicationStateMachine(localLog zerolog.Logger, localDevice *bacnetip.LocalDeviceObject, vlan *bacnetip.Network, applicationStateMachineRequirements ApplicationStateMachineRequirements) (*ApplicationStateMachine, error) {
+ a := &ApplicationStateMachine{
+ ApplicationStateMachineRequirements: applicationStateMachineRequirements,
+ log: localLog,
+ }
+
+ // build and address and save it
+ _, instance := bacnetip.ObjectIdentifierStringToTuple(localDevice.ObjectIdentifier)
+ var err error
+ a.address, err = bacnetip.NewAddress(a.log, instance)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating address")
+ }
+ a.log.Debug().Stringer("address", a.address).Msg("address")
+
+ // continue with initialization
+ a.ApplicationIOController, err = bacnetip.NewApplicationIOController(a.log, localDevice)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating application io controller")
+ }
+ var init func()
+ a.StateMachine, init = tests.NewStateMachine(a.log, a, tests.WithStateMachineName(localDevice.ObjectName))
+ init()
+
+ // include a application decoder
+ a.asap, err = bacnetip.NewApplicationServiceAccessPoint(a.log)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating application service access point")
+ }
+
+ // pass the device object to the state machine access point so it
+ // can know if it should support segmentation
+
+ // the segmentation state machines need access to the same device
+ // information cache as the application
+ deviceInfoCache := a.GetDeviceInfoCache()
+ a.smap, err = bacnetip.NewStateMachineAccessPoint(a.log, localDevice, bacnetip.WithStateMachineAccessPointDeviceInfoCache(deviceInfoCache))
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating state machine access point")
+ }
+
+ // a network service access point will be needed
+ a.nsap, err = bacnetip.NewNetworkServiceAccessPoint(a.log)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating network service access point")
+ }
+
+ // give the NSAP a generic network layer service element
+ a.nse, err = new_NetworkServiceElement(a.log)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating network service element")
+ }
+ err = bacnetip.Bind(a.log, a.nse, a.nsap)
+ if err != nil {
+ return nil, errors.Wrap(err, "error binding network service element")
+ }
+
+ // bind the top layers
+ err = bacnetip.Bind(a.log, a, a.asap, a.smap, a.nsap)
+ if err != nil {
+ return nil, errors.Wrap(err, "error binding top layers")
+ }
+
+ // create a node, added to the network
+ a.node, err = bacnetip.NewNode(a.log, a.address, vlan)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating node")
+ }
+
+ // bind the network service to the node, no network number
+ err = a.nsap.Bind(a.node, nil, nil)
+ if err != nil {
+ return nil, errors.Wrap(err, "error binding nsap")
+ }
+ return a, nil
+}
+
+func (a *ApplicationStateMachine) String() string {
+ return "ApplicationStateMachine" //TODO:
+}
+
+func (a *ApplicationStateMachine) Send(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ a.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Send")
+
+ pdu := args.Get0PDU()
+ // build a IOCB to wrap the request
+ iocb, err := bacnetip.NewIOCB(a.log, pdu, nil)
+ if err != nil {
+ return errors.Wrap(err, "error creating iocb")
+ }
+ return a.Request(bacnetip.NewArgs(iocb), bacnetip.NoKWArgs)
+}
+
+func (a *ApplicationStateMachine) Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ a.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Indication")
+
+ // let the state machine know the request was received
+ err := a.Receive(args, bacnetip.NoKWArgs)
+ if err != nil {
+ return errors.Wrap(err, "error receiving indication")
+ }
+
+ // allow the application to process it
+ return a.ApplicationStateMachineRequirements.Indication(args, kwargs)
+}
+
+func (a *ApplicationStateMachine) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ a.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Confirmation")
+
+ // forward the confirmation to the state machine
+ err := a.Receive(args, bacnetip.NoKWArgs)
+ if err != nil {
+ return errors.Wrap(err, "error receiving indication")
+ }
+
+ // allow the application to process it
+ return a.ApplicationStateMachineRequirements.Confirmation(args, kwargs)
+}
+
+type COVTestClientServicesRequirements interface {
+}
+
+type COVTestClientServices struct {
+ COVTestClientServicesRequirements
+ *bacnetip.Capability
+
+ log zerolog.Logger
+}
+
+func (c *COVTestClientServices) doConfirmedCOVNotificationRequest(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ c.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("doConfirmedCOVNotificationRequest")
+
+ panic("TODO: implement me") // TODO:implement me
+ return nil
+}
+
+func (c *COVTestClientServices) doUnconfirmedCOVNotificationRequest(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ c.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("doUnconfirmedCOVNotificationRequest")
+
+ panic("TODO: implement me") // TODO:implement me
+ return nil
+}
diff --git a/plc4go/internal/bacnetip/tests/test_service/test_cov_av_test.go b/plc4go/internal/bacnetip/tests/test_service/test_cov_av_test.go
new file mode 100644
index 0000000000..d7fae209dc
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/test_service/test_cov_av_test.go
@@ -0,0 +1,22 @@
+/*
+ * 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 test_service
+
+// TODO: implement
diff --git a/plc4go/internal/bacnetip/tests/test_service/test_cov_bv_test.go b/plc4go/internal/bacnetip/tests/test_service/test_cov_bv_test.go
new file mode 100644
index 0000000000..d7fae209dc
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/test_service/test_cov_bv_test.go
@@ -0,0 +1,22 @@
+/*
+ * 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 test_service
+
+// TODO: implement
diff --git a/plc4go/internal/bacnetip/tests/test_service/test_cov_pc_test.go b/plc4go/internal/bacnetip/tests/test_service/test_cov_pc_test.go
new file mode 100644
index 0000000000..d7fae209dc
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/test_service/test_cov_pc_test.go
@@ -0,0 +1,22 @@
+/*
+ * 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 test_service
+
+// TODO: implement
diff --git a/plc4go/internal/bacnetip/tests/test_service/test_cov_test.go b/plc4go/internal/bacnetip/tests/test_service/test_cov_test.go
new file mode 100644
index 0000000000..3939586704
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/test_service/test_cov_test.go
@@ -0,0 +1,66 @@
+/*
+ * 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 test_service
+
+import (
+ "testing"
+
+ "github.com/apache/plc4x/plc4go/internal/bacnetip"
+ "github.com/apache/plc4x/plc4go/internal/bacnetip/service"
+ "github.com/apache/plc4x/plc4go/internal/bacnetip/tests"
+ "github.com/apache/plc4x/plc4go/spi/testutils"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type TODOWhatToDoWithThatWiringWrongQuestionMark struct {
+}
+
+func (T TODOWhatToDoWithThatWiringWrongQuestionMark) Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ //TODO implement me
+ panic("implement me")
+}
+
+func (T TODOWhatToDoWithThatWiringWrongQuestionMark) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ //TODO implement me
+ panic("implement me")
+}
+
+func TestBasic(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+ tests.LockGlobalTimeMachine(t)
+ tests.NewGlobalTimeMachine(testingLogger)
+
+ // create a network
+ anet, err := NewApplicationNetwork(testingLogger, new(TODOWhatToDoWithThatWiringWrongQuestionMark))
+ require.NoError(t, err)
+
+ // add the service capability
+ anet.iut.AddCapability(new(service.ChangeOfValuesServices))
+
+ // all start states are successful
+ anet.td.GetStartState().Success("")
+ anet.iut.GetStartState().Success("")
+
+ // run the group
+ err = anet.Run(0)
+ assert.NoError(t, err)
+}
diff --git a/plc4go/internal/bacnetip/tests/test_service/test_device_2_test.go b/plc4go/internal/bacnetip/tests/test_service/test_device_2_test.go
new file mode 100644
index 0000000000..d7fae209dc
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/test_service/test_device_2_test.go
@@ -0,0 +1,22 @@
+/*
+ * 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 test_service
+
+// TODO: implement
diff --git a/plc4go/internal/bacnetip/tests/test_service/test_device_test.go b/plc4go/internal/bacnetip/tests/test_service/test_device_test.go
new file mode 100644
index 0000000000..d7fae209dc
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/test_service/test_device_test.go
@@ -0,0 +1,22 @@
+/*
+ * 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 test_service
+
+// TODO: implement
diff --git a/plc4go/internal/bacnetip/tests/test_service/test_file_test.go b/plc4go/internal/bacnetip/tests/test_service/test_file_test.go
new file mode 100644
index 0000000000..d7fae209dc
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/test_service/test_file_test.go
@@ -0,0 +1,22 @@
+/*
+ * 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 test_service
+
+// TODO: implement
diff --git a/plc4go/internal/bacnetip/tests/test_service/test_object_test.go b/plc4go/internal/bacnetip/tests/test_service/test_object_test.go
new file mode 100644
index 0000000000..d7fae209dc
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/test_service/test_object_test.go
@@ -0,0 +1,22 @@
+/*
+ * 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 test_service
+
+// TODO: implement
diff --git a/plc4go/internal/bacnetip/tests/test_utilities/test_client_state_machine_test.go b/plc4go/internal/bacnetip/tests/test_utilities/test_client_state_machine_test.go
new file mode 100644
index 0000000000..4e2c30d003
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/test_utilities/test_client_state_machine_test.go
@@ -0,0 +1,63 @@
+/*
+ * 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 test_utilities
+
+import (
+ "testing"
+
+ "github.com/apache/plc4x/plc4go/internal/bacnetip"
+ "github.com/apache/plc4x/plc4go/internal/bacnetip/tests"
+ "github.com/apache/plc4x/plc4go/spi/testutils"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestClientStateMachine(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+ // create a client state machine, trapped server, and bind them together
+ client, err := tests.NewClientStateMachine(testingLogger)
+ require.NoError(t, err)
+ server, err := tests.NewTrappedServer(testingLogger, nil)
+ require.NoError(t, err)
+ err = bacnetip.Bind(testingLogger, client, server)
+ require.NoError(t, err)
+
+ // make pdu object
+ pdu := bacnetip.NewPDU(tests.NewDummyMessage())
+
+ // make a send transition from start to success, run the machine
+ client.GetStartState().Send(pdu, nil).Success("")
+
+ // run the machine
+ err = client.Run()
+ assert.Nil(t, err)
+
+ // check for success
+ assert.False(t, client.IsRunning())
+ assert.True(t, client.GetCurrentState().IsSuccessState())
+
+ // make sure the pdu was sent
+ assert.Equal(t, pdu, server.GetIndicationReceived())
+
+ // check the transaction log
+ assert.Len(t, client.GetTransactionLog(), 1)
+ assert.Contains(t, client.GetTransactionLog()[0], pdu.String())
+}
diff --git a/plc4go/internal/bacnetip/tests/test_utilities/test_server_state_machine_test.go b/plc4go/internal/bacnetip/tests/test_utilities/test_server_state_machine_test.go
new file mode 100644
index 0000000000..e938f5c1de
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/test_utilities/test_server_state_machine_test.go
@@ -0,0 +1,63 @@
+/*
+ * 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 test_utilities
+
+import (
+ "testing"
+
+ "github.com/apache/plc4x/plc4go/internal/bacnetip"
+ "github.com/apache/plc4x/plc4go/internal/bacnetip/tests"
+ "github.com/apache/plc4x/plc4go/spi/testutils"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestServerStateMachine(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+ // create a client state machine, trapped server, and bind them together
+ client, err := tests.NewTrappedClient(testingLogger, nil)
+ require.NoError(t, err)
+ server, err := tests.NewServerStateMachine(testingLogger)
+ require.NoError(t, err)
+ err = bacnetip.Bind(testingLogger, client, server)
+ require.NoError(t, err)
+
+ // make pdu object
+ pdu := bacnetip.NewPDU(tests.NewDummyMessage())
+
+ // make a send transition from start to success, run the machine
+ server.GetStartState().Send(pdu, nil).Success("")
+
+ // run the machine
+ err = server.Run()
+ assert.Nil(t, err)
+
+ // check for success
+ assert.False(t, server.IsRunning())
+ assert.True(t, server.GetCurrentState().IsSuccessState())
+
+ // make sure the pdu was sent
+ assert.Equal(t, pdu, client.GetConfirmationReceived())
+
+ // check the transaction log
+ assert.Len(t, server.GetTransactionLog(), 1)
+ assert.Contains(t, server.GetTransactionLog()[0], pdu.String())
+}
diff --git a/plc4go/internal/bacnetip/tests/test_utilities/test_service_access_point_test.go b/plc4go/internal/bacnetip/tests/test_utilities/test_service_access_point_test.go
new file mode 100644
index 0000000000..b12faba8ea
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/test_utilities/test_service_access_point_test.go
@@ -0,0 +1,247 @@
+/*
+ * 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 test_utilities
+
+import (
+ "testing"
+
+ "github.com/apache/plc4x/plc4go/internal/bacnetip"
+ "github.com/apache/plc4x/plc4go/internal/bacnetip/tests"
+ "github.com/apache/plc4x/plc4go/spi/testutils"
+ "github.com/stretchr/testify/suite"
+
+ "github.com/pkg/errors"
+ "github.com/rs/zerolog"
+)
+
+type EchoAccessPointRequirements interface {
+ SapResponse(args bacnetip.Args, kwArgs bacnetip.KWArgs) error
+}
+
+type EchoAccessPoint struct {
+ *bacnetip.ServiceAccessPoint
+
+ echoAccessPointRequirements EchoAccessPointRequirements
+
+ log zerolog.Logger
+}
+
+func NewEchoAccessPoint(localLog zerolog.Logger, serviceAccessPoint *bacnetip.ServiceAccessPoint) *EchoAccessPoint {
+ e := &EchoAccessPoint{
+ ServiceAccessPoint: serviceAccessPoint,
+
+ log: localLog,
+ }
+ return e
+}
+
+func (e *EchoAccessPoint) SapIndication(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ e.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("SapIndication")
+ return e.echoAccessPointRequirements.SapResponse(args, bacnetip.NoKWArgs)
+}
+
+func (e *EchoAccessPoint) SapConfirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ e.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("SapConfirmation")
+ return nil
+}
+
+type TrappedEchoAccessPoint struct {
+ *bacnetip.ServiceAccessPoint
+ *EchoAccessPoint
+ *tests.TrappedServiceAccessPoint
+
+ log zerolog.Logger
+}
+
+func NewTrappedEchoAccessPoint(localLog zerolog.Logger) (*TrappedEchoAccessPoint, error) {
+ t := &TrappedEchoAccessPoint{}
+ var err error
+ t.ServiceAccessPoint, err = bacnetip.NewServiceAccessPoint(localLog, t)
+ if err != nil {
+ return nil, errors.Wrap(err, "Error creating service access point")
+ }
+ t.EchoAccessPoint = NewEchoAccessPoint(localLog, t.ServiceAccessPoint)
+ t.TrappedServiceAccessPoint, err = tests.NewTrappedServiceAccessPoint(localLog, t.EchoAccessPoint)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating trapped service access point")
+ }
+ t.EchoAccessPoint.echoAccessPointRequirements = t // TODO: isn't multi-inheritance easy to follow? At this point it is pretty confusing
+ return t, nil
+}
+
+func (t *TrappedEchoAccessPoint) SapRequest(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ return t.TrappedServiceAccessPoint.SapRequest(args, kwargs)
+}
+
+func (t *TrappedEchoAccessPoint) SapIndication(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ return t.TrappedServiceAccessPoint.SapIndication(args, kwargs)
+}
+
+func (t *TrappedEchoAccessPoint) SapResponse(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ return t.TrappedServiceAccessPoint.SapResponse(args, kwargs)
+}
+
+func (t *TrappedEchoAccessPoint) SapConfirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ return t.TrappedServiceAccessPoint.SapConfirmation(args, kwargs)
+}
+
+func (t *TrappedEchoAccessPoint) String() string {
+ return "TrappedEchoAccessPoint"
+}
+
+type EchoServiceElementRequirements interface {
+ Response(args bacnetip.Args, kwArgs bacnetip.KWArgs) error
+}
+
+type EchoServiceElement struct {
+ *bacnetip.ApplicationServiceElement
+
+ echoServiceElementRequirements EchoServiceElementRequirements
+
+ log zerolog.Logger
+}
+
+func NewEchoServiceElement(localLog zerolog.Logger, applicationServiceElement *bacnetip.ApplicationServiceElement) *EchoServiceElement {
+ e := &EchoServiceElement{
+ ApplicationServiceElement: applicationServiceElement,
+ log: localLog,
+ }
+ return e
+}
+
+func (e *EchoServiceElement) Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ e.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Indication")
+ return e.echoServiceElementRequirements.Response(args, bacnetip.NoKWArgs)
+}
+
+func (e *EchoServiceElement) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ e.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Confirmation")
+ return nil
+}
+
+func (e *EchoServiceElement) String() string {
+ return "EchoServiceElement" // TODO: fill with some useful
+}
+
+type TrappedEchoServiceElement struct {
+ *bacnetip.ApplicationServiceElement
+ *EchoServiceElement
+ *tests.TrappedApplicationServiceElement
+}
+
+func NewTrappedEchoServiceElement(localLog zerolog.Logger) (*TrappedEchoServiceElement, error) {
+ t := &TrappedEchoServiceElement{}
+ var err error
+ t.ApplicationServiceElement, err = bacnetip.NewApplicationServiceElement(localLog, t)
+ if err != nil {
+ return nil, errors.Wrap(err, "Error creating service access point")
+ }
+ t.EchoServiceElement = NewEchoServiceElement(localLog, t.ApplicationServiceElement)
+ t.TrappedApplicationServiceElement, err = tests.NewTrappedApplicationServiceElement(localLog, t.EchoServiceElement)
+ if err != nil {
+ return nil, errors.Wrap(err, "error creating trapped application service element")
+ }
+ t.EchoServiceElement.echoServiceElementRequirements = t // TODO: isn't multi-inheritance easy to follow? At this point it is pretty confusing
+ return t, nil
+}
+
+func (t *TrappedEchoServiceElement) Request(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ return t.TrappedApplicationServiceElement.Request(args, kwargs)
+}
+
+func (t *TrappedEchoServiceElement) Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ return t.TrappedApplicationServiceElement.Indication(args, kwargs)
+}
+
+func (t *TrappedEchoServiceElement) Response(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ return t.TrappedApplicationServiceElement.Response(args, kwargs)
+}
+
+func (t *TrappedEchoServiceElement) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ return t.TrappedApplicationServiceElement.Confirmation(args, kwargs)
+}
+
+func (t *TrappedEchoServiceElement) String() string {
+ return "TrappedEchoServiceElement"
+}
+
+type TestApplicationSuite struct {
+ suite.Suite
+
+ sap *TrappedEchoAccessPoint
+ ase *TrappedEchoServiceElement
+
+ log zerolog.Logger
+}
+
+func (suite *TestApplicationSuite) SetupSuite() {
+ suite.log = testutils.ProduceTestingLogger(suite.T())
+
+ var err error
+ suite.sap, err = NewTrappedEchoAccessPoint(suite.log)
+ suite.Require().NoError(err)
+
+ suite.ase, err = NewTrappedEchoServiceElement(suite.log)
+ suite.Require().NoError(err)
+
+ err = bacnetip.Bind(suite.log, suite.ase, suite.sap)
+ suite.Require().NoError(err)
+}
+
+func (suite *TestApplicationSuite) TearDownSuite() {
+}
+
+func (suite *TestApplicationSuite) TestSapRequest() {
+ // make a pdu
+ pdu := bacnetip.NewPDU(tests.NewDummyMessage())
+
+ // service access point is going to request something
+ err := suite.sap.SapRequest(bacnetip.NewArgs(pdu), bacnetip.NoKWArgs)
+ suite.Assert().NoError(err)
+
+ // make sure the request was sent and received
+ suite.Equal(pdu, suite.sap.GetSapRequestSent())
+ suite.Equal(pdu, suite.ase.GetIndicationReceived())
+
+ // make sure the echo response was sent and received
+ suite.Equal(pdu, suite.ase.GetResponseSent())
+ suite.Equal(pdu, suite.sap.GetSapConfirmationReceived())
+}
+
+func (suite *TestApplicationSuite) TestAseRequest() {
+ // make a pdu
+ pdu := bacnetip.NewPDU(tests.NewDummyMessage())
+
+ // service access point is going to request something
+ err := suite.ase.Request(bacnetip.NewArgs(pdu), bacnetip.NoKWArgs)
+ suite.Assert().NoError(err)
+
+ // make sure the request was sent and received
+ suite.Equal(pdu, suite.ase.GetRequestSent())
+ suite.Equal(pdu, suite.sap.GetSapIndicationReceived())
+
+ // make sure the echo response was sent and received
+ suite.Equal(pdu, suite.sap.GetSapResponseSent())
+ suite.Equal(pdu, suite.ase.GetConfirmationReceived())
+}
+
+func TestApplicationService(t *testing.T) {
+ suite.Run(t, new(TestApplicationSuite))
+}
diff --git a/plc4go/internal/bacnetip/tests/test_utilities/test_state_machine_test.go b/plc4go/internal/bacnetip/tests/test_utilities/test_state_machine_test.go
new file mode 100644
index 0000000000..8c2ffbbcda
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/test_utilities/test_state_machine_test.go
@@ -0,0 +1,666 @@
+/*
+ * 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 test_utilities
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/apache/plc4x/plc4go/internal/bacnetip"
+ "github.com/apache/plc4x/plc4go/internal/bacnetip/tests"
+ readWriteModel "github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model"
+ "github.com/apache/plc4x/plc4go/spi"
+ "github.com/apache/plc4x/plc4go/spi/testutils"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type TPDU struct {
+ x []byte
+ a, b int
+}
+
+func (t TPDU) X() []byte {
+ return t.x
+}
+
+func (t TPDU) A() int {
+ return t.a
+}
+
+func (t TPDU) B() int {
+ return t.b
+}
+
+func (t TPDU) String() string {
+ content := ""
+ if t.x != nil {
+ content += fmt.Sprintf(" x=%v", t.x)
+ }
+ if t.a != 0 {
+ content += fmt.Sprintf(" a=%v", t.a)
+ }
+ if t.b != 0 {
+ content += fmt.Sprintf(" b=%v", t.b)
+ }
+ return fmt.Sprintf("<TPDU%v>", content)
+}
+
+func (t TPDU) GetMessage() spi.Message {
+ panic("implement me")
+}
+
+func (t TPDU) GetPDUSource() *bacnetip.Address {
+ panic("implement me")
+}
+
+func (t TPDU) SetPDUSource(source *bacnetip.Address) {
+ panic("implement me")
+}
+
+func (t TPDU) GetPDUDestination() *bacnetip.Address {
+ panic("implement me")
+}
+
+func (t TPDU) SetPDUDestination(address *bacnetip.Address) {
+ panic("implement me")
+}
+
+func (t TPDU) GetExpectingReply() bool {
+ panic("implement me")
+}
+
+func (t TPDU) GetNetworkPriority() readWriteModel.NPDUNetworkPriority {
+ panic("implement me")
+}
+
+func (t TPDU) DeepCopy() bacnetip.PDU {
+ panic("implement me")
+}
+
+type Anon struct {
+ TPDU
+}
+
+func TestMatchPdu(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ tpdu := TPDU{x: []byte{1}}
+ anon := Anon{TPDU{x: []byte("Anon")}}
+
+ // no criteria passes
+ assert.True(t, tests.MatchPdu(testingLogger, tpdu, nil, nil))
+ assert.True(t, tests.MatchPdu(testingLogger, anon, nil, nil))
+
+ // matching/not matching types
+ assert.True(t, tests.MatchPdu(testingLogger, tpdu, TPDU{}, nil))
+ assert.False(t, tests.MatchPdu(testingLogger, tpdu, Anon{}, nil))
+ // Note the other testcase is irrelevant as we don't have dynamic types
+
+ // matching/not matching attributes
+ assert.True(t, tests.MatchPdu(testingLogger, tpdu, nil, map[string]any{"x": []byte{1}}))
+ assert.False(t, tests.MatchPdu(testingLogger, tpdu, nil, map[string]any{"x": []byte{2}}))
+ assert.False(t, tests.MatchPdu(testingLogger, tpdu, nil, map[string]any{"y": []byte{1}}))
+ assert.False(t, tests.MatchPdu(testingLogger, anon, nil, map[string]any{"x": []byte{1}}))
+
+ // matching/not matching types and attributes
+ assert.True(t, tests.MatchPdu(testingLogger, tpdu, TPDU{}, map[string]any{"x": []byte{1}}))
+ assert.False(t, tests.MatchPdu(testingLogger, tpdu, TPDU{}, map[string]any{"x": []byte{2}}))
+ assert.False(t, tests.MatchPdu(testingLogger, tpdu, TPDU{}, map[string]any{"y": []byte{1}}))
+ assert.False(t, tests.MatchPdu(testingLogger, anon, Anon{}, map[string]any{"x": []byte{1}}))
+}
+
+func TestState(t *testing.T) {
+ t.Run("test_state_doc", func(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // change the doc string
+ ts := tests.NewState(testingLogger, nil, "")
+ ns := ts.Doc("test state")
+
+ assert.Equal(t, "test state", ts.DocString())
+ assert.Same(t, ts, ns)
+ })
+ t.Run("test_success", func(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ ts := tests.NewState(testingLogger, nil, "")
+ ns := ts.Success("")
+ assert.True(t, ts.IsSuccessState())
+ assert.Same(t, ts, ns)
+
+ assert.Panics(t, func() {
+ ts.Success("")
+ })
+ assert.Panics(t, func() {
+ ts.Fail("")
+ })
+ })
+ t.Run("test_state_fail", func(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ ts := tests.NewState(testingLogger, nil, "")
+ ns := ts.Fail("")
+ assert.True(t, ts.IsFailState())
+ assert.Same(t, ts, ns)
+
+ assert.Panics(t, func() {
+ ts.Success("")
+ })
+ assert.Panics(t, func() {
+ ts.Fail("")
+ })
+ })
+}
+
+func TestStateMachine(t *testing.T) {
+ t.Run("test_state_machine_run", func(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // create a state machine //TODO: fix nil requirement
+ var init func()
+ tsm, init := tests.NewStateMachine(testingLogger, nil)
+ init()
+
+ // run the machine
+ err := tsm.Run()
+ assert.NoError(t, err)
+
+ assert.True(t, tsm.IsRunning())
+ assert.Same(t, tsm.GetStartState(), tsm.GetCurrentState())
+ })
+ t.Run("test_state_machine_success", func(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // create a state machine
+ tsm := tests.NewTrappedStateMachine(testingLogger)
+
+ // make the start state a sucess
+ tsm.GetStartState().Success("")
+
+ // run the machine
+ err := tsm.Run()
+ assert.NoError(t, err)
+
+ assert.False(t, tsm.IsRunning())
+ assert.True(t, tsm.GetCurrentState().IsSuccessState())
+ })
+ t.Run("test_state_machine_fail", func(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // create a state machine
+ tsm := tests.NewTrappedStateMachine(testingLogger)
+
+ // make the start state a sucess
+ tsm.GetStartState().Fail("")
+
+ // run the machine
+ err := tsm.Run()
+ assert.NoError(t, err)
+
+ assert.False(t, tsm.IsRunning())
+ assert.True(t, tsm.GetCurrentState().IsFailState())
+ })
+ t.Run("test_state_machine_send", func(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // create a state machine
+ tsm := tests.NewTrappedStateMachine(testingLogger)
+
+ // Make a pdu object
+ pdu := bacnetip.NewPDU(nil)
+
+ // make a send transition from start to success, run the machine
+ tsm.GetStartState().Send(pdu, nil).Success("")
+ err := tsm.Run()
+ assert.NoError(t, err)
+
+ // check for success
+ assert.False(t, tsm.IsRunning())
+ assert.True(t, tsm.GetCurrentState().IsSuccessState())
+
+ // check the callbacks
+ assert.IsType(t, bacnetip.NewPDU(nil), tsm.GetBeforeSendPdu())
+ assert.IsType(t, bacnetip.NewPDU(nil), tsm.GetAfterSendPdu())
+
+ // make sure pdu was sent
+ assert.Same(t, pdu, tsm.GetSent())
+
+ // check the transaction log
+ require.Equal(t, 1, len(tsm.GetTransactionLog()))
+ assert.Contains(t, tsm.GetTransactionLog()[0], "PDU")
+ })
+ t.Run("test_state_machine_receive", func(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // create a state machine
+ tsm := tests.NewTrappedStateMachine(testingLogger)
+
+ // Make a pdu object
+ pdu := TPDU{}
+
+ // make a send transition from start to success, run the machine
+ tsm.GetStartState().Receive(pdu, nil).Success("")
+ err := tsm.Run()
+ assert.NoError(t, err)
+
+ // check for still running
+ assert.True(t, tsm.IsRunning())
+
+ // tell the machine it is receiving the pdu
+ err = tsm.Receive(bacnetip.NewArgs(pdu), bacnetip.NoKWArgs)
+ require.NoError(t, err)
+
+ // check for success
+ assert.False(t, tsm.IsRunning())
+ assert.True(t, tsm.GetCurrentState().IsSuccessState())
+
+ // check the callbacks
+ assert.IsType(t, TPDU{}, tsm.GetBeforeReceivePdu())
+ assert.IsType(t, TPDU{}, tsm.GetAfterReceivePdu())
+
+ // check the transaction log
+ require.Equal(t, 1, len(tsm.GetTransactionLog()))
+ assert.Contains(t, tsm.GetTransactionLog()[0], pdu.String())
+ })
+ t.Run("test_state_machine_unexpected", func(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // create a state machine
+ tsm := tests.NewTrappedStateMachine(testingLogger)
+
+ // Make a pdu object
+ goodPdu := TPDU{a: 1}
+ _ = goodPdu
+ badPdu := TPDU{b: 2}
+
+ // make a send transition from start to success, run the machine
+ tsm.GetStartState().Receive(TPDU{}, map[string]any{"a": 1}).Success("")
+ err := tsm.Run()
+ assert.NoError(t, err)
+
+ // check for still running
+ assert.True(t, tsm.IsRunning())
+
+ // give the machine a bad pdu
+ err = tsm.Receive(bacnetip.NewArgs(badPdu), bacnetip.NoKWArgs)
+ require.NoError(t, err)
+
+ // check for fail
+ assert.False(t, tsm.IsRunning())
+ assert.True(t, tsm.GetCurrentState().IsFailState())
+ assert.Same(t, tsm.GetCurrentState(), tsm.GetUnexpectedReceiveState())
+
+ // check the callbacks
+ assert.Equal(t, badPdu, tsm.GetUnexpectedReceivePDU())
+
+ // check the transaction log
+ require.Equal(t, 1, len(tsm.GetTransactionLog()))
+ assert.Contains(t, tsm.GetTransactionLog()[0], badPdu.String())
+ })
+ t.Run("test_state_machine_call", func(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // simpleHook
+ called := false
+ _called := func(args bacnetip.Args, kwArgs bacnetip.KWArgs) error {
+ called = args[0].(bool)
+ return nil
+ }
+
+ // create a state machine
+ tsm := tests.NewTrappedStateMachine(testingLogger)
+
+ // make a send transition from start to success, run the machine
+ tsm.GetStartState().Call(_called, bacnetip.NewArgs(true), bacnetip.NoKWArgs).Success("")
+ err := tsm.Run()
+ assert.NoError(t, err)
+
+ // check for success
+ assert.False(t, tsm.IsRunning())
+ assert.True(t, tsm.IsSuccessState())
+
+ // check for the call
+ assert.True(t, called)
+ })
+ t.Run("test_state_machine_call_exception", func(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // simpleHook
+ called := false
+ _called := func(args bacnetip.Args, kwArgs bacnetip.KWArgs) error {
+ called = args[0].(bool)
+ return tests.AssertionError{Message: "error"}
+ }
+
+ // create a state machine
+ tsm := tests.NewTrappedStateMachine(testingLogger)
+
+ // make a send transition from start to success, run the machine
+ tsm.GetStartState().Call(_called, bacnetip.NewArgs(true), bacnetip.NoKWArgs)
+ err := tsm.Run()
+ assert.NoError(t, err)
+
+ // check for success
+ assert.False(t, tsm.IsRunning())
+ assert.True(t, tsm.IsFailState())
+
+ // check for the call
+ assert.True(t, called)
+ })
+ t.Run("test_state_machine_loop_01", func(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // create a state machine
+ tsm := tests.NewTrappedStateMachine(testingLogger)
+
+ // Make a pdu object
+ firstPdu := TPDU{a: 1}
+ t.Log(firstPdu)
+ secondPdu := TPDU{a: 2}
+ t.Log(secondPdu)
+
+ // after sending the first pdu, wait for the second
+ s0 := tsm.GetStartState()
+ s1 := s0.Send(firstPdu, nil)
+ s2 := s1.Receive(TPDU{}, map[string]any{"a": 2})
+ s2.Success("")
+
+ // run the machine
+ err := tsm.Run()
+ assert.NoError(t, err)
+
+ // check for still running and waiting
+ assert.True(t, tsm.IsRunning())
+ assert.Same(t, s1, tsm.GetCurrentState())
+
+ // give the machine the second pdu
+ err = tsm.Receive(bacnetip.NewArgs(secondPdu), bacnetip.NoKWArgs)
+ require.NoError(t, err)
+
+ // check for success
+ assert.False(t, tsm.IsRunning())
+ assert.True(t, tsm.GetCurrentState().IsSuccessState())
+ t.Log("success")
+
+ // check the callbacks
+ assert.Equal(t, firstPdu, tsm.GetBeforeSendPdu())
+ assert.Equal(t, firstPdu, tsm.GetAfterSendPdu())
+ assert.Equal(t, secondPdu, tsm.GetBeforeReceivePdu())
+ assert.Equal(t, secondPdu, tsm.GetAfterReceivePdu())
+ t.Log("callbacks passed")
+
+ // check the transaction log
+ assert.Contains(t, tsm.GetTransactionLog()[0], firstPdu.String())
+ assert.Contains(t, tsm.GetTransactionLog()[1], secondPdu.String())
+ })
+ t.Run("test_state_machine_loop_02", func(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // create a state machine
+ tsm := tests.NewTrappedStateMachine(testingLogger)
+
+ // Make a pdu object
+ firstPdu := TPDU{a: 1}
+ t.Log(firstPdu)
+ secondPdu := TPDU{a: 2}
+ t.Log(secondPdu)
+
+ // when the first pdu is received, send the second
+ s0 := tsm.GetStartState()
+ s1 := s0.Receive(TPDU{}, map[string]any{"a": 1})
+ s2 := s1.Send(secondPdu, nil)
+ s2.Success("")
+
+ // run the machine
+ err := tsm.Run()
+ assert.NoError(t, err)
+
+ // check for still running and waiting
+ assert.True(t, tsm.IsRunning())
+
+ // give the machine the first pdu
+ err = tsm.Receive(bacnetip.NewArgs(firstPdu), bacnetip.NoKWArgs)
+ require.NoError(t, err)
+
+ // check for success
+ assert.False(t, tsm.IsRunning())
+ assert.True(t, tsm.GetCurrentState().IsSuccessState())
+ t.Log("success")
+
+ // check the callbacks
+ assert.Equal(t, firstPdu, tsm.GetBeforeReceivePdu())
+ assert.Equal(t, firstPdu, tsm.GetAfterReceivePdu())
+ assert.Equal(t, secondPdu, tsm.GetBeforeSendPdu())
+ assert.Equal(t, secondPdu, tsm.GetAfterSendPdu())
+ t.Log("callbacks passed")
+
+ // check the transaction log
+ assert.Contains(t, tsm.GetTransactionLog()[0], firstPdu.String())
+ assert.Contains(t, tsm.GetTransactionLog()[1], secondPdu.String())
+ })
+}
+
+func TestStateMachineTimeout1(t *testing.T) {
+ tests.LockGlobalTimeMachine(t)
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // create a state machine
+ tsm := tests.NewTrappedStateMachine(testingLogger)
+
+ // make a timeout transition from start to success
+ tsm.GetStartState().Timeout(1*time.Second, nil).Success("")
+
+ tests.NewGlobalTimeMachine(testingLogger) // TODO: this is really stupid because of concurrency...
+ // reset the time machine
+ tests.ResetTimeMachine(tests.StartTime)
+ t.Log("time machine reset")
+
+ err := tsm.Run()
+ require.NoError(t, err)
+
+ tests.RunTimeMachine(testingLogger, 60*time.Second, time.Time{})
+ t.Log("time machine finished")
+
+ // check for success
+ assert.False(t, tsm.IsRunning())
+ assert.True(t, tsm.GetCurrentState().IsSuccessState())
+}
+
+func TestStateMachineTimeout2(t *testing.T) {
+ t.Skip("not ready yet") // TODO: figure out why it is failing
+ tests.LockGlobalTimeMachine(t)
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // make some pdus
+ firstPdu := TPDU{a: 1}
+ t.Log(firstPdu)
+ secondPdu := TPDU{a: 2}
+ t.Log(secondPdu)
+
+ // create a state machine
+ tsm := tests.NewTrappedStateMachine(testingLogger)
+ s0 := tsm.GetStartState()
+
+ // send something, wait, send something, wait, success
+ s1 := s0.Send(firstPdu, nil)
+ s2 := s1.Timeout(1*time.Millisecond, nil)
+ s3 := s2.Send(secondPdu, nil)
+ s4 := s3.Timeout(1*time.Millisecond, nil).Success("")
+ _ = s4
+
+ tests.NewGlobalTimeMachine(testingLogger) // TODO: this is really stupid because of concurrency...
+ // reset the time machine
+ tests.ResetTimeMachine(tests.StartTime)
+ t.Log("time machine reset")
+
+ err := tsm.Run()
+ require.NoError(t, err)
+
+ tests.RunTimeMachine(testingLogger, 60*time.Millisecond, time.Time{})
+ t.Log("time machine finished")
+
+ // check for success
+ assert.False(t, tsm.IsRunning())
+ assert.True(t, tsm.GetCurrentState().IsSuccessState())
+
+ // check the transaction log
+ assert.Len(t, tsm.GetTransactionLog(), 2)
+ assert.Contains(t, tsm.GetTransactionLog()[0], firstPdu.String())
+ assert.Contains(t, tsm.GetTransactionLog()[1], secondPdu.String())
+}
+
+func TestStateMachineGroup(t *testing.T) {
+ t.Run("test_state_machine_group_success", func(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // create a state machine group
+ smg := tests.NewStateMachineGroup(testingLogger)
+
+ // create a trapped state machine, start state is success
+ tsm := tests.NewTrappedStateMachine(testingLogger)
+ tsm.GetStartState().Success("")
+
+ // add it to the group
+ smg.Append(tsm)
+
+ tests.NewGlobalTimeMachine(testingLogger) // TODO: this is really stupid because of concurrency...
+ // reset the time machine
+ tests.ResetTimeMachine(tests.StartTime)
+ t.Log("time machine reset")
+
+ // tell the group to run
+ err := smg.Run()
+ require.NoError(t, err)
+
+ tests.RunTimeMachine(testingLogger, 60*time.Second, time.Time{})
+ t.Log("time machine finished")
+
+ // check for success
+ assert.False(t, tsm.IsRunning())
+ assert.True(t, tsm.GetCurrentState().IsSuccessState())
+ assert.False(t, smg.IsRunning())
+ assert.True(t, smg.IsSuccessState())
+ })
+ t.Run("test_state_machine_group_success", func(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // create a state machine group
+ smg := tests.NewStateMachineGroup(testingLogger)
+
+ // create a trapped state machine, start state is success
+ tsm := tests.NewTrappedStateMachine(testingLogger)
+ tsm.GetStartState().Fail("")
+
+ // add it to the group
+ smg.Append(tsm)
+
+ tests.NewGlobalTimeMachine(testingLogger) // TODO: this is really stupid because of concurrency...
+ // reset the time machine
+ tests.ResetTimeMachine(tests.StartTime)
+ t.Log("time machine reset")
+
+ // tell the group to run
+ err := smg.Run()
+ require.NoError(t, err)
+
+ tests.RunTimeMachine(testingLogger, 60*time.Second, time.Time{})
+ t.Log("time machine finished")
+
+ // check for success
+ assert.False(t, tsm.IsRunning())
+ assert.True(t, tsm.GetCurrentState().IsFailState())
+ assert.False(t, smg.IsRunning())
+ assert.True(t, smg.IsFailState())
+ })
+}
+
+func TestStateMachineEvents(t *testing.T) {
+ t.Run("test_state_machine_event_01", func(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // create a state machine group
+ smg := tests.NewStateMachineGroup(testingLogger)
+
+ // create a trapped state machine, start state is success
+ tsm1 := tests.NewTrappedStateMachine(testingLogger)
+ tsm1.GetStartState().SetEvent("e").Success("")
+ smg.Append(tsm1)
+
+ // create another trapped state machine, waiting for the event
+ tsm2 := tests.NewTrappedStateMachine(testingLogger)
+ tsm2.GetStartState().WaitEvent("e", nil).Success("")
+ smg.Append(tsm2)
+
+ tests.NewGlobalTimeMachine(testingLogger) // TODO: this is really stupid because of concurrency...
+ // reset the time machine
+ tests.ResetTimeMachine(tests.StartTime)
+ t.Log("time machine reset")
+
+ // tell the group to run
+ err := smg.Run()
+ require.NoError(t, err)
+
+ tests.RunTimeMachine(testingLogger, 60*time.Second, time.Time{})
+ t.Log("time machine finished")
+
+ // check for success
+ assert.True(t, tsm1.GetCurrentState().IsSuccessState())
+ assert.True(t, tsm2.GetCurrentState().IsSuccessState())
+ assert.False(t, smg.IsRunning())
+ assert.True(t, smg.IsSuccessState())
+ })
+ t.Run("test_state_machine_event_02", func(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+
+ // create a state machine group
+ smg := tests.NewStateMachineGroup(testingLogger)
+
+ // create a trapped state machine, waiting for an event
+ tsm1 := tests.NewTrappedStateMachine(testingLogger)
+ tsm1.GetStartState().WaitEvent("e", nil).Success("")
+ smg.Append(tsm1)
+
+ // create another trapped state machine, start state is success
+ tsm2 := tests.NewTrappedStateMachine(testingLogger)
+ tsm2.GetStartState().SetEvent("e").Success("")
+ smg.Append(tsm2)
+
+ tests.NewGlobalTimeMachine(testingLogger) // TODO: this is really stupid because of concurrency...
+ // reset the time machine
+ tests.ResetTimeMachine(tests.StartTime)
+ t.Log("time machine reset")
+
+ // tell the group to run
+ err := smg.Run()
+ require.NoError(t, err)
+
+ tests.RunTimeMachine(testingLogger, 60*time.Second, time.Time{})
+ t.Log("time machine finished")
+
+ // check for success
+ assert.True(t, tsm1.GetCurrentState().IsSuccessState())
+ assert.True(t, tsm2.GetCurrentState().IsSuccessState())
+ assert.False(t, smg.IsRunning())
+ assert.True(t, smg.IsSuccessState())
+ })
+}
diff --git a/plc4go/internal/bacnetip/tests/test_utilities/test_time_machine_test.go b/plc4go/internal/bacnetip/tests/test_utilities/test_time_machine_test.go
new file mode 100644
index 0000000000..062bd37bd1
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/test_utilities/test_time_machine_test.go
@@ -0,0 +1,306 @@
+/*
+ * 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 test_utilities
+
+import (
+ "testing"
+ "time"
+
+ "github.com/apache/plc4x/plc4go/internal/bacnetip"
+ "github.com/apache/plc4x/plc4go/internal/bacnetip/tests"
+ "github.com/apache/plc4x/plc4go/spi/testutils"
+
+ "github.com/rs/zerolog"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/suite"
+)
+
+type TimeMachineSuite struct {
+ suite.Suite
+
+ // flag to make sure the function was called
+ sampleTaskFunctionCalled []time.Time
+
+ log zerolog.Logger
+}
+
+func (suite *TimeMachineSuite) SetupSuite() {
+ suite.log = testutils.ProduceTestingLogger(suite.T())
+}
+
+func (suite *TimeMachineSuite) SetupTest() {
+ tests.LockGlobalTimeMachine(suite.T())
+ tests.NewGlobalTimeMachine(suite.log) // TODO: this is really stupid because of concurrency...
+}
+
+func (suite *TimeMachineSuite) TearDownTest() {
+ tests.ClearGlobalTimeMachine(suite.log)
+}
+
+type SampleOneShotTask struct {
+ *bacnetip.OneShotTask
+
+ processTaskCalled []time.Time
+
+ log zerolog.Logger
+}
+
+func NewSampleOneShotTask(localLog zerolog.Logger) *SampleOneShotTask {
+ s := &SampleOneShotTask{
+ log: localLog,
+ }
+ s.OneShotTask = bacnetip.NewOneShotTask(s, nil)
+ return s
+}
+
+func (s *SampleOneShotTask) ProcessTask() error {
+ s.log.Debug().Time("current_time", tests.GlobalTimeMachineCurrentTime()).Msg("processing task")
+
+ // add the current time
+ s.processTaskCalled = append(s.processTaskCalled, tests.GlobalTimeMachineCurrentTime())
+ return nil
+}
+
+func (suite *TimeMachineSuite) SampleTaskFunction() func(args bacnetip.Args, kwArgs bacnetip.KWArgs) error {
+ return func(args bacnetip.Args, kwArgs bacnetip.KWArgs) error {
+ currentTime := tests.GlobalTimeMachineCurrentTime()
+ suite.log.Debug().Stringer("args", args).Stringer("kwArgs", kwArgs).Time("current_time", currentTime).Msg("sample_task_function")
+
+ suite.sampleTaskFunctionCalled = append(suite.sampleTaskFunctionCalled, currentTime)
+ return nil
+ }
+}
+
+type SampleRecurringTask struct {
+ *bacnetip.RecurringTask
+
+ processTaskCalled []time.Time
+
+ log zerolog.Logger
+}
+
+func NewSampleRecurringTask(localLog zerolog.Logger) *SampleRecurringTask {
+ s := &SampleRecurringTask{
+ log: localLog,
+ }
+ interval := 1 * time.Nanosecond
+ s.RecurringTask = bacnetip.NewRecurringTask(localLog, s, &interval, nil)
+ return s
+}
+
+func (s *SampleRecurringTask) ProcessTask() error {
+ s.log.Debug().Time("current_time", tests.GlobalTimeMachineCurrentTime()).Msg("processing task")
+
+ // add the current time
+ s.processTaskCalled = append(s.processTaskCalled, tests.GlobalTimeMachineCurrentTime())
+ return nil
+}
+
+func (suite *TimeMachineSuite) TestTimeMachineExists() {
+ assert.True(suite.T(), tests.IsGlobalTimeMachineSet())
+}
+
+func (suite *TimeMachineSuite) TestEmptyRun() {
+ // reset the time machine
+ tests.ResetTimeMachine(tests.StartTime)
+
+ // let it run
+ tests.RunTimeMachine(suite.log, 60*time.Second, time.Time{})
+
+ // 60 seconds have passed
+ suite.Equal(60*time.Second, tests.GlobalTimeMachineCurrentTime().Sub(tests.StartTime))
+}
+
+func (suite *TimeMachineSuite) TestOneShotImmediate1() {
+ // create a function task
+ ft := NewSampleOneShotTask(suite.log)
+
+ // Reset time machine
+ tests.ResetTimeMachine(tests.StartTime)
+ var startTime time.Time
+ ft.InstallTask(bacnetip.InstallTaskOptions{When: &startTime})
+ tests.RunTimeMachine(suite.log, 60*time.Second, time.Time{})
+
+ // function called, 60 seconds passed
+ suite.Contains(ft.processTaskCalled, startTime)
+ suite.Equal(60*time.Second, tests.GlobalTimeMachineCurrentTime().Sub(tests.StartTime))
+}
+
+func (suite *TimeMachineSuite) TestOneShotImmediate2() {
+ // create a function task
+ ft := NewSampleOneShotTask(suite.log)
+
+ // run the functions sometime later
+ t1, err := time.Parse("2006-01-02", "2000-06-06")
+ suite.Require().NoError(err)
+ suite.T().Log(t1)
+
+ // reset the time machine to midnight, install the task, let it run
+ startTime, err := time.Parse("2006-01-02", "2000-01-01")
+ suite.Require().NoError(err)
+ tests.ResetTimeMachine(startTime)
+ ft.InstallTask(bacnetip.InstallTaskOptions{When: &t1})
+ tests.RunTimeMachine(suite.log, 0, startTime)
+
+ // function called, 60 seconds passed
+ suite.Contains(ft.processTaskCalled, t1)
+}
+
+func (suite *TimeMachineSuite) TestFunctionTaskImmediate() {
+ // create a function task
+ ft := bacnetip.FunctionTask(suite.SampleTaskFunction(), bacnetip.NoArgs, bacnetip.NoKWArgs)
+ suite.sampleTaskFunctionCalled = nil
+
+ // reset the time machine to midnight, install the task, let it run
+ tests.ResetTimeMachine(tests.StartTime)
+ var now time.Time
+ ft.InstallTask(bacnetip.InstallTaskOptions{When: &now})
+ tests.RunTimeMachine(suite.log, 60*time.Second, time.Time{})
+
+ // function called, 60 seconds passed
+ suite.Contains(suite.sampleTaskFunctionCalled, time.Time{})
+ suite.Equal(60*time.Second, tests.GlobalTimeMachineCurrentTime().Sub(tests.StartTime))
+}
+
+func (suite *TimeMachineSuite) TestFunctionTaskDelay() {
+ sampleDelay := 10 * time.Second
+
+ // create a function task
+ ft := bacnetip.FunctionTask(suite.SampleTaskFunction(), bacnetip.NoArgs, bacnetip.NoKWArgs)
+ suite.sampleTaskFunctionCalled = nil
+
+ // reset the time machine to midnight, install the task, let it run
+ tests.ResetTimeMachine(tests.StartTime)
+ var now time.Time
+ when := now.Add(sampleDelay)
+ ft.InstallTask(bacnetip.InstallTaskOptions{When: &when})
+ tests.RunTimeMachine(suite.log, 60*time.Second, time.Time{})
+
+ // function called, 60 seconds passed
+ suite.Contains(suite.sampleTaskFunctionCalled, when)
+ suite.Equal(60*time.Second, tests.GlobalTimeMachineCurrentTime().Sub(tests.StartTime))
+}
+
+func (suite *TimeMachineSuite) TestRecurringTask1() {
+ // create a function task
+ ft := NewSampleRecurringTask(suite.log)
+
+ // reset the time machine to midnight, install the task, let it run
+ now := tests.StartTime
+ tests.ResetTimeMachine(now)
+ interval := 1 * time.Second
+ ft.InstallTask(bacnetip.InstallTaskOptions{Interval: &interval})
+ tests.RunTimeMachine(suite.log, 5*time.Second, time.Time{})
+
+ // function called, 60 seconds passed
+ suite.Equal(now.Add(1*time.Second), ft.processTaskCalled[0])
+ suite.Equal(now.Add(2*time.Second), ft.processTaskCalled[1])
+ suite.Equal(now.Add(3*time.Second), ft.processTaskCalled[2])
+ suite.Equal(now.Add(4*time.Second), ft.processTaskCalled[3])
+ suite.Equal(now.Add(5*time.Second), ft.processTaskCalled[4])
+ suite.Equal(now.Add(6*time.Second), ft.processTaskCalled[5])
+ suite.InDelta(5*time.Second, tests.GlobalTimeMachineCurrentTime().Sub(now), float64(100*time.Millisecond))
+}
+
+func (suite *TimeMachineSuite) TestRecurringTask2() {
+ // create a function task
+ ft1 := NewSampleRecurringTask(suite.log)
+ ft2 := NewSampleRecurringTask(suite.log)
+
+ // reset the time machine to midnight, install the task, let it run
+ tests.ResetTimeMachine(tests.StartTime)
+ ft1Interval := 1000 * time.Millisecond
+ ft1.InstallTask(bacnetip.InstallTaskOptions{Interval: &ft1Interval})
+ ft2Interval := 1500 * time.Millisecond
+ ft2.InstallTask(bacnetip.InstallTaskOptions{Interval: &ft2Interval})
+ tests.RunTimeMachine(suite.log, 5*time.Second, time.Time{})
+
+ // function called, 60 seconds passed
+ suite.Equal(tests.StartTime.Add(1*time.Second), ft1.processTaskCalled[0])
+ suite.Equal(tests.StartTime.Add(2*time.Second), ft1.processTaskCalled[1])
+ suite.Equal(tests.StartTime.Add(3*time.Second), ft1.processTaskCalled[2])
+ suite.Equal(tests.StartTime.Add(4*time.Second), ft1.processTaskCalled[3])
+ suite.Equal(tests.StartTime.Add(1500*time.Millisecond), ft2.processTaskCalled[0])
+ suite.Equal(tests.StartTime.Add(3000*time.Millisecond), ft2.processTaskCalled[1])
+ suite.Equal(tests.StartTime.Add(4500*time.Millisecond), ft2.processTaskCalled[2])
+ suite.InDelta(5*time.Second, tests.GlobalTimeMachineCurrentTime().Sub(tests.StartTime), float64(100*time.Millisecond))
+}
+
+func (suite *TimeMachineSuite) TestRecurringTask3() {
+ // create a function task
+ ft := NewSampleRecurringTask(suite.log)
+
+ // reset the time machine to midnight, install the task, let it run
+ tests.ResetTimeMachine(tests.StartTime)
+ ftInterval := 1000 * time.Millisecond
+ ftOffset := 100 * time.Millisecond
+ ft.InstallTask(bacnetip.InstallTaskOptions{Interval: &ftInterval, Offset: &ftOffset})
+ tests.RunTimeMachine(suite.log, 5*time.Second, time.Time{})
+
+ // function called, 60 seconds passed
+ suite.Equal(tests.StartTime.Add(100*time.Millisecond), ft.processTaskCalled[0])
+ suite.Equal(tests.StartTime.Add(1100*time.Millisecond), ft.processTaskCalled[1])
+ suite.Equal(tests.StartTime.Add(2100*time.Millisecond), ft.processTaskCalled[2])
+ suite.Equal(tests.StartTime.Add(3100*time.Millisecond), ft.processTaskCalled[3])
+ suite.Equal(tests.StartTime.Add(4100*time.Millisecond), ft.processTaskCalled[4])
+ suite.InDelta(5*time.Second, tests.GlobalTimeMachineCurrentTime().Sub(tests.StartTime), float64(100*time.Millisecond))
+}
+
+func (suite *TimeMachineSuite) TestRecurringTask4() {
+ // create a function task
+ ft := NewSampleRecurringTask(suite.log)
+
+ // reset the time machine to midnight, install the task, let it run
+ tests.ResetTimeMachine(tests.StartTime)
+ ftInterval := 1000 * time.Millisecond
+ ftOffset := -100 * time.Millisecond
+ ft.InstallTask(bacnetip.InstallTaskOptions{Interval: &ftInterval, Offset: &ftOffset})
+ tests.RunTimeMachine(suite.log, 5*time.Second, time.Time{})
+
+ // function called, 60 seconds passed
+ suite.Equal(tests.StartTime.Add(900*time.Millisecond), ft.processTaskCalled[0])
+ suite.Equal(tests.StartTime.Add(1900*time.Millisecond), ft.processTaskCalled[1])
+ suite.Equal(tests.StartTime.Add(2900*time.Millisecond), ft.processTaskCalled[2])
+ suite.Equal(tests.StartTime.Add(3900*time.Millisecond), ft.processTaskCalled[3])
+ suite.Equal(tests.StartTime.Add(4900*time.Millisecond), ft.processTaskCalled[4])
+ suite.InDelta(5*time.Second, tests.GlobalTimeMachineCurrentTime().Sub(tests.StartTime), float64(100*time.Millisecond))
+}
+
+func (suite *TimeMachineSuite) TestRecurringTask5() {
+ // create a function task
+ ft := NewSampleRecurringTask(suite.log)
+
+ // reset the time machine, install the task, let it run
+ now, err := time.Parse("2006-01-02", "2000-01-01")
+ suite.Require().NoError(err)
+ tests.ResetTimeMachine(now)
+ ftInterval := 86400 * time.Second
+ ft.InstallTask(bacnetip.InstallTaskOptions{Interval: &ftInterval})
+ stopTime, err := time.Parse("2006-01-02", "2000-02-01")
+ suite.Require().NoError(err)
+ tests.RunTimeMachine(suite.log, 0, stopTime)
+
+ // function called every day
+ suite.Equal(32, len(ft.processTaskCalled))
+}
+
+func TestTimeMachine(t *testing.T) {
+ suite.Run(t, new(TimeMachineSuite))
+}
diff --git a/plc4go/internal/bacnetip/tests/test_vlan/test_ipnetwork_test.go b/plc4go/internal/bacnetip/tests/test_vlan/test_ipnetwork_test.go
new file mode 100644
index 0000000000..4835a2315c
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/test_vlan/test_ipnetwork_test.go
@@ -0,0 +1,475 @@
+/*
+ * 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 test_vlan
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/apache/plc4x/plc4go/internal/bacnetip"
+ "github.com/apache/plc4x/plc4go/internal/bacnetip/tests"
+ "github.com/apache/plc4x/plc4go/spi/testutils"
+
+ "github.com/pkg/errors"
+ "github.com/rs/zerolog"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/stretchr/testify/suite"
+)
+
+type TIPNetwork struct {
+ *tests.StateMachineGroup
+
+ vlan *bacnetip.IPNetwork
+
+ t *testing.T
+
+ log zerolog.Logger
+}
+
+func NewTIPNetwork(t *testing.T, nodeCount int, addressPattern string, promiscuous bool, spoofing bool) *TIPNetwork {
+ localLog := testutils.ProduceTestingLogger(t)
+ tn := &TIPNetwork{
+ t: t,
+ log: localLog,
+ }
+ tn.StateMachineGroup = tests.NewStateMachineGroup(localLog)
+
+ // make a little LAN
+ tn.vlan = bacnetip.NewIPNetwork(localLog)
+
+ for i := range nodeCount {
+ nodeAddress, err := bacnetip.NewAddress(localLog, fmt.Sprintf(addressPattern, i+1))
+ require.NoError(t, err)
+ node, err := bacnetip.NewIPNode(localLog, nodeAddress, tn.vlan, bacnetip.WithNodePromiscuous(promiscuous), bacnetip.WithNodeSpoofing(spoofing))
+ require.NoError(t, err)
+
+ // bind a client state machine to the ndoe
+ csm, err := tests.NewClientStateMachine(localLog)
+ require.NoError(t, err)
+
+ err = bacnetip.Bind(localLog, csm, node)
+ require.NoError(t, err)
+
+ // add it to this group
+ tn.Append(csm)
+ }
+
+ return tn
+}
+
+func (t *TIPNetwork) Run(timeLimit time.Duration) error {
+ if timeLimit == 0 {
+ timeLimit = 60 * time.Second
+ }
+ t.log.Debug().Dur("time_limit", timeLimit).Msg("run")
+
+ tests.NewGlobalTimeMachine(t.log) // TODO: this is really stupid because of concurrency...
+ // reset the time machine
+ tests.ResetTimeMachine(tests.StartTime)
+ t.log.Trace().Msg("time machine reset")
+
+ // run the group
+ if err := t.StateMachineGroup.Run(); err != nil {
+ return err
+ }
+
+ // run it some time
+ tests.RunTimeMachine(t.log, timeLimit, time.Time{})
+ t.log.Trace().Msg("time machine finished")
+
+ // check for success
+ success, failed := t.CheckForSuccess()
+ if !success {
+ return errors.New("not all succeeded")
+ }
+ if failed {
+ return errors.New("some failed")
+ }
+ return nil
+}
+
+func TestIPVLAN(t *testing.T) {
+ t.Run("test_idle", func(t *testing.T) { // Test that a very quiet network can exist. This is not a network test so much as a state machine group test
+ tests.LockGlobalTimeMachine(t)
+
+ // two element network
+ tnet := NewTIPNetwork(t, 2, "192.168.1.%d/24", false, false)
+
+ stateMachines := tnet.GetStateMachines()
+ tnode1, tnode2 := stateMachines[0], stateMachines[1]
+
+ // set the start states of both machines to success
+ tnode1.GetStartState().Success("")
+ tnode2.GetStartState().Success("")
+
+ // run the group
+ err := tnet.Run(0)
+ assert.NoError(t, err)
+ })
+ t.Run("test_send_receive", func(t *testing.T) { // Test that a node can send a message to another node.
+ testingLogger := testutils.ProduceTestingLogger(t)
+ tests.LockGlobalTimeMachine(t)
+
+ // two element network
+ tnet := NewTIPNetwork(t, 2, "192.168.2.%d/24", false, false)
+
+ stateMachines := tnet.GetStateMachines()
+ tnode1, tnode2 := stateMachines[0], stateMachines[1]
+
+ // make a PDU from node 1 to node 2
+ src, err := bacnetip.NewAddress(testingLogger, "192.168.2.1:47808")
+ require.NoError(t, err)
+ dest, err := bacnetip.NewAddress(testingLogger, "192.168.2.2:47808")
+ require.NoError(t, err)
+ pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest))
+ t.Log(pdu)
+
+ // node 1 sends the pdu, mode 2 gets it
+ tnode1.GetStartState().Send(pdu, nil).Success("")
+ tnode2.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{
+ "pduSource": src,
+ }).Success("")
+
+ // run the group
+ err = tnet.Run(0)
+ assert.NoError(t, err)
+ })
+ t.Run("test_broadcast", func(t *testing.T) { // Test that a node can send out a 'local broadcast' message which will be received by every other node.
+ t.Skip("not ready yet") // TODO: figure out why it is failing
+ testingLogger := testutils.ProduceTestingLogger(t)
+ tests.LockGlobalTimeMachine(t)
+
+ // three element network
+ tnet := NewTIPNetwork(t, 3, "192.168.3.%d/24", false, false)
+
+ stateMachines := tnet.GetStateMachines()
+ tnode1, tnode2, tnode3 := stateMachines[0], stateMachines[1], stateMachines[2]
+
+ // make a PDU from node 1 to node 2
+ src, err := bacnetip.NewAddress(testingLogger, "192.168.3.1:47808")
+ require.NoError(t, err)
+ dest, err := bacnetip.NewAddress(testingLogger, "192.168.3.2:47808")
+ require.NoError(t, err)
+ pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest))
+ t.Log(pdu)
+
+ // node 1 sends the pdu, node 2 and 3 each get it
+ tnode1.GetStartState().Send(pdu, nil).Success("")
+ tnode2.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{
+ "pduSource": src,
+ }).Success("")
+ tnode3.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{
+ "pduSource": src,
+ }).Success("")
+
+ // run the group
+ err = tnet.Run(0)
+ assert.NoError(t, err)
+ })
+ t.Run("test_spoof_fail", func(t *testing.T) { // Test verifying that a node cannot send out packets with a source address other than its own, see also test_spoof_pass().
+ t.Skip("not ready yet") // TODO: figure out why it is failing
+ testingLogger := testutils.ProduceTestingLogger(t)
+ tests.LockGlobalTimeMachine(t)
+
+ // one element network
+ tnet := NewTIPNetwork(t, 1, "192.168.4.%d/24", false, false)
+
+ stateMachines := tnet.GetStateMachines()
+ tnode1 := stateMachines[0]
+
+ // make an unicast PDU with the wrong source
+ src, err := bacnetip.NewAddress(testingLogger, "192.168.4.1:47808")
+ require.NoError(t, err)
+ dest, err := bacnetip.NewAddress(testingLogger, "192.168.4.2:47808")
+ require.NoError(t, err)
+ pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest))
+ t.Log(pdu)
+
+ // node 1 sends the pdu, node 2 and 3 each get it
+ tnode1.GetStartState().Send(pdu, nil).Success("")
+
+ // run the group
+ err = tnet.Run(0)
+ assert.Error(t, err)
+ })
+ t.Run("test_spoof_pass", func(t *testing.T) { // Test allowing a node to send out packets with a source address other than its own, see also test_spoof_fail().
+ t.Skip("not ready yet") // TODO: figure out why it is failing
+ testingLogger := testutils.ProduceTestingLogger(t)
+ tests.LockGlobalTimeMachine(t)
+
+ // one element network
+ tnet := NewTIPNetwork(t, 1, "192.168.5.%d/24", false, true)
+
+ stateMachines := tnet.GetStateMachines()
+ tnode1 := stateMachines[0]
+
+ // make an unicast PDU with the wrong source
+ src, err := bacnetip.NewAddress(testingLogger, "192.168.5.1:47808")
+ require.NoError(t, err)
+ dest, err := bacnetip.NewAddress(testingLogger, "192.168.5.2:47808")
+ require.NoError(t, err)
+ pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest))
+ t.Log(pdu)
+
+ // node 1 sends the pdu, but gets it back as if it was from node 3
+ tnode1.GetStartState().
+ Send(pdu, nil).
+ Receive(bacnetip.NewPDU(nil), map[string]any{
+ "pduSource": src,
+ }).
+ Success("")
+
+ // run the group
+ err = tnet.Run(0)
+ assert.NoError(t, err)
+ })
+ t.Run("test_promiscuous_pass", func(t *testing.T) { // Test 'promiscuous mode' of a node which allows it to receive every packet sent on the network. This is like the network is a hub, or the node is connected to a 'monitor' port on a managed switch.
+ testingLogger := testutils.ProduceTestingLogger(t)
+ tests.LockGlobalTimeMachine(t)
+
+ // three element network
+ tnet := NewTIPNetwork(t, 3, "192.168.6.%d/24", true, false)
+
+ stateMachines := tnet.GetStateMachines()
+ tnode1, tnode2, tnode3 := stateMachines[0], stateMachines[1], stateMachines[2]
+
+ // make a PDU from node 1 to node 2
+ src, err := bacnetip.NewAddress(testingLogger, "192.168.6.1:47808")
+ require.NoError(t, err)
+ dest, err := bacnetip.NewAddress(testingLogger, "192.168.6.2:47808")
+ require.NoError(t, err)
+ pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest))
+ t.Log(pdu)
+
+ // node 1 sends the pdu, node 2 and 3 each get it
+ tnode1.GetStartState().Send(pdu, nil).Success("")
+ tnode2.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{
+ "pduSource": src,
+ }).Success("")
+ tnode3.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{
+ "pduDestination": dest,
+ }).Success("")
+
+ // run the group
+ err = tnet.Run(0)
+ assert.NoError(t, err)
+ })
+ t.Run("test_promiscuous_fail", func(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+ tests.LockGlobalTimeMachine(t)
+
+ // three element network
+ tnet := NewTIPNetwork(t, 3, "192.168.7.%d/24", true, false)
+
+ stateMachines := tnet.GetStateMachines()
+ tnode1, tnode2, tnode3 := stateMachines[0], stateMachines[1], stateMachines[2]
+
+ // make a PDU from node 1 to node 2
+ src, err := bacnetip.NewAddress(testingLogger, "192.168.7.1:47808")
+ require.NoError(t, err)
+ dest, err := bacnetip.NewAddress(testingLogger, "192.168.7.2:47808")
+ require.NoError(t, err)
+ pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest))
+ t.Log(pdu)
+
+ // node 1 sends the pdu to node 2, node 3 waits and gets nothing
+ tnode1.GetStartState().Send(pdu, nil).Success("")
+ tnode2.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{
+ "pduSource": src,
+ }).Success("")
+
+ // if node 3 receives anything it will trigger unexpected receive and fail
+ tnode3.GetStartState().Timeout(500*time.Millisecond, nil).Success("")
+
+ // run the group
+ err = tnet.Run(0)
+ assert.Error(t, err)
+ })
+}
+
+type RouterSuite struct {
+ suite.Suite
+
+ smg *tests.StateMachineGroup
+
+ log zerolog.Logger
+}
+
+func (suite *RouterSuite) SetupSuite() {
+ t := suite.T()
+ suite.log = testutils.ProduceTestingLogger(t)
+}
+
+func (suite *RouterSuite) SetupTest() {
+ t := suite.T()
+ t.Skip("not ready yet") // TODO: figure out why it is failing
+ // create a state machine group that has all nodes on all networks
+ suite.smg = tests.NewStateMachineGroup(suite.log)
+
+ // make some networks
+ vlan10 := bacnetip.NewIPNetwork(suite.log)
+ vlan20 := bacnetip.NewIPNetwork(suite.log)
+
+ // make a router and add the networks
+ trouter := bacnetip.NewIPRouter(suite.log)
+ vlan10Addr, err := bacnetip.NewAddress(suite.log, "192.168.10.1/24")
+ suite.NoError(err)
+ vlan20Addr, err := bacnetip.NewAddress(suite.log, "192.168.20.1/24")
+ suite.NoError(err)
+ trouter.AddNetwork(vlan10Addr, vlan10)
+ trouter.AddNetwork(vlan20Addr, vlan20)
+
+ for pattern, lan := range map[string]*bacnetip.IPNetwork{
+ "192.168.10.%d/24": vlan10,
+ "192.168.20.%d/24": vlan20,
+ } {
+ for i := range 2 {
+ nodeAddress, err := bacnetip.NewAddress(suite.log, fmt.Sprintf(pattern, i+2))
+ suite.NoError(err)
+ node, err := bacnetip.NewIPNode(suite.log, nodeAddress, lan)
+ suite.NoError(err)
+
+ // bind a client state machine to the node
+ csm, err := tests.NewClientStateMachine(suite.log)
+ suite.NoError(err)
+ err = bacnetip.Bind(suite.log, csm, node)
+ suite.NoError(err)
+
+ // add it to the group
+ suite.smg.Append(csm)
+ }
+ }
+}
+
+func (suite *RouterSuite) TearDownTest() {
+ t := suite.T()
+ t.Skip("not ready yet") // TODO: figure out why it is failing
+ // reset the time machine
+ tests.ResetTimeMachine(tests.StartTime)
+ suite.T().Log("time machine reset")
+
+ // run the group
+ err := suite.smg.Run()
+ suite.NoError(err)
+
+ // run it for some time
+ tests.RunTimeMachine(suite.log, 60*time.Second, time.Time{})
+ suite.T().Log("time machine finished")
+
+ // check for success
+ success, failed := suite.smg.CheckForSuccess()
+ suite.True(success)
+ _ = failed
+}
+
+func (suite *RouterSuite) TestIdle() {
+ t := suite.T()
+ tests.LockGlobalTimeMachine(t)
+
+ // all success
+ for _, csm := range suite.smg.GetStateMachines() {
+ csm.GetStartState().Success("")
+ }
+}
+
+func (suite *RouterSuite) TestSendReceive() { // Test that a node can send a message to another node.
+ t := suite.T()
+ t.Skip("not ready yet") // TODO: figure out why it is failing
+ tests.LockGlobalTimeMachine(t)
+
+ stateMachines := suite.smg.GetStateMachines()
+ csm_10_2, csm_10_3, csm_20_2, csm_20_3 := stateMachines[0], stateMachines[1], stateMachines[2], stateMachines[3]
+
+ // make a PDU from network 10 node 1 to network 20 node 2
+ src, err := bacnetip.NewAddress(suite.log, "192.168.10.2:47808")
+ require.NoError(t, err)
+ dest, err := bacnetip.NewAddress(suite.log, "192.168.20.3:47808")
+ require.NoError(t, err)
+ pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest))
+ t.Log(pdu)
+
+ // node 1 sends the pdu, mode 2 gets it
+ csm_10_2.GetStartState().Send(pdu, nil).Success("")
+ csm_20_3.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{
+ "pduSource": src,
+ }).Success("")
+
+ // other nodes get nothing
+ csm_10_3.GetStartState().Timeout(1*time.Second, nil).Success("")
+ csm_20_2.GetStartState().Timeout(1*time.Second, nil).Success("")
+}
+
+func (suite *RouterSuite) TestLocalBroadcast() { // Test that a node can send a message to all of the other nodes on the same network.
+ t := suite.T()
+ t.Skip("not ready yet") // TODO: figure out why it is failing
+ tests.LockGlobalTimeMachine(t)
+
+ stateMachines := suite.smg.GetStateMachines()
+ csm_10_2, csm_10_3, csm_20_2, csm_20_3 := stateMachines[0], stateMachines[1], stateMachines[2], stateMachines[3]
+
+ // make a PDU from network 10 node 1 to network 20 node 2
+ src, err := bacnetip.NewAddress(suite.log, "192.168.10.2:47808")
+ require.NoError(t, err)
+ dest, err := bacnetip.NewAddress(suite.log, "192.168.10.255:47808")
+ require.NoError(t, err)
+ pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest))
+ t.Log(pdu)
+
+ // node 10-2 sends the pdu, node 10-3 gets pdu, nodes 20-2 and 20-3 dont
+ csm_10_2.GetStartState().Send(pdu, nil).Success("")
+ csm_10_3.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{
+ "pduSource": src,
+ }).Success("")
+ csm_20_3.GetStartState().Timeout(1*time.Second, nil).Success("")
+ csm_20_2.GetStartState().Timeout(1*time.Second, nil).Success("")
+}
+
+func (suite *RouterSuite) TestRemoteBroadcast() { // Test that a node can send a message to all of the other nodes on a different network.
+ t := suite.T()
+ t.Skip("not ready yet") // TODO: figure out why it is failing
+ tests.LockGlobalTimeMachine(t)
+
+ stateMachines := suite.smg.GetStateMachines()
+ csm_10_2, csm_10_3, csm_20_2, csm_20_3 := stateMachines[0], stateMachines[1], stateMachines[2], stateMachines[3]
+
+ // make a PDU from network 10 node 1 to network 20 node 2
+ src, err := bacnetip.NewAddress(suite.log, "192.168.10.2:47808")
+ require.NoError(t, err)
+ dest, err := bacnetip.NewAddress(suite.log, "192.168.20.255:47808")
+ require.NoError(t, err)
+ pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest))
+ t.Log(pdu)
+
+ // node 10-2 sends the pdu, node 10-3 gets pdu, nodes 20-2 and 20-3 dont
+ csm_10_2.GetStartState().Send(pdu, nil).Success("")
+ csm_10_3.GetStartState().Timeout(1*time.Second, nil).Success("")
+ csm_20_2.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{
+ "pduSource": src,
+ }).Success("")
+ csm_20_3.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{
+ "pduSource": src,
+ }).Success("")
+}
+
+func TestRouter(t *testing.T) {
+ suite.Run(t, new(RouterSuite))
+}
diff --git a/plc4go/internal/bacnetip/tests/test_vlan/test_network_test.go b/plc4go/internal/bacnetip/tests/test_vlan/test_network_test.go
new file mode 100644
index 0000000000..bf3102f923
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/test_vlan/test_network_test.go
@@ -0,0 +1,348 @@
+/*
+ * 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 test_vlan
+
+import (
+ "testing"
+ "time"
+
+ "github.com/apache/plc4x/plc4go/internal/bacnetip"
+ "github.com/apache/plc4x/plc4go/internal/bacnetip/tests"
+ "github.com/apache/plc4x/plc4go/spi/testutils"
+
+ "github.com/pkg/errors"
+ "github.com/rs/zerolog"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type TNetwork struct {
+ *tests.StateMachineGroup
+
+ vlan *bacnetip.Network
+
+ t *testing.T
+
+ log zerolog.Logger
+}
+
+func NewTNetwork(t *testing.T, nodeCount int, promiscuous bool, spoofing bool) *TNetwork {
+ localLog := testutils.ProduceTestingLogger(t)
+ tn := &TNetwork{
+ t: t,
+ log: localLog,
+ }
+ tn.StateMachineGroup = tests.NewStateMachineGroup(localLog)
+
+ broadcastAddress, err := bacnetip.NewAddress(localLog, 0)
+ require.NoError(t, err)
+ // make a little LAN
+ tn.vlan = bacnetip.NewNetwork(localLog, bacnetip.WithNetworkBroadcastAddress(broadcastAddress))
+
+ for i := range nodeCount {
+ nodeAddress, err := bacnetip.NewAddress(localLog, i+1)
+ require.NoError(t, err)
+ node, err := bacnetip.NewNode(localLog, nodeAddress, tn.vlan, bacnetip.WithNodePromiscuous(promiscuous), bacnetip.WithNodeSpoofing(spoofing))
+ require.NoError(t, err)
+
+ // bind a client state machine to the ndoe
+ csm, err := tests.NewClientStateMachine(localLog)
+ require.NoError(t, err)
+
+ err = bacnetip.Bind(localLog, csm, node)
+ require.NoError(t, err)
+
+ // add it to this group
+ tn.Append(csm)
+ }
+
+ return tn
+}
+
+func (t *TNetwork) Run(timeLimit time.Duration) error {
+ if timeLimit == 0 {
+ timeLimit = 60 * time.Second
+ }
+ t.log.Debug().Dur("time_limit", timeLimit).Msg("run")
+
+ tests.NewGlobalTimeMachine(t.log) // TODO: this is really stupid because of concurrency...
+ // reset the time machine
+ tests.ResetTimeMachine(tests.StartTime)
+ t.log.Trace().Msg("time machine reset")
+
+ // run the group
+ if err := t.StateMachineGroup.Run(); err != nil {
+ return err
+ }
+
+ // run it some time
+ tests.RunTimeMachine(t.log, timeLimit, time.Time{})
+ t.log.Trace().Msg("time machine finished")
+
+ // check for success
+ success, failed := t.CheckForSuccess()
+ if !success {
+ return errors.New("not all succeeded")
+ }
+ if failed {
+ return errors.New("some failed")
+ }
+ return nil
+}
+
+func TestVLAN(t *testing.T) {
+ t.Run("test_idle", func(t *testing.T) { // Test that a very quiet network can exist. This is not a network test so much as a state machine group test
+ tests.LockGlobalTimeMachine(t)
+
+ // two element network
+ tnet := NewTNetwork(t, 2, false, false)
+
+ stateMachines := tnet.GetStateMachines()
+ tnode1, tnode2 := stateMachines[0], stateMachines[1]
+
+ // set the start states of both machines to success
+ tnode1.GetStartState().Success("")
+ tnode2.GetStartState().Success("")
+
+ // run the group
+ err := tnet.Run(0)
+ assert.NoError(t, err)
+ })
+ t.Run("test_send_receive", func(t *testing.T) { // Test that a node can send a message to another node.
+ testingLogger := testutils.ProduceTestingLogger(t)
+ tests.LockGlobalTimeMachine(t)
+
+ // two element network
+ tnet := NewTNetwork(t, 2, false, false)
+
+ stateMachines := tnet.GetStateMachines()
+ tnode1, tnode2 := stateMachines[0], stateMachines[1]
+
+ // make a PDU from node 1 to node 2
+ src, err := bacnetip.NewAddress(testingLogger, 1)
+ require.NoError(t, err)
+ dest, err := bacnetip.NewAddress(testingLogger, 2)
+ require.NoError(t, err)
+ pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest))
+ t.Log(pdu)
+
+ // node 1 sends the pdu, mode 2 gets it
+ tnode1.GetStartState().Send(pdu, nil).Success("")
+ tnode2.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{
+ "pduSource": src,
+ }).Success("")
+
+ // run the group
+ err = tnet.Run(0)
+ assert.NoError(t, err)
+ })
+ t.Run("test_broadcast", func(t *testing.T) { // Test that a node can send out a 'local broadcast' message which will be received by every other node.
+ testingLogger := testutils.ProduceTestingLogger(t)
+ tests.LockGlobalTimeMachine(t)
+
+ // three element network
+ tnet := NewTNetwork(t, 3, false, false)
+
+ stateMachines := tnet.GetStateMachines()
+ tnode1, tnode2, tnode3 := stateMachines[0], stateMachines[1], stateMachines[2]
+
+ // make a PDU from node 1 to node 2
+ src, err := bacnetip.NewAddress(testingLogger, 1)
+ require.NoError(t, err)
+ dest, err := bacnetip.NewAddress(testingLogger, 0)
+ require.NoError(t, err)
+ pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest))
+ t.Log(pdu)
+
+ // node 1 sends the pdu, node 2 and 3 each get it
+ tnode1.GetStartState().Send(pdu, nil).Success("")
+ tnode2.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{
+ "pduSource": src,
+ }).Success("")
+ tnode3.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{
+ "pduSource": src,
+ }).Success("")
+
+ // run the group
+ err = tnet.Run(0)
+ assert.NoError(t, err)
+ })
+ t.Run("test_spoof_fail", func(t *testing.T) { // Test verifying that a node cannot send out packets with a source address other than its own, see also test_spoof_pass().
+ testingLogger := testutils.ProduceTestingLogger(t)
+ tests.LockGlobalTimeMachine(t)
+
+ // one element network
+ tnet := NewTNetwork(t, 1, false, false)
+
+ stateMachines := tnet.GetStateMachines()
+ tnode1 := stateMachines[0]
+
+ // make an unicast PDU with the wrong source
+ src, err := bacnetip.NewAddress(testingLogger, 2)
+ require.NoError(t, err)
+ dest, err := bacnetip.NewAddress(testingLogger, 3)
+ require.NoError(t, err)
+ pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest))
+ t.Log(pdu)
+
+ // node 1 sends the pdu, node 2 and 3 each get it
+ tnode1.GetStartState().Send(pdu, nil).Success("")
+
+ // run the group
+ err = tnet.Run(0)
+ assert.Error(t, err)
+ })
+ t.Run("test_spoof_pass", func(t *testing.T) { // Test allowing a node to send out packets with a source address other than its own, see also test_spoof_fail().
+ testingLogger := testutils.ProduceTestingLogger(t)
+ tests.LockGlobalTimeMachine(t)
+
+ // one element network
+ tnet := NewTNetwork(t, 1, false, true)
+
+ stateMachines := tnet.GetStateMachines()
+ tnode1 := stateMachines[0]
+
+ // make an unicast PDU with the wrong source
+ src, err := bacnetip.NewAddress(testingLogger, 3)
+ require.NoError(t, err)
+ dest, err := bacnetip.NewAddress(testingLogger, 1)
+ require.NoError(t, err)
+ pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest))
+ t.Log(pdu)
+
+ // node 1 sends the pdu, but gets it back as if it was from node 3
+ tnode1.GetStartState().
+ Send(pdu, nil).
+ Receive(bacnetip.NewPDU(nil), map[string]any{
+ "pduSource": src,
+ }).
+ Success("")
+
+ // run the group
+ err = tnet.Run(0)
+ assert.NoError(t, err)
+ })
+ t.Run("test_promiscuous_pass", func(t *testing.T) { // Test 'promiscuous mode' of a node which allows it to receive every packet sent on the network. This is like the network is a hub, or the node is connected to a 'monitor' port on a managed switch.
+ testingLogger := testutils.ProduceTestingLogger(t)
+ tests.LockGlobalTimeMachine(t)
+
+ // three element network
+ tnet := NewTNetwork(t, 3, true, false)
+
+ stateMachines := tnet.GetStateMachines()
+ tnode1, tnode2, tnode3 := stateMachines[0], stateMachines[1], stateMachines[2]
+
+ // make a PDU from node 1 to node 2
+ src, err := bacnetip.NewAddress(testingLogger, 1)
+ require.NoError(t, err)
+ dest, err := bacnetip.NewAddress(testingLogger, 2)
+ require.NoError(t, err)
+ pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest))
+ t.Log(pdu)
+
+ // node 1 sends the pdu, node 2 and 3 each get it
+ tnode1.GetStartState().Send(pdu, nil).Success("")
+ tnode2.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{
+ "pduSource": src,
+ }).Success("")
+ tnode3.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{
+ "pduDestination": dest,
+ }).Success("")
+
+ // run the group
+ err = tnet.Run(0)
+ assert.NoError(t, err)
+ })
+ t.Run("test_promiscuous_fail", func(t *testing.T) {
+ testingLogger := testutils.ProduceTestingLogger(t)
+ tests.LockGlobalTimeMachine(t)
+
+ // three element network
+ tnet := NewTNetwork(t, 3, true, false)
+
+ stateMachines := tnet.GetStateMachines()
+ tnode1, tnode2, tnode3 := stateMachines[0], stateMachines[1], stateMachines[2]
+
+ // make a PDU from node 1 to node 2
+ src, err := bacnetip.NewAddress(testingLogger, 1)
+ require.NoError(t, err)
+ dest, err := bacnetip.NewAddress(testingLogger, 2)
+ require.NoError(t, err)
+ pdu := bacnetip.NewPDU(nil, bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest))
+ t.Log(pdu)
+
+ // node 1 sends the pdu to node 2, node 3 waits and gets nothing
+ tnode1.GetStartState().Send(pdu, nil).Success("")
+ tnode2.GetStartState().Receive(bacnetip.NewPDU(nil), map[string]any{
+ "pduSource": src,
+ }).Success("")
+
+ // if node 3 receives anything it will trigger unexpected receive and fail
+ tnode3.GetStartState().Timeout(500*time.Millisecond, nil).Success("")
+
+ // run the group
+ err = tnet.Run(0)
+ assert.Error(t, err)
+ })
+}
+
+func TestVLANEvents(t *testing.T) {
+ t.Run("test_send_receive", func(t *testing.T) { // Test that a node can send a message to another node and use events to continue with the messages.
+ testingLogger := testutils.ProduceTestingLogger(t)
+ tests.LockGlobalTimeMachine(t)
+
+ // two element network
+ tnet := NewTNetwork(t, 2, false, false)
+
+ stateMachines := tnet.GetStateMachines()
+ tnode1, tnode2 := stateMachines[0], stateMachines[1]
+
+ // make a PDU from node 1 to node 2
+ src, err := bacnetip.NewAddress(testingLogger, 1)
+ require.NoError(t, err)
+ dest, err := bacnetip.NewAddress(testingLogger, 2)
+ require.NoError(t, err)
+
+ deadPDU := bacnetip.NewPDU(tests.NewDummyMessage(0xde, 0xad), bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest))
+ t.Log(deadPDU)
+
+ // make a PDU from node 1 to node 2
+ beefPDU := bacnetip.NewPDU(tests.NewDummyMessage(0xbe, 0xef), bacnetip.WithPDUSource(src), bacnetip.WithPDUDestination(dest))
+ t.Log(beefPDU)
+
+ // node 1 sends dead_pdu, waits for event, sends beef_pdu
+ tnode1.GetStartState().
+ Send(deadPDU, nil).WaitEvent("e", nil).
+ Send(beefPDU, nil).Success("")
+
+ // node 2 receives dead_pdu, sets event, waits for beef_pdu
+ tnode2.GetStartState().
+ Receive(bacnetip.NewPDU(nil), map[string]any{
+ "pduData": tests.NewDummyMessage(0xde, 0xad),
+ }).SetEvent("e").
+ Receive(bacnetip.NewPDU(nil), map[string]any{
+ "pduData": tests.NewDummyMessage(0xbe, 0xef),
+ }).Success("")
+
+ // run the group
+ err = tnet.Run(0)
+ assert.NoError(t, err)
+ })
+}
diff --git a/plc4go/internal/bacnetip/tests/time_machine.go b/plc4go/internal/bacnetip/tests/time_machine.go
new file mode 100644
index 0000000000..7b4fde8ff3
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/time_machine.go
@@ -0,0 +1,236 @@
+/*
+ * 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 tests
+
+import (
+ "fmt"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/apache/plc4x/plc4go/internal/bacnetip"
+
+ "github.com/rs/zerolog"
+)
+
+var globalTimeMachine *TimeMachine
+var globalTimeMachineMutex sync.Mutex
+
+func IsGlobalTimeMachineSet() bool {
+ return globalTimeMachine != nil
+}
+
+func NewGlobalTimeMachine(localLog zerolog.Logger) {
+ if globalTimeMachine != nil {
+ localLog.Warn().Msg("global time machine set, overwriting")
+ }
+ globalTimeMachine = NewTimeMachine(localLog)
+}
+
+func ClearGlobalTimeMachine(localLog zerolog.Logger) {
+ if globalTimeMachine == nil {
+ localLog.Warn().Msg("global time machine not set")
+ }
+ globalTimeMachine = nil
+}
+
+func LockGlobalTimeMachine(t *testing.T) {
+ globalTimeMachineMutex.Lock()
+ t.Cleanup(globalTimeMachineMutex.Unlock)
+}
+
+type TimeMachine struct {
+ bacnetip.TaskManager
+
+ currentTime time.Time
+ timeLimit time.Time
+ startTime time.Time
+
+ log zerolog.Logger
+}
+
+func NewTimeMachine(localLog zerolog.Logger) *TimeMachine {
+ t := &TimeMachine{
+ log: localLog,
+ }
+ t.TaskManager = bacnetip.NewTaskManager(localLog)
+ bacnetip.OverwriteTaskManager(t)
+ return t
+}
+
+func (t *TimeMachine) GetTime() time.Time {
+ t.log.Debug().Time("currentTime", t.currentTime).Msg("GetTime")
+ return t.currentTime
+}
+
+func (t *TimeMachine) InstallTask(task bacnetip.TaskRequirements) {
+ t.log.Debug().Time("currentTime", t.currentTime).Stringer("task", task).Msg("InstallTask")
+ t.TaskManager.InstallTask(task)
+}
+
+func (t *TimeMachine) SuspendTask(task bacnetip.TaskRequirements) {
+ t.log.Debug().Time("currentTime", t.currentTime).Stringer("task", task).Msg("SuspendTask")
+ t.TaskManager.SuspendTask(task)
+}
+
+func (t *TimeMachine) ResumeTask(task bacnetip.TaskRequirements) {
+ t.log.Debug().Time("currentTime", t.currentTime).Stringer("task", task).Msg("ResumeTask")
+ t.TaskManager.ResumeTask(task)
+}
+
+func (t *TimeMachine) MoreToDo() bool {
+ t.log.Debug().Time("currentTime", t.currentTime).Msg("MoreToDo")
+ if len(bacnetip.DeferredFunctions) > 0 {
+ t.log.Trace().Msg("deferredFunctions")
+ return true
+ }
+
+ t.log.Debug().Time("timeLimit", t.timeLimit).Msg("timeLimit")
+ if t.log.Debug().Enabled() {
+ stringers := make([]fmt.Stringer, len(t.GetTasks()))
+ for i, task := range t.GetTasks() { //TODO: check if there is something more efficient
+ stringers[i] = task
+ }
+ t.log.Debug().Stringers("tasks", stringers).Msg("tasks")
+ }
+
+ if !t.timeLimit.IsZero() && t.currentTime.After(t.timeLimit) {
+ t.log.Trace().Msg("time limit reached")
+ return false
+ }
+
+ if len(t.GetTasks()) == 0 {
+ t.log.Trace().Msg("no more tasks")
+ return false
+ }
+
+ task := t.GetTasks()[0]
+ when := task.GetTaskTime()
+ if when.After(t.timeLimit) {
+ t.log.Debug().Msg("New tasks exceeded time limit")
+ return false
+ }
+ t.log.Debug().Stringer("task", task).Msg("task")
+ return true
+}
+
+func (t *TimeMachine) GetNextTask() (bacnetip.TaskRequirements, *time.Duration) {
+ t.log.Debug().Time("currentTime", t.currentTime).Msg("GetNextTask")
+ t.log.Debug().Time("timeLimit", t.timeLimit).Msg("timeLimit")
+ if t.log.Debug().Enabled() {
+ stringers := make([]fmt.Stringer, len(t.GetTasks()))
+ for i, task := range t.GetTasks() { //TODO: check if there is something more efficient
+ stringers[i] = task
+ }
+ t.log.Debug().Stringers("tasks", stringers).Msg("tasks")
+ }
+
+ var task bacnetip.TaskRequirements
+ var delta *time.Duration
+
+ if !t.timeLimit.IsZero() && t.currentTime.After(t.timeLimit) {
+ t.log.Trace().Msg("time limit reached")
+ } else if len(t.GetTasks()) == 0 {
+ t.log.Trace().Msg("no more tasks")
+ } else {
+ task = t.GetTasks()[0]
+ if taskTime := task.GetTaskTime(); taskTime != nil && task.GetTaskTime().After(t.timeLimit) {
+ t.currentTime = *taskTime
+ } else {
+ // pull it off the list
+ task = t.PopTask()
+ t.log.Debug().Stringer("task", task).Msg("when, task")
+
+ // mark that it is no longer scheduled
+ task.SetIsScheduled(false)
+
+ // advance the time
+ if taskTime := task.GetTaskTime(); taskTime != nil {
+ t.currentTime = *taskTime
+ }
+
+ // do not wait, time has moved
+ var newDelta time.Duration
+ delta = &newDelta
+ }
+ }
+ return task, delta
+}
+
+func (t *TimeMachine) ProcessTask(task bacnetip.TaskRequirements) {
+ t.log.Debug().Time("currentTime", t.currentTime).Stringer("task", task).Msg("ProcessTask")
+ t.TaskManager.ProcessTask(task)
+}
+
+// ResetTimeMachine This function is called to reset the clock before running a set of tests.
+func ResetTimeMachine(startTime time.Time) {
+ if globalTimeMachine == nil {
+ panic("no time machine")
+ }
+
+ globalTimeMachine.ClearTasks()
+ globalTimeMachine.currentTime = startTime
+ globalTimeMachine.timeLimit = time.Time{}
+}
+
+// RunTimeMachine This function is called after a set of tasks have been installed
+//
+// and they should Run. The machine will stop when the stop time has been
+// reached (maybe the middle of some tests) and can be called again to
+// continue running.
+func RunTimeMachine(localLog zerolog.Logger, duration time.Duration, stopTime time.Time) {
+ if globalTimeMachine == nil {
+ panic("no time machine")
+ }
+ localLog.Debug().Dur("duration", duration).Time("stopTime", stopTime).Msg("RunTimeMachine")
+
+ /* TODO: we don't have a proper tristate, maybe we change currentTime to a pointer
+ if !globalTimeMachine.currentTime.IsZero() {
+ panic("Reset the time machine before running")
+ }*/
+
+ if duration != 0 {
+ globalTimeMachine.timeLimit = globalTimeMachine.currentTime.Add(duration)
+ } else if !stopTime.IsZero() {
+ globalTimeMachine.timeLimit = stopTime
+ } else {
+ panic("duration or stopTime is required")
+ }
+
+ if len(bacnetip.DeferredFunctions) > 0 {
+ localLog.Debug().Msg("deferredFunctions")
+ }
+
+ for {
+ bacnetip.RunOnce(localLog)
+ localLog.Trace().Msg("ran once")
+ if !globalTimeMachine.MoreToDo() {
+ localLog.Trace().Msg("no more to do")
+ break
+ }
+ }
+
+ globalTimeMachine.currentTime = globalTimeMachine.timeLimit
+}
+
+// GlobalTimeMachineCurrentTime Return the current time from the time machine.
+func GlobalTimeMachineCurrentTime() time.Time {
+ return globalTimeMachine.currentTime
+}
diff --git a/plc4go/internal/bacnetip/tests/trapped_classes.go b/plc4go/internal/bacnetip/tests/trapped_classes.go
new file mode 100644
index 0000000000..37f7df1981
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/trapped_classes.go
@@ -0,0 +1,569 @@
+/*
+ * 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 tests
+
+import (
+ "fmt"
+
+ "github.com/apache/plc4x/plc4go/internal/bacnetip"
+
+ "github.com/pkg/errors"
+ "github.com/rs/zerolog"
+)
+
+type TrapperRequirements interface {
+ BeforeSend(pdu bacnetip.PDU)
+ AfterSend(pdu bacnetip.PDU)
+ BeforeReceive(pdu bacnetip.PDU)
+ AfterReceive(pdu bacnetip.PDU)
+ UnexpectedReceive(pdu bacnetip.PDU)
+}
+
+// Trapper This class provides a set of utility functions that keeps the latest copy of the pdu parameter in the
+// before_send(), after_send(), before_receive(), after_receive() and unexpected_receive() calls.
+type Trapper struct {
+ TrapperRequirements
+
+ beforeSendPdu bacnetip.PDU
+ afterSendPdu bacnetip.PDU
+ beforeReceivePdu bacnetip.PDU
+ afterReceivePdu bacnetip.PDU
+ unexpectedReceivePdu bacnetip.PDU
+
+ log zerolog.Logger
+}
+
+func NewTrapper(localLog zerolog.Logger, requirements TrapperRequirements) *Trapper {
+ trapper := &Trapper{
+ TrapperRequirements: requirements,
+ log: localLog,
+ }
+ // reset to initialize
+ trapper.reset()
+ return trapper
+}
+
+func (t *Trapper) reset() {
+ t.log.Trace().Msg("Reset")
+ // flush the copies
+ t.beforeSendPdu = nil
+ t.afterSendPdu = nil
+ t.beforeReceivePdu = nil
+ t.afterReceivePdu = nil
+ t.unexpectedReceivePdu = nil
+}
+
+// BeforeSend is Called before each PDU about to be sent.
+func (t *Trapper) BeforeSend(pdu bacnetip.PDU) {
+ t.log.Debug().Stringer("pdu", pdu).Msg("BeforeSend")
+ //keep a copy
+ t.beforeSendPdu = pdu
+
+ // continue
+ t.TrapperRequirements.BeforeSend(pdu)
+}
+
+func (t *Trapper) GetBeforeSendPdu() bacnetip.PDU {
+ return t.beforeSendPdu
+}
+
+// AfterSend is Called after each PDU sent.
+func (t *Trapper) AfterSend(pdu bacnetip.PDU) {
+ t.log.Debug().Stringer("pdu", pdu).Msg("AfterSend")
+ //keep a copy
+ t.afterSendPdu = pdu
+
+ // continue
+ t.TrapperRequirements.AfterSend(pdu)
+}
+
+func (t *Trapper) GetAfterSendPdu() bacnetip.PDU {
+ return t.afterSendPdu
+}
+
+// BeforeReceive is Called with each PDU received before matching.
+func (t *Trapper) BeforeReceive(pdu bacnetip.PDU) {
+ t.log.Debug().Stringer("pdu", pdu).Msg("BeforeReceive")
+ //keep a copy
+ t.beforeReceivePdu = pdu
+
+ // continue
+ t.TrapperRequirements.BeforeReceive(pdu)
+}
+
+func (t *Trapper) GetBeforeReceivePdu() bacnetip.PDU {
+ return t.beforeReceivePdu
+}
+
+// AfterReceive is Called with PDU received after match.
+func (t *Trapper) AfterReceive(pdu bacnetip.PDU) {
+ t.log.Debug().Stringer("pdu", pdu).Msg("AfterReceive")
+ //keep a copy
+ t.afterReceivePdu = pdu
+
+ // continue
+ t.TrapperRequirements.AfterReceive(pdu)
+}
+
+func (t *Trapper) GetAfterReceivePdu() bacnetip.PDU {
+ return t.afterReceivePdu
+}
+
+// UnexpectedReceive is Called with PDU that did not match. Unless this is trapped by the state, the default behaviour is to fail.
+func (t *Trapper) UnexpectedReceive(pdu bacnetip.PDU) {
+ t.log.Debug().Stringer("pdu", pdu).Msg("UnexpectedReceive")
+ //keep a copy
+ t.unexpectedReceivePdu = pdu
+
+ // continue
+ t.TrapperRequirements.UnexpectedReceive(pdu)
+}
+
+func (t *Trapper) GetUnexpectedReceivePDU() bacnetip.PDU {
+ return t.unexpectedReceivePdu
+}
+
+// TrappedState This class is a simple wrapper around the state class that keeps the latest copy of the pdu parameter in
+// the BeforeSend(), AfterSend(), BeforeReceive(), AfterReceive() and UnexpectedReceive() calls.
+type TrappedState struct {
+ *Trapper
+ State
+}
+
+func NewTrappedState(state State, trapper *Trapper) *TrappedState {
+ t := &TrappedState{
+ State: state,
+ Trapper: trapper,
+ }
+ return t
+}
+
+func (t *TrappedState) Equals(other State) bool {
+ if t.State.Equals(other) { //TODO: we always want to match the inner
+ return true
+ }
+ if otherTs, ok := other.(*TrappedState); ok {
+ return t.State.Equals(otherTs.State)
+ }
+ return false
+}
+
+func (t *TrappedState) String() string {
+ return fmt.Sprintf("TrappedState(%v)", t.State)
+}
+
+func (t *TrappedState) BeforeSend(pdu bacnetip.PDU) {
+ t.Trapper.BeforeSend(pdu)
+}
+
+func (t *TrappedState) AfterSend(pdu bacnetip.PDU) {
+ t.Trapper.AfterSend(pdu)
+}
+
+func (t *TrappedState) BeforeReceive(pdu bacnetip.PDU) {
+ t.Trapper.BeforeReceive(pdu)
+}
+
+func (t *TrappedState) AfterReceive(pdu bacnetip.PDU) {
+ t.Trapper.AfterReceive(pdu)
+}
+
+func (t *TrappedState) UnexpectedReceive(pdu bacnetip.PDU) {
+ t.Trapper.UnexpectedReceive(pdu)
+}
+
+func (t *TrappedState) getInterceptor() StateInterceptor {
+ return t
+}
+
+// TrappedStateMachine This class is a simple wrapper around the stateMachine class that keeps the
+//
+// latest copy of the pdu parameter in the BeforeSend(), AfterSend(), BeforeReceive(), AfterReceive() and UnexpectedReceive() calls.
+//
+// It also provides a send() function, so when the machine runs it doesn't
+// throw an exception.
+type TrappedStateMachine struct {
+ *Trapper
+ StateMachine
+
+ sent bacnetip.PDU
+
+ log zerolog.Logger
+}
+
+func NewTrappedStateMachine(localLog zerolog.Logger) *TrappedStateMachine {
+ t := &TrappedStateMachine{
+ log: localLog,
+ }
+ var init func()
+ t.StateMachine, init = NewStateMachine(localLog, t, WithStateMachineStateInterceptor(t), WithStateMachineStateDecorator(t.DecorateState))
+ t.Trapper = NewTrapper(localLog, t.StateMachine)
+ init() // bit later so everything is set up
+ return t
+}
+
+func (t *TrappedStateMachine) GetSent() bacnetip.PDU {
+ return t.sent
+}
+
+func (t *TrappedStateMachine) BeforeSend(pdu bacnetip.PDU) {
+ t.StateMachine.BeforeSend(pdu)
+}
+
+func (t *TrappedStateMachine) Send(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ t.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Send")
+ // keep a copy
+ t.sent = args.Get0PDU()
+ return nil
+}
+
+func (t *TrappedStateMachine) AfterSend(pdu bacnetip.PDU) {
+ t.StateMachine.AfterSend(pdu)
+}
+
+func (t *TrappedStateMachine) BeforeReceive(pdu bacnetip.PDU) {
+ t.StateMachine.BeforeReceive(pdu)
+}
+
+func (t *TrappedStateMachine) AfterReceive(pdu bacnetip.PDU) {
+ t.StateMachine.AfterReceive(pdu)
+}
+
+func (t *TrappedStateMachine) UnexpectedReceive(pdu bacnetip.PDU) {
+ t.StateMachine.UnexpectedReceive(pdu)
+}
+
+func (t *TrappedStateMachine) DecorateState(state State) State {
+ return NewTrappedState(state, t.Trapper)
+}
+
+type TrappedClientRequirements interface {
+ Request(bacnetip.Args, bacnetip.KWArgs) error
+}
+
+// TrappedClient An instance of this class sits at the top of a stack.
+type TrappedClient struct {
+ TrappedClientRequirements
+ *bacnetip.Client
+
+ requestSent bacnetip.PDU
+ confirmationReceived bacnetip.PDU
+
+ log zerolog.Logger
+}
+
+func NewTrappedClient(localLog zerolog.Logger, requirements TrappedClientRequirements) (*TrappedClient, error) {
+ t := &TrappedClient{
+ TrappedClientRequirements: requirements,
+ log: localLog,
+ }
+ var err error
+ t.Client, err = bacnetip.NewClient(localLog, t)
+ if err != nil {
+ return nil, errors.Wrap(err, "error building client")
+ }
+ return t, nil
+}
+
+func (t *TrappedClient) GetRequestSent() bacnetip.PDU {
+ return t.requestSent
+}
+
+func (t *TrappedClient) GetConfirmationReceived() bacnetip.PDU {
+ return t.confirmationReceived
+}
+
+func (t *TrappedClient) Request(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ t.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Request")
+ // a reference for checking
+ t.requestSent = args.Get0PDU()
+
+ // Call sub
+ return t.TrappedClientRequirements.Request(args, kwargs)
+}
+
+func (t *TrappedClient) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ t.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Confirmation")
+ // a reference for checking
+ t.confirmationReceived = args.Get0PDU()
+ return nil
+}
+
+type TrappedServerRequirements interface {
+ Response(bacnetip.Args, bacnetip.KWArgs) error
+}
+
+// TrappedServer An instance of this class sits at the bottom of a stack.
+type TrappedServer struct {
+ TrappedServerRequirements
+ *bacnetip.Server
+
+ indicationReceived bacnetip.PDU
+ responseSent bacnetip.PDU
+
+ log zerolog.Logger
+}
+
+func NewTrappedServer(localLog zerolog.Logger, requirements TrappedServerRequirements) (*TrappedServer, error) {
+ t := &TrappedServer{
+ TrappedServerRequirements: requirements,
+ log: localLog,
+ }
+ var err error
+ t.Server, err = bacnetip.NewServer(localLog, t)
+ if err != nil {
+ return nil, errors.Wrap(err, "error building server")
+ }
+ return t, nil
+}
+
+func (t *TrappedServer) GetIndicationReceived() bacnetip.PDU {
+ return t.indicationReceived
+}
+
+func (t *TrappedServer) GetResponseSent() bacnetip.PDU {
+ return t.responseSent
+}
+
+func (t *TrappedServer) Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ t.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Indication")
+ // a reference for checking
+ t.indicationReceived = args.Get0PDU()
+
+ return nil
+}
+
+func (t *TrappedServer) Response(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ t.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Response")
+ // a reference for checking
+ t.responseSent = args.Get0PDU()
+
+ // Call sub
+ return t.TrappedServerRequirements.Response(args, kwargs)
+}
+
+type TrappedServerStateMachine struct {
+ *TrappedServer
+ *TrappedStateMachine
+
+ log zerolog.Logger
+}
+
+func NewTrappedServerStateMachine(localLog zerolog.Logger) (*TrappedServerStateMachine, error) {
+ t := &TrappedServerStateMachine{log: localLog}
+ var err error
+ t.TrappedServer, err = NewTrappedServer(localLog, t)
+ if err != nil {
+ return nil, errors.Wrap(err, "error building trapped server")
+ }
+ t.TrappedStateMachine = NewTrappedStateMachine(localLog)
+ return t, nil
+}
+
+func (t *TrappedServerStateMachine) Send(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ t.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Send")
+ return t.Response(args, kwargs)
+}
+
+func (t *TrappedServerStateMachine) Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ t.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Indication")
+ return t.Receive(args, kwargs)
+}
+
+type TrappedServiceAccessPointRequirements interface {
+ SapRequest(args bacnetip.Args, kwargs bacnetip.KWArgs) error
+ SapIndication(args bacnetip.Args, kwargs bacnetip.KWArgs) error
+ SapResponse(args bacnetip.Args, kwargs bacnetip.KWArgs) error
+ SapConfirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error
+}
+
+// TrappedServiceAccessPoint Note that while this class inherits from ServiceAccessPoint, it doesn't
+//
+// provide any stubbed behavior for SapIndication() or SapConfirmation(),
+// so if these functions are called it will still raise panic.
+//
+// To provide these functions, write a ServiceAccessPoint derived class and
+// stuff it in the inheritance sequence:
+//
+// struct Snort{
+// ServiceAccessPoint
+// }
+// func SapIndication(pdu):
+// ...do something...
+// func SapConfirmation(pdu):
+// ...do something...
+//
+// struct TrappedSnort(TrappedServiceAccessPoint, Snort)
+//
+// The Snort functions will be called after the PDU is trapped.
+type TrappedServiceAccessPoint struct {
+ TrappedServiceAccessPointRequirements
+ *bacnetip.ServiceAccessPoint
+
+ sapRequestSent bacnetip.PDU
+ sapIndicationReceived bacnetip.PDU
+ sapResponseSent bacnetip.PDU
+ sapConfirmationReceived bacnetip.PDU
+
+ log zerolog.Logger
+}
+
+func NewTrappedServiceAccessPoint(localLog zerolog.Logger, requirements TrappedServiceAccessPointRequirements) (*TrappedServiceAccessPoint, error) {
+ t := &TrappedServiceAccessPoint{
+ TrappedServiceAccessPointRequirements: requirements,
+ log: localLog,
+ }
+ var err error
+ t.ServiceAccessPoint, err = bacnetip.NewServiceAccessPoint(localLog, t)
+ if err != nil {
+ return nil, errors.Wrap(err, "error building service access point")
+ }
+ return t, nil
+}
+
+func (s *TrappedServiceAccessPoint) GetSapRequestSent() bacnetip.PDU {
+ return s.sapRequestSent
+}
+func (s *TrappedServiceAccessPoint) GetSapIndicationReceived() bacnetip.PDU {
+ return s.sapIndicationReceived
+}
+func (s *TrappedServiceAccessPoint) GetSapResponseSent() bacnetip.PDU {
+ return s.sapResponseSent
+}
+func (s *TrappedServiceAccessPoint) GetSapConfirmationReceived() bacnetip.PDU {
+ return s.sapConfirmationReceived
+}
+
+func (s *TrappedServiceAccessPoint) SapRequest(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("SapRequest")
+ s.sapRequestSent = args.Get0PDU()
+ return s.TrappedServiceAccessPointRequirements.SapRequest(args, kwargs)
+}
+
+func (s *TrappedServiceAccessPoint) SapIndication(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("SapIndication")
+ s.sapIndicationReceived = args.Get0PDU()
+ return s.TrappedServiceAccessPointRequirements.SapIndication(args, kwargs)
+}
+
+func (s *TrappedServiceAccessPoint) SapResponse(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("SapResponse")
+ s.sapResponseSent = args.Get0PDU()
+ return s.TrappedServiceAccessPointRequirements.SapResponse(args, kwargs)
+}
+
+func (s *TrappedServiceAccessPoint) SapConfirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("SapConfirmation")
+ s.sapConfirmationReceived = args.Get0PDU()
+ return s.TrappedServiceAccessPointRequirements.SapConfirmation(args, kwargs)
+}
+
+type TrappedApplicationServiceElementRequirements interface {
+ Request(args bacnetip.Args, kwargs bacnetip.KWArgs) error
+ Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error
+ Response(args bacnetip.Args, kwargs bacnetip.KWArgs) error
+ Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error
+}
+
+// TrappedApplicationServiceElement Note that while this class inherits from ApplicationServiceElement, it
+//
+// doesn't provide any stubbed behavior for indication() or confirmation(),
+// so if these functions are called it will still raise NotImplementedError.
+//
+// To provide these functions, write a ServiceAccessPoint derived class and
+// stuff it in the inheritance sequence:
+//
+// class Snort(ApplicationServiceElement):
+// def indication(self, pdu):
+// ...do something...
+// def confirmation(self, pdu):
+// ...do something...
+//
+// class TrappedSnort(TrappedApplicationServiceElement, Snort): pass
+//
+// The Snort functions will be called after the PDU is trapped.
+type TrappedApplicationServiceElement struct {
+ TrappedApplicationServiceElementRequirements
+ *bacnetip.ApplicationServiceElement
+
+ requestSent bacnetip.PDU
+ indicationReceived bacnetip.PDU
+ responseSent bacnetip.PDU
+ confirmationReceived bacnetip.PDU
+
+ log zerolog.Logger
+}
+
+func NewTrappedApplicationServiceElement(localLog zerolog.Logger, requirements TrappedApplicationServiceElementRequirements) (*TrappedApplicationServiceElement, error) {
+ t := &TrappedApplicationServiceElement{
+ TrappedApplicationServiceElementRequirements: requirements,
+ log: localLog,
+ }
+ var err error
+ t.ApplicationServiceElement, err = bacnetip.NewApplicationServiceElement(localLog, t)
+ if err != nil {
+ return nil, errors.Wrap(err, "error building application service element")
+ }
+ return t, nil
+}
+
+func (s *TrappedApplicationServiceElement) GetRequestSent() bacnetip.PDU {
+ return s.requestSent
+}
+
+func (s *TrappedApplicationServiceElement) GetIndicationReceived() bacnetip.PDU {
+ return s.indicationReceived
+}
+
+func (s *TrappedApplicationServiceElement) GetResponseSent() bacnetip.PDU {
+ return s.responseSent
+}
+
+func (s *TrappedApplicationServiceElement) GetConfirmationReceived() bacnetip.PDU {
+ return s.confirmationReceived
+}
+
+func (s *TrappedApplicationServiceElement) String() string {
+ return fmt.Sprintf("TrappedApplicationServiceElement(TBD...)") // TODO: fill some info here
+}
+
+func (s *TrappedApplicationServiceElement) Request(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Request")
+ s.requestSent = args.Get0PDU()
+ return s.TrappedApplicationServiceElementRequirements.Request(args, kwargs)
+}
+
+func (s *TrappedApplicationServiceElement) Indication(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Indication")
+ s.indicationReceived = args.Get0PDU()
+ return s.TrappedApplicationServiceElementRequirements.Indication(args, kwargs)
+}
+
+func (s *TrappedApplicationServiceElement) Response(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Response")
+ s.responseSent = args.Get0PDU()
+ return s.TrappedApplicationServiceElementRequirements.Response(args, kwargs)
+}
+
+func (s *TrappedApplicationServiceElement) Confirmation(args bacnetip.Args, kwargs bacnetip.KWArgs) error {
+ s.log.Debug().Stringer("args", args).Stringer("kwargs", kwargs).Msg("Confirmation")
+ s.confirmationReceived = args.Get0PDU()
+ return s.TrappedApplicationServiceElementRequirements.Confirmation(args, kwargs)
+}
diff --git a/plc4go/internal/bacnetip/tests/util.go b/plc4go/internal/bacnetip/tests/util.go
new file mode 100644
index 0000000000..8288809128
--- /dev/null
+++ b/plc4go/internal/bacnetip/tests/util.go
@@ -0,0 +1,67 @@
+/*
+ * 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 tests
+
+import (
+ "context"
+ "encoding/hex"
+ "fmt"
+ "time"
+
+ "github.com/apache/plc4x/plc4go/spi/utils"
+)
+
+var StartTime = time.Time{}.Add(1 * time.Hour)
+
+type DummyMessage struct {
+ Data []byte
+}
+
+func NewDummyMessage(data ...byte) *DummyMessage {
+ return &DummyMessage{Data: data}
+}
+
+func (d DummyMessage) String() string {
+ return hex.EncodeToString(d.Data)
+}
+
+func (d DummyMessage) Serialize() ([]byte, error) {
+ return d.Data, nil
+}
+
+func (d DummyMessage) SerializeWithWriteBuffer(ctx context.Context, writeBuffer utils.WriteBuffer) error {
+ return writeBuffer.WriteSerializable(ctx, d)
+}
+
+func (d DummyMessage) GetLengthInBytes(_ context.Context) uint16 {
+ return uint16(len(d.Data))
+}
+
+func (d DummyMessage) GetLengthInBits(_ context.Context) uint16 {
+ return uint16(len(d.Data))
+}
+
+type AssertionError struct {
+ Message string
+}
+
+func (a AssertionError) Error() string {
+ return fmt.Sprintf("AssertionError: %s", a.Message)
+}