From 3ea710f92fec8696ed00f5ad3a39b1fa9e7aad6f Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Sat, 15 Nov 2025 13:33:36 -0600 Subject: [PATCH] Added segmentation support for server reply. (#974) Added segmentation support for server devices for some services. Configure BACNET_SEGMENTATION_ENABLED=1 to include in the library, and adjust BACNET_MAX_SEGMENTS_ACCEPTED for maximum number of segments. --- CHANGELOG.md | 5 + CMakeLists.txt | 9 + Makefile | 4 + apps/Makefile | 5 + apps/server/main.c | 14 + ports/atmega328/device.c | 5 + src/bacnet/apdu.h | 15 + src/bacnet/bacdef.h | 16 + src/bacnet/basic/binding/address.c | 42 +- src/bacnet/basic/binding/address.h | 8 + src/bacnet/basic/object/device.c | 8 + src/bacnet/basic/service/h_apdu.c | 314 ++++++- src/bacnet/basic/service/h_apdu.h | 18 + src/bacnet/basic/service/h_dcc.c | 5 +- src/bacnet/basic/service/h_rd.c | 5 +- src/bacnet/basic/service/h_rp.c | 45 +- src/bacnet/basic/service/h_rpm.c | 131 ++- src/bacnet/basic/service/h_wp.c | 5 +- src/bacnet/basic/service/h_wpm.c | 2 + src/bacnet/basic/service/s_iam.c | 4 +- src/bacnet/basic/tsm/tsm.c | 1306 ++++++++++++++++++++++++++-- src/bacnet/basic/tsm/tsm.h | 113 ++- src/bacnet/config.h | 5 + src/bacnet/segmentack.c | 104 +++ src/bacnet/segmentack.h | 68 ++ 25 files changed, 2112 insertions(+), 144 deletions(-) create mode 100644 src/bacnet/segmentack.c create mode 100644 src/bacnet/segmentack.h diff --git a/CHANGELOG.md b/CHANGELOG.md index ccd5c35b..3f391b64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,11 @@ The git repositories are hosted at the following sites: ### Security ### Added + +* Added segmentation support for server devices for some services. + Configure BACNET_SEGMENTATION_ENABLED=1 to include in the library, + and adjust BACNET_MAX_SEGMENTS_ACCEPTED for maximum number of segments. (#974) + ### Changed ### Fixed diff --git a/CMakeLists.txt b/CMakeLists.txt index cb5c0df3..3514f6f8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -84,6 +84,11 @@ option( "compile with secure-connect support" OFF) +option( + BACNET_SEGMENTATION_ENABLED + "enable segmentation" + ON) + if(NOT (BACDL_ETHERNET OR BACDL_MSTP OR BACDL_ARCNET OR @@ -672,6 +677,8 @@ add_library(${PROJECT_NAME} src/bacnet/rp.h src/bacnet/rpm.c src/bacnet/rpm.h + $<$:src/bacnet/segmentack.c> + $<$:src/bacnet/segmentack.h> src/bacnet/timer_value.c src/bacnet/timer_value.h src/bacnet/timestamp.c @@ -723,6 +730,7 @@ target_compile_definitions( $<$:BACDL_NONE> $<$:BACNET_PROPERTY_LISTS=1> $<$:BAC_ROUTING> + $<$:BACNET_SEGMENTATION_ENABLED> $<$>:BACNET_STACK_STATIC_DEFINE> PRIVATE PRINT_ENABLED=1) @@ -1275,3 +1283,4 @@ message(STATUS "BACNET: BACDL_ARCNET:...................\"${BACDL_ARCNET}\"") message(STATUS "BACNET: BACDL_MSTP:.....................\"${BACDL_MSTP}\"") message(STATUS "BACNET: BACDL_ZIGBEE:...................\"${BACDL_ZIGBEE}\"") message(STATUS "BACNET: BACDL_ETHERNET:.................\"${BACDL_ETHERNET}\"") +message(STATUS "BACNET: BACNET_SEGMENTATION_ENABLED:....\"${BACNET_SEGMENTATION_ENABLED}\"") diff --git a/Makefile b/Makefile index 442c5728..8e8e6d23 100644 --- a/Makefile +++ b/Makefile @@ -224,6 +224,10 @@ server-discover: server-mini: $(MAKE) LEGACY=true NOTIFY=false -s -C apps $@ +.PHONY: server-segmentation +server-segmentation: + $(MAKE) LEGACY=true SEGMENT=true -s -C apps server + .PHONY: sc-hub sc-hub: $(MAKE) LEGACY=true BACDL=bsc -s -C apps $@ diff --git a/apps/Makefile b/apps/Makefile index bfe60035..6aa43e92 100644 --- a/apps/Makefile +++ b/apps/Makefile @@ -223,6 +223,11 @@ ifeq (${LEGACY},true) BACNET_DEFINES += -DBACNET_STACK_DEPRECATED_DISABLE endif +ifeq (${SEGMENT},true) +# enable segmentation support +BACNET_DEFINES += -DBACNET_SEGMENTATION_ENABLED=1 +endif + ifeq (${NOTIFY},false) # disable intrinsic reporting else diff --git a/apps/server/main.c b/apps/server/main.c index 249997a6..960cddc0 100644 --- a/apps/server/main.c +++ b/apps/server/main.c @@ -174,6 +174,20 @@ static void Init_Service_Handlers(void) (unsigned)object_data.object_instance); } } +#if BACNET_SEGMENTATION_ENABLED + printf("Segmentation is enabled.\n"); + /* create extra object to stress the object list */ + for (i = 0; i < 500; i++) { + object_data.object_instance = BACNET_MAX_INSTANCE; + object_data.object_type = OBJECT_ANALOG_INPUT; + if (Device_Create_Object(&object_data)) { + printf( + "Created object %s-%u\n", + bactext_object_type_name(object_data.object_type), + (unsigned)object_data.object_instance); + } + } +#endif /* we need to handle who-is to support dynamic device binding */ apdu_set_unconfirmed_handler( SERVICE_UNCONFIRMED_WHO_IS, handler_who_is_who_am_i_unicast); diff --git a/ports/atmega328/device.c b/ports/atmega328/device.c index dca50848..ad01a4a2 100644 --- a/ports/atmega328/device.c +++ b/ports/atmega328/device.c @@ -58,6 +58,11 @@ bool Device_Set_Object_Instance_Number(uint32_t object_id) return status; } +BACNET_SEGMENTATION Device_Segmentation_Supported(void) +{ + return SEGMENTATION_NONE; +} + bool Device_Valid_Object_Instance_Number(uint32_t object_id) { /* BACnet allows for a wildcard instance number */ diff --git a/src/bacnet/apdu.h b/src/bacnet/apdu.h index 057470cf..502dddd6 100644 --- a/src/bacnet/apdu.h +++ b/src/bacnet/apdu.h @@ -32,6 +32,21 @@ typedef struct _confirmed_service_ack_data { uint8_t proposed_window_number; } BACNET_CONFIRMED_SERVICE_ACK_DATA; +typedef struct BACnet_Apdu_Fixed_Header { + /* pdu type Confirmed Request or Complex ACK */ + uint8_t pdu_type; + union { + /* Data for pdu type PDU_TYPE_CONFIRMED_SERVICE_REQUEST */ + struct _confirmed_service_data request_data; + /* Data for pdu type PDU_TYPE_COMPLEX_ACK */ + struct _confirmed_service_ack_data ack_data; + /* Common data for both types */ + struct _confirmed_service_ack_data common_data; + } service_data; + /* Service number */ + uint8_t service_choice; +} BACNET_APDU_FIXED_HEADER; + uint8_t apdu_network_priority(void); void apdu_network_priority_set(uint8_t pri); diff --git a/src/bacnet/bacdef.h b/src/bacnet/bacdef.h index aa35e05b..f6b43d50 100644 --- a/src/bacnet/bacdef.h +++ b/src/bacnet/bacdef.h @@ -212,8 +212,24 @@ typedef struct BACnet_Object_Id { uint32_t instance; } BACNET_OBJECT_ID; +#if !defined(BACNET_MAX_SEGMENTS_ACCEPTED) +#if BACNET_SEGMENTATION_ENABLED +/* note: BACNET_MAX_SEGMENTS_ACCEPTED can be 1..255. + ASDU in this library is usually sized for 16-bit at 65535 max. + Therefore, the default here is limited to avoid overflow warnings. */ +#define BACNET_MAX_SEGMENTS_ACCEPTED 32 +#else +#define BACNET_MAX_SEGMENTS_ACCEPTED 1 +#endif +#endif +#if !defined(MAX_APDU) +#define MAX_APDU 1476 +#endif #define MAX_NPDU (1 + 1 + 2 + 1 + MAX_MAC_LEN + 2 + 1 + MAX_MAC_LEN + 1 + 1 + 2) #define MAX_PDU (MAX_APDU + MAX_NPDU) +/* Application Service Data Unit (ASDU) that has not yet been segmented + into a protocol data unit (PDU) by the lower layer. */ +#define MAX_ASDU ((MAX_APDU * BACNET_MAX_SEGMENTS_ACCEPTED) + MAX_NPDU) #define BACNET_ID_VALUE(bacnet_object_instance, bacnet_object_type) \ ((((bacnet_object_type) & BACNET_MAX_OBJECT) << BACNET_INSTANCE_BITS) | \ diff --git a/src/bacnet/basic/binding/address.c b/src/bacnet/basic/binding/address.c index 25669f03..1f092bc9 100644 --- a/src/bacnet/basic/binding/address.c +++ b/src/bacnet/basic/binding/address.c @@ -45,6 +45,10 @@ static struct Address_Cache_Entry { uint8_t Flags; uint32_t device_id; unsigned max_apdu; +#if BACNET_SEGMENTATION_ENABLED + uint8_t segmentation; + uint16_t maxsegments; +#endif BACNET_ADDRESS address; uint32_t TimeToLive; } Address_Cache[MAX_ADDRESS_CACHE]; @@ -346,9 +350,16 @@ void address_set_device_TTL( * @param device_id Device-Id * @param max_apdu Pointer to a variable, taking the maximum APDU size. * @param src Pointer to address structure for return. + * @param segmentation Pointer to a variable, taking the BACNET_SEGMENTATION + * flag. + * @param maxsegments Pointer to a variable, taking the maximum segments. */ -bool address_get_by_device( - uint32_t device_id, unsigned *max_apdu, BACNET_ADDRESS *src) +bool address_segment_get_by_device( + uint32_t device_id, + unsigned *max_apdu, + BACNET_ADDRESS *src, + uint8_t *segmentation, + uint16_t *maxsegments) { struct Address_Cache_Entry *pMatch; bool found = false; /* return value */ @@ -364,6 +375,20 @@ bool address_get_by_device( if (max_apdu) { *max_apdu = pMatch->max_apdu; } + if (segmentation) { +#if BACNET_SEGMENTATION_ENABLED + *segmentation = pMatch->segmentation; +#else + *segmentation = SEGMENTATION_NONE; +#endif + } + if (maxsegments) { +#if BACNET_SEGMENTATION_ENABLED + *maxsegments = pMatch->maxsegments; +#else + *maxsegments = 1; +#endif + } /* Prove we found it */ found = true; } @@ -375,6 +400,19 @@ bool address_get_by_device( return found; } +/** + * @brief Return the cached addresBACNET_STACK_EXPORT +s for the given device-id + * @param device_id Device-Id + * @param max_apdu Pointer to a variable, taking the maximum APDU size. + * @param src Pointer to address structure for return. + */ +bool address_get_by_device( + uint32_t device_id, unsigned *max_apdu, BACNET_ADDRESS *src) +{ + return address_segment_get_by_device(device_id, max_apdu, src, NULL, NULL); +} + /** * Find a device id from a given MAC address. * diff --git a/src/bacnet/basic/binding/address.h b/src/bacnet/basic/binding/address.h index 059dc1c4..692a1a9b 100644 --- a/src/bacnet/basic/binding/address.h +++ b/src/bacnet/basic/binding/address.h @@ -43,6 +43,14 @@ BACNET_STACK_EXPORT bool address_get_by_device( uint32_t device_id, unsigned *max_apdu, BACNET_ADDRESS *src); +BACNET_STACK_EXPORT +bool address_segment_get_by_device( + uint32_t device_id, + unsigned *max_apdu, + BACNET_ADDRESS *src, + uint8_t *segmentation, + uint16_t *maxsegments); + BACNET_STACK_EXPORT bool address_get_by_index( unsigned index, diff --git a/src/bacnet/basic/object/device.c b/src/bacnet/basic/object/device.c index 6d4e42d2..47c70169 100644 --- a/src/bacnet/basic/object/device.c +++ b/src/bacnet/basic/object/device.c @@ -1143,7 +1143,11 @@ uint8_t Device_Protocol_Revision(void) BACNET_SEGMENTATION Device_Segmentation_Supported(void) { +#if BACNET_SEGMENTATION_ENABLED + return SEGMENTATION_BOTH; +#else return SEGMENTATION_NONE; +#endif } /** @@ -1614,8 +1618,12 @@ int Device_Read_Property_Local(BACNET_READ_PROPERTY_DATA *rpdata) rpdata->object_instance, rpdata->array_index, Device_Object_List_Element_Encode, count, apdu, apdu_max); if (apdu_len == BACNET_STATUS_ABORT) { +#if BACNET_SEGMENTATION_ENABLED + rpdata->error_code = ERROR_CODE_ABORT_BUFFER_OVERFLOW; +#else rpdata->error_code = ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; +#endif } else if (apdu_len == BACNET_STATUS_ERROR) { rpdata->error_class = ERROR_CLASS_PROPERTY; rpdata->error_code = ERROR_CODE_INVALID_ARRAY_INDEX; diff --git a/src/bacnet/basic/service/h_apdu.c b/src/bacnet/basic/service/h_apdu.c index 1e0d0325..2bfef566 100644 --- a/src/bacnet/basic/service/h_apdu.c +++ b/src/bacnet/basic/service/h_apdu.c @@ -26,6 +26,10 @@ static uint16_t Timeout_Milliseconds = 3000; /* Number of APDU Retries */ static uint8_t Number_Of_Retries = 3; static uint8_t Local_Network_Priority; /* Fixing test 10.1.2 Network priority */ +#if BACNET_SEGMENTATION_ENABLED +/* APDU Segment Timeout in Milliseconds */ +static uint16_t Segment_Timeout_Milliseconds = 5000; +#endif /* a simple table for crossing the services supported */ static BACNET_SERVICES_SUPPORTED @@ -552,6 +556,109 @@ static bool apdu_unconfirmed_dcc_disabled(uint8_t service_choice) return status; } +/* Invoke special handler for confirmed service */ +static void invoke_confirmed_service_service_request( + BACNET_ADDRESS *src, + BACNET_CONFIRMED_SERVICE_DATA *service_data, + uint8_t service_choice, + uint8_t *service_request, + uint32_t service_request_len) +{ + if (apdu_confirmed_dcc_disabled(service_choice)) { + /* When network communications are completely disabled, + only DeviceCommunicationControl and ReinitializeDevice + APDUs shall be processed and no messages shall be + initiated. */ + return; + } + if ((service_choice < MAX_BACNET_CONFIRMED_SERVICE) && + (Confirmed_Function[service_choice])) { + Confirmed_Function[service_choice]( + service_request, service_request_len, src, service_data); + } else if (Unrecognized_Service_Handler) { + Unrecognized_Service_Handler( + service_request, service_request_len, src, service_data); + } +} + +#if BACNET_SEGMENTATION_ENABLED +/** Handler for messages with segmentation : + - store new packet if sequence number is ok + - NACK packet if sequence number is not ok + - call the final functions with reassembled data when last packet ok is + received +*/ +static void apdu_handler_confirmed_service_segment( + BACNET_ADDRESS *src, + uint8_t *apdu, /* APDU data */ + uint32_t apdu_len) +{ + BACNET_CONFIRMED_SERVICE_DATA service_data = { 0 }; + uint8_t internal_service_id = 0; + uint8_t service_choice = 0; + uint8_t *service_request = NULL; + uint16_t service_request_len = 0; + uint32_t len = 0; /* counts where we are in PDU */ + bool segment_ok; + + len = apdu_decode_confirmed_service_request( + &apdu[0], /* APDU data */ + apdu_len, &service_data, &service_choice, &service_request, + &service_request_len); + if (len == 0) { + /* service data unable to be decoded - simply drop */ + return; + } + /* new segment : memorize it */ + segment_ok = tsm_set_segmented_confirmed_service_received( + src, &service_data, &internal_service_id, &service_request, + &service_request_len); + /* last segment */ + if (segment_ok && !service_data.more_follows) { + /* Clear peer information */ + tsm_clear_peer_id(internal_service_id); + /* Invoke service handler */ + invoke_confirmed_service_service_request( + src, &service_data, service_choice, service_request, + service_request_len); + /* We must free invoke_id, and associated data */ + tsm_free_invoke_id_check(internal_service_id, NULL, true); + } +} +#endif + +/* Handler for normal message without segmentation, or segmented complete + * message reassembled all-in-one */ +static void apdu_handler_confirmed_service( + BACNET_ADDRESS *src, + uint8_t *apdu, /* APDU data */ + uint32_t apdu_len) +{ + BACNET_CONFIRMED_SERVICE_DATA service_data = { 0 }; + uint8_t service_choice = 0; + uint8_t *service_request = NULL; + uint16_t service_request_len = 0; + uint32_t len = 0; /* counts where we are in PDU */ + + len = apdu_decode_confirmed_service_request( + &apdu[0], /* APDU data */ + apdu_len, &service_data, &service_choice, &service_request, + &service_request_len); + if (len == 0) { + /* service data unable to be decoded - simply drop */ + return; + } +#if BACNET_SEGMENTATION_ENABLED + /* Check for unexpected request is received in active TSM state */ + if (tsm_is_invalid_apdu_in_this_state(src, &service_data)) { + return; + } +#endif + invoke_confirmed_service_service_request( + src, &service_data, service_choice, service_request, + service_request_len); +} + /** Process the APDU header and invoke the appropriate service handler * to manage the received request. * Almost all requests and ACKs invoke this function. @@ -567,11 +674,15 @@ void apdu_handler( uint16_t apdu_len) { BACNET_PDU_TYPE pdu_type; - BACNET_CONFIRMED_SERVICE_DATA service_data = { 0 }; uint8_t service_choice = 0; uint8_t *service_request = NULL; uint16_t service_request_len = 0; int len = 0; /* counts where we are in PDU */ +#if BACNET_SEGMENTATION_ENABLED + uint8_t sequence_number = 0; + uint8_t actual_window_size = 0; + bool nak = false; +#endif #if !BACNET_SVC_SERVER uint8_t invoke_id = 0; BACNET_CONFIRMED_SERVICE_ACK_DATA service_ack_data = { 0 }; @@ -590,27 +701,14 @@ void apdu_handler( pdu_type = apdu[0] & 0xF0; switch (pdu_type) { case PDU_TYPE_CONFIRMED_SERVICE_REQUEST: - len = apdu_decode_confirmed_service_request( - apdu, apdu_len, &service_data, &service_choice, - &service_request, &service_request_len); - if (len == 0) { - /* service data unable to be decoded - simply drop */ - break; - } - if (apdu_confirmed_dcc_disabled(service_choice)) { - /* When network communications are completely disabled, - only DeviceCommunicationControl and ReinitializeDevice - APDUs shall be processed and no messages shall be - initiated. */ - break; - } - if ((service_choice < MAX_BACNET_CONFIRMED_SERVICE) && - (Confirmed_Function[service_choice])) { - Confirmed_Function[service_choice]( - service_request, service_request_len, src, &service_data); - } else if (Unrecognized_Service_Handler) { - Unrecognized_Service_Handler( - service_request, service_request_len, src, &service_data); + /* segmented_message_reception ? */ +#if BACNET_SEGMENTATION_ENABLED + if (apdu[0] & BIT(3)) { + apdu_handler_confirmed_service_segment(src, apdu, apdu_len); + } else +#endif + { + apdu_handler_confirmed_service(src, apdu, apdu_len); } break; case PDU_TYPE_UNCONFIRMED_SERVICE_REQUEST: @@ -685,9 +783,24 @@ void apdu_handler( } break; case PDU_TYPE_SEGMENT_ACK: +#if !BACNET_SEGMENTATION_ENABLED /* FIXME: what about a denial of service attack here? we could check src to see if that matched the tsm */ tsm_free_invoke_id(invoke_id); +#else + if (apdu_len < 4) { + break; + } + server = apdu[0] & 0x01; + nak = apdu[0] & 0x02; + invoke_id = apdu[1]; + sequence_number = apdu[2]; + actual_window_size = apdu[3]; + /* we care because we support segmented message sending */ + tsm_segmentack_received( + invoke_id, sequence_number, actual_window_size, nak, server, + src); +#endif break; case PDU_TYPE_ERROR: if (apdu_len < 3) { @@ -714,7 +827,12 @@ void apdu_handler( (BACNET_ERROR_CODE)error_code); } } +#if BACNET_SEGMENTATION_ENABLED + /*Release the data*/ + tsm_free_invoke_id_segmentation(src, invoke_id); +#else tsm_free_invoke_id(invoke_id); +#endif break; case PDU_TYPE_REJECT: if (apdu_len < 3) { @@ -725,7 +843,12 @@ void apdu_handler( if (Reject_Function) { Reject_Function(src, invoke_id, reason); } +#if BACNET_SEGMENTATION_ENABLED + /*Release the data*/ + tsm_free_invoke_id_segmentation(src, invoke_id); +#else tsm_free_invoke_id(invoke_id); +#endif break; case PDU_TYPE_ABORT: if (apdu_len < 3) { @@ -734,13 +857,156 @@ void apdu_handler( server = apdu[0] & 0x01; invoke_id = apdu[1]; reason = apdu[2]; - if (Abort_Function) { - Abort_Function(src, invoke_id, reason, server); + if (!server) { + /*AbortPDU_Received*/ + if (Abort_Function) { + Abort_Function(src, invoke_id, reason, server); + } } +#if BACNET_SEGMENTATION_ENABLED + else { + /*SendAbort*/ + tsm_abort_pdu_send(invoke_id, src, reason, server); + } + /*Release the data*/ + tsm_free_invoke_id_segmentation(src, invoke_id); +#else tsm_free_invoke_id(invoke_id); +#endif break; #endif default: break; } } + +#if BACNET_SEGMENTATION_ENABLED +/*Return the APDU segment timeout*/ +uint16_t apdu_segment_timeout(void) +{ + return Segment_Timeout_Milliseconds; +} + +/*Set the APDU segment timeout*/ +void apdu_segment_timeout_set(uint16_t milliseconds) +{ + Segment_Timeout_Milliseconds = milliseconds; +} + +/** Process the APDU header and invoke the appropriate service handler + * to manage the received request. + * Almost all requests and ACKs invoke this function. + * @ingroup MISCHNDLR + * + * @param apdu [in] The apdu portion of the response, to be sent + * @param fixed_pdu_header [in] The apdu header for the response + * @return apdu_length[out] The length of the apdu header + */ +int apdu_encode_fixed_header( + uint8_t *apdu, BACNET_APDU_FIXED_HEADER *fixed_pdu_header) +{ + int apdu_len = 0; + switch (fixed_pdu_header->pdu_type) { + case PDU_TYPE_CONFIRMED_SERVICE_REQUEST: + apdu[apdu_len++] = fixed_pdu_header->pdu_type + /* flag 'SA' if we accept many segments */ + | (fixed_pdu_header->service_data.request_data + .segmented_response_accepted + ? 0x02 + : 0x00) + /* flag 'MOR' if we more segments are coming */ + | (fixed_pdu_header->service_data.request_data.more_follows + ? 0x04 + : 0x00) + /* flag 'SEG' if we more segments are coming */ + | (fixed_pdu_header->service_data.request_data.segmented_message + ? 0x08 + : 0x00); + apdu[apdu_len++] = encode_max_segs_max_apdu( + fixed_pdu_header->service_data.request_data.max_segs, + fixed_pdu_header->service_data.request_data.max_resp); + apdu[apdu_len++] = + fixed_pdu_header->service_data.request_data.invoke_id; + /* extra data for segmented messages sending */ + if (fixed_pdu_header->service_data.request_data.segmented_message) { + /* packet sequence number */ + apdu[apdu_len++] = + fixed_pdu_header->service_data.request_data.sequence_number; + /* window size proposal */ + apdu[apdu_len++] = fixed_pdu_header->service_data.request_data + .proposed_window_number; + } + /* service choice */ + apdu[apdu_len++] = fixed_pdu_header->service_choice; + break; + case PDU_TYPE_COMPLEX_ACK: + apdu[apdu_len++] = fixed_pdu_header->pdu_type + /* flag 'MOR' if we more segments are coming */ + | (fixed_pdu_header->service_data.ack_data.more_follows ? 0x04 + : 0x00) + /* flag 'SEG' if we more segments are coming */ + | (fixed_pdu_header->service_data.ack_data.segmented_message + ? 0x08 + : 0x00); + apdu[apdu_len++] = + fixed_pdu_header->service_data.ack_data.invoke_id; + /* extra data for segmented messages sending */ + if (fixed_pdu_header->service_data.ack_data.segmented_message) { + /* packet sequence number */ + apdu[apdu_len++] = + fixed_pdu_header->service_data.ack_data.sequence_number; + /* window size proposal */ + apdu[apdu_len++] = fixed_pdu_header->service_data.ack_data + .proposed_window_number; + } + /* service choice */ + apdu[apdu_len++] = fixed_pdu_header->service_choice; + break; + default: + break; + } + return apdu_len; +} + +/** Handler to assign the header fields to the response + * The response can be segmented/unsegmented + * + * @param fixed_pdu_header [in] The apdu header of the response, to be sent. + * @param pdu_type [in] The pdu_type of the response. + * @param invoke_id [in] The invoike_id of the response + * @param service[in] The service choice for which the response has to be + * processed + * @param max_apdu[in] The maximum apdu length + */ +void apdu_init_fixed_header( + BACNET_APDU_FIXED_HEADER *fixed_pdu_header, + uint8_t pdu_type, + uint8_t invoke_id, + uint8_t service, + int max_apdu) +{ + fixed_pdu_header->pdu_type = pdu_type; + + fixed_pdu_header->service_data.common_data.invoke_id = invoke_id; + fixed_pdu_header->service_data.common_data.more_follows = false; + fixed_pdu_header->service_data.common_data.proposed_window_number = 0; + fixed_pdu_header->service_data.common_data.sequence_number = 0; + fixed_pdu_header->service_data.common_data.segmented_message = false; + switch (pdu_type) { + case PDU_TYPE_CONFIRMED_SERVICE_REQUEST: + fixed_pdu_header->service_data.request_data.max_segs = + BACNET_MAX_SEGMENTS_ACCEPTED; + /* allow to specify a lower APDU size : support arbitrary reduction + * of APDU packets between peers */ + fixed_pdu_header->service_data.request_data.max_resp = + max_apdu < MAX_APDU ? max_apdu : MAX_APDU; + fixed_pdu_header->service_data.request_data + .segmented_response_accepted = BACNET_MAX_SEGMENTS_ACCEPTED > 1; + break; + case PDU_TYPE_COMPLEX_ACK: + default: + break; + } + fixed_pdu_header->service_choice = service; +} +#endif diff --git a/src/bacnet/basic/service/h_apdu.h b/src/bacnet/basic/service/h_apdu.h index 549942ca..4f9db9ee 100644 --- a/src/bacnet/basic/service/h_apdu.h +++ b/src/bacnet/basic/service/h_apdu.h @@ -162,6 +162,24 @@ void apdu_handler( uint8_t *apdu, /* APDU data */ uint16_t pdu_len); /* for confirmed messages */ +BACNET_STACK_EXPORT +uint16_t apdu_segment_timeout(void); + +BACNET_STACK_EXPORT +void apdu_segment_timeout_set(uint16_t milliseconds); + +BACNET_STACK_EXPORT +int apdu_encode_fixed_header( + uint8_t *apdu, BACNET_APDU_FIXED_HEADER *fixed_pdu_header); + +BACNET_STACK_EXPORT +void apdu_init_fixed_header( + BACNET_APDU_FIXED_HEADER *fixed_pdu_header, + uint8_t pdu_type, + uint8_t invoke_id, + uint8_t service, + int max_apdu); + #ifdef __cplusplus } #endif /* __cplusplus */ diff --git a/src/bacnet/basic/service/h_dcc.c b/src/bacnet/basic/service/h_dcc.c index bc0e0fe6..8a65efff 100644 --- a/src/bacnet/basic/service/h_dcc.c +++ b/src/bacnet/basic/service/h_dcc.c @@ -108,7 +108,9 @@ void handler_device_communication_control( debug_print("DeviceCommunicationControl: " "Missing Required Parameter. Sending Reject!\n"); goto DCC_FAILURE; - } else if (service_data->segmented_message) { + } +#if !BACNET_SEGMENTATION_ENABLED + else if (service_data->segmented_message) { len = abort_encode_apdu( &Handler_Transmit_Buffer[pdu_len], service_data->invoke_id, ABORT_REASON_SEGMENTATION_NOT_SUPPORTED, true); @@ -116,6 +118,7 @@ void handler_device_communication_control( "Sending Abort - segmented message.\n"); goto DCC_FAILURE; } +#endif /* decode the service request only */ len = dcc_decode_service_request( service_request, service_len, &timeDuration, &state, &password); diff --git a/src/bacnet/basic/service/h_rd.c b/src/bacnet/basic/service/h_rd.c index 334a0453..5112bde6 100644 --- a/src/bacnet/basic/service/h_rd.c +++ b/src/bacnet/basic/service/h_rd.c @@ -71,13 +71,16 @@ void handler_reinitialize_device( debug_print("ReinitializeDevice: Missing Required Parameter. " "Sending Reject!\n"); goto RD_ABORT; - } else if (service_data->segmented_message) { + } +#if !BACNET_SEGMENTATION_ENABLED + else if (service_data->segmented_message) { len = abort_encode_apdu( &Handler_Transmit_Buffer[pdu_len], service_data->invoke_id, ABORT_REASON_SEGMENTATION_NOT_SUPPORTED, true); debug_print("ReinitializeDevice: Sending Abort - segmented message.\n"); goto RD_ABORT; } +#endif /* decode the service request only */ len = rd_decode_service_request( service_request, service_len, &rd_data.state, &rd_data.password); diff --git a/src/bacnet/basic/service/h_rp.c b/src/bacnet/basic/service/h_rp.c index f4f17f73..a7bc87ee 100644 --- a/src/bacnet/basic/service/h_rp.c +++ b/src/bacnet/basic/service/h_rp.c @@ -30,6 +30,12 @@ #include "bacnet/basic/sys/debug.h" #include "bacnet/datalink/datalink.h" +#if BACNET_SEGMENTATION_ENABLED +#define BACNET_RP_BUFFER_OVERFLOW ERROR_CODE_ABORT_BUFFER_OVERFLOW +#else +#define BACNET_RP_BUFFER_OVERFLOW ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED +#endif + /** @file h_rp.c Handles Read Property requests. */ /** Handler for a ReadProperty Service request. @@ -38,7 +44,7 @@ * by a call to apdu_set_confirmed_handler(). * This handler builds a response packet, which is * - an Abort if - * - the message is segmented + * - the message is segmented and segmentation is not supported * - if decoding fails * - if the response would be too large * - the result from Device_Read_Property(), if it succeeds @@ -59,6 +65,7 @@ void handler_read_property( { BACNET_READ_PROPERTY_DATA rpdata; int len = 0; + int max_resp = 0; int pdu_len = 0; int apdu_len = -1; int npdu_len = -1; @@ -66,9 +73,13 @@ void handler_read_property( bool error = true; /* assume that there is an error */ int bytes_sent = 0; BACNET_ADDRESS my_address; +#if BACNET_SEGMENTATION_ENABLED + BACNET_APDU_FIXED_HEADER apdu_fixed_header; + int apdu_header_len = 3; +#endif /* configure default error code as an abort since it is common */ - rpdata.error_code = ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; + rpdata.error_code = BACNET_RP_BUFFER_OVERFLOW; /* encode the NPDU portion of the packet */ datalink_get_my_address(&my_address); npdu_encode_npdu_data(&npdu_data, false, service_data->priority); @@ -82,10 +93,12 @@ void handler_read_property( len = BACNET_STATUS_REJECT; rpdata.error_code = ERROR_CODE_REJECT_MISSING_REQUIRED_PARAMETER; debug_print("RP: Missing Required Parameter. Sending Reject!\n"); +#if !BACNET_SEGMENTATION_ENABLED } else if (service_data->segmented_message) { /* we don't support segmentation - send an abort */ len = BACNET_STATUS_ABORT; debug_print("RP: Segmented message. Sending Abort!\n"); +#endif } else { len = rp_decode_service_request(service_request, service_len, &rpdata); if (len <= 0) { @@ -116,6 +129,13 @@ void handler_read_property( rpdata.object_instance = Network_Port_Index_To_Instance(0); } #endif + +#if BACNET_SEGMENTATION_ENABLED + apdu_init_fixed_header( + &apdu_fixed_header, PDU_TYPE_COMPLEX_ACK, + service_data->invoke_id, SERVICE_CONFIRMED_READ_PROPERTY, + service_data->max_resp); +#endif apdu_len = rp_ack_encode_apdu_init( &Handler_Transmit_Buffer[npdu_len], service_data->invoke_id, &rpdata); @@ -134,7 +154,25 @@ void handler_read_property( len = rp_ack_encode_apdu_object_property_end( &Handler_Transmit_Buffer[npdu_len + apdu_len]); apdu_len += len; - if (apdu_len > service_data->max_resp) { + /* pick the smaller response packet: ours or theirs */ + max_resp = min(service_data->max_resp, MAX_APDU); + if (apdu_len > max_resp) { +#if BACNET_SEGMENTATION_ENABLED + if (service_data->segmented_response_accepted) { + npdu_encode_npdu_data( + &npdu_data, true, MESSAGE_PRIORITY_NORMAL); + npdu_len = npdu_encode_pdu( + &Handler_Transmit_Buffer[0], src, &my_address, + &npdu_data); + + tsm_set_complexack_transaction( + src, &npdu_data, &apdu_fixed_header, service_data, + &Handler_Transmit_Buffer + [npdu_len + apdu_header_len], + (apdu_len - apdu_header_len)); + return; + } +#else /* too big for the sender - send an abort! Setting of error code needed here as read property processing may have overridden the default set at start @@ -143,6 +181,7 @@ void handler_read_property( ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; len = BACNET_STATUS_ABORT; debug_print("RP: Message too large.\n"); +#endif } else { debug_print("RP: Sending Ack!\n"); error = false; diff --git a/src/bacnet/basic/service/h_rpm.c b/src/bacnet/basic/service/h_rpm.c index 59d9d162..c9991f24 100644 --- a/src/bacnet/basic/service/h_rpm.c +++ b/src/bacnet/basic/service/h_rpm.c @@ -12,6 +12,7 @@ #include #include #include +#include /* BACnet Stack defines - first */ #include "bacnet/bacdef.h" /* BACnet Stack API */ @@ -33,7 +34,17 @@ #include "bacnet/basic/sys/debug.h" #include "bacnet/datalink/datalink.h" -static uint8_t Temp_Buf[MAX_APDU] = { 0 }; +/* Smaller single threaded implementations prefer a + single buffer for encoding each property from the RPM request. */ +#ifndef BACNET_RPM_PROPERTY_BUFFER_USE_CSTACK +static uint8_t RPM_Prop_Buffer[MAX_ASDU - MAX_NPDU] = { 0 }; +#endif + +#if BACNET_SEGMENTATION_ENABLED +#define BACNET_RPM_BUFFER_OVERFLOW ERROR_CODE_ABORT_BUFFER_OVERFLOW +#else +#define BACNET_RPM_BUFFER_OVERFLOW ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED +#endif /** * @brief Fetches the lists of properties (array of BACNET_PROPERTY_ID's) for @@ -124,12 +135,15 @@ static int RPM_Encode_Property( size_t copy_len = 0; int apdu_len = 0; BACNET_READ_PROPERTY_DATA rpdata; +#ifdef BACNET_RPM_PROPERTY_BUFFER_USE_CSTACK + uint8_t RPM_Prop_Buffer[MAX_ASDU - MAX_NPDU] = { 0 }; +#endif len = rpm_ack_encode_apdu_object_property( - &Temp_Buf[0], rpmdata->object_property, rpmdata->array_index); - copy_len = memcopy(&apdu[0], &Temp_Buf[0], offset, len, max_apdu); + &RPM_Prop_Buffer[0], rpmdata->object_property, rpmdata->array_index); + copy_len = memcopy(&apdu[0], &RPM_Prop_Buffer[0], offset, len, max_apdu); if (copy_len == 0) { - rpmdata->error_code = ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; + rpmdata->error_code = BACNET_RPM_BUFFER_OVERFLOW; return BACNET_STATUS_ABORT; } apdu_len += len; @@ -139,8 +153,8 @@ static int RPM_Encode_Property( rpdata.object_instance = rpmdata->object_instance; rpdata.object_property = rpmdata->object_property; rpdata.array_index = rpmdata->array_index; - rpdata.application_data = &Temp_Buf[0]; - rpdata.application_data_len = sizeof(Temp_Buf); + rpdata.application_data = &RPM_Prop_Buffer[0]; + rpdata.application_data_len = sizeof(RPM_Prop_Buffer); if ((rpmdata->object_property == PROP_ALL) || (rpmdata->object_property == PROP_REQUIRED) || @@ -161,21 +175,21 @@ static int RPM_Encode_Property( } /* error was returned - encode that for the response */ len = rpm_ack_encode_apdu_object_property_error( - &Temp_Buf[0], rpdata.error_class, rpdata.error_code); - copy_len = - memcopy(&apdu[0], &Temp_Buf[0], offset + apdu_len, len, max_apdu); + &RPM_Prop_Buffer[0], rpdata.error_class, rpdata.error_code); + copy_len = memcopy( + &apdu[0], &RPM_Prop_Buffer[0], offset + apdu_len, len, max_apdu); if (copy_len == 0) { - rpmdata->error_code = ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; + rpmdata->error_code = BACNET_RPM_BUFFER_OVERFLOW; return BACNET_STATUS_ABORT; } } else if ((offset + apdu_len + 1 + len + 1) < max_apdu) { /* enough room to fit the property value and tags */ len = rpm_ack_encode_apdu_object_property_value( - &apdu[offset + apdu_len], &Temp_Buf[0], len); + &apdu[offset + apdu_len], &RPM_Prop_Buffer[0], len); } else { /* not enough room - abort! */ - rpmdata->error_code = ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; + rpmdata->error_code = BACNET_RPM_BUFFER_OVERFLOW; return BACNET_STATUS_ABORT; } apdu_len += len; @@ -189,7 +203,7 @@ static int RPM_Encode_Property( * by a call to apdu_set_confirmed_handler(). * This handler builds a response packet, which is * - an Abort if - * - the message is segmented + * - the message is segmented and SEGMENTATION_NONE * - if decoding fails * - if the response would be too large * - the result from each included read request, if it succeeds @@ -220,6 +234,14 @@ void handler_read_property_multiple( int apdu_len = 0; int npdu_len = 0; int error = 0; + int max_resp = 0; +#if BACNET_SEGMENTATION_ENABLED + BACNET_APDU_FIXED_HEADER apdu_fixed_header; + int apdu_header_len = 3; +#endif +#ifdef BACNET_RPM_PROPERTY_BUFFER_USE_CSTACK + uint8_t RPM_Prop_Buffer[MAX_ASDU - MAX_NPDU] = { 0 }; +#endif if (service_data) { datalink_get_my_address(&my_address); @@ -230,10 +252,12 @@ void handler_read_property_multiple( rpmdata.error_code = ERROR_CODE_REJECT_MISSING_REQUIRED_PARAMETER; error = BACNET_STATUS_REJECT; debug_print("RPM: Missing Required Parameter. Sending Reject!\n"); +#if !BACNET_SEGMENTATION_ENABLED } else if (service_data->segmented_message) { rpmdata.error_code = ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; error = BACNET_STATUS_ABORT; debug_print("RPM: Segmented message. Sending Abort!\r\n"); +#endif } else { /* decode apdu request & encode apdu reply encode complex ack, invoke id, service choice */ @@ -279,14 +303,14 @@ void handler_read_property_multiple( } #endif /* Stick this object id into the reply - if it will fit */ - len = rpm_ack_encode_apdu_object_begin(&Temp_Buf[0], &rpmdata); + len = rpm_ack_encode_apdu_object_begin( + &RPM_Prop_Buffer[0], &rpmdata); copy_len = memcopy( - &Handler_Transmit_Buffer[npdu_len], &Temp_Buf[0], apdu_len, - len, MAX_APDU); + &Handler_Transmit_Buffer[npdu_len], &RPM_Prop_Buffer[0], + apdu_len, len, sizeof(RPM_Prop_Buffer)); if (copy_len == 0) { debug_print("RPM: Response too big!\n"); - rpmdata.error_code = - ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; + rpmdata.error_code = BACNET_RPM_BUFFER_OVERFLOW; error = BACNET_STATUS_ABORT; berror = true; break; @@ -321,7 +345,9 @@ void handler_read_property_multiple( rpmdata.object_type, rpmdata.object_instance)) { len = RPM_Encode_Property( &Handler_Transmit_Buffer[npdu_len], - (uint16_t)apdu_len, MAX_APDU, &rpmdata); + (uint16_t)apdu_len, + sizeof(Handler_Transmit_Buffer) - npdu_len, + &rpmdata); if (len > 0) { apdu_len += len; } else { @@ -336,18 +362,18 @@ void handler_read_property_multiple( /* No array index options for this special property. Encode error for this object property response */ len = rpm_ack_encode_apdu_object_property( - &Temp_Buf[0], rpmdata.object_property, + &RPM_Prop_Buffer[0], rpmdata.object_property, rpmdata.array_index); copy_len = memcopy( &Handler_Transmit_Buffer[npdu_len], - &Temp_Buf[0], apdu_len, len, MAX_APDU); + &RPM_Prop_Buffer[0], apdu_len, len, + sizeof(RPM_Prop_Buffer)); if (copy_len == 0) { debug_print( "RPM: Too full to encode property!\n"); - rpmdata.error_code = - ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; + rpmdata.error_code = BACNET_RPM_BUFFER_OVERFLOW; error = BACNET_STATUS_ABORT; /* The berror flag ensures that both loops will be broken! */ @@ -357,17 +383,17 @@ void handler_read_property_multiple( apdu_len += len; len = rpm_ack_encode_apdu_object_property_error( - &Temp_Buf[0], ERROR_CLASS_PROPERTY, + &RPM_Prop_Buffer[0], ERROR_CLASS_PROPERTY, ERROR_CODE_PROPERTY_IS_NOT_AN_ARRAY); copy_len = memcopy( &Handler_Transmit_Buffer[npdu_len], - &Temp_Buf[0], apdu_len, len, MAX_APDU); + &RPM_Prop_Buffer[0], apdu_len, len, + sizeof(RPM_Prop_Buffer)); if (copy_len == 0) { debug_print("RPM: Too full to encode error!\n"); - rpmdata.error_code = - ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; + rpmdata.error_code = BACNET_RPM_BUFFER_OVERFLOW; error = BACNET_STATUS_ABORT; /* The berror flag ensures that both loops will be broken! */ @@ -396,7 +422,10 @@ void handler_read_property_multiple( rpmdata.object_instance)) { len = RPM_Encode_Property( &Handler_Transmit_Buffer[npdu_len], - (uint16_t)apdu_len, MAX_APDU, &rpmdata); + (uint16_t)apdu_len, + sizeof(Handler_Transmit_Buffer) - + npdu_len, + &rpmdata); if (len > 0) { apdu_len += len; } else { @@ -418,7 +447,10 @@ void handler_read_property_multiple( special_object_property, index); len = RPM_Encode_Property( &Handler_Transmit_Buffer[npdu_len], - (uint16_t)apdu_len, MAX_APDU, &rpmdata); + (uint16_t)apdu_len, + sizeof(Handler_Transmit_Buffer) - + npdu_len, + &rpmdata); if (len > 0) { apdu_len += len; } else { @@ -437,7 +469,9 @@ void handler_read_property_multiple( /* handle an individual property */ len = RPM_Encode_Property( &Handler_Transmit_Buffer[npdu_len], - (uint16_t)apdu_len, MAX_APDU, &rpmdata); + (uint16_t)apdu_len, + sizeof(Handler_Transmit_Buffer) - npdu_len, + &rpmdata); if (len > 0) { apdu_len += len; } else { @@ -456,15 +490,16 @@ void handler_read_property_multiple( /* Reached end of property list so cap the result list */ decode_len++; - len = rpm_ack_encode_apdu_object_end(&Temp_Buf[0]); + len = + rpm_ack_encode_apdu_object_end(&RPM_Prop_Buffer[0]); copy_len = memcopy( - &Handler_Transmit_Buffer[npdu_len], &Temp_Buf[0], - apdu_len, len, MAX_APDU); + &Handler_Transmit_Buffer[npdu_len], + &RPM_Prop_Buffer[0], apdu_len, len, + sizeof(Handler_Transmit_Buffer) - npdu_len); if (copy_len == 0) { debug_print( "RPM: Too full to encode object end!\n"); - rpmdata.error_code = - ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; + rpmdata.error_code = BACNET_RPM_BUFFER_OVERFLOW; error = BACNET_STATUS_ABORT; /* The berror flag ensures that both loops will be broken! */ @@ -487,12 +522,36 @@ void handler_read_property_multiple( } /* If not having an error so far, check the remaining space. */ if (!berror) { - if (apdu_len > service_data->max_resp) { + max_resp = min(service_data->max_resp, MAX_APDU); + if (apdu_len > max_resp) { +#if BACNET_SEGMENTATION_ENABLED + if (service_data->segmented_response_accepted) { + apdu_init_fixed_header( + &apdu_fixed_header, PDU_TYPE_COMPLEX_ACK, + service_data->invoke_id, + SERVICE_CONFIRMED_READ_PROP_MULTIPLE, + service_data->max_resp); + + npdu_encode_npdu_data( + &npdu_data, true, MESSAGE_PRIORITY_NORMAL); + npdu_len = npdu_encode_pdu( + &Handler_Transmit_Buffer[0], src, &my_address, + &npdu_data); + + tsm_set_complexack_transaction( + src, &npdu_data, &apdu_fixed_header, service_data, + &Handler_Transmit_Buffer + [npdu_len + apdu_header_len], + (apdu_len - apdu_header_len)); + return; + } +#else /* too big for the sender - send an abort */ rpmdata.error_code = ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; error = BACNET_STATUS_ABORT; debug_print("RPM: Message too large. Sending Abort!\n"); +#endif } } } diff --git a/src/bacnet/basic/service/h_wp.c b/src/bacnet/basic/service/h_wp.c index ca3b097e..c0cc446d 100644 --- a/src/bacnet/basic/service/h_wp.c +++ b/src/bacnet/basic/service/h_wp.c @@ -107,13 +107,16 @@ void handler_write_property( REJECT_REASON_MISSING_REQUIRED_PARAMETER); debug_print("WP: Missing Required Parameter. Sending Reject!\n"); bcontinue = false; - } else if (service_data->segmented_message) { + } +#if !BACNET_SEGMENTATION_ENABLED + else if (service_data->segmented_message) { len = abort_encode_apdu( &Handler_Transmit_Buffer[pdu_len], service_data->invoke_id, ABORT_REASON_SEGMENTATION_NOT_SUPPORTED, true); debug_print("WP: Segmented message. Sending Abort!\n"); bcontinue = false; } +#endif if (bcontinue) { /* decode the service request only */ len = wp_decode_service_request(service_request, service_len, &wp_data); diff --git a/src/bacnet/basic/service/h_wpm.c b/src/bacnet/basic/service/h_wpm.c index c40ac968..d0bdd8d1 100644 --- a/src/bacnet/basic/service/h_wpm.c +++ b/src/bacnet/basic/service/h_wpm.c @@ -141,11 +141,13 @@ void handler_write_property_multiple( len = BACNET_STATUS_REJECT; debug_print("WPM: Missing Required Parameter. " "Sending Reject!\n"); +#if !BACNET_SEGMENTATION_ENABLED } else if (service_data->segmented_message) { wp_data.error_code = ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; len = BACNET_STATUS_ABORT; debug_print("WPM: Segmented message. " "Sending Abort!\n"); +#endif } else { /* first time - detect malformed request before writing any data */ len = write_property_multiple_decode( diff --git a/src/bacnet/basic/service/s_iam.c b/src/bacnet/basic/service/s_iam.c index 6b7399b8..76a7cb6b 100644 --- a/src/bacnet/basic/service/s_iam.c +++ b/src/bacnet/basic/service/s_iam.c @@ -85,7 +85,7 @@ int iam_encode_pdu( /* encode the APDU portion of the packet */ len = iam_encode_apdu( &buffer[pdu_len], Device_Object_Instance_Number(), MAX_APDU, - SEGMENTATION_NONE, Device_Vendor_Identifier()); + Device_Segmentation_Supported(), Device_Vendor_Identifier()); pdu_len += len; return pdu_len; @@ -158,7 +158,7 @@ int iam_unicast_encode_pdu( /* encode the APDU portion of the packet */ apdu_len = iam_encode_apdu( &buffer[npdu_len], Device_Object_Instance_Number(), MAX_APDU, - SEGMENTATION_NONE, Device_Vendor_Identifier()); + Device_Segmentation_Supported(), Device_Vendor_Identifier()); pdu_len = npdu_len + apdu_len; return pdu_len; diff --git a/src/bacnet/basic/tsm/tsm.c b/src/bacnet/basic/tsm/tsm.c index 43b3b85c..85d0e5ff 100644 --- a/src/bacnet/basic/tsm/tsm.c +++ b/src/bacnet/basic/tsm/tsm.c @@ -20,10 +20,32 @@ #include "bacnet/datalink/datalink.h" #include "bacnet/basic/services.h" #include "bacnet/basic/binding/address.h" +#if BACNET_SEGMENTATION_ENABLED +#include +#include "bacnet/segmentack.h" +#include "bacnet/abort.h" +#include "bacnet/basic/sys/platform.h" + +#ifndef BACNET_SEGMENTATION_WINDOW_SIZE_DEFAULT +#define BACNET_SEGMENTATION_WINDOW_SIZE_DEFAULT 32 +#endif + +/* for confirmed segmented messages, this is the number of peer + segmented requests we can stand at the same time. */ +#if !defined(BACNET_SEGMENTATION_TSM_PEERS_MAX) +#define BACNET_SEGMENTATION_TSM_PEERS_MAX 16 +#endif + +/*Number of Duplicate Segments Received. */ +static uint8_t Duplicate_Count = 0; + +/* Indirection of state machine data with peer unique id values */ +static BACNET_TSM_INDIRECT_DATA TSM_Peer_Ids[BACNET_SEGMENTATION_TSM_PEERS_MAX]; +#endif /** @file tsm.c BACnet Transaction State Machine operations */ /* FIXME: modify basic service handlers to use TSM rather than this buffer! */ -uint8_t Handler_Transmit_Buffer[MAX_PDU]; +uint8_t Handler_Transmit_Buffer[MAX_ASDU]; #if (MAX_TSM_TRANSACTIONS) /* Really only needed for segmented messages */ @@ -200,6 +222,30 @@ uint8_t tsm_next_free_invokeID(void) return invokeID; } +/** + * @brief Copy new data to current APDU sending blob data + * @param data [in] The TSM data + * @param bdata [in] The data to copy + * @param data_len [in] The length of the data + */ +static void tsm_blob_data_copy( + BACNET_TSM_DATA *data, const uint8_t *bdata, uint32_t data_len) +{ +#if BACNET_SEGMENTATION_ENABLED + if (data->apdu) { + free(data->apdu); + } + data->apdu = NULL; + data->apdu = calloc(1, data_len); + if (!data->apdu) { + DEBUG_FPRINTF(stderr, "TSM: failed to allocate %d bytes\n", data_len); + return; + } +#endif + memcpy(data->apdu, bdata, data_len); + data->apdu_len = data_len; +} + /** Set for an unsegmented transaction * the state to await confirmation. * @@ -216,7 +262,6 @@ void tsm_set_confirmed_unsegmented_transaction( const uint8_t *apdu, uint16_t apdu_len) { - uint16_t j = 0; uint8_t index; BACNET_TSM_DATA *plist; @@ -230,10 +275,7 @@ void tsm_set_confirmed_unsegmented_transaction( /* start the timer */ plist->RequestTimer = apdu_timeout(); /* copy the data */ - for (j = 0; j < apdu_len; j++) { - plist->apdu[j] = apdu[j]; - } - plist->apdu_len = apdu_len; + tsm_blob_data_copy(plist, apdu, apdu_len); npdu_copy_data(&plist->npdu_data, ndpu_data); bacnet_address_copy(&plist->dest, dest); } @@ -270,13 +312,13 @@ bool tsm_get_transaction_pdu( index = tsm_find_invokeID_index(invokeID); /* how much checking is needed? state? dest match? just invokeID? */ if (index < MAX_TSM_TRANSACTIONS) { - /* FIXME: we may want to free the transaction so it doesn't timeout - */ + /* FIXME: should we free the transaction so it doesn't timeout? */ /* retrieve the transaction */ plist = &TSM_List[index]; - *apdu_len = (uint16_t)plist->apdu_len; - if (*apdu_len > MAX_PDU) { - *apdu_len = MAX_PDU; + if (plist->apdu_len > min(MAX_ASDU, UINT16_MAX)) { + *apdu_len = min(MAX_ASDU, UINT16_MAX); + } else { + *apdu_len = (uint16_t)plist->apdu_len; } for (j = 0; j < *apdu_len; j++) { apdu[j] = plist->apdu[j]; @@ -290,57 +332,6 @@ bool tsm_get_transaction_pdu( return found; } -/** Called once a millisecond or slower. - * This function calls the handler for a - * timeout 'Timeout_Function', if necessary. - * - * @param milliseconds - Count of milliseconds passed, since the last call. - */ -void tsm_timer_milliseconds(uint16_t milliseconds) -{ - unsigned i = 0; /* counter */ - int bytes_sent = 0; - - BACNET_TSM_DATA *plist = &TSM_List[0]; - - for (i = 0; i < MAX_TSM_TRANSACTIONS; i++, plist++) { - if (plist->state == TSM_STATE_AWAIT_CONFIRMATION) { - if (plist->RequestTimer > milliseconds) { - plist->RequestTimer -= milliseconds; - } else { - plist->RequestTimer = 0; - } - /* AWAIT_CONFIRMATION */ - if (plist->RequestTimer == 0) { - if (plist->RetryCount < apdu_retries()) { - plist->RequestTimer = apdu_timeout(); - plist->RetryCount++; - bytes_sent = datalink_send_pdu( - &plist->dest, &plist->npdu_data, &plist->apdu[0], - plist->apdu_len); - DEBUG_PRINTF( - "invoke-id[%u] Retry %u of %u after %ums\n", - plist->InvokeID, plist->RetryCount, apdu_retries(), - plist->RequestTimer); - if (bytes_sent <= 0) { - debug_perror("invoke-id[%u] Failed to Send Retry"); - } - } else { - /* note: the invoke id has not been cleared yet - and this indicates a failed message: - IDLE and a valid invoke id */ - plist->state = TSM_STATE_IDLE; - if (plist->InvokeID != 0) { - if (Timeout_Function) { - Timeout_Function(plist->InvokeID); - } - } - } - } - } - } -} - /** Frees the invokeID and sets its state to IDLE * * @param invokeID Invoke-ID @@ -376,7 +367,8 @@ bool tsm_invoke_id_free(uint8_t invokeID) return status; } -/** See if we failed get a confirmation for the message associated +/** + * @brief Check if a confirmation failed to arrive for the message associated * with this invoke ID. * @param invokeID [in] The invokeID to be checked, normally of last message * sent. @@ -399,4 +391,1192 @@ bool tsm_invoke_id_failed(uint8_t invokeID) return status; } + +#if BACNET_SEGMENTATION_ENABLED +/** + * @brief Send a Segment Ack PDU + * @param dest [in] The destination address + * @param negativeack [in] true if negative ack, false if positive ack + * @param server [in] true if server, false if client + * @param invoke_id [in] The invoke ID + * @param sequence_number [in] The sequence number + * @param actual_window_size [in] The actual window size + */ +static void tsm_segmentack_pdu_send( + BACNET_ADDRESS *dest, + bool negativeack, + bool server, + uint8_t invoke_id, + uint8_t sequence_number, + uint8_t actual_window_size) +{ + int pdu_len = 0; + BACNET_NPDU_DATA npdu_data; + int bytes_sent; + BACNET_ADDRESS my_address; + int apdu_len = 0; + int npdu_len = 0; + uint8_t Transmit_Buffer[MAX_PDU] = { 0 }; + datalink_get_my_address(&my_address); + npdu_encode_npdu_data(&npdu_data, false, MESSAGE_PRIORITY_NORMAL); + npdu_len = + npdu_encode_pdu(&Transmit_Buffer[0], dest, &my_address, &npdu_data); + apdu_len = segmentack_encode_apdu( + &Transmit_Buffer[npdu_len], negativeack, server, invoke_id, + sequence_number, actual_window_size); + pdu_len = apdu_len + npdu_len; + bytes_sent = + datalink_send_pdu(dest, &npdu_data, &Transmit_Buffer[0], pdu_len); + DEBUG_FPRINTF(stderr, "bytes sent=%d\n", bytes_sent); +} + +/** + * @brief determine the theoretical size of an apdu fixed header + * @param header [in] The fixed header + * @param segmented [in] true if segmented, false if unsegmented + * @return the estimated size of the header + */ +static uint32_t +tsm_apdu_header_typical_size(BACNET_APDU_FIXED_HEADER *header, bool segmented) +{ + int segmented_ack = 5; + int unsegmented_ack = 3; + int segmented_request = 6; + int unsegmented_request = 4; + switch (header->pdu_type) { + case PDU_TYPE_COMPLEX_ACK: + return segmented ? segmented_ack : unsegmented_ack; + case PDU_TYPE_CONFIRMED_SERVICE_REQUEST: + return segmented ? segmented_request : unsegmented_request; + default: + break; + } + return unsegmented_ack; +} + +/** + * @brief Free the allocated blob data + * @param data [in] The TSM data + */ +static void tsm_blob_free(BACNET_TSM_DATA *data) +{ + /* Free received data blobs */ + if (data->apdu_blob) { + free(data->apdu_blob); + } + data->apdu_blob = NULL; + data->apdu_blob_allocated = 0; + data->apdu_blob_size = 0; + /* Free sent data blobs */ + if (data->apdu) { + free(data->apdu); + } + data->apdu = NULL; + data->apdu_len = 0; +} + +/** + * @param Reset blob current size, but keep allocated data + * @param data [in] The TSM data + */ +static void tsm_blob_reset(BACNET_TSM_DATA *data) +{ + data->apdu_blob_size = 0; +} + +/** + * @brief Pad the data blob by allocating new data if necessary + * while keeping existing bytes. + * @param data [in] The TSM data + * @param allocation_unit [in] The size of the data to allocate + */ +static void tsm_blob_pad(BACNET_TSM_DATA *data, uint32_t allocation_unit) +{ + if (!allocation_unit) { /* NOP */ + return; + } + /* allocation needed ? */ + if ((!data->apdu_blob) || (!data->apdu_blob_allocated) || + ((allocation_unit + data->apdu_blob_size) > + data->apdu_blob_allocated)) { + /* stupid idiot allocation algorithm : space allocated shall augment + * exponentially */ + /* (nb: here there may be extra space remaining) */ + uint8_t *apdu_new_blob = + calloc(1, data->apdu_blob_allocated + allocation_unit); + if (!apdu_new_blob) { + DEBUG_FPRINTF( + stderr, "TSM: failed to allocate %d bytes\n", + data->apdu_blob_allocated + allocation_unit); + return; + } + /* recopy old data */ + if (data->apdu_blob_size) { + memcpy(apdu_new_blob, data->apdu_blob, data->apdu_blob_size); + } + /* new values */ + if (data->apdu_blob) { + free(data->apdu_blob); + } + data->apdu_blob = apdu_new_blob; + data->apdu_blob_allocated = data->apdu_blob_allocated + allocation_unit; + } +} + +/** + * Add new data to current blob (allocate extra space if necessary) + * @param data [in] The TSM data + * @param bdata [in] The data to add + * @param data_len [in] The length of the data + */ +static void +tsm_blob_data_add(BACNET_TSM_DATA *data, uint8_t *bdata, uint32_t data_len) +{ + tsm_blob_pad(data, data_len); + memcpy(&data->apdu_blob[data->apdu_blob_size], bdata, data_len); + data->apdu_blob_size += data_len; +} + +/** + * @brief Get the current blob data + * @param data [in] The TSM data + * @param data_len [out] The length of the data + * @return Pointer to the blob data + */ +static uint8_t *tsm_blob_data_get(BACNET_TSM_DATA *data, uint16_t *data_len) +{ + *data_len = data->apdu_blob_size; + return data->apdu_blob; +} + +/** + * @brief Get the Nth packet data to send in a segmented operation, + * or get the only data packet in unsegmented world. + * @param data [in] The TSM data + * @param segment_number [in] The segment number to get + * @param data_len [out] The length of the data + * @return Pointer to the data segment + */ +static uint8_t *tsm_blob_data_segment_get( + BACNET_TSM_DATA *data, int segment_number, uint32_t *data_len) +{ + /* Data is split in N blocks of, at maximum, ( APDU_MAX - APDU_HEADER ) + * bytes */ + bool segmented = + data->apdu_fixed_header.service_data.common_data.segmented_message; + int header_size = + tsm_apdu_header_typical_size(&data->apdu_fixed_header, segmented); + int block_request_size = data->apdu_maximum_length - header_size; + int data_position = segment_number * block_request_size; + int remaining_size = (int)data->apdu_len - data_position; + *data_len = (uint32_t)max(0, min(remaining_size, block_request_size)); + return data->apdu + data_position; +} + +/** + * @brief Clear TSM Peer data + * @param InternalInvokeID [in] The internal invoke ID + */ +void tsm_clear_peer_id(uint8_t InternalInvokeID) +{ + int ix; + + /* look for a matching internal invoke ID */ + for (ix = 0; ix < BACNET_SEGMENTATION_TSM_PEERS_MAX; ix++) { + /* see if it matches the internal number */ + if (TSM_Peer_Ids[ix].InternalInvokeID == InternalInvokeID) { + TSM_Peer_Ids[ix].InternalInvokeID = 0; + } + } +} + +/** + * @brief frees the invokeID and sets its state to IDLE + * @param invokeID [in] The invokeID to be checked, normally of last message + * sent. + * @param peer_address [in] The peer address to check against + * @param cleanup [in] If true, free the blob data + */ +void tsm_free_invoke_id_check( + uint8_t invokeID, BACNET_ADDRESS *peer_address, bool cleanup) +{ + uint8_t index; + + index = tsm_find_invokeID_index(invokeID); + + if ((index < MAX_TSM_TRANSACTIONS) && + (!peer_address || address_match(peer_address, &TSM_List[index].dest))) { + /* check "double-free" cases */ + TSM_List[index].state = TSM_STATE_IDLE; + /* Clear Peer data, if any. Lookup with our internal ID status. */ + tsm_clear_peer_id(invokeID); + /* flag slot as "unused" */ + TSM_List[index].InvokeID = 0; + + if (cleanup) { + /* Release segmented data */ + tsm_blob_free(&TSM_List[index]); + } + } +} + +/** + * @brief Finds (optionally creates) an existing peer data + * @param src [in] The source address + * @param invokeID [in] The invoke ID + * @param createPeerId [in] Create a new peer ID if not found + * @return Pointer to the peer data or NULL if not found + */ +static BACNET_TSM_INDIRECT_DATA * +tsm_get_peer_id_data(BACNET_ADDRESS *src, uint8_t invokeID, bool createPeerId) +{ + int ix; + int index; + int free_ix_found = -1; + BACNET_TSM_INDIRECT_DATA *item = NULL; + + /* look for an empty slot, or a matching (address,peer invoke ID) */ + for (ix = 0; ix < BACNET_SEGMENTATION_TSM_PEERS_MAX && !item; ix++) { + /* not free : see if it matches */ + if (TSM_Peer_Ids[ix].InternalInvokeID != 0) { + if (invokeID == TSM_Peer_Ids[ix].PeerInvokeID && + address_match(src, &TSM_Peer_Ids[ix].PeerAddress)) { + item = &TSM_Peer_Ids[ix]; + } + } else if (free_ix_found < 0) { + /* mark free slot found */ + free_ix_found = ix; + } + } + + /* create new data */ + if ((!item) && createPeerId && (free_ix_found > -1)) { + /* memorize peer data */ + TSM_Peer_Ids[free_ix_found].PeerInvokeID = invokeID; + TSM_Peer_Ids[free_ix_found].PeerAddress = *src; + /* create an internal TSM slot (with internal invokeID number which is + * not relevant) */ + TSM_Peer_Ids[free_ix_found].InternalInvokeID = tsm_next_free_invokeID(); + index = tsm_find_invokeID_index( + TSM_Peer_Ids[free_ix_found].InternalInvokeID); + if (index < MAX_TSM_TRANSACTIONS) { + /* explicitly memorize peer InvokeID */ + TSM_List[index].InvokeID = + TSM_Peer_Ids[free_ix_found].InternalInvokeID; + TSM_List[index].dest = *src; + item = &TSM_Peer_Ids[free_ix_found]; + } else { + /* problem : reset slot (NULL returned) */ + TSM_Peer_Ids[free_ix_found].InternalInvokeID = 0; + } + } + + return item; +} + +/** + * @brief Associates a Peer address and invoke ID with our TSM + * @param src [in] The source address + * @param invokeID [in] The invoke ID + * @return A local InvokeID unique number, 0 in case of error. + */ +uint8_t tsm_get_peer_id(BACNET_ADDRESS *src, uint8_t invokeID) +{ + BACNET_TSM_INDIRECT_DATA *peer_data; + peer_data = tsm_get_peer_id_data(src, invokeID, true); + if (peer_data) { + return peer_data->InternalInvokeID; + } + return 0; +} + +/** + * @brief Check if the segment is a duplicate + * @param tsm_data [in] The TSM data + * @param seqA [in] The sequence number of the segment + * @param first_sequence_number [in] The first sequence number in the window + * @param last_sequence_number [in] The last sequence number in the window + * @return true if the segment is a duplicate + */ +static bool DuplicateInWindow( + BACNET_TSM_DATA *tsm_data, + uint8_t seqA, + uint32_t first_sequence_number, + uint32_t last_sequence_number) +{ + uint8_t received_count = + (last_sequence_number - first_sequence_number) % 256; + if (received_count > tsm_data->ActualWindowSize) { + return false; + } else if ((seqA - first_sequence_number) % 256 <= received_count) { + return true; + } else if ( + (received_count == 0) && + ((first_sequence_number - seqA) % 256 <= tsm_data->ActualWindowSize)) { + return true; + } else { + return false; + } +} + +/** + * @brief Check if the segment is a duplicate + * @param index [in] The index of the TSM + * @param service_data [in] The service data + * @param src [in] The source address + * @return true if the segment is a duplicate + */ +static bool tsm_duplicate_segment_received( + uint8_t index, + BACNET_CONFIRMED_SERVICE_DATA *service_data, + BACNET_ADDRESS *src) +{ + BACNET_NPDU_DATA npdu_data; + bool isDuplicate = false; + uint8_t Ndup = TSM_List[index].ActualWindowSize; + + if (Duplicate_Count < Ndup) { + /* DuplicateSegmentReceived */ + TSM_List[index].SegmentTimer = apdu_segment_timeout(); + Duplicate_Count++; + isDuplicate = true; + } else if (Duplicate_Count == Ndup) { + /* TooManyDuplicateSegmentsReceived */ + npdu_encode_npdu_data(&npdu_data, false, MESSAGE_PRIORITY_NORMAL); + tsm_segmentack_pdu_send( + src, true, true, service_data->invoke_id, + TSM_List[index].LastSequenceNumber, + TSM_List[index].ActualWindowSize); + TSM_List[index].SegmentTimer = apdu_segment_timeout(); + TSM_List[index].InitialSequenceNumber = + TSM_List[index].LastSequenceNumber; + Duplicate_Count = 0; + isDuplicate = true; + } + + return isDuplicate; +} + +/** + * @brief send an Abort-PDU message because of incorrect segment/PDU received + * @param invoke_id [in] The invokeID to be checked, normally of last message + * sent. + * @param dest [in] The destination address + * @param reason [in] The reason for the abort + * @param server [in] true if the server, false if the client + */ +void tsm_abort_pdu_send( + uint8_t invoke_id, BACNET_ADDRESS *dest, uint8_t reason, bool server) +{ + int pdu_len = 0; + BACNET_NPDU_DATA npdu_data; + BACNET_ADDRESS my_address; + int apdu_len = 0; + int npdu_len = 0; + uint8_t Transmit_Buffer[MAX_PDU] = { 0 }; + + datalink_get_my_address(&my_address); + npdu_encode_npdu_data(&npdu_data, false, MESSAGE_PRIORITY_NORMAL); + npdu_len = + npdu_encode_pdu(&Transmit_Buffer[0], dest, &my_address, &npdu_data); + apdu_len = abort_encode_apdu( + &Transmit_Buffer[npdu_len], invoke_id, reason, server); + pdu_len = apdu_len + npdu_len; + (void)datalink_send_pdu(dest, &npdu_data, &Transmit_Buffer[0], pdu_len); +} + +/** + * @brief We received a segment of a ConfirmedService packet, + * check TSM state and reassemble the full packet + * @param src [in] Source address + * @param service_data [in] Service data + * @param internal_invoke_id [out] Internal invoke ID + * @param pservice_request [in/out] Service request buffer + * @param pservice_request_len [in/out] Service request length + * @return true if the segment was received and processed + */ +bool tsm_set_segmented_confirmed_service_received( + BACNET_ADDRESS *src, + BACNET_CONFIRMED_SERVICE_DATA *service_data, + uint8_t *internal_invoke_id, + uint8_t **pservice_request, + uint16_t *pservice_request_len) +{ + uint8_t index; + uint8_t *service_request = *pservice_request; + uint32_t service_request_len = *pservice_request_len; + bool result = false; + bool ack_needed = false; + uint8_t internal_service_id = tsm_get_peer_id(src, service_data->invoke_id); + *internal_invoke_id = internal_service_id; + if (!internal_service_id) { + /* failed : could not allocate enough slot for this transaction */ + tsm_abort_pdu_send( + service_data->invoke_id, src, + ABORT_REASON_PREEMPTED_BY_HIGHER_PRIORITY_TASK, true); + + /* We must free invoke_id ! */ + tsm_free_invoke_id_check(internal_service_id, NULL, true); + return false; + } + index = tsm_find_invokeID_index(internal_service_id); + if (index >= MAX_TSM_TRANSACTIONS) { /* shall not fail */ + tsm_abort_pdu_send( + service_data->invoke_id, src, ABORT_REASON_OTHER, true); + return false; + } + /* check states */ + switch (TSM_List[index].state) { + /* Initial state: ConfirmedSegmentReceived */ + case TSM_STATE_IDLE: + /* We never stay in IDLE state */ + TSM_List[index].state = TSM_STATE_SEGMENTED_REQUEST_SERVER; + /* First time : Compute Actual WindowSize */ + /* we automatically accept the proposed window size */ + TSM_List[index].ActualWindowSize = + TSM_List[index].ProposedWindowSize = + service_data->proposed_window_number; + /* Init sequence numbers */ + TSM_List[index].InitialSequenceNumber = 0; + TSM_List[index].LastSequenceNumber = 0; + /* resets counters */ + TSM_List[index].RetryCount = 0; + TSM_List[index].SegmentRetryCount = 0; + TSM_List[index].ReceivedSegmentsCount = 1; + /* stop unsegmented timer */ + TSM_List[index].RequestTimer = 0; /* unused */ + /* start the segmented timer */ + TSM_List[index].SegmentTimer = apdu_segment_timeout() * 4; + /* reset memorized data */ + tsm_blob_reset(&TSM_List[index]); + if (service_data->sequence_number == 0 && + (TSM_List[index].ProposedWindowSize == 0 || + TSM_List[index].ProposedWindowSize > 127)) { + /* ConfirmedSegmentedReceivedWindowSizeOutofRange */ + tsm_abort_pdu_send( + service_data->invoke_id, src, + ABORT_REASON_WINDOW_SIZE_OUT_OF_RANGE, true); + + /* We must free invoke_id ! */ + tsm_free_invoke_id_check(internal_service_id, NULL, true); + break; + } + + /* Test : sequence number MUST be 0 */ + /* UnexpectedPDU_Received */ + if (service_data->sequence_number != 0) { + /* Release data */ + tsm_blob_free(&TSM_List[index]); + /* Abort */ + tsm_abort_pdu_send( + service_data->invoke_id, src, + ABORT_REASON_INVALID_APDU_IN_THIS_STATE, true); + /* We must free invoke_id ! */ + tsm_free_invoke_id_check(internal_service_id, NULL, true); + } else { + /* Okay : memorize data */ + tsm_blob_data_add( + &TSM_List[index], service_request, service_request_len); + /* We ACK the first segment of the segmented message */ + tsm_segmentack_pdu_send( + src, false, true, service_data->invoke_id, + TSM_List[index].LastSequenceNumber, + TSM_List[index].ActualWindowSize); + } + break; + /* New segments */ + case TSM_STATE_SEGMENTED_REQUEST_SERVER: + /* reset the segment timer */ + TSM_List[index].RequestTimer = 0; /* unused */ + /* ANSI/ASHRAE 135-2008 5.4.5.2 SEGMENTED_REQUEST / Timeout */ + /* ...SegmentTimer becomes greater than Tseg times four, ... */ + TSM_List[index].SegmentTimer = apdu_segment_timeout() * 4; + /* Sequence number MUST be (LastSequenceNumber+1 modulo 256) */ + if ((service_data->sequence_number != + (uint8_t)(TSM_List[index].LastSequenceNumber + 1) % 256)) { + if (DuplicateInWindow( + &TSM_List[index], service_data->sequence_number, + (TSM_List[index].InitialSequenceNumber) % 256, + TSM_List[index].LastSequenceNumber)) { + /* DuplicateSegmentReceived */ + if (tsm_duplicate_segment_received( + index, service_data, src)) { + /* state is in TSM_STATE_SEGMENTED_REQUEST_SERVER */ + break; + } + } else { + /* Recoverable Error: SegmentReceivedOutOfOrder */ + /* ACK of last segment correctly received. */ + tsm_segmentack_pdu_send( + src, true, true, service_data->invoke_id, + TSM_List[index].LastSequenceNumber, + TSM_List[index].ActualWindowSize); + + Duplicate_Count = 0; + } + } else { + /* Count maximum segments */ + if (++TSM_List[index].ReceivedSegmentsCount > + BACNET_MAX_SEGMENTS_ACCEPTED) { + /* ABORT: SegmentReceivedOutOfSpace */ + tsm_abort_pdu_send( + service_data->invoke_id, src, + ABORT_REASON_BUFFER_OVERFLOW, true); + /* Release data */ + tsm_blob_free(&TSM_List[index]); + /* Enter IDLE state */ + TSM_List[index].state = TSM_STATE_IDLE; + /* We must free invoke_id ! */ + tsm_free_invoke_id_check(internal_service_id, NULL, true); + } else { + /* NewSegmentReceived */ + TSM_List[index].LastSequenceNumber = + service_data->sequence_number; + tsm_blob_data_add( + &TSM_List[index], service_request, service_request_len); + /* LastSegmentOfComplexACK_Received */ + if (service_data->sequence_number == + (uint8_t)(TSM_List[index].InitialSequenceNumber + + TSM_List[index].ActualWindowSize)) { + ack_needed = true; + TSM_List[index].InitialSequenceNumber = + service_data->sequence_number; + } + /* LastSegmentOfComplexACK_Received */ + if (!service_data->more_follows) { + /* Resulting segment data */ + *pservice_request = tsm_blob_data_get( + &TSM_List[index], pservice_request_len); + result = + true; /* Returns true on final segment received */ + ack_needed = true; + } + /* LastSegmentOfComplexACK_Received or + * LastSegmentOfGroupReceived */ + if (ack_needed) { + /* ACK received segment */ + tsm_segmentack_pdu_send( + src, false, true, service_data->invoke_id, + TSM_List[index].LastSequenceNumber, + TSM_List[index].ActualWindowSize); + } + } + } + break; + default: + break; + } + return result; +} + +/** + * @brief calculate how many segments will be used to send data + * in this TSM slot + * @param data - TSM data + * @return 1 : No segmentation needed, >1 segmentation needed (number of + * segments). + */ +static uint32_t tsm_apdu_max_segments_get(BACNET_TSM_DATA *data) +{ + uint32_t header_size; + uint32_t packets; + + /* Are we unsegmented ? */ + header_size = tsm_apdu_header_typical_size(&data->apdu_fixed_header, false); + if (header_size + data->apdu_len <= data->apdu_maximum_length) { + return 1; + } + + /* We are segmented : calculate how many segments to use */ + header_size = tsm_apdu_header_typical_size(&data->apdu_fixed_header, true); + + /* Number of packets to use formula : p = ( ( total_length - 1 ) / + * packet_length ) + 1; */ + packets = + ((data->apdu_len - 1) / (data->apdu_maximum_length - header_size)) + 1; + + return packets; +} + +/** + * @brief calculate the maximum APDU length + * @param dest - destination address + * @param confirmed_service_data - confirmed service data + * @param apdu_max - maximum APDU length + * @param total_max - total maximum APDU length + */ +static void tsm_apdu_transmittable_length( + BACNET_ADDRESS *dest, + BACNET_CONFIRMED_SERVICE_DATA *confirmed_service_data, + uint32_t *apdu_max, + uint32_t *total_max) +{ + uint32_t deviceId = 0; + unsigned max_apdu; + uint8_t segmentation = 0; + uint16_t maxsegments = 0; + BACNET_ADDRESS src; + + /* either we are replying to a confirmed service, so we use prompted values + ; either we are requesting a peer, so we use memorised information about + the peer device. + */ + if (confirmed_service_data) { + /* use maximum available APDU */ + *total_max = *apdu_max = + min(confirmed_service_data->max_resp, MAX_APDU); + /* segmented : compute maximum number of packets */ + if (confirmed_service_data->segmented_response_accepted) { + maxsegments = confirmed_service_data->max_segs; + /* if unspecified, try the maximum available, not just 2 segments */ + if (!maxsegments || maxsegments > 64) { + maxsegments = BACNET_MAX_SEGMENTS_ACCEPTED; + } + /* maximum size we are able to transmit */ + *total_max = + min(maxsegments, BACNET_MAX_SEGMENTS_ACCEPTED) * (*apdu_max); + } + return; + } + + if (address_get_device_id(dest, &deviceId)) { + if (address_segment_get_by_device( + deviceId, &max_apdu, &src, &segmentation, &maxsegments)) { + /* Best possible APDU size */ + *total_max = *apdu_max = min(max_apdu, MAX_APDU); + /* if device is able to receive segments */ + if (segmentation == SEGMENTATION_BOTH || + segmentation == SEGMENTATION_RECEIVE) { + /* XXX - TODO: Number of segments accepted by peer device : + If zero segments we should fallback to 2 segments. + Or Maybe we just didn't ask the device about the maximum + segments supported. + */ + if (!maxsegments) { + maxsegments = BACNET_MAX_SEGMENTS_ACCEPTED; + } + /* maximum size we are able to transmit */ + if (maxsegments) { + *total_max = + min(maxsegments, BACNET_MAX_SEGMENTS_ACCEPTED) * + (*apdu_max); + } + } + return; + } + } + *apdu_max = MAX_APDU; + *total_max = *apdu_max * BACNET_MAX_SEGMENTS_ACCEPTED; +} + +/** + * @brief room checks to prevent buffer overflows + * @param apdu_len - current APDU length + * @param max_apdu - maximum APDU length + * @param space_needed - space needed for the new data + * @return true if there is enough space, false otherwise + */ +static bool +tsm_apdu_space_available(int apdu_len, int max_apdu, int space_needed) +{ + return (apdu_len + space_needed) < max_apdu; +} + +/** + * @brief send a packet to peer + * @param tsm_data - TSM data + * @param segment_number - segment number to send + * @return number of bytes sent or -1 on error + */ +static int tsm_pdu_send(BACNET_TSM_DATA *tsm_data, uint32_t segment_number) +{ + uint8_t Transmit_Buffer[MAX_PDU] = { 0 }; + BACNET_ADDRESS my_address; + int len = 0; + int pdu_len = 0; + uint8_t *service_data = NULL; + uint32_t service_len = 0; + uint32_t total_segments = 0; + + /* Rebuild PDU */ + datalink_get_my_address(&my_address); + len = npdu_encode_pdu( + &Transmit_Buffer[pdu_len], &tsm_data->dest, &my_address, + &tsm_data->npdu_data); + if (len < 0) { + return -1; + } + pdu_len += len; + /* Header tweaks ! */ + total_segments = tsm_apdu_max_segments_get(tsm_data); + /* Index out of bounds */ + if (segment_number >= total_segments) { + return -1; + } + if (total_segments == 1) { + tsm_data->apdu_fixed_header.service_data.common_data.segmented_message = + false; + } else { + /* SEG */ + tsm_data->apdu_fixed_header.service_data.common_data.segmented_message = + true; + /* MORE */ + tsm_data->apdu_fixed_header.service_data.common_data.more_follows = + (segment_number < total_segments - 1); + /* Window size : do not modify */ + /* tsm_data->apdu_fixed_header.service_data.common_data.proposed_window_number + * = 127; */ + /* SEQ# */ + tsm_data->apdu_fixed_header.service_data.common_data.sequence_number = + segment_number; + } + /* Rebuild APDU Header */ + len = apdu_encode_fixed_header( + &Transmit_Buffer[pdu_len], &tsm_data->apdu_fixed_header); + if (len < 0) { + return -1; + } + pdu_len += len; + /* Rebuild APDU service data */ + /* gets Nth packet data */ + service_data = + tsm_blob_data_segment_get(tsm_data, segment_number, &service_len); + if (!service_data) { /* May be zero-size ! */ + return -1; + } + /* enough room ? */ + if (!tsm_apdu_space_available( + pdu_len, sizeof(Transmit_Buffer), service_len)) { + return -1; + } + memcpy(&Transmit_Buffer[pdu_len], service_data, service_len); + pdu_len += service_len; + return datalink_send_pdu( + &tsm_data->dest, &tsm_data->npdu_data, &Transmit_Buffer[0], pdu_len); +} + +/** + * @brief Process and send segmented/unsegmented complex acknoweldegement + * based on the response data length + * For unsegmented response, send the whole data + * For segmented response, send the 1st segment of response data + * @param dest - destination address + * @param npdu_data - NPDU data + * @param apdu_fixed_header - APDU fixed header + * @param confirmed_service_data - confirmed service data + * @param pdu - PDU data + * @param pdu_len - PDU length + * @return number of bytes sent or -1 on error + */ +int tsm_set_complexack_transaction( + BACNET_ADDRESS *dest, + BACNET_NPDU_DATA *npdu_data, + BACNET_APDU_FIXED_HEADER *apdu_fixed_header, + BACNET_CONFIRMED_SERVICE_DATA *confirmed_service_data, + uint8_t *pdu, + uint32_t pdu_len) +{ + uint8_t index; + int bytes_sent; + BACNET_TSM_DATA *tsm_data; + uint32_t apdu_segments; + uint8_t internal_service_id = + tsm_get_peer_id(dest, confirmed_service_data->invoke_id); + + if (!internal_service_id) { + /* failed : could not allocate enough slot for this transaction */ + tsm_abort_pdu_send( + confirmed_service_data->invoke_id, dest, + ABORT_REASON_PREEMPTED_BY_HIGHER_PRIORITY_TASK, true); + return -1; + } + index = tsm_find_invokeID_index(internal_service_id); + if (index >= MAX_TSM_TRANSACTIONS) { /* shall not fail */ + tsm_abort_pdu_send( + confirmed_service_data->invoke_id, dest, ABORT_REASON_OTHER, true); + tsm_free_invoke_id_check(internal_service_id, dest, true); + return -1; + } + tsm_data = &TSM_List[index]; + + /* Choice between a segmented or a non-segmented transaction */ + + /* fill in maximum fill values */ + tsm_apdu_transmittable_length( + dest, confirmed_service_data, &tsm_data->apdu_maximum_length, + &tsm_data->maximum_transmittable_length); + /* copy the apdu service data */ + tsm_blob_data_copy(tsm_data, &pdu[0], pdu_len); + /* copy npdu data */ + npdu_copy_data(&tsm_data->npdu_data, npdu_data); + /* copy apdu header data */ + tsm_data->apdu_fixed_header = *apdu_fixed_header; + /* destination address */ + bacnet_address_copy(&tsm_data->dest, dest); + /* absolute "retry" count : won't be reinitialized later */ + tsm_data->RetryCount = apdu_retries(); + + tsm_data->ActualWindowSize = 1; + tsm_data->ProposedWindowSize = BACNET_SEGMENTATION_WINDOW_SIZE_DEFAULT; + tsm_data->InitialSequenceNumber = 0; + tsm_data->SentAllSegments = false; + + /* Choice between a segmented or a non-segmented transaction */ + if (1 == (apdu_segments = tsm_apdu_max_segments_get(tsm_data))) { + /* UNSEGMENTED MODE : Free transaction afterwards */ + bytes_sent = tsm_pdu_send(tsm_data, 0); + if (bytes_sent > 0) { + tsm_free_invoke_id_check(internal_service_id, dest, true); + } + } else { + /* SEGMENTED-MODE */ + /* Take into account the fact that the APDU header is repeated on every + * segment */ + if (pdu_len + + apdu_segments * + tsm_apdu_header_typical_size(apdu_fixed_header, true) > + tsm_data->maximum_transmittable_length) { + /* Too much data : we cannot send that much, or the API cannot + * receive that much ! */ + tsm_blob_free(&TSM_List[index]); + /* Abort */ + tsm_abort_pdu_send( + confirmed_service_data->invoke_id, dest, + ABORT_REASON_BUFFER_OVERFLOW, true); + bytes_sent = -2; + } else { + /* Window size proposal */ + tsm_data->apdu_fixed_header.service_data.common_data + .proposed_window_number = tsm_data->ProposedWindowSize; + /* assign the transaction */ + tsm_data->state = TSM_STATE_SEGMENTED_RESPONSE_SERVER; + tsm_data->SegmentRetryCount = apdu_retries(); + /* start the timer */ + tsm_data->RequestTimer = 0; + tsm_data->SegmentTimer = apdu_segment_timeout(); + /* Send first packet */ + bytes_sent = tsm_pdu_send(tsm_data, 0); + } + } + /* If we cannot initiate, free transaction so we don't wait on a timeout to + realize it has failed. Caller don't free invoke ID : we must clear it + now. */ + if (bytes_sent <= 0) { + tsm_free_invoke_id_check(internal_service_id, dest, true); + } + return bytes_sent; +} + +/** + * @brief Sends PDU segments either until the window is full or + * until the last segment of a message has been sent. + * @param tsm_data - TSM data + * @param sequence_number - sequence number of the segment + */ +static void FillWindow(BACNET_TSM_DATA *tsm_data, uint32_t sequence_number) +{ + uint32_t ix; + uint32_t total_segments = tsm_apdu_max_segments_get(tsm_data); + for (ix = 0; (ix < tsm_data->ActualWindowSize) && + (sequence_number + ix < total_segments); + ix++) { + tsm_pdu_send(tsm_data, sequence_number + ix); + } + /* sent all segments ? */ + if (ix + sequence_number >= total_segments) { + tsm_data->SentAllSegments = true; + } +} + +/** + * @brief Check if the sequence number is in the window + * @param data - TSM data + * @param seqA - sequence number A + * @param seqB - sequence number B + * @return true if the sequence number is in the window + */ +static bool InWindow(BACNET_TSM_DATA *data, uint8_t seqA, uint8_t seqB) +{ + uint8_t requiredWindowSize = seqA - seqB; + return requiredWindowSize < data->ActualWindowSize; +} + +/** + * @brief Process the received segment ack and send + * the next segment accordingly if available + * @param invoke_id - invoke ID of the peer + * @param sequence_number - sequence number of the segment + * @param actual_window_size - actual window size + * @param nak - nak flag + * @param server - true if server + * @param src - BACnet address of the peer + */ +void tsm_segmentack_received( + uint8_t invoke_id, + uint8_t sequence_number, + uint8_t actual_window_size, + bool nak, + bool server, + BACNET_ADDRESS *src) +{ + uint8_t index; + uint32_t big_segment_number; + uint8_t window; + bool some_segment_remains; + BACNET_TSM_INDIRECT_DATA *peer_data; + + (void)nak; + + /* bad invoke number from server peer (we never use 0) */ + if (server && !invoke_id) { + return; + } + /* Peer invoke id number : translate to our internal numbers */ + if (!server) { + peer_data = tsm_get_peer_id_data(src, invoke_id, false); + if (!peer_data) { + /* failed : unknown message */ + return; + } + /* now we use our internal number */ + invoke_id = peer_data->InternalInvokeID; + } + /* Find an active TSM slot that matches the Segment-Ack */ + index = tsm_find_invokeID_index(invoke_id); + if (index >= MAX_TSM_TRANSACTIONS) { + return; + } + + /* Almost the same code for segment handling between segmented requests and + * responses */ + if (!server && + TSM_List[index].state == TSM_STATE_SEGMENTED_RESPONSE_SERVER) { + /* DuplicateAck_Received */ + if (!InWindow( + &TSM_List[index], sequence_number, + TSM_List[index].InitialSequenceNumber)) { + /* Restart timer */ + TSM_List[index].SegmentTimer = apdu_segment_timeout(); + } else { + /* total segment number (not modulo 256) */ + window = sequence_number - + (uint8_t)TSM_List[index].InitialSequenceNumber; + big_segment_number = TSM_List[index].InitialSequenceNumber + window; + + /* 1..N segment number < number of segments ? */ + some_segment_remains = (big_segment_number + 1) < + tsm_apdu_max_segments_get(&TSM_List[index]); + if (some_segment_remains) { + /* NewAck_Received : do we have a segment remaining to send */ + TSM_List[index].InitialSequenceNumber = big_segment_number + 1; + TSM_List[index].ActualWindowSize = actual_window_size; + TSM_List[index].SegmentRetryCount = apdu_retries(); + TSM_List[index].SegmentTimer = apdu_segment_timeout(); + FillWindow( + &TSM_List[index], TSM_List[index].InitialSequenceNumber); + TSM_List[index].SegmentTimer = apdu_segment_timeout(); + } else { + /* FinalAck_Received */ + TSM_List[index].SegmentTimer = 0; + if (TSM_List[index].state == + TSM_STATE_SEGMENTED_RESPONSE_SERVER) { + /* Response : end communications */ + /* Release data */ + TSM_List[index].state = TSM_STATE_IDLE; + /* Completely free data */ + tsm_free_invoke_id_check(invoke_id, NULL, true); + } else { + /* Request : Wait confirmation */ + TSM_List[index].RequestTimer = apdu_timeout(); + TSM_List[index].state = TSM_STATE_AWAIT_CONFIRMATION; + } + } + } + } else { + /* UnexpectedPDU_Received */ + /* Release data */ + tsm_blob_free(&TSM_List[index]); + /* Abort */ + tsm_abort_pdu_send( + invoke_id, src, ABORT_REASON_INVALID_APDU_IN_THIS_STATE, true); + /* We must free invoke_id ! */ + tsm_free_invoke_id_check(invoke_id, NULL, true); + } +} + +/** + * @brief Check unexpected PDU is received in active TSM state other + * than idle state for server + * @param src - BACnet address of the peer + * @param service_data - BACnet confirmed service data + * @return true if the unexpected PDU is received in active TSM state + */ +bool tsm_is_invalid_apdu_in_this_state( + BACNET_ADDRESS *src, BACNET_CONFIRMED_SERVICE_DATA *service_data) +{ + uint8_t index = 0; + bool status = false; + BACNET_TSM_INDIRECT_DATA *peer_data; + peer_data = tsm_get_peer_id_data(src, service_data->invoke_id, false); + if (peer_data) { + index = tsm_find_invokeID_index(peer_data->InternalInvokeID); + if (index < MAX_TSM_TRANSACTIONS) { + BACNET_TSM_DATA *plist = &TSM_List[index]; + if ((plist->state == TSM_STATE_SEGMENTED_RESPONSE_SERVER) || + (plist->state == TSM_STATE_SEGMENTED_REQUEST_SERVER)) { + tsm_abort_pdu_send( + service_data->invoke_id, src, + ABORT_REASON_INVALID_APDU_IN_THIS_STATE, true); + tsm_free_invoke_id_check(plist->InvokeID, src, true); + status = true; + } + } + } + return status; +} + +/** + * @brief frees the invokeID for segmented messages + * @param src - BACnet address of the peer + * @param invoke_id - invoke ID to free + */ +void tsm_free_invoke_id_segmentation(BACNET_ADDRESS *src, uint8_t invoke_id) +{ + uint8_t peer_id = 0; + peer_id = tsm_get_peer_id(src, invoke_id); + /* Peer_id = 0 refers to free slot */ + if (peer_id) { + tsm_free_invoke_id_check(peer_id, src, true); + } +} +#endif + +/** + * @brief Called once a millisecond or slower. + * This function calls the handler for a + * timeout 'Timeout_Function', if necessary. + * + * @note Only supports segmentation for Server by + * implementing two states: + * TSM_STATE_SEGMENTED_RESPONSE_SERVER + * TSM_STATE_SEGMENTED_REQUEST_SERVER + * Client segmentation is not supported at this time. + * + * @param milliseconds - Count of milliseconds passed, since the last call. + */ +void tsm_timer_milliseconds(uint16_t milliseconds) +{ + unsigned i = 0; /* counter */ + BACNET_TSM_DATA *plist = &TSM_List[0]; + + for (i = 0; i < MAX_TSM_TRANSACTIONS; i++, plist++) { + if (plist->state == TSM_STATE_AWAIT_CONFIRMATION) { + if (plist->RequestTimer > milliseconds) { + plist->RequestTimer -= milliseconds; + } else { + plist->RequestTimer = 0; + } + /* AWAIT_CONFIRMATION */ + if (plist->RequestTimer == 0) { + if (plist->RetryCount < apdu_retries()) { + plist->RequestTimer = apdu_timeout(); + plist->RetryCount++; + datalink_send_pdu( + &plist->dest, &plist->npdu_data, &plist->apdu[0], + plist->apdu_len); + DEBUG_PRINTF( + "invoke-id[%u] Retry %u of %u after %ums\n", + plist->InvokeID, plist->RetryCount, apdu_retries(), + plist->RequestTimer); + } else { + /* note: the invoke id has not been cleared yet + and this indicates a failed message: + IDLE and a valid invoke id */ + plist->state = TSM_STATE_IDLE; + if (plist->InvokeID != 0) { + if (Timeout_Function) { + Timeout_Function(plist->InvokeID); + } + } + } + } + } + if (plist->state == TSM_STATE_AWAIT_RESPONSE) { + /* 5.4.5.3 AWAIT_RESPONSE + In the AWAIT_RESPONSE state, the device waits for the + local application program to respond to a BACnet-Confirmed- + Request-PDU. See Clause 9.8 for specific considerations + in MS/TP networks.*/ + /* If RequestTimer becomes greater than Tout, + then issue an N-UNITDATA.request with + 'data_expecting_reply' = FALSE to transmit + a BACnet-Abort-PDU with 'server' = TRUE and + 'abort-reason' = APPLICATION_EXCEEDED_REPLY_TIME; + send ABORT.indication with 'server' = TRUE and + 'abort-reason' = APPLICATION_EXCEEDED_REPLY_TIME + to the local application program; and + enter the IDLE state. */ + /* note: this TSM implementation doesn't do this state */ + } +#if BACNET_SEGMENTATION_ENABLED + if (plist->state == TSM_STATE_SEGMENTED_RESPONSE_SERVER) { + /* RequestTimer stopped in this state */ + if (plist->SegmentTimer > milliseconds) { + plist->SegmentTimer -= milliseconds; + } else { + plist->SegmentTimer = 0; + } + /* timeout. retry? */ + if (plist->SegmentTimer == 0) { + plist->SegmentRetryCount--; + plist->SegmentTimer = apdu_segment_timeout(); + if (plist->SegmentRetryCount) { + /* Re-send PDU data */ + FillWindow(plist, plist->InitialSequenceNumber); + } else { + /* Reached max retries, Clear Peer data */ + tsm_clear_peer_id(plist->InvokeID); + /* Release segmented data */ + tsm_blob_free(&TSM_List[i]); + + /* flag slot as "unused" */ + plist->InvokeID = 0; + /* set to IDLE state */ + plist->state = TSM_STATE_IDLE; + } + } + } + if ((plist->state == TSM_STATE_SEGMENTED_REQUEST_SERVER) || + (plist->state == TSM_STATE_SEGMENTED_CONFIRMATION)) { + /* 5.4.5.2 SEGMENTED_REQUEST + In the SEGMENTED_REQUEST state, the device waits for segments + of a BACnet-Confirmed-Request-PDU. */ + /* 5.4.4.4 SEGMENTED_CONF + In the SEGMENTED_CONF state, the device waits for one or + more segments in response to a BACnet-SegmentACK-PDU.*/ + /* RequestTimer stopped in this state */ + if (plist->SegmentTimer > milliseconds) { + plist->SegmentTimer -= milliseconds; + } else { + plist->SegmentTimer = 0; + } + /* Timeout + If SegmentTimer becomes greater than Tseg times four, + then stop SegmentTimer and enter the IDLE state. */ + if (plist->SegmentTimer == 0) { + /* Clear Peer data, if any. Lookup with our internal ID + status. */ + tsm_clear_peer_id(plist->InvokeID); + /* Release segmented data */ + tsm_blob_free(plist); + + /* flag slot as "unused" */ + plist->InvokeID = 0; + /* free all memory associated */ + plist->state = TSM_STATE_IDLE; + } + } +#endif + } +} #endif diff --git a/src/bacnet/basic/tsm/tsm.h b/src/bacnet/basic/tsm/tsm.h index e7aef5b6..b94d3473 100644 --- a/src/bacnet/basic/tsm/tsm.h +++ b/src/bacnet/basic/tsm/tsm.h @@ -15,6 +15,9 @@ #include "bacnet/bacdef.h" /* BACnet Stack API */ #include "bacnet/npdu.h" +#if BACNET_SEGMENTATION_ENABLED +#include "bacnet/apdu.h" +#endif /* note: TSM functionality is optional - only needed if we are doing client requests */ @@ -24,7 +27,7 @@ extern "C" { #endif /* __cplusplus */ /* FIXME: modify basic service handlers to use TSM rather than this buffer! */ -BACNET_STACK_EXPORT extern uint8_t Handler_Transmit_Buffer[MAX_PDU]; +BACNET_STACK_EXPORT extern uint8_t Handler_Transmit_Buffer[MAX_ASDU]; #ifdef __cplusplus } @@ -37,31 +40,47 @@ typedef enum { TSM_STATE_IDLE, TSM_STATE_AWAIT_CONFIRMATION, TSM_STATE_AWAIT_RESPONSE, - TSM_STATE_SEGMENTED_REQUEST, - TSM_STATE_SEGMENTED_CONFIRMATION + TSM_STATE_SEGMENTED_REQUEST_SERVER, + TSM_STATE_SEGMENTED_CONFIRMATION, + TSM_STATE_SEGMENTED_RESPONSE_SERVER } BACNET_TSM_STATE; +#if BACNET_SEGMENTATION_ENABLED +/* Indirect data state : */ +typedef struct BACnet_TSM_Indirect_Data { + /* the address we received data from */ + BACNET_ADDRESS PeerAddress; + /* the peer unique id */ + uint8_t PeerInvokeID; + /* the unique id to use within our internal states. + zero means : "unused slot". */ + uint8_t InternalInvokeID; +} BACNET_TSM_INDIRECT_DATA; +#endif + /* 5.4.1 Variables And Parameters */ /* The following variables are defined for each instance of */ /* Transaction State Machine: */ typedef struct BACnet_TSM_Data { /* used to count APDU retries */ uint8_t RetryCount; +#if BACNET_SEGMENTATION_ENABLED /* used to count segment retries */ - /*uint8_t SegmentRetryCount; */ + uint8_t SegmentRetryCount; /* used to control APDU retries and the acceptance of server replies */ - /*bool SentAllSegments; */ + bool SentAllSegments; /* stores the sequence number of the last segment received in order */ - /*uint8_t LastSequenceNumber; */ + uint8_t LastSequenceNumber; /* stores the sequence number of the first segment of */ /* a sequence of segments that fill a window */ - /*uint8_t InitialSequenceNumber; */ + uint8_t InitialSequenceNumber; /* stores the current window size */ - /*uint8_t ActualWindowSize; */ + uint8_t ActualWindowSize; /* stores the window size proposed by the segment sender */ - /*uint8_t ProposedWindowSize; */ + uint8_t ProposedWindowSize; /* used to perform timeout on PDU segments */ - /*uint8_t SegmentTimer; */ + uint16_t SegmentTimer; +#endif /* used to perform timeout on Confirmed Requests */ /* in milliseconds */ uint16_t RequestTimer; @@ -73,9 +92,28 @@ typedef struct BACnet_TSM_Data { BACNET_ADDRESS dest; /* the network layer info */ BACNET_NPDU_DATA npdu_data; + unsigned apdu_len; +#if BACNET_SEGMENTATION_ENABLED + /* APDU header information */ + BACNET_APDU_FIXED_HEADER apdu_fixed_header; + /* calculated max APDU length / packet */ + uint32_t apdu_maximum_length; + /* calculated max APDU length / total */ + uint32_t maximum_transmittable_length; + /* Multiple APDU segments blob memorized here */ + uint8_t *apdu_blob; + /* Size of allocated Multiple APDU segments blob */ + uint32_t apdu_blob_allocated; + /* Size of data within the multiple APDU segments blob */ + uint32_t apdu_blob_size; + /* Count received segments (prevents D.O.S.) */ + uint32_t ReceivedSegmentsCount; + /* copy of the APDU, should we need to send it again */ + uint8_t *apdu; +#else /* copy of the APDU, should we need to send it again */ uint8_t apdu[MAX_PDU]; - unsigned apdu_len; +#endif } BACNET_TSM_DATA; typedef void (*tsm_timeout_function)(uint8_t invoke_id); @@ -123,6 +161,59 @@ bool tsm_invoke_id_free(uint8_t invokeID); BACNET_STACK_EXPORT bool tsm_invoke_id_failed(uint8_t invokeID); +#if BACNET_SEGMENTATION_ENABLED +/** Clear TSM Peer data */ +BACNET_STACK_EXPORT +void tsm_clear_peer_id(uint8_t InternalInvokeID); + +/* frees the invokeID and sets its state to IDLE */ +BACNET_STACK_EXPORT +void tsm_free_invoke_id_check( + uint8_t invokeID, BACNET_ADDRESS *peer_address, bool cleanup); + +/* Associates a Peer address and invoke ID with our TSM */ +BACNET_STACK_EXPORT +uint8_t tsm_get_peer_id(BACNET_ADDRESS *src, uint8_t invokeID); + +BACNET_STACK_EXPORT +bool tsm_set_segmented_confirmed_service_received( + BACNET_ADDRESS *src, + BACNET_CONFIRMED_SERVICE_DATA *service_data, + uint8_t *internal_invoke_id, + uint8_t **pservice_request, /* IN/OUT */ + uint16_t *pservice_request_len /* IN/OUT */ +); + +BACNET_STACK_EXPORT +int tsm_set_complexack_transaction( + BACNET_ADDRESS *dest, + BACNET_NPDU_DATA *npdu_data, + BACNET_APDU_FIXED_HEADER *apdu_fixed_header, + BACNET_CONFIRMED_SERVICE_DATA *confirmed_service_data, + uint8_t *pdu, + uint32_t pdu_len); + +BACNET_STACK_EXPORT +void tsm_segmentack_received( + uint8_t invoke_id, + uint8_t sequence_number, + uint8_t actual_window_size, + bool nak, + bool server, + BACNET_ADDRESS *src); + +BACNET_STACK_EXPORT +bool tsm_is_invalid_apdu_in_this_state( + BACNET_ADDRESS *src, BACNET_CONFIRMED_SERVICE_DATA *service_data); + +BACNET_STACK_EXPORT +void tsm_abort_pdu_send( + uint8_t invoke_id, BACNET_ADDRESS *dest, uint8_t reason, bool server); + +BACNET_STACK_EXPORT +void tsm_free_invoke_id_segmentation(BACNET_ADDRESS *src, uint8_t invoke_id); + +#endif #ifdef __cplusplus } #endif /* __cplusplus */ diff --git a/src/bacnet/config.h b/src/bacnet/config.h index 69c72538..27cbcd47 100644 --- a/src/bacnet/config.h +++ b/src/bacnet/config.h @@ -164,6 +164,11 @@ #endif #endif +/* Enable or disable segmentation support in the library */ +#ifndef BACNET_SEGMENTATION_ENABLED +#define BACNET_SEGMENTATION_ENABLED 0 +#endif + /* for confirmed messages, this is the number of transactions */ /* that we hold in a queue waiting for timeout. */ /* Configure to zero if you don't want any confirmed messages */ diff --git a/src/bacnet/segmentack.c b/src/bacnet/segmentack.c new file mode 100644 index 00000000..e768d8ec --- /dev/null +++ b/src/bacnet/segmentack.c @@ -0,0 +1,104 @@ +/*####COPYRIGHTBEGIN#### + ------------------------------------------- + Copyright (C) 2010 Julien Bennet + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to: + The Free Software Foundation, Inc. + 59 Temple Place - Suite 330 + Boston, MA 02111-1307, USA. + + As a special exception, if other files instantiate templates or + use macros or inline functions from this file, or you compile + this file and link it with other works to produce a work based + on this file, this file does not by itself cause the resulting + work to be covered by the GNU General Public License. However + the source code for this file must still be made available in + accordance with section (3) of the GNU General Public License. + + This exception does not invalidate any other reasons why a work + based on this file might be covered by the GNU General Public + License. + ------------------------------------------- +####COPYRIGHTEND####*/ +#include "segmentack.h" + +/** Method to encode the segment ack . + * + * @param apdu[in] Pointer to the buffer for encoding. + * @param negativeack[in] Acknowedlegment for the segment. + * @param server[in] Set to True if the acknowledgment is from the server, else + * false. + * @param invoke_id[in] Invoke Id + * @param sequence_number[in] Sequence number of the segment to be acknowledged + * @param actual_window_size[in] Actual window size. + * + * @return Length of encoded data or zero on error. + */ +int segmentack_encode_apdu( + uint8_t *apdu, + bool negativeack, + bool server, + uint8_t invoke_id, + uint8_t sequence_number, + uint8_t actual_window_size) +{ + int apdu_len = 0; /* total length of the apdu, return value */ + uint8_t server_code = server ? 0x01 : 0x00; + uint8_t nak_code = negativeack ? 0x02 : 0x00; + + if (apdu) { + apdu[0] = PDU_TYPE_SEGMENT_ACK | server_code | nak_code; + apdu[1] = invoke_id; + apdu[2] = sequence_number; + apdu[3] = actual_window_size; + apdu_len = 4; + } + + return apdu_len; +} + +/** Method to decode the segment ack service request + * + * @param apdu[in] The apdu portion of the ACK reply. + * @param apdu_len[in] The total length of the apdu. + * @param invoke_id[in] Invoke Id of the request. + * @param sequence_number[in] Sequence number of the segment received. + * @param actual_window_size[in] Actual window size. + * + * @return Length of decoded data or zero on error. + */ +int segmentack_decode_service_request( + uint8_t *apdu, + unsigned apdu_len, + uint8_t *invoke_id, + uint8_t *sequence_number, + uint8_t *actual_window_size) +{ + int len = 0; + int apdu_header_size = 3; + + if (apdu_len >= apdu_header_size) { + if (invoke_id) { + *invoke_id = apdu[0]; + } + if (sequence_number) { + *sequence_number = apdu[1]; + } + if (actual_window_size) { + *actual_window_size = apdu[2]; + } + } + + return len; +} diff --git a/src/bacnet/segmentack.h b/src/bacnet/segmentack.h new file mode 100644 index 00000000..20eaf25a --- /dev/null +++ b/src/bacnet/segmentack.h @@ -0,0 +1,68 @@ +/*####COPYRIGHTBEGIN#### + ------------------------------------------- + Copyright (C) 2010 Julien Bennet + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to: + The Free Software Foundation, Inc. + 59 Temple Place - Suite 330 + Boston, MA 02111-1307, USA. + + As a special exception, if other files instantiate templates or + use macros or inline functions from this file, or you compile + this file and link it with other works to produce a work based + on this file, this file does not by itself cause the resulting + work to be covered by the GNU General Public License. However + the source code for this file must still be made available in + accordance with section (3) of the GNU General Public License. + + This exception does not invalidate any other reasons why a work + based on this file might be covered by the GNU General Public + License. + ------------------------------------------- +####COPYRIGHTEND####*/ +#ifndef BACNET_SEGMENT_ACK_H +#define BACNET_SEGMENT_ACK_H + +#include +#include + +#include "bacenum.h" +#include "bacdcode.h" +#include "bacdef.h" + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +BACNET_STACK_EXPORT +int segmentack_encode_apdu( + uint8_t *apdu, + bool negativeack, + bool server, + uint8_t invoke_id, + uint8_t sequence_number, + uint8_t actual_window_size); + +BACNET_STACK_EXPORT +int segmentack_decode_service_request( + uint8_t *apdu, + unsigned apdu_len, + uint8_t *invoke_id, + uint8_t *sequence_number, + uint8_t *actual_window_size); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ +#endif /* SEGMENT_ACK_H */