From fd6d695275f88e83ebba6fa39afc044e329a690f Mon Sep 17 00:00:00 2001
From: "Matthias P. Braendli" <matthias.braendli@mpb.li>
Date: Sun, 25 Dec 2016 23:53:51 +0100
Subject: Add first version of EDI input

---
 lib/edi/ETIDecoder.cpp    | 388 ++++++++++++++++++++++++++++++++
 lib/edi/ETIDecoder.hpp    |  74 +++++++
 lib/edi/ETIWriter.cpp     | 277 +++++++++++++++++++++++
 lib/edi/ETIWriter.hpp     | 114 ++++++++++
 lib/edi/PFT.cpp           | 553 ++++++++++++++++++++++++++++++++++++++++++++++
 lib/edi/PFT.hpp           | 154 +++++++++++++
 lib/edi/README.md         |   1 +
 lib/edi/buffer_unpack.hpp |  62 ++++++
 lib/edi/eti.cpp           |  64 ++++++
 lib/edi/eti.hpp           | 114 ++++++++++
 10 files changed, 1801 insertions(+)
 create mode 100644 lib/edi/ETIDecoder.cpp
 create mode 100644 lib/edi/ETIDecoder.hpp
 create mode 100644 lib/edi/ETIWriter.cpp
 create mode 100644 lib/edi/ETIWriter.hpp
 create mode 100644 lib/edi/PFT.cpp
 create mode 100644 lib/edi/PFT.hpp
 create mode 100644 lib/edi/README.md
 create mode 100644 lib/edi/buffer_unpack.hpp
 create mode 100644 lib/edi/eti.cpp
 create mode 100644 lib/edi/eti.hpp

(limited to 'lib/edi')

diff --git a/lib/edi/ETIDecoder.cpp b/lib/edi/ETIDecoder.cpp
new file mode 100644
index 0000000..36e49fa
--- /dev/null
+++ b/lib/edi/ETIDecoder.cpp
@@ -0,0 +1,388 @@
+/*
+   Copyright (C) 2016
+   Matthias P. Braendli, matthias.braendli@mpb.li
+
+   http://opendigitalradio.org
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+#include "ETIDecoder.hpp"
+#include "buffer_unpack.hpp"
+#include "crc.h"
+#include "Log.h"
+#include <stdio.h>
+#include <cassert>
+#include <sstream>
+
+namespace EdiDecoder {
+
+using namespace std;
+
+ETIDecoder::ETIDecoder(ETIWriter& eti_writer) :
+    m_eti_writer(eti_writer)
+{
+}
+
+void ETIDecoder::push_bytes(const vector<uint8_t> &buf)
+{
+    copy(buf.begin(), buf.end(), back_inserter(m_input_data));
+
+    while (m_input_data.size() > 2) {
+        if (m_input_data[0] == 'A' and m_input_data[1] == 'F') {
+            decode_state_t st = decode_afpacket(m_input_data);
+
+            if (st.num_bytes_consumed == 0 and not st.complete) {
+                // We need to refill our buffer
+                break;
+            }
+
+            if (st.num_bytes_consumed) {
+                vector<uint8_t> remaining_data;
+                copy(m_input_data.begin() + st.num_bytes_consumed,
+                        m_input_data.end(),
+                        back_inserter(remaining_data));
+                m_input_data = remaining_data;
+            }
+
+            if (st.complete) {
+                m_eti_writer.assemble();
+            }
+
+        }
+        else if (m_input_data[0] == 'P' and m_input_data[1] == 'F') {
+            PFT::Fragment fragment;
+            const size_t fragment_bytes = fragment.loadData(m_input_data);
+
+            if (fragment_bytes == 0) {
+                // We need to refill our buffer
+                break;
+            }
+
+            vector<uint8_t> remaining_data;
+            copy(m_input_data.begin() + fragment_bytes,
+                    m_input_data.end(),
+                    back_inserter(remaining_data));
+            m_input_data = remaining_data;
+
+            if (fragment.isValid()) {
+                m_pft.pushPFTFrag(fragment);
+            }
+
+            auto af = m_pft.getNextAFPacket();
+            if (not af.empty()) {
+                decode_state_t st = decode_afpacket(af);
+
+                if (st.complete) {
+                    m_eti_writer.assemble();
+                }
+            }
+
+        }
+        else {
+            etiLog.log(warn,"Unknown %c!", *m_input_data.data());
+            m_input_data.erase(m_input_data.begin());
+        }
+    }
+}
+
+void ETIDecoder::push_packet(const vector<uint8_t> &buf)
+{
+    if (buf.size() < 2) {
+        throw std::invalid_argument("Not enough bytes to read EDI packet header");
+    }
+
+    if (buf[0] == 'A' and buf[1] == 'F') {
+        const decode_state_t st = decode_afpacket(buf);
+
+        if (st.complete) {
+            m_eti_writer.assemble();
+        }
+
+    }
+    else if (buf[0] == 'P' and buf[1] == 'F') {
+        PFT::Fragment fragment;
+        fragment.loadData(buf);
+
+        if (fragment.isValid()) {
+            m_pft.pushPFTFrag(fragment);
+        }
+
+        auto af = m_pft.getNextAFPacket();
+        if (not af.empty()) {
+            decode_state_t st = decode_afpacket(af);
+
+            if (st.complete) {
+                m_eti_writer.assemble();
+            }
+        }
+    }
+    else {
+        const char packettype[3] = {(char)buf[0], (char)buf[1], '\0'};
+        std::stringstream ss;
+        ss << "Unknown EDI packet ";
+        ss << packettype;
+        throw std::invalid_argument(ss.str());
+    }
+}
+
+void ETIDecoder::setMaxDelay(int num_af_packets)
+{
+    m_pft.setMaxDelay(num_af_packets);
+}
+
+#define AFPACKET_HEADER_LEN 10 // includes SYNC
+
+decode_state_t ETIDecoder::decode_afpacket(
+        const std::vector<uint8_t> &input_data)
+{
+    if (input_data.size() < AFPACKET_HEADER_LEN) {
+        return {false, 0};
+    }
+
+    // read length from packet
+    uint32_t taglength = read_32b(input_data.begin() + 2);
+    uint16_t seq = read_16b(input_data.begin() + 6);
+
+    bool has_crc = (input_data[8] & 0x80) ? true : false;
+    uint8_t revision = input_data[8] & 0x7F;
+    uint8_t pt = input_data[9];
+    if (pt != 'T') {
+        // only support Tag
+        return {false, 0};
+    }
+
+    const size_t crclength = 2;
+    if (input_data.size() < AFPACKET_HEADER_LEN + taglength + crclength) {
+        return {false, 0};
+    }
+
+    if (not has_crc) {
+        throw invalid_argument("AF packet not supported, has no CRC");
+    }
+
+    uint16_t crc = 0xffff;
+    for (size_t i = 0; i < AFPACKET_HEADER_LEN + taglength; i++) {
+        crc = crc16(crc, &input_data[i], 1);
+    }
+    crc ^= 0xffff;
+
+    uint16_t packet_crc = read_16b(input_data.begin() + AFPACKET_HEADER_LEN + taglength);
+
+    if (packet_crc != crc) {
+        throw invalid_argument(
+                "AF Packet crc wrong");
+    }
+    else {
+        vector<uint8_t> payload(taglength);
+        copy(input_data.begin() + AFPACKET_HEADER_LEN,
+                input_data.begin() + AFPACKET_HEADER_LEN + taglength,
+                payload.begin());
+
+        return {decode_tagpacket(payload),
+            AFPACKET_HEADER_LEN + taglength + 2};
+    }
+}
+
+bool ETIDecoder::decode_tagpacket(const vector<uint8_t> &payload)
+{
+    size_t length = 0;
+
+    bool success = true;
+
+    for (size_t i = 0; i + 8 < payload.size(); i += 8 + length) {
+        char tag_sz[5];
+        tag_sz[4] = '\0';
+        copy(payload.begin() + i, payload.begin() + i + 4, tag_sz);
+
+        string tag(tag_sz);
+
+        uint32_t taglength = read_32b(payload.begin() + i + 4);
+
+        if (taglength % 8 != 0) {
+            etiLog.log(warn, "Invalid tag length!");
+            break;
+        }
+        taglength /= 8;
+
+        length = taglength;
+
+        vector<uint8_t> tag_value(taglength);
+        copy(   payload.begin() + i+8,
+                payload.begin() + i+8+taglength,
+                tag_value.begin());
+
+        bool tagsuccess = false;
+        if (tag == "*ptr") {
+            tagsuccess = decode_starptr(tag_value);
+        }
+        else if (tag == "deti") {
+            tagsuccess = decode_deti(tag_value);
+        }
+        else if (tag.substr(0, 3) == "est") {
+            uint8_t n = tag_sz[3];
+            tagsuccess = decode_estn(tag_value, n);
+        }
+        else if (tag == "*dmy") {
+            tagsuccess = decode_stardmy(tag_value);
+        }
+        else {
+            etiLog.log(warn, "Unknown TAG %s", tag.c_str());
+            break;
+        }
+
+        if (not tagsuccess) {
+            etiLog.log(warn, "Error decoding TAG %s", tag.c_str());
+            success = tagsuccess;
+            break;
+        }
+    }
+
+    return success;
+}
+
+bool ETIDecoder::decode_starptr(const vector<uint8_t> &value)
+{
+    if (value.size() != 0x40 / 8) {
+        etiLog.log(warn, "Incorrect length %02lx for *PTR", value.size());
+        return false;
+    }
+
+    char protocol_sz[5];
+    protocol_sz[4] = '\0';
+    copy(value.begin(), value.begin() + 4, protocol_sz);
+    string protocol(protocol_sz);
+
+    uint16_t major = read_16b(value.begin() + 4);
+    uint16_t minor = read_16b(value.begin() + 6);
+
+    m_eti_writer.update_protocol(protocol, major, minor);
+
+    return true;
+}
+
+bool ETIDecoder::decode_deti(const vector<uint8_t> &value)
+{
+    /*
+    uint16_t detiHeader = fct | (fcth << 8) | (rfudf << 13) | (ficf << 14) | (atstf << 15);
+    packet.push_back(detiHeader >> 8);
+    packet.push_back(detiHeader & 0xFF);
+    */
+
+    uint16_t detiHeader = read_16b(value.begin());
+
+    eti_fc_data fc;
+
+    fc.atstf = (detiHeader >> 15) & 0x1;
+    fc.ficf = (detiHeader >> 14) & 0x1;
+    bool rfudf = (detiHeader >> 13) & 0x1;
+    uint8_t fcth = (detiHeader >> 8) & 0x1F;
+    uint8_t fct = detiHeader & 0xFF;
+
+    fc.dflc = fcth * 250 + fct; // modulo 5000 counter
+
+    uint32_t etiHeader = read_32b(value.begin() + 2);
+
+    uint8_t stat = (etiHeader >> 24) & 0xFF;
+
+    fc.mid = (etiHeader >> 22) & 0x03;
+    fc.fp = (etiHeader >> 19) & 0x07;
+    bool rfa = (etiHeader >> 17) & 0x1;
+    bool rfu = (etiHeader >> 16) & 0x1;
+    uint16_t mnsc = etiHeader & 0xFFFF;
+
+    const size_t fic_length_words = (fc.ficf ? (fc.mid == 3 ? 32 : 24) : 0);
+    const size_t fic_length = 4 * fic_length_words;
+
+    const size_t expected_length = 2 + 4 +
+        (fc.atstf ? 1 + 4 + 3 : 0) +
+        fic_length +
+        (rfudf ? 3 : 0);
+
+    if (value.size() != expected_length) {
+        etiLog.log(warn, "     Assertion error: value.size() != expected_length: %zu %zu\n",
+                value.size(), expected_length);
+        assert(false);
+    }
+
+    m_eti_writer.update_err(stat);
+    m_eti_writer.update_mnsc(mnsc);
+
+    size_t i = 2 + 4;
+
+    if (fc.atstf) {
+        uint8_t utco = value[i];
+        i++;
+
+        uint32_t seconds = read_32b(value.begin() + i);
+        i += 4;
+
+        m_eti_writer.update_edi_time(utco, seconds);
+
+        fc.tsta = read_24b(value.begin() + i);
+        i += 3;
+    }
+    else {
+        // Null timestamp, ETSI ETS 300 799, C.2.2
+        fc.tsta = 0xFFFFFF;
+    }
+
+
+    if (fc.ficf) {
+        vector<uint8_t> fic(fic_length);
+        copy(   value.begin() + i,
+                value.begin() + i + fic_length,
+                fic.begin());
+        i += fic_length;
+
+        m_eti_writer.update_fic(fic);
+    }
+
+    if (rfudf) {
+        uint32_t rfud = read_24b(value.begin() + i);
+
+        i += 3;
+    }
+
+    m_eti_writer.update_fc_data(fc);
+
+    return true;
+}
+
+bool ETIDecoder::decode_estn(const vector<uint8_t> &value, uint8_t n)
+{
+    uint32_t sstc = read_24b(value.begin());
+
+    eti_stc_data stc;
+
+    stc.scid = (sstc >> 18) & 0x3F;
+    stc.sad = (sstc >> 8) & 0x3FF;
+    stc.tpl = (sstc >> 2) & 0x3F;
+    uint8_t rfa = sstc & 0x3;
+
+    copy(   value.begin() + 3,
+            value.end(),
+            back_inserter(stc.mst));
+
+    m_eti_writer.add_subchannel(stc);
+
+    return true;
+}
+
+bool ETIDecoder::decode_stardmy(const vector<uint8_t> &value)
+{
+    return true;
+}
+
+}
diff --git a/lib/edi/ETIDecoder.hpp b/lib/edi/ETIDecoder.hpp
new file mode 100644
index 0000000..e0c7218
--- /dev/null
+++ b/lib/edi/ETIDecoder.hpp
@@ -0,0 +1,74 @@
+/*
+   Copyright (C) 2016
+   Matthias P. Braendli, matthias.braendli@mpb.li
+
+   http://opendigitalradio.org
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+#pragma once
+
+#include <stdint.h>
+#include <deque>
+#include <string>
+#include <vector>
+#include "ETIWriter.hpp"
+#include "PFT.hpp"
+
+namespace EdiDecoder {
+
+struct decode_state_t {
+    decode_state_t(bool _complete, size_t _num_bytes_consumed) :
+        complete(_complete), num_bytes_consumed(_num_bytes_consumed) {}
+    bool complete;
+    size_t num_bytes_consumed;
+};
+
+class ETIDecoder {
+    public:
+        ETIDecoder(ETIWriter& eti_writer);
+
+        /* Push bytes into the decoder. The buf can contain more
+         * than a single packet. This is useful when reading from streams
+         * (files, TCP)
+         */
+        void push_bytes(const std::vector<uint8_t> &buf);
+
+        /* Push a complete packet into the decoder. Useful for UDP and other
+         * datagram-oriented protocols.
+         */
+        void push_packet(const std::vector<uint8_t> &buf);
+
+        /* Set the maximum delay in number of AF Packets before we
+         * abandon decoding a given pseq.
+         */
+        void setMaxDelay(int num_af_packets);
+
+    private:
+        decode_state_t decode_afpacket(const std::vector<uint8_t> &input_data);
+        bool decode_tagpacket(const std::vector<uint8_t> &payload);
+        bool decode_starptr(const std::vector<uint8_t> &value);
+        bool decode_deti(const std::vector<uint8_t> &value);
+        bool decode_estn(const std::vector<uint8_t> &value, uint8_t n);
+        bool decode_stardmy(const std::vector<uint8_t> &value);
+
+        ETIWriter& m_eti_writer;
+
+        PFT::PFT m_pft;
+
+        std::vector<uint8_t> m_input_data;
+};
+
+}
diff --git a/lib/edi/ETIWriter.cpp b/lib/edi/ETIWriter.cpp
new file mode 100644
index 0000000..0eb3feb
--- /dev/null
+++ b/lib/edi/ETIWriter.cpp
@@ -0,0 +1,277 @@
+/*
+   Copyright (C) 2016
+   Matthias P. Braendli, matthias.braendli@mpb.li
+
+   http://opendigitalradio.org
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+#include "ETIWriter.hpp"
+#include "crc.h"
+#include "Log.h"
+#include <stdio.h>
+#include <cassert>
+#include <stdexcept>
+#include <sstream>
+
+namespace EdiDecoder {
+
+using namespace std;
+
+void ETIWriter::update_protocol(
+        const std::string& proto,
+        uint16_t major,
+        uint16_t minor)
+{
+    m_proto_valid = (proto == "DETI" and major == 0 and minor == 0);
+
+    if (not m_proto_valid) {
+        throw std::invalid_argument("Wrong EDI protocol");
+    }
+}
+
+void ETIWriter::reinit()
+{
+    m_proto_valid = false;
+    m_fc_valid = false;
+    m_fic.clear();
+    m_etiFrame.clear();
+    m_subchannels.clear();
+}
+
+void ETIWriter::update_err(uint8_t err)
+{
+    if (not m_proto_valid) {
+        throw std::logic_error("Cannot update ERR before protocol");
+    }
+    m_err = err;
+}
+
+void ETIWriter::update_fc_data(const eti_fc_data& fc_data)
+{
+    if (not m_proto_valid) {
+        throw std::logic_error("Cannot update FC before protocol");
+    }
+
+    m_fc_valid = false;
+    m_fc = fc_data;
+
+    if (not m_fc.ficf) {
+        throw std::invalid_argument("FIC must be present");
+    }
+
+    if (m_fc.mid > 4) {
+        throw std::invalid_argument("Invalid MID");
+    }
+
+    if (m_fc.fp > 7) {
+        throw std::invalid_argument("Invalid FP");
+    }
+
+    m_fc_valid = true;
+}
+
+void ETIWriter::update_fic(const std::vector<uint8_t>& fic)
+{
+    if (not m_proto_valid) {
+        throw std::logic_error("Cannot update FIC before protocol");
+    }
+
+    m_fic = fic;
+}
+
+void ETIWriter::update_edi_time(
+        uint32_t utco,
+        uint32_t seconds)
+{
+    if (not m_proto_valid) {
+        throw std::logic_error("Cannot update time before protocol");
+    }
+
+    m_utco = utco;
+    m_seconds = seconds;
+
+    // TODO check validity
+    m_time_valid = true;
+
+}
+
+void ETIWriter::update_mnsc(uint16_t mnsc)
+{
+    if (not m_proto_valid) {
+        throw std::logic_error("Cannot update MNSC before protocol");
+    }
+
+    m_mnsc = mnsc;
+}
+
+void ETIWriter::add_subchannel(const eti_stc_data& stc)
+{
+    if (not m_proto_valid) {
+        throw std::logic_error("Cannot add subchannel before protocol");
+    }
+
+    m_subchannels.push_back(stc);
+
+    if (m_subchannels.size() > 64) {
+        throw std::invalid_argument("Too many subchannels");
+    }
+
+}
+
+void ETIWriter::assemble()
+{
+    if (not m_proto_valid) {
+        throw std::logic_error("Cannot assemble ETI before protocol");
+    }
+
+    if (not m_fc_valid) {
+        throw std::logic_error("Cannot assemble ETI without FC");
+    }
+
+    if (m_fic.empty()) {
+        throw std::logic_error("Cannot assemble ETI without FIC data");
+    }
+
+    // Accept zero subchannels, because of an edge-case that can happen
+    // during reconfiguration. See ETS 300 799 Clause 5.3.3
+
+    // TODO check time validity
+
+    // ETS 300 799 Clause 5.3.2, but we don't support not having
+    // a FIC
+    if (    (m_fc.mid == 3 and m_fic.size() != 32 * 4) or
+            (m_fc.mid != 3 and m_fic.size() != 24 * 4) ) {
+        stringstream ss;
+        ss << "Invalid FIC length " << m_fic.size() <<
+            " for MID " << m_fc.mid;
+        throw std::invalid_argument(ss.str());
+    }
+
+
+    std::vector<uint8_t> eti;
+    eti.reserve(6144);
+
+    eti.push_back(m_err);
+
+    // FSYNC
+    if (m_fc.fct() % 2 == 1) {
+        eti.push_back(0xf8);
+        eti.push_back(0xc5);
+        eti.push_back(0x49);
+    }
+    else {
+        eti.push_back(0x07);
+        eti.push_back(0x3a);
+        eti.push_back(0xb6);
+    }
+
+    // LIDATA
+    // FC
+    eti.push_back(m_fc.fct());
+
+    const uint8_t NST = m_subchannels.size();
+
+    eti.push_back((m_fc.ficf << 7) | NST);
+
+    // We need to pack:
+    //  FP 3 bits
+    //  MID 2 bits
+    //  FL 11 bits
+
+    // FL: EN 300 799 5.3.6
+    uint16_t FL = NST + 1 + m_fic.size();
+    for (const auto& subch : m_subchannels) {
+        FL += subch.mst.size();
+    }
+
+    const uint16_t fp_mid_fl = (m_fc.fp << 13) | (m_fc.mid << 11) | FL;
+
+    eti.push_back(fp_mid_fl >> 8);
+    eti.push_back(fp_mid_fl & 0xFF);
+
+    // STC
+    for (const auto& subch : m_subchannels) {
+        eti.push_back( (subch.scid << 2) | (subch.sad & 0x300) );
+        eti.push_back( subch.sad & 0xff );
+        eti.push_back( (subch.tpl << 2) | ((subch.stl() & 0x300) >> 8) );
+        eti.push_back( subch.stl() & 0xff );
+    }
+
+    // EOH
+    // MNSC
+    eti.push_back(m_mnsc >> 8);
+    eti.push_back(m_mnsc & 0xFF);
+
+    // CRC
+    // Calculate CRC from eti[4] to current position
+    uint16_t eti_crc = 0xFFFF;
+    eti_crc = crc16(eti_crc, &eti[4], eti.size() - 4);
+    eti_crc ^= 0xffff;
+    eti.push_back(eti_crc >> 8);
+    eti.push_back(eti_crc & 0xFF);
+
+    const size_t mst_start = eti.size();
+    // MST
+    // FIC data
+    copy(m_fic.begin(), m_fic.end(), back_inserter(eti));
+
+    // Data stream
+    for (const auto& subch : m_subchannels) {
+        copy(subch.mst.begin(), subch.mst.end(), back_inserter(eti));
+    }
+
+    // EOF
+    // CRC
+    uint16_t mst_crc = 0xFFFF;
+    mst_crc = crc16(mst_crc, &eti[mst_start], eti.size() - mst_start);
+    mst_crc ^= 0xffff;
+    eti.push_back(mst_crc >> 8);
+    eti.push_back(mst_crc & 0xFF);
+
+    // RFU
+    eti.push_back(0xff);
+    eti.push_back(0xff);
+
+    // TIST
+    eti.push_back(m_fc.tsta >> 24);
+    eti.push_back((m_fc.tsta >> 16) & 0xFF);
+    eti.push_back((m_fc.tsta >> 8) & 0xFF);
+    eti.push_back(m_fc.tsta & 0xFF);
+
+    if (eti.size() > 6144) {
+        throw std::logic_error("ETI frame cannot be longer than 6144");
+    }
+
+    eti.resize(6144, 0x55);
+
+    m_etiFrame = eti;
+
+}
+
+std::vector<uint8_t> ETIWriter::getEtiFrame()
+{
+    if (m_etiFrame.empty()) {
+        return {};
+    }
+
+    vector<uint8_t> eti(move(m_etiFrame));
+    reinit();
+
+    return eti;
+}
+
+}
+
diff --git a/lib/edi/ETIWriter.hpp b/lib/edi/ETIWriter.hpp
new file mode 100644
index 0000000..001f537
--- /dev/null
+++ b/lib/edi/ETIWriter.hpp
@@ -0,0 +1,114 @@
+/*
+   Copyright (C) 2016
+   Matthias P. Braendli, matthias.braendli@mpb.li
+
+   http://opendigitalradio.org
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+#pragma once
+
+#include <stdint.h>
+#include <string>
+#include <vector>
+#include <list>
+#include "eti.hpp"
+
+namespace EdiDecoder {
+
+// Information for Frame Characterisation available in
+// EDI.
+//
+// Number of streams is given separately, and frame length
+// is calculeted in the ETIWriter
+struct eti_fc_data {
+    bool atstf;
+    uint32_t tsta;
+    bool ficf;
+    uint16_t dflc;
+    uint8_t mid;
+    uint8_t fp;
+
+    uint8_t fct(void) const { return dflc % 250; }
+};
+
+// Information for a subchannel available in EDI
+struct eti_stc_data {
+    uint8_t scid;
+    uint8_t sad;
+    uint8_t tpl;
+    std::vector<uint8_t> mst;
+
+    // Return the length of the MST in multiples of 64 bits
+    uint16_t stl(void) const { return mst.size() / 8; }
+};
+
+class ETIWriter {
+    public:
+        // Tell the ETIWriter what EDI protocol we receive in *ptr.
+        // This is not part of the ETI data, but is used as check
+        void update_protocol(
+                const std::string& proto,
+                uint16_t major,
+                uint16_t minor);
+
+        // Update the data for the frame characterisation
+        void update_fc_data(const eti_fc_data& fc_data);
+
+        void update_fic(const std::vector<uint8_t>& fic);
+
+        void update_err(uint8_t err);
+
+        // In addition to TSTA in ETI, EDI also transports more time
+        // stamp information.
+        void update_edi_time(
+                uint32_t utco,
+                uint32_t seconds);
+
+        void update_mnsc(uint16_t mnsc);
+
+        void add_subchannel(const eti_stc_data& stc);
+
+        // Tell the ETIWriter that the AFPacket is complete
+        void assemble(void);
+
+        // Return the assembled ETI frame or an empty frame if not ready
+        std::vector<uint8_t> getEtiFrame(void);
+
+    private:
+        void reinit(void);
+
+        bool m_proto_valid = false;
+
+        uint8_t m_err;
+
+        bool m_fc_valid = false;
+        eti_fc_data m_fc;
+
+        // m_fic is valid if non-empty
+        std::vector<uint8_t> m_fic;
+
+        std::vector<uint8_t> m_etiFrame;
+
+        std::list<eti_stc_data> m_subchannels;
+
+        bool m_time_valid = false;
+        uint32_t m_utco;
+        uint32_t m_seconds;
+
+        uint16_t m_mnsc = 0xffff;
+};
+
+}
diff --git a/lib/edi/PFT.cpp b/lib/edi/PFT.cpp
new file mode 100644
index 0000000..fa5840c
--- /dev/null
+++ b/lib/edi/PFT.cpp
@@ -0,0 +1,553 @@
+/* ------------------------------------------------------------------
+ * Copyright (C) 2017 AVT GmbH - Fabien Vercasson
+ * Copyright (C) 2016 Matthias P. Braendli
+ *                    matthias.braendli@mpb.li
+ *
+ * http://opendigitalradio.org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ * -------------------------------------------------------------------
+ */
+
+#include <stdio.h>
+#include <cassert>
+#include <cstring>
+#include <sstream>
+#include <stdexcept>
+#include <algorithm>
+#include "crc.h"
+#include "PFT.hpp"
+#include "Log.h"
+#include "buffer_unpack.hpp"
+extern "C" {
+#include "fec/fec.h"
+}
+
+namespace EdiDecoder {
+
+namespace PFT {
+
+using namespace std;
+
+const findex_t NUM_AFBUILDERS_TO_KEEP = 10;
+
+static bool checkCRC(const uint8_t *buf, size_t size)
+{
+    const uint16_t crc_from_packet = read_16b(buf + size - 2);
+    uint16_t crc_calc = 0xffff;
+    crc_calc = crc16(crc_calc, buf, size - 2);
+    crc_calc ^= 0xffff;
+
+    return crc_from_packet == crc_calc;
+}
+
+class FECDecoder {
+    public:
+        FECDecoder() {
+            m_rs_handler = init_rs_char(
+                    symsize, gfPoly, firstRoot, primElem, nroots, pad);
+        }
+        FECDecoder(const FECDecoder& other) = delete;
+        FECDecoder& operator=(const FECDecoder& other) = delete;
+        ~FECDecoder() {
+            free_rs_char(m_rs_handler);
+        }
+
+        // return -1 in case of failure, non-negative value if errors
+        // were corrected.
+        // Known positions of erasures should be given in eras_pos to
+        // improve decoding probability. After calling this function
+        // eras_pos will contain the positions of the corrected errors.
+        int decode(vector<uint8_t> &data, vector<int> &eras_pos) {
+            assert(data.size() == N);
+            const size_t no_eras = eras_pos.size();
+
+            eras_pos.resize(nroots);
+            int num_err = decode_rs_char(m_rs_handler, data.data(),
+                    eras_pos.data(), no_eras);
+            if (num_err > 0) {
+                eras_pos.resize(num_err);
+            }
+            return num_err;
+        }
+
+        // return -1 in case of failure, non-negative value if errors
+        // were corrected. No known erasures.
+        int decode(vector<uint8_t> &data) {
+            assert(data.size() == N);
+            int num_err = decode_rs_char(m_rs_handler, data.data(), nullptr, 0);
+            return num_err;
+        }
+
+    private:
+        void* m_rs_handler;
+
+        const int firstRoot = 1; // Discovered by analysing EDI dump
+        const int gfPoly = 0x11d;
+
+        // The encoding has to be 255, 207 always, because the chunk has to
+        // be padded at the end, and not at the beginning as libfec would
+        // do
+        const size_t N = 255;
+        const size_t K = 207;
+        const int primElem = 1;
+        const int symsize = 8;
+        const size_t nroots = N - K; // For EDI PFT, this must be 48
+        const size_t pad = ((1 << symsize) - 1) - N; // is 255-N
+
+};
+
+size_t Fragment::loadData(const std::vector<uint8_t> &buf)
+{
+    const size_t header_len = 14;
+    if (buf.size() < header_len) {
+        return 0;
+    }
+
+    size_t index = 0;
+
+    // Parse PFT Fragment Header (ETSI TS 102 821 V1.4.1 ch7.1)
+    if (not (buf[0] == 'P' and buf[1] == 'F') ) {
+        throw invalid_argument("Invalid PFT SYNC bytes");
+    }
+    index += 2; // Psync
+
+    _Pseq = read_16b(buf.begin()+index); index += 2;
+    _Findex = read_24b(buf.begin()+index); index += 3;
+    _Fcount = read_24b(buf.begin()+index); index += 3;
+    _FEC = unpack1bit(buf[index], 0);
+    _Addr = unpack1bit(buf[index], 1);
+    _Plen = read_16b(buf.begin()+index) & 0x3FFF; index += 2;
+
+    const size_t required_len = header_len +
+        (_FEC ? 1 : 0) +
+        (_Addr ? 2 : 0) +
+        2; // CRC
+    if (buf.size() < required_len) {
+        return 0;
+    }
+
+    // Optional RS Header
+    _RSk = 0;
+    _RSz = 0;
+    if (_FEC) {
+        _RSk = buf[index]; index += 1;
+        _RSz = buf[index]; index += 1;
+    }
+
+    // Optional transport header
+    _Source = 0;
+    _Dest = 0;
+    if (_Addr) {
+        _Source = read_16b(buf.begin()+index); index += 2;
+        _Dest = read_16b(buf.begin()+index); index += 2;
+    }
+
+    index += 2;
+    const bool crc_valid = checkCRC(buf.data(), index);
+    const bool buf_has_enough_data = (buf.size() >= index + _Plen);
+
+    if (not buf_has_enough_data) {
+        return 0;
+    }
+
+    _valid = ((not _FEC) or crc_valid) and buf_has_enough_data;
+
+#if 0
+    if (!_valid) {
+        stringstream ss;
+        ss << "Invalid PF fragment: ";
+        if (_FEC) {
+            ss << " RSk=" << (uint32_t)_RSk << " RSz=" << (uint32_t)_RSz;
+        }
+
+        if (_Addr) {
+            ss << " Source=" << _Source << " Dest=" << _Dest;
+        }
+        etiLog.log(debug, "%s\n", ss.str().c_str());
+    }
+#endif
+
+    _payload.clear();
+    if (_valid) {
+        copy( buf.begin()+index,
+                buf.begin()+index+_Plen,
+                back_inserter(_payload));
+        index += _Plen;
+    }
+
+    return index;
+}
+
+
+AFBuilder::AFBuilder(pseq_t Pseq, findex_t Fcount, int lifetime)
+{
+    _Pseq = Pseq;
+    _Fcount = Fcount;
+    lifeTime = lifetime;
+}
+
+void AFBuilder::pushPFTFrag(const Fragment &frag)
+{
+    if (_Pseq != frag.Pseq() or _Fcount != frag.Fcount()) {
+        throw invalid_argument("Invalid PFT fragment Pseq or Fcount");
+    }
+    const auto Findex = frag.Findex();
+    const bool fragment_already_received = _fragments.count(Findex);
+
+    if (not fragment_already_received)
+    {
+        _fragments[Findex] = frag;
+    }
+}
+
+bool Fragment::checkConsistency(const Fragment& other) const
+{
+    /* Consistency check, TS 102 821 Clause 7.3.2.
+     *
+     * Every PFT Fragment produced from a single AF or RS Packet shall have
+     * the same values in all of the PFT Header fields except for the Findex,
+     * Plen and HCRC fields.
+     */
+
+    return other._Fcount == _Fcount and
+        other._FEC == _FEC and
+        other._RSk == _RSk and
+        other._RSz == _RSz and
+        other._Addr == _Addr and
+        other._Source == _Source and
+        other._Dest == _Dest and
+
+        /* The Plen field of all fragments shall be the s for the initial f-1
+         * fragments and s - (L%f) for the final fragment.
+         * Note that when Reed Solomon has been used, all fragments will be of
+         * length s.
+         */
+        (_FEC ? other._Plen == _Plen : true);
+}
+
+
+AFBuilder::decode_attempt_result_t AFBuilder::canAttemptToDecode() const
+{
+    if (_fragments.empty()) {
+        return AFBuilder::decode_attempt_result_t::no;
+    }
+
+    if (_fragments.size() == _Fcount) {
+        return AFBuilder::decode_attempt_result_t::yes;
+    }
+
+    /* Check that all fragments are consistent */
+    const Fragment& first = _fragments.begin()->second;
+    if (not std::all_of(_fragments.begin(), _fragments.end(),
+            [&](const pair<int, Fragment>& pair) {
+                const Fragment& frag = pair.second;
+                return first.checkConsistency(frag) and _Pseq == frag.Pseq();
+            }) ) {
+        throw invalid_argument("Inconsistent PFT fragments");
+    }
+
+    // Calculate the minimum number of fragments necessary to apply FEC.
+    // This can't be done with the last fragment that may have a
+    // smaller size
+    // ETSI TS 102 821 V1.4.1 ch 7.4.4
+    auto frag_it = _fragments.begin();
+    if (frag_it->second.Fcount() == _Fcount - 1) {
+        frag_it++;
+
+        if (frag_it == _fragments.end()) {
+            return AFBuilder::decode_attempt_result_t::no;
+        }
+    }
+
+    const Fragment& frag = frag_it->second;
+
+    if ( frag.FEC() )
+    {
+        const uint16_t _Plen = frag.Plen();
+
+        /* max number of RS chunks that may have been sent */
+        const uint32_t _cmax = (_Fcount*_Plen) / (frag.RSk()+48);
+        assert(_cmax > 0);
+
+        /* Receiving _rxmin fragments does not guarantee that decoding
+         * will succeed! */
+        const uint32_t _rxmin = _Fcount - (_cmax*48)/_Plen;
+
+        if (_fragments.size() >= _rxmin) {
+            return AFBuilder::decode_attempt_result_t::maybe;
+        }
+    }
+
+    return AFBuilder::decode_attempt_result_t::no;
+}
+
+std::vector<uint8_t> AFBuilder::extractAF() const
+{
+    if (not _af_packet.empty()) {
+        return _af_packet;
+    }
+
+    bool ok = false;
+
+    if (canAttemptToDecode() != AFBuilder::decode_attempt_result_t::no) {
+
+        auto frag_it = _fragments.begin();
+        if (frag_it->second.Fcount() == _Fcount - 1) {
+            frag_it++;
+
+            if (frag_it == _fragments.end()) {
+                throw std::runtime_error("Invalid attempt at extracting AF");
+            }
+        }
+
+        const Fragment& ref_frag = frag_it->second;
+        const auto RSk = ref_frag.RSk();
+        const auto RSz = ref_frag.RSz();
+        const auto Plen = ref_frag.Plen();
+
+        if ( ref_frag.FEC() )
+        {
+            const uint32_t cmax = (_Fcount*Plen) / (RSk+48);
+
+            // Keep track of erasures (missing fragments) for
+            // every chunk
+            map<int, vector<int> > erasures;
+
+
+            // Assemble fragments into a RS block, immediately
+            // deinterleaving it.
+            vector<uint8_t> rs_block(Plen * _Fcount);
+            for (size_t j = 0; j < _Fcount; j++) {
+                const bool fragment_present = _fragments.count(j);
+                if (fragment_present) {
+                    const auto& fragment = _fragments.at(j).payload();
+
+                    if (j != _Fcount - 1 and fragment.size() != Plen) {
+                        throw runtime_error("Incorrect fragment length " +
+                                to_string(fragment.size()) + " " +
+                                to_string(Plen));
+                    }
+
+                    if (j == _Fcount - 1 and fragment.size() > Plen) {
+                        throw runtime_error("Incorrect last fragment length " +
+                                to_string(fragment.size()) + " " +
+                                to_string(Plen));
+                    }
+
+                    size_t k = 0;
+                    for (; k < fragment.size(); k++) {
+                        rs_block[k * _Fcount + j] = fragment[k];
+                    }
+
+                    for (; k < Plen; k++) {
+                        rs_block[k * _Fcount + j] = 0x00;
+                    }
+                }
+                else {
+                    // fill with zeros if fragment is missing
+                    for (size_t k = 0; k < Plen; k++) {
+                        rs_block[k * _Fcount + j] = 0x00;
+
+                        const size_t chunk_ix = (k * _Fcount + j) / (RSk + 48);
+                        const size_t chunk_offset = (k * _Fcount + j) % (RSk + 48);
+                        erasures[chunk_ix].push_back(chunk_offset);
+                    }
+                }
+            }
+
+            // The RS block is a concatenation of chunks of RSk bytes + 48 parity
+            // followed by RSz padding
+
+            FECDecoder fec;
+            for (size_t i = 0; i < cmax; i++) {
+                // We need to pad the chunk ourself
+                vector<uint8_t> chunk(255);
+                const auto& block_begin = rs_block.begin() + (RSk + 48) * i;
+                copy(block_begin, block_begin + RSk, chunk.begin());
+                // bytes between RSk and 207 are 0x00 already
+                copy(block_begin + RSk, block_begin + RSk + 48,
+                        chunk.begin() + 207);
+
+                int errors_corrected = -1;
+                if (erasures.count(i)) {
+                    errors_corrected = fec.decode(chunk, erasures[i]);
+                }
+                else {
+                    errors_corrected = fec.decode(chunk);
+                }
+
+                if (errors_corrected == -1) {
+                    _af_packet.clear();
+                    return {};
+                }
+
+#if 0
+                if (errors_corrected > 0) {
+                    etiLog.log(debug, "Corrected %d errors at ", errors_corrected);
+                    for (const auto &index : erasures[i]) {
+                        etiLog.log(debug, " %d", index);
+                    }
+                    etiLog.log(debug, "\n");
+                }
+#endif
+
+                _af_packet.insert(_af_packet.end(), chunk.begin(), chunk.begin() + RSk);
+            }
+
+            _af_packet.resize(_af_packet.size() - RSz);
+        }
+        else {
+            // No FEC: just assemble fragments
+
+            for (size_t j = 0; j < _Fcount; ++j) {
+                const bool fragment_present = _fragments.count(j);
+                if (fragment_present)
+                {
+                    const auto& fragment = _fragments.at(j);
+
+                    _af_packet.insert(_af_packet.end(),
+                       fragment.payload().begin(),
+                       fragment.payload().end());
+                }
+                else {
+                    throw logic_error("Missing fragment");
+                }
+            }
+        }
+
+        // EDI specific, must have a CRC.
+        if( _af_packet.size() >= 12 ) {
+            ok = checkCRC(_af_packet.data(), _af_packet.size());
+
+            if (not ok) {
+                etiLog.log(debug, "Too many errors to reconstruct AF from %zu/%u"
+                        " PFT fragments\n", _fragments.size(), _Fcount);
+            }
+        }
+    }
+
+    if (not ok) {
+        _af_packet.clear();
+    }
+
+    return _af_packet;
+}
+
+void PFT::pushPFTFrag(const Fragment &fragment)
+{
+    // Start decoding the first pseq we receive. In normal
+    // operation without interruptions, the map should
+    // never become empty
+    if (m_afbuilders.empty()) {
+        m_next_pseq = fragment.Pseq();
+        etiLog.log(debug,"Initialise next_pseq to %u\n", m_next_pseq);
+    }
+
+    if (m_afbuilders.count(fragment.Pseq()) == 0) {
+        // The AFBuilder wants to know the lifetime in number of fragments,
+        // we know the delay in number of AF packets. Every AF packet
+        // is cut into Fcount fragments.
+        const int lifetime = fragment.Fcount() * m_max_delay;
+
+        // Build the afbuilder in the map in-place
+        m_afbuilders.emplace(std::piecewise_construct,
+                /* key */
+                std::forward_as_tuple(fragment.Pseq()),
+                /* builder */
+                std::forward_as_tuple(fragment.Pseq(), fragment.Fcount(), lifetime));
+    }
+
+    auto& p = m_afbuilders.at(fragment.Pseq());
+    p.pushPFTFrag(fragment);
+
+#if 0
+    etiLog.log(debug, "After new frag with pseq %u, afbuilders: ", fragment.Pseq());
+    for (const auto &k : m_afbuilders) {
+        etiLog.log(debug, "%u ", k.first);
+    }
+    etiLog.log(debug, "\n");
+#endif
+}
+
+
+std::vector<uint8_t> PFT::getNextAFPacket()
+{
+    if (m_afbuilders.count(m_next_pseq) == 0) {
+        assert(m_afbuilders.empty());
+        return {};
+    }
+
+    auto &builder = m_afbuilders.at(m_next_pseq);
+    //const auto lt = builder.lifeTime;
+    //const auto nf = builder.numberOfFragments();
+
+    using dar_t = AFBuilder::decode_attempt_result_t;
+
+    if (builder.canAttemptToDecode() == dar_t::yes) {
+        //etiLog.log(debug, "pseq %d (%d %d/%d) yes\n", m_next_pseq, lt, nf.first, nf.second);
+        auto afpacket = builder.extractAF();
+        assert(not afpacket.empty());
+        incrementNextPseq();
+        return afpacket;
+    }
+    else if (builder.canAttemptToDecode() == dar_t::maybe) {
+        //etiLog.log(debug, "pseq %d (%d %d/%d) maybe\n", m_next_pseq, lt, nf.first, nf.second);
+        builder.lifeTime--;
+        if (builder.lifeTime == 0) {
+            // Attempt Reed-Solomon decoding
+            auto afpacket = builder.extractAF();
+
+            if (afpacket.empty()) {
+                etiLog.log(debug,"pseq %d timed out after RS\n", m_next_pseq);
+            }
+            incrementNextPseq();
+            return afpacket;
+        }
+    }
+    else {
+        //etiLog.log(debug, "pseq %d (%d %d/%d) no\n", m_next_pseq, lt, nf.first, nf.second);
+        builder.lifeTime--;
+        if (builder.lifeTime == 0) {
+            etiLog.log(debug, "pseq %d timed out\n", m_next_pseq);
+            incrementNextPseq();
+        }
+    }
+
+    return {};
+}
+
+void PFT::setMaxDelay(int num_af_packets)
+{
+    m_max_delay = num_af_packets;
+}
+
+bool PFT::isPacketBuildable(pseq_t pseq) const
+{
+    return m_afbuilders.count(pseq) > 0 and
+        not m_afbuilders.at(pseq).extractAF().empty();
+}
+
+void PFT::incrementNextPseq()
+{
+    if (m_afbuilders.count(m_next_pseq - NUM_AFBUILDERS_TO_KEEP) > 0) {
+        m_afbuilders.erase(m_next_pseq - NUM_AFBUILDERS_TO_KEEP);
+    }
+
+    m_next_pseq++;
+}
+
+}
+
+}
diff --git a/lib/edi/PFT.hpp b/lib/edi/PFT.hpp
new file mode 100644
index 0000000..9ab14ae
--- /dev/null
+++ b/lib/edi/PFT.hpp
@@ -0,0 +1,154 @@
+/* ------------------------------------------------------------------
+ * Copyright (C) 2017 AVT GmbH - Fabien Vercasson
+ * Copyright (C) 2016 Matthias P. Braendli
+ *                    matthias.braendli@mpb.li
+ *
+ * http://opendigitalradio.org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ * -------------------------------------------------------------------
+ */
+
+#pragma once
+#include <stdio.h>
+#include <cassert>
+#include <vector>
+#include <map>
+#include <stdint.h>
+
+namespace EdiDecoder {
+namespace PFT {
+
+using pseq_t = uint16_t;
+using findex_t = uint32_t; // findex is a 24-bit value
+
+class Fragment
+{
+    public:
+        // Load the data for one fragment from buf into
+        // the Fragment.
+        // \returns the number of bytes of useful data found in buf
+        // A non-zero return value doesn't imply a valid fragment
+        // the isValid() method must be used to verify this.
+        size_t loadData(const std::vector<uint8_t> &buf);
+
+        bool isValid() const { return _valid; }
+        pseq_t Pseq() const { return _Pseq; }
+        findex_t Findex() const { return _Findex; }
+        findex_t Fcount() const { return _Fcount; }
+        bool FEC() const { return _FEC; }
+        uint16_t Plen() const { return _Plen; }
+        uint8_t RSk() const { return _RSk; }
+        uint8_t RSz() const { return _RSz; }
+        const std::vector<uint8_t>& payload() const
+            { return _payload; }
+
+        bool checkConsistency(const Fragment& other) const;
+
+    private:
+        std::vector<uint8_t> _payload;
+
+        pseq_t _Pseq;
+        findex_t _Findex;
+        findex_t _Fcount;
+        bool _FEC;
+        bool _Addr;
+        uint16_t _Plen;
+        uint8_t _RSk;
+        uint8_t _RSz;
+        uint16_t _Source;
+        uint16_t _Dest;
+        bool _valid;
+};
+
+/* The AFBuilder collects Fragments and builds an Application Frame
+ * out of them. It does error correction if necessary
+ */
+class AFBuilder
+{
+    public:
+        enum class decode_attempt_result_t {
+            yes,   // The AF packet can be build because all fragments are present
+            maybe, // RS decoding may correctly decode the AF packet
+            no,    // Not enough fragments present to permit RS
+        };
+
+        AFBuilder(pseq_t Pseq, findex_t Fcount, int lifetime);
+
+        void pushPFTFrag(const Fragment &frag);
+
+        /* Assess if it may be possible to decode this AF packet */
+        decode_attempt_result_t canAttemptToDecode() const;
+
+        /* Try to build the AF with received fragments.
+         * Apply error correction if necessary (missing packets/CRC errors)
+         * \return an empty vector if building the AF is not possible
+         */
+        std::vector<uint8_t> extractAF(void) const;
+
+        std::pair<findex_t, findex_t>
+            numberOfFragments(void) const {
+                return {_fragments.size(), _Fcount};
+            }
+
+        /* The user of this instance can keep track of the lifetime of this
+         * builder
+         */
+        int lifeTime;
+
+    private:
+
+        // A map from fragment index to fragment
+        std::map<findex_t, Fragment> _fragments;
+
+        // cached version of decoded AF packet
+        mutable std::vector<uint8_t> _af_packet;
+
+        pseq_t _Pseq;
+        findex_t _Fcount;
+};
+
+class PFT
+{
+    public:
+        void pushPFTFrag(const Fragment &fragment);
+
+        /* Try to build the AF packet for the next pseq. This might
+         * skip one pseq according to the maximum delay setting.
+         *
+         * \return an empty vector if building the AF is not possible
+         */
+        std::vector<uint8_t> getNextAFPacket(void);
+
+        /* Set the maximum delay in number of AF Packets before we
+         * abandon decoding a given pseq.
+         */
+        void setMaxDelay(int num_af_packets);
+
+    private:
+        bool isPacketBuildable(pseq_t pseq) const;
+
+        void incrementNextPseq(void);
+
+        pseq_t m_next_pseq;
+        int m_max_delay = 10;
+
+        // Keep one AFBuilder for each Pseq
+        std::map<pseq_t, AFBuilder> m_afbuilders;
+
+};
+
+}
+
+}
diff --git a/lib/edi/README.md b/lib/edi/README.md
new file mode 100644
index 0000000..b6ab67a
--- /dev/null
+++ b/lib/edi/README.md
@@ -0,0 +1 @@
+These files are copied from the odr-edilib project.
diff --git a/lib/edi/buffer_unpack.hpp b/lib/edi/buffer_unpack.hpp
new file mode 100644
index 0000000..05a1534
--- /dev/null
+++ b/lib/edi/buffer_unpack.hpp
@@ -0,0 +1,62 @@
+/*
+   Copyright (C) 2016
+   Matthias P. Braendli, matthias.braendli@mpb.li
+
+   http://opendigitalradio.org
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#pragma once
+#include <stdint.h>
+
+namespace EdiDecoder {
+
+template<class T>
+uint16_t read_16b(T buf)
+{
+    uint16_t value = 0;
+    value = (uint16_t)(buf[0]) << 8;
+    value |= (uint16_t)(buf[1]);
+    return value;
+}
+
+template<class T>
+uint32_t read_24b(T buf)
+{
+    uint32_t value = 0;
+    value = (uint32_t)(buf[0]) << 16;
+    value |= (uint32_t)(buf[1]) << 8;
+    value |= (uint32_t)(buf[2]);
+    return value;
+}
+
+template<class T>
+uint32_t read_32b(T buf)
+{
+    uint32_t value = 0;
+    value = (uint32_t)(buf[0]) << 24;
+    value |= (uint32_t)(buf[1]) << 16;
+    value |= (uint32_t)(buf[2]) << 8;
+    value |= (uint32_t)(buf[3]);
+    return value;
+}
+
+inline uint32_t unpack1bit(uint8_t byte, int bitpos)
+{
+    return (byte & 1 << (7-bitpos)) > (7-bitpos);
+}
+
+}
diff --git a/lib/edi/eti.cpp b/lib/edi/eti.cpp
new file mode 100644
index 0000000..3fc99ef
--- /dev/null
+++ b/lib/edi/eti.cpp
@@ -0,0 +1,64 @@
+/*
+   Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the
+   Queen in Right of Canada (Communications Research Center Canada)
+
+   Copyright (C) 2016
+   Matthias P. Braendli, matthias.braendli@mpb.li
+
+   http://opendigitalradio.org
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "eti.hpp"
+
+namespace EdiDecoder {
+
+//definitions des structures des champs du ETI(NI, G703)
+
+uint16_t eti_FC::getFrameLength()
+{
+    return (uint16_t)((FL_high << 8) | FL_low);
+}
+
+void eti_FC::setFrameLength(uint16_t length)
+{
+    FL_high = (length >> 8) & 0x07;
+    FL_low = length & 0xff;
+}
+
+void eti_STC::setSTL(uint16_t length)
+{
+    STL_high = length >> 8;
+    STL_low = length & 0xff;
+}
+
+uint16_t eti_STC::getSTL()
+{
+    return (uint16_t)((STL_high << 8) + STL_low);
+}
+
+void eti_STC::setStartAddress(uint16_t address)
+{
+    startAddress_high = address >> 8;
+    startAddress_low = address & 0xff;
+}
+
+uint16_t eti_STC::getStartAddress()
+{
+    return (uint16_t)((startAddress_high << 8) + startAddress_low);
+}
+
+}
diff --git a/lib/edi/eti.hpp b/lib/edi/eti.hpp
new file mode 100644
index 0000000..451ca48
--- /dev/null
+++ b/lib/edi/eti.hpp
@@ -0,0 +1,114 @@
+/*
+   Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the
+   Queen in Right of Canada (Communications Research Center Canada)
+
+   Copyright (C) 2016
+   Matthias P. Braendli, matthias.braendli@mpb.li
+
+   http://opendigitalradio.org
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#pragma once
+
+#include <stdint.h>
+#define PACKED __attribute__ ((packed))
+
+#include <time.h>
+
+namespace EdiDecoder {
+
+struct eti_SYNC {
+    uint32_t ERR:8;
+    uint32_t FSYNC:24;
+} PACKED;
+
+struct eti_FC {
+    uint32_t FCT:8;
+    uint32_t NST:7;
+    uint32_t FICF:1;
+    uint32_t FL_high:3;
+    uint32_t MID:2;
+    uint32_t FP:3;
+    uint32_t FL_low:8;
+    uint16_t getFrameLength();
+    void setFrameLength(uint16_t length);
+} PACKED;
+
+struct eti_STC {
+    uint32_t startAddress_high:2;
+    uint32_t SCID:6;
+    uint32_t startAddress_low:8;
+    uint32_t STL_high:2;
+    uint32_t TPL:6;
+    uint32_t STL_low:8;
+    void setSTL(uint16_t length);
+    uint16_t getSTL();
+    void setStartAddress(uint16_t address);
+    uint16_t getStartAddress();
+} PACKED;
+
+struct eti_EOH {
+    uint16_t MNSC;
+    uint16_t CRC;
+} PACKED;
+
+struct eti_EOF {
+    uint16_t CRC;
+    uint16_t RFU;
+} PACKED;
+
+struct eti_TIST {
+    uint32_t TIST;
+} PACKED;
+
+struct eti_MNSC_TIME_0 {
+    uint32_t type:4;
+    uint32_t identifier:4;
+    uint32_t rfa:8;
+} PACKED;
+
+struct eti_MNSC_TIME_1 {
+    uint32_t second_unit:4;
+    uint32_t second_tens:3;
+    uint32_t accuracy:1;
+
+    uint32_t minute_unit:4;
+    uint32_t minute_tens:3;
+    uint32_t sync_to_frame:1;
+} PACKED;
+
+struct eti_MNSC_TIME_2 {
+    uint32_t hour_unit:4;
+    uint32_t hour_tens:4;
+
+    uint32_t day_unit:4;
+    uint32_t day_tens:4;
+} PACKED;
+
+struct eti_MNSC_TIME_3 {
+    uint32_t month_unit:4;
+    uint32_t month_tens:4;
+
+    uint32_t year_unit:4;
+    uint32_t year_tens:4;
+} PACKED;
+
+struct eti_extension_TIME {
+    uint32_t TIME_SECONDS;
+} PACKED;
+
+}
-- 
cgit v1.2.3