Index: ammosreader/AmmosAudioDataHeader.py
===================================================================
--- ammosreader/AmmosAudioDataHeader.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
+++ ammosreader/AmmosAudioDataHeader.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
@@ -0,0 +1,54 @@
+import struct
+import numpy
+
+class AmmosAudioDemodType():
+
+    @classmethod
+    @property
+    def mapping(cls):
+        return {0: 'FM', 1: 'AM', 5: 'ISB', 6: 'CW',
+                7: 'USB', 8: 'LSB', 256: 'DIGITAL',
+                0xFFFFFFFF: 'UNKNOWN'}
+
+    def __init__(self, demod_type):
+        self.demod_type = demod_type
+
+    def __str__(self):
+        return AmmosAudioDemodType.mapping[self.demod_type]
+
+class AmmosAudioDataHeader():
+
+    @classmethod
+    def from_bytes(cls, bytes):
+        elements = struct.unpack('<IIQIIIII', bytes)
+        sample_rate = elements[0]
+        status = elements[1]
+        frequency = elements[2]
+        demod_bandwidth = elements[3]
+        demod_type = elements[4]
+        sample_count = elements[5]
+        channel_count = elements[6]
+        sample_size = elements[7]
+        return AmmosAudioDataHeader(sample_rate, status, frequency, demod_bandwidth, demod_type,
+                                    sample_count, channel_count, sample_size)
+
+    def __init__(self, sample_rate, status, frequency, demod_bandwidth, demod_type, sample_count, channel_count, sample_size):
+        self.sample_rate = sample_rate
+        self.status = status
+        self.frequency = frequency
+        self.demod_bandwidth = demod_bandwidth
+        self.demod_type = AmmosAudioDemodType(demod_type)
+        self.sample_count = sample_count
+        self.channel_count = channel_count
+        self.sample_size = sample_size
+
+    def __str__(self):
+        return ("\nAmmosAudioDataHeader\n" +
+                "Sample rate:" + str(self.sample_rate) + "\n" +
+                "Status:" + str(self.status) + "\n" +
+                "Frequency:" + str(self.frequency) + "\n" +
+                "Demodulation bandwidth:" + str(self.demod_bandwidth) + "\n" +
+                "Demodulation type:" + str(self.demod_type) + "\n" +
+                "Sample count:" + str(self.sample_count) + "\n" +
+                "Channel count:" + str(self.channel_count) + "\n" +
+                "Sample size:" + str(self.sample_size) + "\n")
Index: ammosreader/AmmosAudioReader.py
===================================================================
--- ammosreader/AmmosAudioReader.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
+++ ammosreader/AmmosAudioReader.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
@@ -0,0 +1,200 @@
+"""I parse an R&S AMMOS recording."""
+
+import os
+
+from ammosreader.AmmosGlobalFrameBody import AmmosGlobalFrameBody
+from ammosreader.AmmosAudioDataHeader import AmmosAudioDataHeader
+from ammosreader.AmmosExtendedAudioDataHeader import AmmosExtendedAudioDataHeader
+from ammosreader.AmmosGlobalFrameHeader import AmmosGlobalFrameHeader
+from ammosreader.AmmosSingleFrame import AmmosSingleFrame
+from ammosreader.AmmosContainer import AmmosContainer
+
+
+class AmmosAudioReader():
+    """I read the audio data embedded in an R&S AMMOS recording."""
+
+    GLOBAL_HEADER_SIZE = 24  # 8 words
+    STANDARD_AUDIO_DATA_HEADER_SIZE = 36  # 9 words
+    EXTENDED_AUDIO_DATA_HEADER_SIZE = 44  # 11 words
+
+    def __init__(self, file_name):
+        """
+        I return an instance of AmmosAudioReader initialized with a given file name.
+
+        :param file_name: the file to read from
+        :type file_name: str
+        """
+        self.file_name = file_name
+        self.file = open(self.file_name, "rb")
+        self.file_size = os.path.getsize(self.file_name)
+
+        self.container = AmmosContainer(self.file_name, [])
+
+        self.tags = {}
+
+    def rewind_to_start(self):
+        """I set the file pointer to the beginning of the file for the next operation."""
+        self.file.seek(0)
+
+    def add_tag(self, tag):
+        """
+        I add a tag to my tag list.
+
+        :param tag: The tag to add to my tag list
+        :type tag: dict
+        """
+        self.tags[tag.key] = tag.value
+
+    def read_all_frames_left(self):
+        """
+        I read all remaining frames into my container until end of file is reached.
+
+        :return: a container containing all frames read
+        :rtype: AmmosContainer
+        """
+        frames_read = 0
+        while True:
+            print("Reading single frame", frames_read, '...')
+            current_frame = self.read_next_single_frame()
+            if current_frame is not None:
+                frames_read += 1
+                self.container.add_frame(current_frame)
+                if frames_read % 10000 == 0:
+                    print("#", end="")
+            else:
+                print("Frame:", frames_read+1, " incomplete")
+                break
+
+        print(len(self.container.global_frames), "frames read")
+        return self.container
+
+    def read_next_global_frame_header(self):
+        """
+        I return the next global frame header read from current position in file.
+
+        :return: the next global frame header or None if incomplete
+        :rtype: AmmosGlobalFrameHeader
+        """
+        bytes = self.file.read(AmmosAudioReader.GLOBAL_HEADER_SIZE)
+        print("Reading next global frame header")
+        if ((not bytes) or (len(bytes) < AmmosAudioReader.GLOBAL_HEADER_SIZE)):
+            print("Can not read all", AmmosAudioReader.GLOBAL_HEADER_SIZE, "bytes of global frame header")
+            return None
+
+        # FIXME: Catch exceptions and add some asserts
+        current_global_frame_header = AmmosGlobalFrameHeader.from_bytes(bytes)
+        # print("Current global frame header", current_global_frame_header)
+        return current_global_frame_header
+
+    def read_next_global_frame_body_data_header(self):
+        """
+        I return the next global frame body data header from current position in file.
+
+        :param data_header_size: the number of bytes to read
+        :type data_header_size: int
+        :return: the next Ammos Audio Data header or None if incomplete
+        :rtype: AmmosAudioDataHeader
+        """
+        bytes = self.file.read(AmmosAudioReader.STANDARD_AUDIO_DATA_HEADER_SIZE)
+
+        # print("\nReading global frame body standard data header\n")
+        if ((not bytes) or (len(bytes) < AmmosAudioReader.STANDARD_AUDIO_DATA_HEADER_SIZE)):
+            print("Can not read all", AmmosAudioReader.STANDARD_AUDIO_DATA_HEADER_SIZE,
+                  "bytes of global frame body data header")
+            return None
+        return AmmosAudioDataHeader.from_bytes(bytes)
+
+    def read_next_global_frame_body_extended_data_header(self):
+        """
+        I return the next global frame body extended data header from current position in file.
+
+        :return: the next Ammos Audio Extended Data header or None if incomplete
+        :rtype: AmmosExtendedAudioDataHeader
+        """
+        bytes = self.file.read(AmmosAudioReader.EXTENDED_AUDIO_DATA_HEADER_SIZE)
+
+        if ((not bytes) or (len(bytes) < AmmosAudioReader.EXTENDED_AUDIO_DATA_HEADER_SIZE)):
+            print("Can not read all ", AmmosAudioReader.EXTENDED_AUDIO_DATA_HEADER_SIZE,
+                  " bytes of global frame extended data header")
+            return None
+        return AmmosExtendedAudioDataHeader.from_bytes(bytes)
+
+    def read_next_audio_data_body(self, sample_count, channel_count, sample_size):
+        """
+        I return the next audio data read from current position in file.
+
+        :param sample_count: the number of samples per channel inside data body
+        :type sample_count: int
+
+        :param channel_count: number of channels (e.g. mono, stereo or even more)
+        :type channel_count: int
+
+        :param sample_size: sample size in bytes (1, 2 or 4 bytes)
+        :type sample_size: int
+
+        :return: the next audio data or None if incomplete
+        :rtype: bytes
+        """
+        # FIXME: Describe the parameters better
+
+        total = sample_count*channel_count*sample_size
+
+        byte_string = self.file.read(total)
+
+        if len(byte_string) != total:
+            print("Can not read all", total, "bytes of data body")
+            return None
+        print([hex(c) for c in byte_string])
+        return byte_string
+
+    def read_next_global_frame_body(self, global_frame_header):
+
+        audio_data_header = None
+
+        if global_frame_header.data_header_length == AmmosAudioReader.STANDARD_AUDIO_DATA_HEADER_SIZE:
+            print("Read standard data header")
+            audio_data_header = self.read_next_global_frame_body_data_header()
+
+        if global_frame_header.data_header_length == AmmosAudioReader.EXTENDED_AUDIO_DATA_HEADER_SIZE:
+            print("Read extended data header")
+            audio_data_header = self.read_next_global_frame_body_extended_data_header()
+
+        if audio_data_header is None:
+            print("Data header missing or format unknown")
+            return None
+
+        audio_data_body = self.read_next_audio_data_body(audio_data_header.sample_count,
+                                                         audio_data_header.channel_count,
+                                                         audio_data_header.sample_size)
+
+        if audio_data_body is None:
+            print("Data body missing")
+            return None
+
+        return AmmosGlobalFrameBody(audio_data_header, audio_data_body)
+
+    def read_next_single_frame(self):
+
+        global_frame_header = self.read_next_global_frame_header()
+
+        print(global_frame_header)
+
+        if global_frame_header is None:
+            print("Global frame header missing")
+            return None
+
+        if global_frame_header.data_header_length is None:
+            print("Data header length empty")
+            return None
+
+        if global_frame_header.frame_type == 256:
+            print("Audio Datastream found")
+            global_frame_body = self.read_next_global_frame_body(global_frame_header)
+            if global_frame_body is None:
+                return None
+        else:
+            print("Unsupported frame type", global_frame_header.frame_type, "found")
+            return None
+
+        ammos_single_frame = AmmosSingleFrame(global_frame_header, global_frame_body)
+        return ammos_single_frame
Index: ammosreader/AmmosAudioSocketReader.py
===================================================================
--- ammosreader/AmmosAudioSocketReader.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
+++ ammosreader/AmmosAudioSocketReader.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
@@ -0,0 +1,156 @@
+"""I read a Ammos datastream from a socket."""
+
+import select
+import socket
+from collections import deque
+import numpy as np
+from ammosreader.AmmosAudioDataHeader import AmmosAudioDataHeader
+from ammosreader.AmmosExtendedAudioDataHeader import AmmosExtendedAudioDataHeader
+from ammosreader.AmmosGlobalFrameHeader import AmmosGlobalFrameHeader
+
+
+class AmmosAudioSocketReader:
+    def __init__(self, socket:socket.socket, debug=True):
+        """
+        Initializes the AmmosAudioSocketReader
+
+        Args:
+            socket (socket.socket): socket to read from
+            debug (bool): if true, prints debug information
+        """
+
+        #buffer for reading socket bytewise und check for the magic word
+        self.__magic_word_buffer = deque(maxlen=4)
+
+        #input socket to read from
+        self.__socket = socket
+
+        #
+        self.DEBUG_MODE = debug
+
+    def __get_next_data(self, byte_count: int) -> bytearray:
+        """
+        Gets the next bytes from the socket, for example headers and body data.
+
+        Args:
+            byte_count (int): number of bytes to read
+
+        Raises:
+            TimeoutError: Raises TimeoutError if the socket does not serve data anymore
+
+        Returns:
+            bytearray: data from socket as bytearray
+        """
+
+        byte_array = []
+        
+        while len(b''.join(byte_array)) < byte_count:
+            if self.DEBUG_MODE:
+                print(f"Remaining Bytes: {byte_count - len(b''.join(byte_array))}")
+            self.__socket.settimeout(5)
+            new_bytes = self.__socket.recv(byte_count - len(b''.join(byte_array)), socket.MSG_WAITALL)
+
+            if not new_bytes:
+                raise TimeoutError("Socket timed out while reading data")
+
+            if self.DEBUG_MODE:
+                print(f"Got {len(new_bytes)} bytes of {byte_count - len(b''.join(byte_array))} ramining")
+
+            byte_array.append(new_bytes)
+
+        return b''.join(byte_array)
+
+    def __audio_data_body_to_numpy(self, audio_data_body:bytearray) -> np.ndarray:
+        """
+        converts the audio data body to a numpy array
+
+        Args:
+            audio_data_body (bytearray): audio data from audio data body
+
+        Returns:
+            np.ndarray: audio data as numpy array
+        """
+
+        return np.frombuffer(audio_data_body, dtype=np.int16)
+
+    def read_next_frame(self) -> tuple[np.ndarray, int]:
+        """Reads the next ammos audio frame from socket
+
+        Raises:
+            TimeoutError: Raisees TimeoutError if the socket does not serve data anymore
+
+        Returns:
+            tuple[np.ndarray, int]: Contains the audio data and the sample rate
+        """
+
+        # get first byte of the day
+        self.__socket.settimeout(5)
+
+        new_byte = self.__socket.recv(1, socket.MSG_WAITALL)
+        # raise Exception if socket does not return anything
+        if len(new_byte) < 1:
+            raise TimeoutError      
+
+        #read loop
+        while new_byte:
+            #
+            self.__magic_word_buffer.append(new_byte)
+            byte_array = b''.join(self.__magic_word_buffer)
+
+            if byte_array.hex() == '726574fb':
+                #print(byte_array.hex())
+
+                ammos_global_header_buffer = list(self.__magic_word_buffer)
+                ammos_global_header_buffer.append(self.__get_next_data(20))
+                #while len(b''.join(ammos_global_header_buffer)) < 24:
+                #    ammos_global_header_buffer.append(self.__socket.recv(24 - len(b''.join(ammos_global_header_buffer))))
+                    
+                ammos_global_header = AmmosGlobalFrameHeader.from_bytes(b''.join(ammos_global_header_buffer))
+                if self.DEBUG_MODE:
+                    print(ammos_global_header)
+
+                if ammos_global_header.data_header_length == 44 and ammos_global_header.frame_type == 256:
+                    byte_array_header = self.__get_next_data(44)
+                    #while len(b''.join(byte_array_header)) < 44:
+                    #    byte_array_header.append(self.__socket.recv(44 - len(b''.join(byte_array_header))))
+
+                    ammos_extended_audio_data_header = AmmosExtendedAudioDataHeader.from_bytes(byte_array_header)
+                    if self.DEBUG_MODE:
+                        print(ammos_extended_audio_data_header.sample_count, ammos_extended_audio_data_header.channel_count, ammos_extended_audio_data_header.sample_size)
+                    audio_body = self.__get_next_data(ammos_extended_audio_data_header.sample_count* 
+                                                      ammos_extended_audio_data_header.channel_count* 
+                                                      ammos_extended_audio_data_header.sample_size)
+
+                    audio_array = self.__audio_data_body_to_numpy(audio_body)
+                    if self.DEBUG_MODE:
+                        print(len(audio_array), len(audio_array)/ammos_extended_audio_data_header.sample_rate)
+
+                    return [audio_array, ammos_extended_audio_data_header.sample_rate]
+
+                elif ammos_global_header.data_header_length == 36 and ammos_global_header.frame_type == 256:
+                    byte_array_header = self.__get_next_data(36)
+                    #while len(b''.join(byte_array_header)) < 36:
+                    #    byte_array_header.append(self.__socket.recv(36 - len(b''.join(byte_array_header))))
+
+                    ammos_audio_data_header = AmmosAudioDataHeader.from_bytes(byte_array_header)
+                    if self.DEBUG_MODE:
+                        print(ammos_audio_data_header.sample_count, ammos_audio_data_header.channel_count, ammos_audio_data_header.sample_size)
+                    audio_body = self.__get_next_data(ammos_extended_audio_data_header.sample_count* 
+                                                      ammos_extended_audio_data_header.channel_count* 
+                                                      ammos_extended_audio_data_header.sample_size)
+
+                    audio_array = self.__audio_data_body_to_numpy(audio_body)
+                    if self.DEBUG_MODE:
+                        print(len(audio_array), len(audio_array)/ammos_audio_data_header.sample_rate)
+
+                    return [audio_array, ammos_audio_data_header.sample_rate]
+
+            # get the next byte
+            self.__socket.settimeout(5)
+
+            new_byte = self.__socket.recv(1, socket.MSG_WAITALL)
+            # raise Exception if socket does not return anything
+            if len(new_byte) < 1:
+                raise TimeoutError   
+
+        return None
Index: ammosreader/AmmosContainer.py
===================================================================
--- ammosreader/AmmosContainer.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
+++ ammosreader/AmmosContainer.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
@@ -0,0 +1,29 @@
+"""I store the content of an R&S Ammos file in a more accessible way."""
+
+class AmmosContainer():
+
+    def __init__(self, name, frames):
+        self.name = name
+        self.global_frames = frames
+        self.tags = []
+
+    def add_tag(self, tag):
+        self.tags.append(tag)
+
+    def add_frame(self, frame):
+        self.global_frames.append(frame)
+
+    def size(self):
+        len(self.global_frames)
+
+    def frequencies(self):
+        return set(list(filter(lambda frame: frame.global_frame_body.data_header.frequency, self.global_frames)))
+
+    def __str__(self):
+        start_time = self.global_frames[0].global_frame_body.data_header.timestamp
+        end_time = self.global_frames[-1].global_frame_body.data_header.timestamp
+
+        frq = str(self.global_frames[0].global_frame_body.data_header.frequency)
+
+        return ("Start time: " + str(start_time) +
+                "\nEnd time  : " + str(end_time) + "\nFrequencies: " + frq)
Index: ammosreader/AmmosExtendedAudioDataHeader.py
===================================================================
--- ammosreader/AmmosExtendedAudioDataHeader.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
+++ ammosreader/AmmosExtendedAudioDataHeader.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
@@ -0,0 +1,34 @@
+import struct
+import numpy as np
+from ammosreader.AmmosAudioDataHeader import AmmosAudioDataHeader
+
+
+class AmmosExtendedAudioDataHeader():
+
+    @classmethod
+    def from_bytes(cls, bytes):
+        standard_header = AmmosAudioDataHeader.from_bytes(bytes[0:36])
+        extended_header_elements = struct.unpack('Q', bytes[36:])
+        timestamp = extended_header_elements[0]
+        sample_rate = standard_header.sample_rate
+        status = standard_header.status
+        frequency = standard_header.frequency
+        demod_bandwidth = standard_header.demod_bandwidth
+        demod_type = standard_header.demod_type
+        sample_count = standard_header.sample_count
+        channel_count = standard_header.channel_count
+        sample_size = standard_header.sample_size
+        return AmmosExtendedAudioDataHeader(sample_rate, status, frequency, demod_bandwidth, demod_type,
+                                            sample_count, channel_count, sample_size, timestamp)
+
+    def __init__(self, sample_rate, status, frequency, demod_bandwidth, demod_type,
+                 sample_count, channel_count, sample_size, timestamp):
+        self.sample_rate = sample_rate
+        self.status = status
+        self.frequency = frequency
+        self.demod_bandwidth = demod_bandwidth
+        self.demod_type = demod_type
+        self.sample_count = sample_count
+        self.channel_count = channel_count
+        self.sample_size = sample_size
+        self.timestamp = timestamp
Index: ammosreader/AmmosExtendedIFDataHeader.py
===================================================================
--- ammosreader/AmmosExtendedIFDataHeader.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
+++ ammosreader/AmmosExtendedIFDataHeader.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
@@ -0,0 +1,69 @@
+import struct
+import numpy as np
+from ammosreader.AmmosIFDataHeader import AmmosIFDataHeader
+
+
+class AmmosExtendedIFDataHeader():
+
+    @classmethod
+    def from_bytes(cls, bytes):
+        standard_header = AmmosIFDataHeader.from_bytes(bytes[0:56])
+        extended_header_elements = struct.unpack('QQI', bytes[56:76])
+        block_count = standard_header.block_count
+        block_length = standard_header.block_length
+        timestamp = standard_header.timestamp
+        status = standard_header.status
+        source_id = standard_header.source_id
+        source_state = standard_header.source_state
+        frequency = standard_header.frequency
+        bandwidth = standard_header.bandwidth
+        sample_rate = standard_header.sample_rate
+        interpolation = standard_header.interpolation
+        decimation = standard_header.decimation
+        voltage_ref = standard_header.voltage_ref
+        stream_start = np.datetime64(int(extended_header_elements[0]), 'ns')
+        sample_counter = extended_header_elements[1]
+        antenna_correction = extended_header_elements[2]
+        size = len(bytes)
+        return AmmosExtendedIFDataHeader(size, block_count, block_length, timestamp, status, source_id,
+                                         source_state, frequency, bandwidth, sample_rate, interpolation,
+                                         decimation, voltage_ref, stream_start, sample_counter,
+                                         antenna_correction)
+
+    def __init__(self, size, block_count, block_length, timestamp, status, source_id, source_state, frequency,
+                 bandwidth, sample_rate, interpolation, decimation, voltage_ref, stream_start, sample_counter,
+                 antenna_correction):
+
+        self.size = size
+        self.block_count = block_count
+        self.block_length = block_length
+        self.timestamp = timestamp
+        self.status = status
+        self.source_id = source_id
+        self.source_state = source_state
+        self.frequency = frequency
+        self.bandwidth = bandwidth
+        self.sample_rate = sample_rate
+        self.interpolation = interpolation
+        self.decimation = decimation
+        self.voltage_ref = voltage_ref
+        self.stream_start = stream_start
+        self.sample_counter = sample_counter
+        self.antenna_correction = antenna_correction
+
+    def __str__(self):
+        output = (
+            "\nGlobal frame body data header\n" +
+            "-----------------------------\n" +
+            "Block count:" + str(self.block_count) + "\n" +
+            "Block length:" + str(self.block_length) + "\n" +
+            "Time stamp:" + str(self.timestamp) + "\n" +
+            "Frequency:" + str(self.frequency) + "\n" +
+            "Bandwidth:" + str(self.bandwidth) + "\n" +
+            "Sample rate:" + str(self.sample_rate) + "\n" +
+            "Stream start:" + str(self.stream_start) + "\n" +
+            "Sample counter:" + str(self.sample_counter) + "\n" +
+            "Antenna correction:" + str(self.antenna_correction) + "\n"
+        )
+
+        return output
Index: ammosreader/AmmosGlobalFrameBody.py
===================================================================
--- ammosreader/AmmosGlobalFrameBody.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
+++ ammosreader/AmmosGlobalFrameBody.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
@@ -0,0 +1,17 @@
+class AmmosGlobalFrameBody():
+
+    def __init__(self, data_header, data_body):
+        self.data_header = data_header
+        self.data_body = data_body
+
+    def data_bytes_only(self):
+
+        byte_string = ""
+
+        for each_block in self.data_body:
+            if not each_block:
+                print("Block is nil")
+
+            byte_string += each_block.if_data
+
+        return byte_string
Index: ammosreader/AmmosGlobalFrameHeader.py
===================================================================
--- ammosreader/AmmosGlobalFrameHeader.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
+++ ammosreader/AmmosGlobalFrameHeader.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
@@ -0,0 +1,53 @@
+import struct
+
+
+class AmmosGlobalFrameHeader():
+
+    MAGIC_WORD = "726574fb"
+
+    @classmethod
+    def from_bytes(cls, bytes):
+
+        elements = struct.unpack('4s4s4s4s4s4s', bytes)
+
+        magic_word = elements[0].hex()
+
+        frame_length = (int.from_bytes(elements[1], byteorder='little')*4)
+
+        running_frame_number = (int.from_bytes(elements[2], byteorder='little'))
+
+        frame_type = (int.from_bytes(elements[3], byteorder='little'))
+
+        data_header_length = 4 * int.from_bytes(elements[4], byteorder='little')
+
+        reserved = elements[5]
+
+        return AmmosGlobalFrameHeader(magic_word, frame_length, running_frame_number,
+                                      frame_type, data_header_length, reserved)
+
+    def __init__(self, magic_word, frame_length, running_frame_number, frame_type, data_header_length, reserved):
+
+        if magic_word != self.MAGIC_WORD:
+            print("Wrong magic word")
+            self.magic_word = magic_word
+            return None
+        else:
+            self.magic_word = magic_word
+            self.frame_length = frame_length
+            self.running_frame_number = running_frame_number
+            self.frame_type = frame_type
+            self.data_header_length = data_header_length
+            self.reserved = reserved
+
+    def __str__(self):
+        output = ("Global frame header info\n" +
+                  "------------------------\n" +
+                  "Magic word:" + str(self.magic_word) + "\n" +
+                  "Frame length:" + str(self.frame_length) + "\n" +
+                  "Running frame:" + str(self.running_frame_number) + "\n" +
+                  "Frame Type:" + str(self.frame_type) + "\n" +
+                  "Data header length:" + str(self.data_header_length) + "\n")
+        return output
+
+    def size(self):
+        return 24
Index: ammosreader/AmmosIFDataBlock.py
===================================================================
--- ammosreader/AmmosIFDataBlock.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
+++ ammosreader/AmmosIFDataBlock.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
@@ -0,0 +1,5 @@
+class AmmosIFDataBlock():
+
+    def __init__(self, if_datablock_header, if_data):
+        self.if_datablock_header = if_datablock_header
+        self.if_data = if_data
Index: ammosreader/AmmosIFDataHeader.py
===================================================================
--- ammosreader/AmmosIFDataHeader.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
+++ ammosreader/AmmosIFDataHeader.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
@@ -0,0 +1,54 @@
+import struct
+import numpy as np
+
+
+class AmmosIFDataHeader():
+
+    @classmethod
+    def from_bytes(cls, bytes):
+        elements = struct.unpack('<IIQIIIQIIIIi', bytes)
+        block_count = elements[0]
+        block_length = int(elements[1])*4
+        timestamp = np.datetime64(int(elements[2])*1000, 'ns')
+        status = elements[3]
+        source_id = elements[4]
+        source_state = elements[5]
+        frequency = elements[6]
+        bandwidth = elements[7]
+        sample_rate = elements[8]
+        interpolation = elements[9]
+        decimation = elements[10]
+        voltage_ref = elements[11]
+
+        return AmmosIFDataHeader(block_count, block_length, timestamp, status, source_id,
+                                 source_state, frequency, bandwidth, sample_rate,
+                                 interpolation, decimation, voltage_ref)
+
+    def __init__(self, block_count, block_length, timestamp, status, source_id, source_state, frequency,
+                 bandwidth, sample_rate, interpolation, decimation, voltage_ref):
+        self.block_count = block_count
+        self.block_length = block_length
+        self.timestamp = timestamp
+        self.status = status
+        self.source_id = source_id
+        self.source_state = source_state
+        self.frequency = frequency
+        self.bandwidth = bandwidth
+        self.sample_rate = sample_rate
+        self.interpolation = interpolation
+        self.decimation = decimation
+        self.voltage_ref = voltage_ref
+
+    def header_size(self):
+        return 56
+
+    def __str_(self):
+        output = ("\nGlobal frame body data header\n" +
+                  "-----------------------------\n" +
+                  "Block count:" + str(self.block_count) + "\n" +
+                  "Block length:" + str(self.block_length) + "\n" +
+                  "Time stamp:" + str(self.timestamp) + "\n" +
+                  "Frequency:" + str(self.frequency) + "\n" +
+                  "Bandwidth:" + str(self.bandwidth) + "\n" +
+                  "Sample rate:" + str(self.sample_rate) + "\n")
+        return output
Index: ammosreader/AmmosIFReader.py
===================================================================
--- ammosreader/AmmosIFReader.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
+++ ammosreader/AmmosIFReader.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
@@ -0,0 +1,147 @@
+import math
+import os
+
+from ammosreader.AmmosGlobalFrameBody import AmmosGlobalFrameBody
+from ammosreader.AmmosIFDataHeader import AmmosIFDataHeader
+from ammosreader.AmmosExtendedIFDataHeader import AmmosExtendedIFDataHeader
+from ammosreader.AmmosGlobalFrameHeader import AmmosGlobalFrameHeader
+from ammosreader.AmmosSingleFrame import AmmosSingleFrame
+from ammosreader.AmmosIFDataBlock import AmmosIFDataBlock
+from ammosreader.AmmosContainer import AmmosContainer
+
+
+class AmmosIFReader():
+
+    def __init__(self, file_name):
+
+        self.file_name = file_name
+        self.file = open(self.file_name, "rb")
+
+        self.container = AmmosContainer(self.file_name, [])
+
+        self.tags = []
+
+    def add_tag(self, tag):
+        self.tags.append(tag)
+
+    def read_all_frames_left(self):
+
+        frames_read = 0
+
+        while True:
+            # print("Reading single frame", frames_read, '...')
+            current_frame = self.read_next_single_frame()
+            if current_frame is not None:
+                frames_read += 1
+                self.container.add_frame(current_frame)
+                # if frames_read % 10000 == 0:
+                #    print("#", end="")
+            else:
+                # print("Frame:", frames_read+1, " incomplete")
+                break
+
+        # print(len(self.container.global_frames), "frames read")
+
+    def read_next_global_frame_header(self):
+        bytes = self.file.read(24)
+        # print("Reading next global frame header")
+        if ((not bytes) or (len(bytes) < 24)):
+            # print("Can not read all 24 bytes of global frame header")
+            return None
+
+        return AmmosGlobalFrameHeader.from_bytes(bytes)
+
+    def read_next_global_frame_body_data_header(self):
+
+        bytes = self.file.read(56)
+
+        # print("\nReading global frame body standard data header\n")
+        if ((not bytes) or (len(bytes) < 56)):
+            # print("Can not read all 56 bytes of global frame body data header")
+            return None
+
+        data_header = AmmosIFDataHeader.from_bytes(bytes)
+        # print("Data header", data_header)
+        return data_header
+
+    def read_next_global_frame_body_extended_data_header(self):
+
+        bytes = self.file.read(76)
+        # print("\nReading global frame body extended data header\n")
+
+        if ((not bytes) or (len(bytes) < 76)):
+            # print("Can not read all ", 76, "bytes of global frame extended data header")
+            return None
+        extended_data_header = AmmosExtendedIFDataHeader.from_bytes(bytes)
+        # print("Extended data header", extended_data_header)
+        return extended_data_header
+
+    def read_next_if_data_blocks(self, n, length):
+
+        # FIXME: Describe the parameters better
+
+        data_blocks = []
+
+        block_length = 4 + length
+
+        total = n*block_length
+
+        byte_string = self.file.read(block_length)
+
+        if len(byte_string) != total:
+            # print("Can not read all", total, "bytes of data body")
+            return None
+
+        for i in range(0, n):
+            result = byte_string[i*block_length:(i*block_length+block_length)]
+            data_blocks.append(AmmosIFDataBlock(result[0:4], result[4:]))
+
+        return data_blocks
+
+    def read_next_global_frame_body(self, global_frame_header):
+
+        if_data_header = None
+
+        if global_frame_header.data_header_length == 56:
+            if_data_header = self.read_next_global_frame_body_data_header()
+        else:
+            if_data_header = self.read_next_global_frame_body_extended_data_header()
+
+        if if_data_header is None:
+            # print("Data header missing")
+            return None
+
+        if_data_body = self.read_next_if_data_blocks(if_data_header.block_count, if_data_header.block_length)
+
+        if if_data_body is None:
+            # print("Data body missing")
+            return None
+
+        return AmmosGlobalFrameBody(if_data_header, if_data_body)
+
+    def read_next_single_frame(self):
+
+        global_frame_header = self.read_next_global_frame_header()
+
+        # print("\nReading next global frame header\n", global_frame_header)
+        # print("File pointer", self.file.tell())
+
+        if global_frame_header is None:
+            # print("Global frame header missing")
+            return None
+
+        if global_frame_header.data_header_length is None:
+            # print("Data header length empty")
+            return None
+
+        if global_frame_header.frame_type == 2:
+
+            global_frame_body = self.read_next_global_frame_body(global_frame_header)
+            if global_frame_body is None:
+                return None
+
+        else:
+            # print("Unsupported frame type", global_frame_header.frame_type, "found")
+            return None
+
+        return AmmosSingleFrame(global_frame_header, global_frame_body)
Index: ammosreader/AmmosSingleFrame.py
===================================================================
--- ammosreader/AmmosSingleFrame.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
+++ ammosreader/AmmosSingleFrame.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
@@ -0,0 +1,25 @@
+class AmmosSingleFrame():
+
+    def __init__(self, global_frame_header, global_frame_body):
+        self.global_frame_header = global_frame_header
+        self.global_frame_body = global_frame_body
+
+    def data(self):
+        return self.global_frame_body.data_bytes_only()
+
+    def size_correct(self):
+        return (self.global_frame_header.frame_length == (24 + self.global_frame_header.data_header_length +
+                                                          (self.global_frame_body.data_header.block_count *
+                                                           (self.global_frame_body.data_header.block_length + 4))))
+    # FIXME: Use str method instead
+
+    def __str__(self):
+        output = (
+            "Global frame header\n" +
+            "-------------------\n" +
+            "Frame type:" + str(self.global_frame_header.frame_type) + "\n" +
+            "Frame count:" + str(self.global_frame_header.running_frame_number) + "\n" +
+            "Data header length:" + str(self.global_frame_header.data_header_length) + " bytes\n" +
+            "Frame length:" + str(self.global_frame_header.frame_length) + " bytes\n"
+        )
+        return output
Index: ammosreader/IQDWTXBlock.py
===================================================================
--- ammosreader/IQDWTXBlock.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
+++ ammosreader/IQDWTXBlock.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
@@ -0,0 +1,14 @@
+from ammosreader.PDW import PDW
+
+
+class IQDWTXBLOCK():
+
+    @classmethod
+    def from_bytes(cls, byte_string):
+        pdw = PDW.from_bytes(bytes[0:32])
+        return IQDWTXBLOCK(pdw, bytes[32:])
+
+    def __init__(self, pdw, iq):
+
+        self.pdw = pdw
+        self.iq = iq
Index: ammosreader/PDW.py
===================================================================
--- ammosreader/PDW.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
+++ ammosreader/PDW.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
@@ -0,0 +1,240 @@
+"""I store the information of a single PDW block."""
+
+import struct
+import math
+import numpy as np
+
+
+class PDW():
+    """
+    I store information from a single ppdw data block.
+
+    .. automethod:: __init__
+    """
+
+    @classmethod
+    def from_bytes(cls, byte_string):
+        """
+        I create an instance of class PDW from data body (8 * 32 bits).
+
+        :param byte_string: a byte string containing a single data body read from a ppdw file
+        :type byte_string: byte string
+
+        :return: an instance of class PDW with attributes set according to the data of a data body
+        :rtype: PDW
+        """
+        assert(len(byte_string) == 32)
+
+        parts = struct.unpack('Q4s4s4s4s4s4s', byte_string)
+        nanoseconds = (parts[0])
+        time_of_arrival = np.datetime64(nanoseconds, 'ns')
+
+        third_entry = bin(int.from_bytes(parts[1], byteorder='little'))
+        padding = 32-len(str(third_entry)[2:])
+        third_entry_bit_string = "0" * padding + str(third_entry)[2:]
+        pdw_format_identifier = int(third_entry_bit_string[0:6], 2)
+        center_frequency = int(third_entry_bit_string[5:32], 2)
+
+        fourth_entry = bin(int.from_bytes(parts[2], byteorder='little'))
+        padding = 32-len(str(fourth_entry)[2:])
+        fourth_entry_bit_string = "0" * padding + str(fourth_entry)[2:]
+        is_valid = bool(int(fourth_entry_bit_string[0]))
+        is_pulse = bool(int(fourth_entry_bit_string[1]))
+        level_unit = int(fourth_entry_bit_string[2])
+        signal_start_missing = bool(int(fourth_entry_bit_string[3]))
+        signal_end_missing = bool(int(fourth_entry_bit_string[4]))
+        pulse_width = int(fourth_entry_bit_string[7:33], 2)
+
+        fifth_entry = bin(int.from_bytes(parts[3], byteorder='little'))
+        padding = 32-len(str(fifth_entry)[2:])
+        fifth_entry_bit_string = "0" * padding + str(fifth_entry)[2:]
+        frequency_shift_or_bandwidth = int(fifth_entry_bit_string[0:20], 2)
+        # FIXME: You have to scale me to the range from -200.0 to 200.0 in 0.1 steps
+        pulse_level_or_pulse_field_strength = math.ceil(int(fifth_entry_bit_string[20:32], 2)) / 10
+
+        sixth_entry = bin(int.from_bytes(parts[4], byteorder='little'))
+        padding = 32-len(str(sixth_entry)[2:])
+        sixth_entry_bit_string = "0" * padding + str(sixth_entry)[2:]
+        region_of_interest = bool(int(sixth_entry_bit_string[0]))
+        # FIXME: You have to scale me to a range from 0.0 to 6.2 in steps of 0.1 - 6.3 means unknown
+        azimuth_confidence = math.ceil(int(sixth_entry_bit_string[1:7], 2)) / 10
+        modulations = {0: 'Unknown', 1: 'Unmodulated', 2: 'FM', 3: 'LFM', 4: 'PSK-2', 5: 'PSK-3', 6: 'PSK-4',
+                       7: 'PSK-m', 8: 'NLFM', 9: 'SFM', 10: 'TFM', 11: 'Pulse too short'}
+        modulation = modulations[int(sixth_entry_bit_string[7:12], 2)]
+        sector = int(sixth_entry_bit_string[28:32], 2)
+
+        seventh_entry = bin(int.from_bytes(parts[5], byteorder='little'))
+        padding = 32-len(str(seventh_entry)[2:])
+        seventh_entry_bit_string = "0" * padding + str(seventh_entry)[2:]
+        polarities = {0: 'Horizontal/Unknown', 1: 'Vertical', 2: 'Counter clockwise', 3: 'Clockwise'}
+        polarity = polarities[int(seventh_entry_bit_string[0:2], 2)]
+        df_quality = int(seventh_entry_bit_string[2:9], 2)
+        # FIXME: You have to scale me from -90 to 90 in 0.1 degree steps
+        elevation = int(seventh_entry_bit_string[9:20], 2)
+        # FIXME: You have to check me for a range from 0.0 to 359.9 in steps of 0.1
+        azimuth = 0.1 * (int(seventh_entry_bit_string[20:32], 2))
+
+        eighth_entry = bin(int.from_bytes(parts[5], byteorder='little'))
+        padding = 32-len(str(eighth_entry)[2:])
+        eighth_entry_bit_string = "0" * padding + str(eighth_entry)[2:]
+        channel = int(eighth_entry_bit_string[0:4], 2)
+
+        return PDW(time_of_arrival, pdw_format_identifier, center_frequency, is_valid, is_pulse, level_unit,
+                   signal_start_missing, signal_end_missing, pulse_width, frequency_shift_or_bandwidth,
+                   pulse_level_or_pulse_field_strength, region_of_interest, azimuth_confidence, modulation,
+                   sector, polarity, df_quality, elevation, azimuth, channel)
+
+    def __init__(self, time_of_arrival, pdw_format_identifier, center_frequency, is_valid, is_pulse,
+                 level_unit, signal_start_missing, signal_end_missing, pulse_width, frequency_shift_or_bandwidth,
+                 pulse_level_or_pulse_field_strength, region_of_interest, azimuth_confidence, modulation,
+                 sector, polarity, df_quality, elevation, azimuth, channel):
+        r"""
+        I return an instance of an Pulse Data word.
+
+        :param time_of_arrival: nanoseconds since 1970-01-01 00:00:00
+        :type time_of_arrival: Integer
+        :param pdw_format: format code
+        :type pdw_format: Integer
+        :param center_frequency: center frequency in KHz
+        :type center_frequency: Integer
+        :param is_valid: flag to mark if pdw data body is valid
+        :type is_valid: Boolean
+        :param is_pulse: flag to mark if pdw data body contains a pulse or a continuous wave signal
+        :type is_pulse: Boolean
+        :param level_unit: 0 means dBµV - 1 means dBµV/m
+        :type level_unit: Integer
+        :param signal_start_missing: signal started before time of arrival
+        :type signal_start_missing: Boolean
+        :param signal_end_missing: signal stops after time of arrival
+        :type signal_end_missing: Boolean
+        :param pulse_width: pulse width in nanoseconds - Zero if no valid pulse detected
+        :type pulse_width: Integer
+        :param frequency_shift_or_bandwidth: Value in KHz - Value set to 1048575 means Unknown
+        :type frequency_shift_or_bandwidth: Integer
+        :param pulse_level_or_pulse_field_strength: Pulse level or Pulse Field Strength depending on level_unit \
+         (-200.0...200.0) in 0.1 steps / minus 204.8 means no valid level detected
+        :type pulse_level_or_pulse_field_strength: Float
+        :param region_of_interest: Marks if signal is from region of interest
+        :type region_of_interest: Boolean
+        :param azimuth_confidence: degree in steps of 0.1 (0.0-6.2) / 6.3 means confidence unknown
+        :type azimuth_confidence: Float
+        :param modulation: type of modulation (e.g. PSK-2, PSK-4, FM etc.)
+        :type modulation: String
+        :param sector: reference antenna sector (0-15)
+        :type sector: Integer
+        :param polarity: Horizontal, Vertical, Clockwise, Counter clockwise
+        :type polarity: String
+        :param df_quality: Direction finding quality in percent (0-100) - Zero means unknown
+        :type df_quality: Integer
+        :param elevation: elevation of incoming signal (from -90 to 90 degree) in steps of 0.1 degree \
+        minus 102.4 means unknown
+        :type elevation: Float
+        :param azimuth: azimuth of incoming signal (from 0 to 359.9 degree) in steps of 0.1 degree \
+        plus 409.5 means unknown
+        :type azimuth: Float
+        :param channel: detecting channel (0-16) - Zero means unknown
+        :type channel: Integer
+        :return: An instance of class PDW with attributes set according to the data of a data body
+        :rtype: PDW
+        """
+        self.time_of_arrival = time_of_arrival #
+        self.pdw_format_identifier = pdw_format_identifier
+        self.center_frequency = center_frequency #
+        self.is_valid = is_valid #
+        self.is_pulse = is_pulse #
+        self.level_unit = level_unit #
+        self.signal_start_missing = signal_start_missing
+        self.signal_end_missing = signal_end_missing
+        self.pulse_width = pulse_width #
+        self.frequency_shift_or_bandwidth = frequency_shift_or_bandwidth #
+        self.pulse_level_or_pulse_field_strength = pulse_level_or_pulse_field_strength #
+        self.region_of_interest = region_of_interest
+        self.azimuth_confidence = azimuth_confidence
+        self.modulation = modulation #
+        self.sector = sector
+        self.polarity = polarity
+        self.df_quality = df_quality #
+        self.elevation = elevation #
+        self.azimuth = azimuth
+        self.channel = channel #
+
+    def __str__(self):
+        """
+        I return the string representation of myself.
+
+        :rtype: str
+        """
+        output = ("Time of arrival: " + str(self.time_of_arrival) + "\n" +
+                  "PDW Format identifier: " + str(self.pdw_format_identifier) + "\n" +
+                  "Center frequency: " + str(self.center_frequency) + " KHz\n")
+
+        if self.is_valid:
+            output += "Signal: Valid\n"
+        else:
+            output += "Signal: Invalid\n"
+
+        if self.is_pulse:
+            output += "Signal type: Pulse\n"
+        else:
+            output += "Signal type: Continuous wave\n"
+
+        if self.level_unit == 1:
+            output += "Pulse level: " + str(self.pulse_level_or_pulse_field_strength) + " dbµV\n"
+        else:
+            output += "Pulse field strength: " + str(self.pulse_level_or_pulse_field_strength) + " dbµV/meter\n"
+
+        output += ("Pulse width: " + str(self.pulse_width) + " nanoseconds\n" +
+                   "Frequency shift or bandwidth: " + str(self.frequency_shift_or_bandwidth) + " KHz\n")
+
+        if self.region_of_interest:
+            output += "Region of interest: Yes\n"
+        else:
+            output += "Region of interest: No\n"
+
+        if self.azimuth_confidence == 6.3:
+            output += "Azimuth confidence: Invalid\n"
+        else:
+            output += "Azimuth confidence: " + str(self.azimuth_confidence) + " degree\n"
+
+        output += "Modulation: " + str(self.modulation) + "\n"
+
+        if self.sector == 0:
+            output += "Sector: Unknown\n"
+        else:
+            output += "Sector:" + str(self.sector) + "\n"
+
+        output += "Polarity: " + str(self.polarity) + "\n"
+
+        output += "DF quality: " + str(self.df_quality) + " %\n"
+
+        if self.elevation == 1024:
+            output += "Elevation: Unknown\n"
+        else:
+            output += "Elevation: " + str(self.elevation) + " degree\n"
+
+        if self.azimuth == 409.5:
+            output += "Azimuth: Unknown\n"
+        else:
+            output += "Azimuth: " + str(self.azimuth) + " degree\n"
+
+        output += "Channel: " + str(self.channel) + "\n"
+
+        return output
+
+    def to_json(self):
+        return {'TIMEOFARRIVAL': self.time_of_arrival.item(),
+                'CENTERFREQUENCY': self.center_frequency,
+                'VALID': self.is_valid,
+                'PULSE': self.is_pulse,
+                'PULSEWIDTH': self.pulse_width,
+                'LEVELUNIT': self.level_unit,
+                'FREQUENCYSHIFTORBANDWIDTH': self.frequency_shift_or_bandwidth,
+                'PULSELEVELORFIELDSTRENGTH': self.pulse_level_or_pulse_field_strength,
+                'MODULATION': self.modulation,
+                'ELEVATION': self.elevation,
+                'AZIMUTH': self.azimuth,
+                'CHANNEL': self.channel
+                }
+
+if __name__ == '__main__':
+    pass
Index: ammosreader/PPDWContainer.py
===================================================================
--- ammosreader/PPDWContainer.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
+++ ammosreader/PPDWContainer.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
@@ -0,0 +1,60 @@
+import numpy as np
+from datetime import datetime
+
+
+class PPDWContainer():
+    """
+    I store multiple signals imported from one or more .ppdw files
+    .. automethod:: __init__
+    """
+
+    def __init__(self, name, signals=None):
+
+        if signals is None:
+            self.signals = []
+        else:
+            self.signals = signals
+
+    def __str__(self):
+        return "\n".join(["Number of pulses:" + str(len(self.signals)),
+                          "Start time:" + str(self.start_time()),
+                          "End time:" + str(self.end_time())])
+
+    def add(self, a_pdw):
+        self.signals.append(a_pdw)
+
+    def julian_date_string(self):
+        ts = (self.start_time() - np.datetime64('1970-01-01T00:00:00')) / np.timedelta64(1, 's')
+        time_tuple = datetime.utcfromtimestamp(ts).timetuple()
+        return str(time_tuple.tm_year)[2:] + str(time_tuple.tm_yday).zfill(3)
+
+    def center_frequencies(self):
+        return list({each.center_frequency for each in self.signals})
+
+    def channels(self):
+        return list({each.channel for each in self.signals})
+
+    def modulations(self):
+        return list({each.modulation for each in self.signals})
+
+    def bandwidths(self):
+        return list({each.frequency_shift_or_bandwidth for each in self.signals})
+
+    def start_time(self):
+        return min([each.time_of_arrival for each in self.signals])
+
+    def end_time(self):
+        return max([each.time_of_arrival for each in self.signals])
+
+    def to_json(self):
+        return {'JULIANDATE': self.julian_date_string(),
+                'STARTTIME': str(self.start_time()),
+                'ENDTIME': str(self.end_time()),
+                # 'CENTERFREQUENCIES': self.center_frequencies(),
+                'CHANNELS': self.channels()
+                # 'MODULATIONS': self.modulations(),
+                # 'BANDWIDTHS': self.bandwidths()
+                }
+
+if __name__ == '__main__':
+    pass
Index: ammosreader/PPDWReader.py
===================================================================
--- ammosreader/PPDWReader.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
+++ ammosreader/PPDWReader.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
@@ -0,0 +1,28 @@
+from pathlib import Path
+
+from ammosreader.PDW import PDW
+from ammosreader.PPDWContainer import PPDWContainer
+
+class PPDWReader():
+
+    def __init__(self, a_file):
+
+        self.file = a_file
+        assert self.file.is_file()
+        self.content = self.file.read_bytes()
+        self.cursor = 0
+        self.container = PPDWContainer(self.file.stem)
+
+    def read_all_frames_left(self):
+        while self.cursor <= len(self.content) - 32:
+            current_bytes = self.content[self.cursor:self.cursor+32]
+            assert len(current_bytes) == 32
+            if not current_bytes:
+                # print('End of file detected')
+                break
+            if self.cursor + 32 >= len(self.content):
+                # print('Can not read all 32 bytes of next PDW')
+                break
+            self.container.add(PDW.from_bytes(current_bytes))
+            self.cursor += 32
+        return self.container
Index: setup.py
===================================================================
--- setup.py	(revision 2a19d78092d43c92dae9cc0d315e949a16351f9b)
+++ setup.py	(revision 752e2a925f1a1d4d5d27497a0b70f60501daa5b8)
@@ -4,4 +4,4 @@
 	name="ammosreader",
 	version="1.2.5",
-    packages=['src.ammosreader',],
+    packages=['ammosreader',],
     scripts=['sample_scripts/iqdw_reader.py'])
