Segmentation

Automatic segmented message assembly and transmission per Clause 5.2. Messages exceeding the maximum APDU size are transparently split into segments and reassembled. This works in both directions – sending segmented requests and receiving segmented responses.

APDU segmentation and reassembly per ASHRAE 135-2016 Clause 5.2/5.4.

Segmentation Manager

APDU segmentation and reassembly logic per ASHRAE 135-2016 Clause 5.2/5.4.

This module contains pure segmentation logic with no I/O dependencies. TSMs drive instances of SegmentSender/SegmentReceiver via method calls.

exception bac_py.segmentation.manager.SegmentationError(abort_reason, message='')[source]

Bases: Exception

Raised when segmentation fails (e.g., APDU too long for peer).

Parameters:
Return type:

None

class bac_py.segmentation.manager.SegmentAction(*values)[source]

Bases: Enum

Action the receiver should take after processing a segment.

CONTINUE = 'continue'
SEND_ACK = 'send_ack'
RESEND_LAST_ACK = 'resend'
COMPLETE = 'complete'
ABORT = 'abort'
bac_py.segmentation.manager.in_window(seq_a, seq_b, actual_window_size)[source]

Determine if segment seq_a is within the window starting at seq_b.

Per Clause 5.4: (seqA - seqB) mod 256 < ActualWindowSize.

Return type:

bool

Parameters:
  • seq_a (int)

  • seq_b (int)

  • actual_window_size (int)

bac_py.segmentation.manager.duplicate_in_window(seq_a, seq_b, actual_window_size, proposed_window_size)[source]

Determine if segment seq_a is a duplicate the receiver has already seen.

Per Clause 5.4: Wm < (seqA - seqB) mod 256 <= 255 where Wm = max(ActualWindowSize, ProposedWindowSize).

Return type:

bool

Parameters:
  • seq_a (int)

  • seq_b (int)

  • actual_window_size (int)

  • proposed_window_size (int)

bac_py.segmentation.manager.compute_max_segment_payload(max_apdu_length, pdu_type)[source]

Return the maximum service data bytes that fit in one segment.

Parameters:
  • max_apdu_length (int) – Max APDU size for the link (e.g. 480, 1476).

  • pdu_type (Literal['confirmed_request', 'complex_ack']) – Which PDU type determines the header overhead.

Return type:

int

Returns:

Max payload bytes per segment.

bac_py.segmentation.manager.split_payload(payload, max_segment_size)[source]

Split a byte payload into segments of at most max_segment_size bytes.

Parameters:
  • payload (bytes) – The raw service data to split.

  • max_segment_size (int) – Maximum bytes per segment.

Return type:

list[bytes]

Returns:

List of byte segments. At least one segment is always returned (which may be empty if the payload is empty).

Raises:

ValueError – If max_segment_size is not positive.

bac_py.segmentation.manager.check_segment_count(num_segments, max_segments)[source]

Check that the segment count does not exceed the peer’s limit.

Parameters:
  • num_segments (int) – Total number of segments.

  • max_segments (int | None) – Peer’s max-segments-accepted (None = unlimited).

Return type:

bool

Returns:

True if within limits.

class bac_py.segmentation.manager.SegmentSender(segments, invoke_id, service_choice, proposed_window_size, actual_window_size, _window_start_idx=0)[source]

Bases: object

Manages the send side of a segmented transaction.

Tracks segments by absolute 0-based index and converts to 8-bit sequence numbers (index & 0xFF) for the wire protocol.

Parameters:
  • segments (list[bytes])

  • invoke_id (int)

  • service_choice (int)

  • proposed_window_size (int)

  • actual_window_size (int)

  • _window_start_idx (int)

segments: list[bytes]
invoke_id: int
service_choice: int
proposed_window_size: int
actual_window_size: int
classmethod create(payload, invoke_id, service_choice, max_apdu_length, pdu_type, proposed_window_size=16, peer_max_segments=None)[source]

Create a SegmentSender by splitting the payload.

Raises:

SegmentationError – If the segment count exceeds peer_max_segments.

Return type:

SegmentSender

Parameters:
  • payload (bytes)

  • invoke_id (int)

  • service_choice (int)

  • max_apdu_length (int)

  • pdu_type (Literal['confirmed_request', 'complex_ack'])

  • proposed_window_size (int)

  • peer_max_segments (int | None)

fill_window()[source]

Return segments for the current window.

Return type:

list[tuple[int, bytes, bool]]

Returns:

List of (sequence_number, segment_data, more_follows) tuples.

handle_segment_ack(ack_seq, actual_window_size, negative)[source]

Process a SegmentACK.

Parameters:
  • ack_seq (int) – The sequence number in the SegmentACK.

  • actual_window_size (int) – The receiver’s advertised window size.

  • negative (bool) – Whether this is a negative ACK (retransmit request).

Return type:

bool

Returns:

True if all segments have been acknowledged.

property is_complete: bool

True when all segments have been acknowledged.

property total_segments: int

Total number of segments.

class bac_py.segmentation.manager.SegmentReceiver(_segments=<factory>, _expected_idx=0, actual_window_size=16, proposed_window_size=1, service_choice=0, _final_idx=None, _last_ack_seq=0, _window_start_idx=0, _total_bytes=0, created_at=<factory>)[source]

Bases: object

Manages reassembly of received segments.

Tracks segments by absolute 0-based index.

Parameters:
  • _segments (dict[int, bytes])

  • _expected_idx (int)

  • actual_window_size (int)

  • proposed_window_size (int)

  • service_choice (int)

  • _final_idx (int | None)

  • _last_ack_seq (int)

  • _window_start_idx (int)

  • _total_bytes (int)

  • created_at (float)

actual_window_size: int = 16
proposed_window_size: int = 1
service_choice: int = 0
created_at: float
classmethod create(first_segment_data, service_choice, proposed_window_size, more_follows=True, our_window_size=16)[source]

Create a receiver from the first segment (sequence number 0).

Parameters:
  • first_segment_data (bytes) – Payload of the first segment.

  • service_choice (int) – Service choice from the PDU header.

  • proposed_window_size (int) – Window size proposed by the sender.

  • more_follows (bool (default: True)) – The more-follows flag from the first segment.

  • our_window_size (int (default: 16)) – Our preferred window size.

Return type:

SegmentReceiver

Returns:

New SegmentReceiver with the first segment stored.

receive_segment(seq_num, data, more_follows)[source]

Process a received segment.

Per Clause 5.4, SegmentACKs are sent at window boundaries (when the window is full) or when the transfer is complete, not for every individual segment.

Parameters:
  • seq_num (int) – 8-bit sequence number from the PDU.

  • data (bytes) – Segment payload data.

  • more_follows (bool) – The more-follows flag from the PDU.

Return type:

tuple[SegmentAction, int]

Returns:

(action, ack_sequence_number) indicating what the caller should do next. For ABORT, ack_sequence_number is -1. CONTINUE means the segment was stored but no ACK is needed yet.

property last_ack_seq: int

The sequence number of the last acknowledged segment.

property is_complete: bool

True when all segments have been received.

reassemble()[source]

Concatenate all segments in order.

Raises:

ValueError – If not all segments have been received.

Return type:

bytes