diff --git a/devices/clients/StatisticsPacket.py b/devices/clients/StatisticsPacket.py
deleted file mode 100644
index 3b656f4873152027fb579f470ade5a0904a05cda..0000000000000000000000000000000000000000
--- a/devices/clients/StatisticsPacket.py
+++ /dev/null
@@ -1,323 +0,0 @@
-import struct
-from datetime import datetime, timezone
-from typing import Tuple
-import numpy
-
-__all__ = ["StatisticsPacket", "SSTPacket", "XSTPacket", "BSTPacket"]
-
-def get_bit_value(value: bytes, first_bit: int, last_bit:int=None) -> int:
-    """ Return bits [first_bit:last_bit] from value, and return their integer value. Bit 0 = LSB.
-
-        For example, extracting bits 2-3 from b'01100' returns 11 binary = 3 decimal:
-            get_bit_value(b'01100', 2, 3) == 3
-    
-        If 'last_bit' is not given, just the value of bit 'first_bit' is returned. """
-
-    # default last_bit to first_bit
-    if last_bit is None:
-        last_bit = first_bit
-
-    return value >> first_bit & ((1 << (last_bit - first_bit + 1)) - 1)
-
-class StatisticsPacket(object):
-    """
-       Models a statistics UDP packet from SDP.
-
-       Packets are expected to be UDP payload only (so no Ethernet/IP/UDP headers).
-
-       The following fields are exposed as properties & functions. The _raw fields come directly
-       from the packet, and have more user-friendly alternatives for intepretation:
-
-       marker_raw                 packet marker as byte.
-       marker()                   packet marker as character. 'S' = SST, 'X' = XST, 'B' = BST
-
-       version_id                 packet format version.
-       observation_id             observation identifier.
-       station_id                 station identifier.
-
-       source_info:               bit field with input information, encoding several other properties.
-       antenna_band_index:        antenna type. 0 = low band, 1 = high band.
-       nyquist_zone_index:        nyquist zone of filter:
-                                     0 =           0 -- 1/2 * t_adc Hz (low band),
-                                     1 = 1/2 * t_adc -- t_adc Hz       (high band),
-                                     2 =       t_adc -- 3/2 * t_adc Hz (high band).
-       t_adc:                     sampling clock. 0 = 160 MHz, 1 = 200 MHz.
-       fsub_type:                 sampling method. 0 = critically sampled, 1 = oversampled.
-       payload_error:             0 = data is ok, 1 = data is corrupted (a fault was encountered).
-       beam_repositioning_flag:   0 = data is ok, 1 = beam got repositioned during packet construction (BST only).
-       subband_calibrated_flag:   1 = subband data had subband calibration values applied, 0 = not.
-       gn_index:                  global index of FPGA that emitted this packet.
-
-       data_id:                   bit field with payload information, encoding several other properties.
-
-       nof_signal_inputs:         number of inputs that contributed to data in this packet.
-       nof_bytes_per_statistics:  word size of each statistic.
-       nof_statistics_per_packet: number of statistic data points in the payload.
-
-       integration_interval_raw:  integration interval, in block periods.
-       integration_interval():    integration interval, in seconds.
-       block_period_raw:          block period, in nanoseconds.
-       block_period():            block period, in seconds.
-       block_serial_number:       timestamp of the data, in block periods since 1970.
-       timestamp():               timestamp of the data, as a datetime object.
-        
-    """
-
-    def __init__(self, packet: bytes):
-        self.packet = packet
-
-        self.unpack()
-
-        # Only parse valid statistics packets from SDP, reject everything else
-        if self.marker_raw not in b'SBX':
-            raise ValueError("Invalid SDP statistics packet: packet marker (first byte) is {}, not one of 'SBX'.".format(self.marker))
-
-    def unpack(self):
-        """ Unpack the packet into properties of this object. """
-
-        # format string for the header, see unpack below
-        self.header_format = ">cBL HHB BHL BBH HQ"
-        self.header_size = struct.calcsize(self.header_format)
-
-        # unpack fields
-        try:
-            (self.marker_raw,
-            self.version_id,
-            self.observation_id,
-            self.station_id,
-            self.source_info,
-            # reserved byte
-            _,
-            # integration interval, in block periods. This field is 3 bytes, big endian -- combine later
-            integration_interval_hi,
-            integration_interval_lo,
-            self.data_id,
-            self.nof_signal_inputs,
-            self.nof_bytes_per_statistic,
-            self.nof_statistics_per_packet,
-            self.block_period_raw,
-            self.block_serial_number) = struct.unpack(self.header_format, self.packet[:self.header_size])
-
-            self.integration_interval_raw = (integration_interval_hi << 16) + integration_interval_lo
-        except struct.error as e:
-            raise ValueError("Error parsing statistics packet") from e
-
-        # unpack the fields we just updated
-        self.unpack_source_info()
-        self.unpack_data_id()
-
-    def unpack_source_info(self):
-        """ Unpack the source_info field into properties of this object. """
-
-        self.antenna_band_index      = get_bit_value(self.source_info, 15)
-        self.nyquist_zone_index      = get_bit_value(self.source_info, 13, 14)
-        self.t_adc                   = get_bit_value(self.source_info, 12)
-        self.fsub_type               = get_bit_value(self.source_info, 11)
-        self.payload_error           = get_bit_value(self.source_info, 10)
-        self.beam_repositioning_flag = get_bit_value(self.source_info, 9)
-        self.subband_calibrated_flag = get_bit_value(self.source_info, 8)
-        # self.source_info 5-7 are reserved
-        self.gn_index                = get_bit_value(self.source_info, 0, 4)
-
-    def unpack_data_id(self):
-        """ Unpack the data_id field into properties of this object. """
-
-        # only useful in specialisations (XST/SST/BST)
-        pass
-
-    def expected_size(self) -> int:
-        """ The size this packet should be (header + payload), according to the header. """
-
-        return self.header_size + self.nof_statistics_per_packet * self.nof_bytes_per_statistic
-
-    @property
-    def marker(self) -> str:
-        """ Return the type of statistic:
-        
-            'S' = SST
-            'B' = BST
-            'X' = XST
-        """
-
-        try:
-            return self.marker_raw.decode('ascii')
-        except UnicodeDecodeError:
-            # non-ascii (>127) character, return as binary
-            #
-            # this is typically not visible to the user, as these packets are not SDP statistics packets,
-            # which the constructor will refuse to accept.
-            return self.marker_raw
-
-    def integration_interval(self) -> float:
-        """ Returns the integration interval, in seconds. """
-
-        # Translate to seconds using the block period
-        return self.integration_interval_raw * self.block_period()
-
-    def block_period(self) -> float:
-        """ Return the block period, in seconds. """
-
-        return self.block_period_raw / 1e9
-
-    def timestamp(self) -> datetime:
-        """ Returns the timestamp of the data in this packet. 
-        
-            Returns datetime.min if the block_serial_number in the packet is not set (0),
-            Returns datetime.max if the timestamp cannot be represented in python (likely because it is too large). """
-
-        try:
-            return datetime.fromtimestamp(self.block_serial_number * self.block_period(), timezone.utc)
-        except ValueError:
-            # Let's not barf anytime we want to print a header
-            return datetime.max
-
-    def header(self) -> dict:
-        """ Return all the header fields as a dict. """
-
-        header = {
-          "marker": self.marker,
-          "version_id": self.version_id,
-          "observation_id": self.observation_id,
-          "station_id": self.station_id,
-          "source_info": {
-              "_raw": self.source_info,
-              "antenna_band_index": self.antenna_band_index,
-              "nyquist_zone_index": self.nyquist_zone_index,
-              "t_adc": self.t_adc,
-              "fsub_type": self.fsub_type,
-              "payload_error": self.payload_error,
-              "beam_repositioning_flag": self.beam_repositioning_flag,
-              "subband_calibrated_flag": self.subband_calibrated_flag,
-              "gn_index": self.gn_index,
-          },
-          "data_id": {
-              "_raw": self.data_id,
-          },
-          "integration_interval_raw": self.integration_interval_raw,
-          "integration_interval": self.integration_interval(),
-          "nof_signal_inputs": self.nof_signal_inputs,
-          "nof_bytes_per_statistic": self.nof_bytes_per_statistic,
-          "nof_statistics_per_packet": self.nof_statistics_per_packet,
-          "block_period_raw": self.block_period_raw,
-          "block_period": self.block_period(),
-          "block_serial_number": self.block_serial_number,
-          "timestamp": self.timestamp(),
-        }
-
-        return header
-
-class SSTPacket(StatisticsPacket):
-    """
-       Models an SST statistics UDP packet from SDP.
-
-       The following fields are exposed as properties & functions.
-
-
-       signal_input_index:                 input (antenna polarisation) index for which this packet contains statistics
-
-       payload[nof_statistics_per_packet]: SST statistics, an array of amplitudes per subband.
-    """
-
-    def __init__(self, packet):
-        super().__init__(packet)
-
-        # We only parse SST packets
-        if self.marker != 'S':
-            raise Exception("Payload of SST requested of a non-SST packet. Actual packet marker is '{}', but must be 'S'.".format(self.marker))
-
-    def unpack_data_id(self):
-        super().unpack_data_id()
-
-        self.signal_input_index = get_bit_value(self.data_id, 0, 7)
-
-    def header(self):
-        header = super().header()
-
-        header["data_id"]["signal_input_index"] = self.signal_input_index
-
-        return header
-
-    @property
-    def payload(self) -> numpy.array:
-        """ The payload of this packet, interpreted as SST data. """
-
-        # derive which and how many elements to read from the packet header
-        bytecount_to_unsigned_struct_type = { 1: 'B', 2: 'H', 4: 'I', 8: 'Q' }
-        format_str = ">{}{}".format(self.nof_statistics_per_packet, bytecount_to_unsigned_struct_type[self.nof_bytes_per_statistic])
-
-        return numpy.array(struct.unpack(format_str, self.packet[self.header_size:self.header_size + struct.calcsize(format_str)]))
-
-class XSTPacket(StatisticsPacket):
-    """
-       Models an XST statistics UDP packet from SDP.
-
-       The following fields are exposed as properties & functions.
-
-
-       subband_index:                      subband number for which this packet contains statistics.
-       baseline:                           antenna pair for which this packet contains statistics.
-    """
-
-    def __init__(self, packet):
-        super().__init__(packet)
-
-        # We only parse XST packets
-        if self.marker != 'X':
-            raise Exception("Payload of XST requested of a non-XST packet. Actual packet marker is '{}', but must be 'X'.".format(self.marker))
-
-    def unpack_data_id(self):
-        super().unpack_data_id()
-
-        self.subband_index = get_bit_value(self.data_id, 16, 24)
-        self.baseline = (get_bit_value(self.data_id, 8, 15), get_bit_value(self.data_id, 0, 7))
-
-    def header(self):
-        header = super().header()
-
-        header["data_id"]["subband_index"] = self.subband_index
-        header["data_id"]["baseline"]      = self.baseline
-
-        return header
-
-class BSTPacket(StatisticsPacket):
-    """
-       Models an BST statistics UDP packet from SDP.
-
-       The following fields are exposed as properties & functions.
-
-       beamlet_index:                     the number of the beamlet for which this packet holds statistics.
-    """
-
-    def __init__(self, packet):
-        super().__init__(packet)
-
-        # We only parse BST packets
-        if self.marker != 'B':
-            raise Exception("Payload of BST requested of a non-BST packet. Actual packet marker is '{}', but must be 'B'.".format(self.marker))
-
-    def unpack_data_id(self):
-        super().unpack_data_id()
-
-        self.beamlet_index = get_bit_value(self.data_id, 0, 15)
-
-    def header(self):
-        header = super().header()
-
-        header["data_id"]["beamlet_index"] = self.beamlet_index
-
-        return header
-
-if __name__ == "__main__":
-    # parse one packet from stdin
-    import sys
-    import pprint
-
-    # read all of stdin, even though we only parse the first packet. we're too lazy to intelligently decide when
-    # the packet is complete and can stop reading.
-    data = sys.stdin.buffer.read()
-    packet = SSTPacket(data)
-
-    # print header & payload
-    pprint.pprint(packet.header())
-    pprint.pprint(packet.payload)
-
diff --git a/devices/devices/__init__.py b/devices/devices/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/devices/devices/sdp_statistics/__init__.py b/devices/devices/sdp_statistics/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/devices/devices/sdp_statistics/statistics_packet.py b/devices/devices/sdp_statistics/statistics_packet.py
index 37565935da2d6f10cd5898b6a3cc46f7dafcc259..8bc4e1e4609ba0b33e652a20f29fd8d8d1abe6c6 100644
--- a/devices/devices/sdp_statistics/statistics_packet.py
+++ b/devices/devices/sdp_statistics/statistics_packet.py
@@ -1,8 +1,9 @@
-from struct import unpack, calcsize
+import struct
 from datetime import datetime, timezone
 import numpy
 
-__all__ = ["StatisticsPacket"]
+__all__ = ["StatisticsPacket", "SSTPacket", "XSTPacket", "BSTPacket"]
+
 
 def get_bit_value(value: bytes, first_bit: int, last_bit:int=None) -> int:
     """ Return bits [first_bit:last_bit] from value, and return their integer value. Bit 0 = LSB.
@@ -18,19 +19,117 @@ def get_bit_value(value: bytes, first_bit: int, last_bit:int=None) -> int:
 
     return value >> first_bit & ((1 << (last_bit - first_bit + 1)) - 1)
 
+
 class StatisticsPacket(object):
     """
        Models a statistics UDP packet from SDP.
 
        Packets are expected to be UDP payload only (so no Ethernet/IP/UDP headers).
+
+       The following fields are exposed as properties & functions. The _raw fields come directly
+       from the packet, and have more user-friendly alternatives for intepretation:
+
+       marker_raw                 packet marker as byte.
+       marker()                   packet marker as character. 'S' = SST, 'X' = XST, 'B' = BST
+
+       version_id                 packet format version.
+       observation_id             observation identifier.
+       station_id                 station identifier.
+
+       source_info:               bit field with input information, encoding several other properties.
+       antenna_band_index:        antenna type. 0 = low band, 1 = high band.
+       nyquist_zone_index:        nyquist zone of filter:
+                                     0 =           0 -- 1/2 * t_adc Hz (low band),
+                                     1 = 1/2 * t_adc -- t_adc Hz       (high band),
+                                     2 =       t_adc -- 3/2 * t_adc Hz (high band).
+       t_adc:                     sampling clock. 0 = 160 MHz, 1 = 200 MHz.
+       fsub_type:                 sampling method. 0 = critically sampled, 1 = oversampled.
+       payload_error:             0 = data is ok, 1 = data is corrupted (a fault was encountered).
+       beam_repositioning_flag:   0 = data is ok, 1 = beam got repositioned during packet construction (BST only).
+       subband_calibrated_flag:   1 = subband data had subband calibration values applied, 0 = not.
+       gn_index:                  global index of FPGA that emitted this packet.
+
+       data_id:                   bit field with payload information, encoding several other properties.
+
+       nof_signal_inputs:         number of inputs that contributed to data in this packet.
+       nof_bytes_per_statistics:  word size of each statistic.
+       nof_statistics_per_packet: number of statistic data points in the payload.
+
+       integration_interval_raw:  integration interval, in block periods.
+       integration_interval():    integration interval, in seconds.
+       block_period_raw:          block period, in nanoseconds.
+       block_period():            block period, in seconds.
+       block_serial_number:       timestamp of the data, in block periods since 1970.
+       timestamp():               timestamp of the data, as a datetime object.
+        
     """
 
     def __init__(self, packet: bytes):
         self.packet = packet
 
-        # Only parse valid packets
-        if self.marker not in 'SBX':
-            raise ValueError("Invalid SDP statistics packet: packet marker (first byte) is '{}', not one of 'SBX'.".format(self.marker))
+        self.unpack()
+
+        # Only parse valid statistics packets from SDP, reject everything else
+        if self.marker_raw not in b'SBX':
+            raise ValueError("Invalid SDP statistics packet: packet marker (first byte) is {}, not one of 'SBX'.".format(self.marker))
+
+    def unpack(self):
+        """ Unpack the packet into properties of this object. """
+
+        # format string for the header, see unpack below
+        self.header_format = ">cBL HHB BHL BBH HQ"
+        self.header_size = struct.calcsize(self.header_format)
+
+        # unpack fields
+        try:
+            (self.marker_raw,
+            self.version_id,
+            self.observation_id,
+            self.station_id,
+            self.source_info,
+            # reserved byte
+            _,
+            # integration interval, in block periods. This field is 3 bytes, big endian -- combine later
+            integration_interval_hi,
+            integration_interval_lo,
+            self.data_id,
+            self.nof_signal_inputs,
+            self.nof_bytes_per_statistic,
+            self.nof_statistics_per_packet,
+            self.block_period_raw,
+            self.block_serial_number) = struct.unpack(self.header_format, self.packet[:self.header_size])
+
+            self.integration_interval_raw = (integration_interval_hi << 16) + integration_interval_lo
+        except struct.error as e:
+            raise ValueError("Error parsing statistics packet") from e
+
+        # unpack the fields we just updated
+        self.unpack_source_info()
+        self.unpack_data_id()
+
+    def unpack_source_info(self):
+        """ Unpack the source_info field into properties of this object. """
+
+        self.antenna_band_index      = get_bit_value(self.source_info, 15)
+        self.nyquist_zone_index      = get_bit_value(self.source_info, 13, 14)
+        self.t_adc                   = get_bit_value(self.source_info, 12)
+        self.fsub_type               = get_bit_value(self.source_info, 11)
+        self.payload_error           = get_bit_value(self.source_info, 10)
+        self.beam_repositioning_flag = get_bit_value(self.source_info, 9)
+        self.subband_calibrated_flag = get_bit_value(self.source_info, 8)
+        # self.source_info 5-7 are reserved
+        self.gn_index                = get_bit_value(self.source_info, 0, 4)
+
+    def unpack_data_id(self):
+        """ Unpack the data_id field into properties of this object. """
+
+        # only useful in specialisations (XST/SST/BST)
+        pass
+
+    def expected_size(self) -> int:
+        """ The size this packet should be (header + payload), according to the header. """
+
+        return self.header_size + self.nof_statistics_per_packet * self.nof_bytes_per_statistic
 
     @property
     def marker(self) -> str:
@@ -41,80 +140,14 @@ class StatisticsPacket(object):
             'X' = XST
         """
 
-        raw_marker = unpack("c",self.packet[0:1])[0]
-
         try:
-            return raw_marker.decode('ascii')
+            return self.marker_raw.decode('ascii')
         except UnicodeDecodeError:
             # non-ascii (>127) character, return as binary
             #
             # this is typically not visible to the user, as these packets are not SDP statistics packets,
             # which the constructor will refuse to accept.
-            return raw_marker
-
-    @property
-    def version_id(self) -> int:
-        """ Return the version of this packet. """
-
-        return unpack("B",self.packet[1:2])[0]
-
-    @property
-    def observation_id(self) -> int:
-        """ Return the ID of the observation running when this packet was generated. """
-
-        return unpack("<I",self.packet[2:6])[0]
-
-    @property
-    def station_id(self) -> int:
-        """ Return the number of the station this packet was generated on. """
-
-        return unpack("<H",self.packet[6:8])[0]
-
-    @property
-    def source_info(self) -> int:
-        """ Return a dict with the source_info flags. The dict contains the following fields:
-        
-            _raw:                    raw value of the source_info field in the packet, as an integer.
-            antenna_band_index:      antenna type. 0 = low band, 1 = high band.
-            nyquist_zone_index:      nyquist zone of filter:
-                                         0 =           0 -- 1/2 * t_adc Hz (low band),
-                                         1 = 1/2 * t_adc -- t_adc Hz       (high band),
-                                         2 =       t_adc -- 3/2 * t_adc Hz (high band).
-            t_adc:                   sampling clock. 0 = 160 MHz, 1 = 200 MHz.
-            fsub_type:               sampling method. 0 = critically sampled, 1 = oversampled.
-            payload_error:           0 = data is ok, 1 = data is corrupted (a fault was encountered).
-            beam_repositioning_flag: 0 = data is ok, 1 = beam got repositioned during packet construction (BST only).
-            subband_calibrated_flag: 1 = subband data had subband calibration values applied, 0 = not
-            reserved:                reserved bits
-            gn_index:                global index of FPGA that emitted this packet. """
-
-        bits = unpack("<H",self.packet[8:10])[0]
-
-        return {
-          "_raw": bits,
-          "antenna_band_index": get_bit_value(bits, 15),
-          "nyquist_zone_index": get_bit_value(bits, 13, 14),
-          "t_adc": get_bit_value(bits, 12),
-          "fsub_type": get_bit_value(bits, 11),
-          "payload_error": get_bit_value(bits, 10),
-          "beam_repositioning_flag": get_bit_value(bits, 9),
-          "subband_calibrated_flag": get_bit_value(bits, 8),
-          "reserved": get_bit_value(bits, 5, 7),
-          "gn_index": get_bit_value(bits, 0, 4),
-        }
-
-    @property
-    def reserved(self) -> bytes:
-        """ Reserved bytes. """
-
-        return self.packet[10:11]
-
-    @property
-    def integration_interval_raw(self) -> int:
-        """ Returns the integration interval, in blocks. """
-
-        # This field is 3 bytes, little endian, so we need to append a 0 to parse it as a 32-bit integer.
-        return unpack("<I", self.packet[11:14] + b'0')[0]
+            return self.marker_raw
 
     def integration_interval(self) -> float:
         """ Returns the integration interval, in seconds. """
@@ -122,64 +155,47 @@ class StatisticsPacket(object):
         # Translate to seconds using the block period
         return self.integration_interval_raw * self.block_period()
 
-    @property
-    def data_id(self) -> int:
-        """ Returns the generic data identifier. """
-
-        return unpack("<I",self.packet[14:18])[0]
-
-    @property
-    def nof_signal_inputs(self) -> int:
-        """ Number of inputs that were used for constructing the payload. """
-        return unpack("<B",self.packet[18:19])[0]
-
-    @property
-    def nof_bytes_per_statistic(self) -> int:
-        """ Word size for the payload. """
-
-        return unpack("<B",self.packet[19:20])[0]
-
-    @property
-    def nof_statistics_per_packet(self) -> int:
-        """ Number of data points in the payload. """
-
-        return unpack("<H",self.packet[20:22])[0]
-
-    @property
-    def block_period_raw(self) -> int:
-        """ Return the block period, in nanoseconds. """
-
-        return unpack("<H",self.packet[22:24])[0]
-
     def block_period(self) -> float:
         """ Return the block period, in seconds. """
 
         return self.block_period_raw / 1e9
 
-    @property
-    def block_serial_number(self) -> int:
-        """ Block index since epoch (1970). """
-
-        return unpack("<Q",self.packet[24:32])[0]
-
     def timestamp(self) -> datetime:
-        """ Returns the timestamp of the data in this packet. """
+        """ Returns the timestamp of the data in this packet. 
+        
+            Returns datetime.min if the block_serial_number in the packet is not set (0),
+            Returns datetime.max if the timestamp cannot be represented in python (likely because it is too large). """
 
-        return datetime.fromtimestamp(self.block_serial_number * self.block_period(), timezone.utc)
+        try:
+            return datetime.fromtimestamp(self.block_serial_number * self.block_period(), timezone.utc)
+        except ValueError:
+            # Let's not barf anytime we want to print a header
+            return datetime.max
 
     def header(self) -> dict:
         """ Return all the header fields as a dict. """
 
-        return {
+        header = {
           "marker": self.marker,
           "version_id": self.version_id,
           "observation_id": self.observation_id,
           "station_id": self.station_id,
-          "source_info": self.source_info,
-          "reserved": self.reserved,
+          "source_info": {
+              "_raw": self.source_info,
+              "antenna_band_index": self.antenna_band_index,
+              "nyquist_zone_index": self.nyquist_zone_index,
+              "t_adc": self.t_adc,
+              "fsub_type": self.fsub_type,
+              "payload_error": self.payload_error,
+              "beam_repositioning_flag": self.beam_repositioning_flag,
+              "subband_calibrated_flag": self.subband_calibrated_flag,
+              "gn_index": self.gn_index,
+          },
+          "data_id": {
+              "_raw": self.data_id,
+          },
           "integration_interval_raw": self.integration_interval_raw,
           "integration_interval": self.integration_interval(),
-          "data_id": self.data_id,
           "nof_signal_inputs": self.nof_signal_inputs,
           "nof_bytes_per_statistic": self.nof_bytes_per_statistic,
           "nof_statistics_per_packet": self.nof_statistics_per_packet,
@@ -189,19 +205,108 @@ class StatisticsPacket(object):
           "timestamp": self.timestamp(),
         }
 
-    @property
-    def payload_sst(self) -> numpy.array:
-        """ The payload of this packet, interpreted as SST data. """
+        return header
+
+class SSTPacket(StatisticsPacket):
+    """
+       Models an SST statistics UDP packet from SDP.
+
+       The following fields are exposed as properties & functions.
+
+
+       signal_input_index:                 input (antenna polarisation) index for which this packet contains statistics
+
+       payload[nof_statistics_per_packet]: SST statistics, an array of amplitudes per subband.
+    """
 
+    def __init__(self, packet):
+        super().__init__(packet)
+
+        # We only parse SST packets
         if self.marker != 'S':
             raise Exception("Payload of SST requested of a non-SST packet. Actual packet marker is '{}', but must be 'S'.".format(self.marker))
 
+    def unpack_data_id(self):
+        super().unpack_data_id()
+
+        self.signal_input_index = get_bit_value(self.data_id, 0, 7)
+
+    def header(self):
+        header = super().header()
+
+        header["data_id"]["signal_input_index"] = self.signal_input_index
+
+        return header
+
+    @property
+    def payload(self) -> numpy.array:
+        """ The payload of this packet, interpreted as SST data. """
+
         # derive which and how many elements to read from the packet header
         bytecount_to_unsigned_struct_type = { 1: 'B', 2: 'H', 4: 'I', 8: 'Q' }
-        format_str = "<{}{}".format(self.nof_statistics_per_packet, bytecount_to_unsigned_struct_type[self.nof_bytes_per_statistic])
+        format_str = ">{}{}".format(self.nof_statistics_per_packet, bytecount_to_unsigned_struct_type[self.nof_bytes_per_statistic])
+
+        return numpy.array(struct.unpack(format_str, self.packet[self.header_size:self.header_size + struct.calcsize(format_str)]))
+
+class XSTPacket(StatisticsPacket):
+    """
+       Models an XST statistics UDP packet from SDP.
+
+       The following fields are exposed as properties & functions.
+
+
+       subband_index:                      subband number for which this packet contains statistics.
+       baseline:                           antenna pair for which this packet contains statistics.
+    """
+
+    def __init__(self, packet):
+        super().__init__(packet)
+
+        # We only parse XST packets
+        if self.marker != 'X':
+            raise Exception("Payload of XST requested of a non-XST packet. Actual packet marker is '{}', but must be 'X'.".format(self.marker))
+
+    def unpack_data_id(self):
+        super().unpack_data_id()
+
+        self.subband_index = get_bit_value(self.data_id, 16, 24)
+        self.baseline = (get_bit_value(self.data_id, 8, 15), get_bit_value(self.data_id, 0, 7))
+
+    def header(self):
+        header = super().header()
+
+        header["data_id"]["subband_index"] = self.subband_index
+        header["data_id"]["baseline"]      = self.baseline
+
+        return header
+
+class BSTPacket(StatisticsPacket):
+    """
+       Models an BST statistics UDP packet from SDP.
+
+       The following fields are exposed as properties & functions.
+
+       beamlet_index:                     the number of the beamlet for which this packet holds statistics.
+    """
+
+    def __init__(self, packet):
+        super().__init__(packet)
+
+        # We only parse BST packets
+        if self.marker != 'B':
+            raise Exception("Payload of BST requested of a non-BST packet. Actual packet marker is '{}', but must be 'B'.".format(self.marker))
+
+    def unpack_data_id(self):
+        super().unpack_data_id()
+
+        self.beamlet_index = get_bit_value(self.data_id, 0, 15)
+
+    def header(self):
+        header = super().header()
 
-        return numpy.array(unpack(format_str, self.packet[32:32+calcsize(format_str)]))
+        header["data_id"]["beamlet_index"] = self.beamlet_index
 
+        return header
 
 if __name__ == "__main__":
     # parse one packet from stdin
@@ -211,8 +316,8 @@ if __name__ == "__main__":
     # read all of stdin, even though we only parse the first packet. we're too lazy to intelligently decide when
     # the packet is complete and can stop reading.
     data = sys.stdin.buffer.read()
-    packet = StatisticsPacket(data)
+    packet = SSTPacket(data)
 
     # print header & payload
     pprint.pprint(packet.header())
-    pprint.pprint(packet.payload_sst)
+    pprint.pprint(packet.payload)