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)
+}