[
  {
    "path": ".editorconfig",
    "content": "# top-most EditorConfig file\nroot = true\n\n# Unix-style newlines with a newline ending every file\n[*]\nend_of_line = lf\ninsert_final_newline = true\n\n# 4 space indentation\n[*]\nindent_style = space\nindent_size = 4"
  },
  {
    "path": ".gitattributes",
    "content": "# Auto detect text files and perform LF normalization\n* text=auto\n\n# Custom for Visual Studio\n*.cs     diff=csharp\n\n# Standard to msysgit\n*.doc\t diff=astextplain\n*.DOC\t diff=astextplain\n*.docx diff=astextplain\n*.DOCX diff=astextplain\n*.dot  diff=astextplain\n*.DOT  diff=astextplain\n*.pdf  diff=astextplain\n*.PDF\t diff=astextplain\n*.rtf\t diff=astextplain\n*.RTF\t diff=astextplain\n"
  },
  {
    "path": ".gitignore",
    "content": "# g++ temporary files\n*.o\n*.bak\n*.bak\n"
  },
  {
    "path": "Dockerfile",
    "content": "# make the buil container\nFROM alpine as build-env\nRUN apk add --no-cache build-base\nWORKDIR /vbit2\n# copy everything from source dir into build container\nCOPY . .\nRUN make -j8\n\n# make the runtime container\nFROM alpine\nRUN apk update && apk add --no-cache libstdc++ libgcc tzdata\nCOPY --from=build-env /vbit2/vbit2 /vbit2/vbit2\nWORKDIR /vbit2\nCMD [\"/vbit2/vbit2\"]\n\n# build:\n# docker build -t vbit2 .\n\n# run:\n# docker run -it --mount type=bind,source=\"/path/to/pages,target=/vbit2/pages,readonly vbit2\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (C) 2025, Peter Kwan, Alistair Cree, Alistair Buxton\n\nPermission to use, copy, modify, and distribute this software\nand its documentation for any purpose and without fee is hereby\ngranted, provided that the above copyright notice appear in all\ncopies and that both that the copyright notice and this\npermission notice and warranty disclaimer appear in supporting\ndocumentation, and that the names of the authors not be used in\nadvertising or publicity pertaining to distribution of the\nsoftware without specific, written prior permission.\n\nThe authors disclaim all warranties with regard to this\nsoftware, including all implied warranties of merchantability\nand fitness.  In no event shall the authors be liable for any\nspecial, indirect or consequential damages or any damages\nwhatsoever resulting from loss of use, data or profits, whether\nin an action of contract, negligence or other tortious action,\narising out of or in connection with the use or performance of\nthis software.\n"
  },
  {
    "path": "Makefile",
    "content": "CXX=g++\n\nCXXFLAGS = -g -O2 -Wall -MMD -MP -std=gnu++11 -fstack-protector-all -Wextra -I.\n\nLIBS = -lpthread -fstack-protector -s\n\nifeq ($(OS),Windows_NT)\n\tCXXFLAGS += -DWIN32\n\tLIBS += -lwsock32 -static\nelse\n\tifeq ($(shell test -e /etc/os-release && echo -n yes),yes)\n\t\tifeq ($(shell if [ `grep -c raspbian /etc/os-release` -gt 0 ]; then echo true ; else echo false ; fi), true)\n\t\t\tCXXFLAGS += -DRASPBIAN\n\t\tendif\n\tendif\nendif\n\nsrcs = $(wildcard *.cpp)\nobjs = $(srcs:.cpp=.o)\ndeps = $(srcs:.cpp=.d)\n\nvbit2: $(objs)\n\t$(CXX) -o $@ $^ $(LIBS)\n\n%.o: %.c $(deps)\n\t$(CXX) -c $< -o $@\n\n#Cleanup\n.PHONY: clean\n\nclean:\n\trm -f $(objs) $(deps) vbit2\n\n-include $(deps)\n\nall: vbit2\n"
  },
  {
    "path": "README.md",
    "content": "# VBIT2\n\nAn installation guide and more can be found in the [github wiki](https://github.com/peterkvt80/vbit2/wiki).\n\n## About\n\nThis program takes a set of teletext [page files](https://github.com/peterkvt80/vbit2/wiki/Page-files) and generates a feature rich transmission stream on stdout.\n\nThe transmission stream can be piped to raspi-teletext or any other application that needs a teletext packet stream.\nIt is a console application that can be compiled for Raspberry Pi or Windows.\n\nIt generates a T42 teletext stream that can be piped to [raspi-teletext](https://github.com/ali1234/raspi-teletext) to add a teletext signal to the Raspberry Pi composite output, [vbit-py](https://github.com/peterkvt80/vbit-py) to drive a Vbit teletext inserter board, or into the [vbit-iv](https://github.com/peterkvt80/vbit-iv) in-vision renderer.\n\nVBIT2 can also optionally generate output an mpeg transport stream containing DVB-TXT for merging into a digital television stream.\n\n## Features\n\nVBIT2 includes the following features:\n* Parallel mode transmission of teletext magazines.\n* Cycling subpage carousels.\n* Level 1.5 and 2.5 features including dynamic character and object downloading pages and Packet 29 insertion.\n* Format 1 Broadcast Service Data Packet generation with automatic daylight saving time.\n* Fastext and TOP navigation support.\n* IDL Format A data broadcast transmission.\n* TCP control interface for live manipulation of teletext pages and service parameters.\n"
  },
  {
    "path": "carousel.cpp",
    "content": "\n#include \"carousel.h\"\n\nusing namespace vbit;\n\nCarousel::Carousel(int mag, PageList *pageList, Debug *debug) :\n    _mag(mag),\n    _pageList(pageList),\n    _debug(debug)\n{\n    //ctor\n}\n\nCarousel::~Carousel()\n{\n    //dtor\n}\n\nvoid Carousel::addPage(std::shared_ptr<TTXPageStream> p)\n{\n    // @todo Don't allow duplicate entries\n    p->SetCarouselFlag(true);\n    int t = 1;\n    if (std::shared_ptr<Subpage> s = p->GetSubpage())\n        t += s->GetCycleTime();\n    \n    p->SetTransitionTime(t);\n    _carouselList.push_front(p);\n}\n\nstd::shared_ptr<TTXPageStream> Carousel::nextCarousel()\n{\n    std::shared_ptr<TTXPageStream> p;\n    if (_carouselList.size()==0) return nullptr;\n    \n    for (std::list<std::shared_ptr<TTXPageStream>>::iterator it=_carouselList.begin();it!=_carouselList.end();++it)\n    {\n        p=*it;\n        if (p->GetOneShotFlag())\n        {\n            p->SetCarouselFlag(false);\n            _carouselList.erase(it--);\n            continue;\n        }\n        \n        if (p->GetLock()) // try to lock this page against changes\n        {\n            if (p->GetIsMarked() && p->GetCarouselFlag()) // only remove it once\n            {\n                std::stringstream ss;\n                ss << \"[Carousel::nextCarousel] Deleted \" << std::hex << (p->GetPageNumber());\n                _debug->Log(Debug::LogLevels::logINFO,ss.str());\n                \n                p->SetCarouselFlag(false);\n                _carouselList.erase(it--);\n                \n                _pageList->RemovePage(p); // try to remove it from the pagelist immediately - will free the lock\n                continue; // jump back to loop\n            }\n            else if ((!(p->IsCarousel())) || p->Special())\n            {\n                std::stringstream ss;\n                ss << \"[Carousel::nextCarousel] no longer a carousel \" << std::hex << (p->GetPageNumber());\n                _debug->Log(Debug::LogLevels::logINFO,ss.str());\n                \n                p->SetCarouselFlag(false);\n                _carouselList.erase(it--);\n            }\n            else\n            {\n                if (p->Expired())\n                {\n                    // We found a carousel that is ready to step\n                    if (std::shared_ptr<Subpage> s = p->GetSubpage()) // make sure there is a subpage\n                    {\n                        if (s->GetSubpageStatus() & PAGESTATUS_C9_INTERRUPTED)\n                        {\n                            // carousel should go out now out of sequence\n                            return p; // return page locked\n                        }\n                    }\n                }\n            }\n            \n            p->FreeLock(); // unlock\n        }\n    }\n    return nullptr;\n}\n"
  },
  {
    "path": "carousel.h",
    "content": "#ifndef _CAROUSEL_H\n#define _CAROUSEL_H\n\n#include <list>\n\n#include \"debug.h\"\n#include \"ttxpagestream.h\"\n#include \"pagelist.h\"\n\n/** Carousel maintains a list of carousel pages.\n *  nextCarousel() iterates through the list to find pages which have hard cycle timings\n */\n\nnamespace vbit\n{\n\nclass Carousel\n{\n    public:\n        /** Default constructor */\n        Carousel(int mag, PageList *pageList, Debug *debug);\n        /** Default destructor */\n        virtual ~Carousel();\n\n        /** Add a page to the list\n         */\n        void addPage(std::shared_ptr<TTXPageStream> p);\n\n        /** Find the next carousel page that needs to be transmitted\n         *  @return The next carousel if it is time to go or NULL\n         */\n        std::shared_ptr<TTXPageStream> nextCarousel();\n\n\n    protected:\n\n    private:\n        int _mag;\n        PageList* _pageList;\n        Debug* _debug;\n\n        std::list<std::shared_ptr<TTXPageStream>> _carouselList; /// The list of carousel pages\n};\n\n}\n\n#endif // _CAROUSEL_H\n"
  },
  {
    "path": "configure.cpp",
    "content": "/** Configure\n */\n#include \"configure.h\"\n\nusing namespace vbit;\n\nint Configure::DirExists(std::string *path)\n{\n    struct stat info;\n\n    if(stat(path->c_str(), &info ) != 0)\n        return 0;\n    else if(info.st_mode & S_IFDIR)\n        return 1;\n    else\n        return 0;\n}\n\nConfigure::Configure(Debug *debug, int argc, char** argv) :\n    _debug(debug),\n    // settings for generation of packet 8/30\n    _initialMag(1),\n    _initialPage(0x00),\n    _initialSubcode(0x3F7F),\n    _NetworkIdentificationCode(0x0000),\n    _CountryNetworkIdentificationCode(0x0000),\n    _reservedBytes{0x15, 0x15, 0x15, 0x15}, // initialise reserved bytes to hamming 8/4 encoded 0\n    _serviceStatusString(20, ' ')\n{\n    _configFile = CONFIGFILE;\n    \n#ifdef RASPBIAN\n    _pageDir = \"/home/pi/teletext\";\n#else\n    _pageDir = \"./pages\"; // a relative path as a sensible default\n#endif\n    // This is where the default header template is defined.\n    _headerTemplate = \"VBIT2    %%# %%a %d %%b\" \"\\x03\" \"%H:%M:%S\";\n    \n    _reverseBits = false;\n\n    _rowAdaptive = false;\n    _linesPerField = 16; // default to 16 lines per field\n    _datacastLines = 0; // no dedicated datacast lines\n\n    _multiplexedSignalFlag = false; // using this would require changing all the line counting and a way to send full field through raspi-teletext - something for the distant future when everything else is done...\n    \n    _OutputFormat = T42; // t42 output is the default behaviour\n    \n    _PID = 0x20; // default PID is 0x20\n    \n    _packetServerPort = 0; // port 0 disables packet server\n    _packetServerMaxClients = 5; // default to 5 connection limit\n    _interfaceServerPort = 0; // port 0 disables interface server\n    _interfaceServerMaxClients = 5; // default to 5 connection limit\n    \n    uint8_t priority[8]={9,3,3,6,3,3,5,6}; // 1=High priority,9=low. Note: priority[0] is mag 8\n    \n    for (int i=0; i<8; i++)\n        _magazinePriority[i] = priority[i];\n\n    //Scan the command line for overriding the pages file.\n    if (argc>1)\n    {\n        for (int i=1;i<argc;++i)\n        {\n            std::string arg = argv[i];\n            if (arg == \"--dir\")\n            {\n                if (i + 1 < argc)\n                    _pageDir = argv[++i];\n                else\n                {\n                    std::cerr << \"--dir requires an argument\\n\";\n                    exit(EXIT_FAILURE);\n                }\n            }\n            else if (arg == \"--format\")\n            {\n                if (i + 1 < argc)\n                {\n                    arg = argv[++i];\n                    \n                    if (arg == \"none\")\n                    {\n                        _OutputFormat = None;\n                    }\n                    else if (arg == \"t42\")\n                    {\n                        _OutputFormat = T42;\n                    }\n                    else if (arg == \"raw\")\n                    {\n                        _OutputFormat = Raw;\n                    }\n                    else if (arg == \"ts\")\n                    {\n                        _OutputFormat = TS;\n                    }\n                    else if (arg == \"tsnpts\")\n                    {\n                        _OutputFormat = TSNPTS;\n                    }\n                    else\n                    {\n                        std::cerr << \"invalid --format type\\n\";\n                        exit(EXIT_FAILURE);\n                    }\n                    \n                    if (_reverseBits && _OutputFormat != T42)\n                    {\n                        std::cerr << \"--reverse requires t42 format\\n\";\n                        exit(EXIT_FAILURE);\n                    }\n                }\n                else\n                {\n                    std::cerr << \"--format requires an argument\\n\";\n                    exit(EXIT_FAILURE);\n                }\n            }\n            else if (arg == \"--reverse\")\n            {\n                _reverseBits = true;\n                \n                if (_OutputFormat != T42)\n                {\n                    std::cerr << \"--reverse requires t42 format\\n\";\n                    exit(EXIT_FAILURE);\n                }\n            }\n            else if (arg == \"--pid\")\n            {\n                if (i + 1 < argc)\n                {\n                    std::istringstream ss(argv[++i]);\n                    ss >> _PID;\n                    if (_PID < 0x20 || _PID >= 0x1FFF || !ss.eof())\n                    {\n                        std::cerr << \"invalid PID\\n\";\n                        exit(EXIT_FAILURE);\n                    }\n                }\n                else\n                {\n                    std::cerr << \"--pid requires an argument\\n\";\n                    exit(EXIT_FAILURE);\n                }\n            }\n            else if (arg == \"--reserved\")\n            {\n                if (i + 1 < argc)\n                {\n                    // Take a 32 bit hexadecimal value to set the four reserved bytes in the BSDP\n                    // Store bytes big endian so that the order digits appear on the command line is the same as they appear in packet\n                    errno = 0;\n                    char *end_ptr;\n                    unsigned long l = std::strtoul(argv[++i], &end_ptr, 16);\n                    if (errno == 0 && *end_ptr == '\\0')\n                    {\n                        _reservedBytes[0] = (l >> 24) & 0xff;\n                        _reservedBytes[1] = (l >> 16) & 0xff;\n                        _reservedBytes[2] = (l >> 8) & 0xff;\n                        _reservedBytes[3] = (l >> 0) & 0xff;\n                    }\n                    else\n                    {\n                        std::cerr << \"invalid reserved bytes argument\\n\";\n                        exit(EXIT_FAILURE);\n                    }\n                }\n                else\n                {\n                    std::cerr << \"--reserved requires an argument\\n\";\n                    exit(EXIT_FAILURE);\n                }\n            }\n            else if (arg == \"--debug\")\n            {\n                if (i + 1 < argc)\n                {\n                    errno = 0;\n                    char *end_ptr;\n                    long l = std::strtol(argv[++i], &end_ptr, 10);\n                    if (errno == 0 && *end_ptr == '\\0' && l > -1)\n                    {\n                        switch(l){\n                            case 0:\n                                _debug->SetDebugLevel(Debug::LogLevels::logNONE);\n                                break;\n                            case 1:\n                                _debug->SetDebugLevel(Debug::LogLevels::logERROR);\n                                break;\n                            case 2:\n                                _debug->SetDebugLevel(Debug::LogLevels::logWARN);\n                                break;\n                            case 3:\n                                _debug->SetDebugLevel(Debug::LogLevels::logINFO);\n                                break;\n                            case 4:\n                            default:\n                                _debug->SetDebugLevel(Debug::LogLevels::logDEBUG);\n                                break;\n                        }\n                        \n                    }\n                    else\n                    {\n                        std::cerr << \"invalid debug level argument\\n\";\n                        exit(EXIT_FAILURE);\n                    }\n                }\n                else\n                {\n                    std::cerr << \"--debug requires an argument\\n\";\n                    exit(EXIT_FAILURE);\n                }\n            }\n            else if (arg == \"--packetserver\")\n            {\n                if (i + 1 < argc)\n                {\n                    errno = 0;\n                    char *end_ptr;\n                    long l = std::strtol(argv[++i], &end_ptr, 10);\n                    if (errno == 0 && *end_ptr == '\\0' && l > 0 && l < 65536)\n                    {\n                        _packetServerPort = (int)l;\n                    }\n                    else\n                    {\n                        std::cerr << \"invalid packetserver port number\\n\";\n                        exit(EXIT_FAILURE);\n                    }\n                    \n                    if (i + 1 < argc)\n                    {\n                        arg = argv[i+1];\n                        if (arg.compare(0,2,\"--\"))\n                        {\n                            // optional max clients argument\n                            errno = 0;\n                            l = std::strtol(argv[++i], &end_ptr, 10);\n                            if (errno == 0 && *end_ptr == '\\0' && l > -1 && l < 65536)\n                            {\n                                _packetServerMaxClients = (uint16_t)l;\n                            }\n                            else\n                            {\n                                std::cerr << \"invalid packetserver max clients argument\\n\";\n                                exit(EXIT_FAILURE);\n                            }\n                        }\n                    }\n                }\n                else\n                {\n                    std::cerr << \"--packetserver requires a port number\\n\";\n                    exit(EXIT_FAILURE);\n                }\n            }\n            else if (arg == \"--interface\")\n            {\n                if (i + 1 < argc)\n                {\n                    errno = 0;\n                    char *end_ptr;\n                    long l = std::strtol(argv[++i], &end_ptr, 10);\n                    if (errno == 0 && *end_ptr == '\\0' && l > 0 && l < 65536)\n                    {\n                        _interfaceServerPort = (int)l;\n                    }\n                    else\n                    {\n                        std::cerr << \"invalid interface port number\\n\";\n                        exit(EXIT_FAILURE);\n                    }\n                    \n                    if (i + 1 < argc)\n                    {\n                        arg = argv[i+1];\n                        if (arg.compare(0,2,\"--\"))\n                        {\n                            // optional max clients argument\n                            errno = 0;\n                            l = std::strtol(argv[++i], &end_ptr, 10);\n                            if (errno == 0 && *end_ptr == '\\0' && l > -1 && l < 65536)\n                            {\n                                _interfaceServerMaxClients = (uint16_t)l;\n                            }\n                            else\n                            {\n                                std::cerr << \"invalid interface max clients argument\\n\";\n                                exit(EXIT_FAILURE);\n                            }\n                        }\n                    }\n                }\n                else\n                {\n                    std::cerr << \"--interface requires a port number\\n\";\n                    exit(EXIT_FAILURE);\n                }\n            }\n            else\n            {\n                std::cerr << \"unrecognised argument: \" << arg << std::endl;\n                exit(EXIT_FAILURE);\n            }\n        }\n    }\n    \n    if (!DirExists(&_pageDir))\n    {\n        std::stringstream ss;\n        ss << _pageDir << \" does not exist or is not a directory\\n\";\n        std::cerr << ss.str();\n        exit(EXIT_FAILURE);\n    }\n    \n    // TODO: allow overriding config file from command line\n    _debug->Log(Debug::LogLevels::logINFO,\"[Configure::Configure] Pages directory is \" + _pageDir);\n    _debug->Log(Debug::LogLevels::logINFO,\"[Configure::Configure] Config file is \" + _configFile);\n    \n    std::string path;\n    path = _pageDir;\n    path += \"/\";\n    path += _configFile;\n    LoadConfigFile(path); // load main config file (vbit.conf)\n    \n    LoadConfigFile(path+\".override\"); // allow overriding main config file for local configuration where main config is in version control\n    \n    if (_datacastLines > _linesPerField)\n    {\n        _debug->Log(Debug::LogLevels::logERROR,\"[Configure] datacast lines cannot be greater than lines per field\");\n        _datacastLines = _linesPerField; // clamp\n    }\n}\n\nConfigure::~Configure()\n{\n    \n}\n\nvoid Configure::SetHeaderTemplate(std::shared_ptr<TTXLine> line)\n{\n    std::string str = \"\";\n    for (int i=8; i<40; i++)\n        str += line->GetCharAt(i) & 0x7f;\n    _headerTemplate.assign(str);\n}\n\nint Configure::LoadConfigFile(std::string filename)\n{\n    std::ifstream filein(filename.c_str()); // Open the file\n\n    std::vector<std::string>::iterator iter;\n    // these are all the valid strings for config lines\n    std::vector<std::string> nameStrings{ \"header_template\", \"initial_teletext_page\", \"row_adaptive_mode\", \"network_identification_code\", \"country_network_identification\", \"full_field\", \"status_display\",\"lines_per_field\",\"datacast_lines\",\"magazine_priority\"};\n\n    if (filein.is_open())\n    {\n        _debug->Log(Debug::LogLevels::logINFO,\"[Configure::LoadConfigFile] opened \" + filename);\n\n        std::string line;\n        std::string name;\n        std::string value;\n        while (std::getline(filein >> std::ws, line))\n        {\n            if (line.front() != ';') // ignore comments\n            { \n                std::size_t delim = line.find(\"=\", 0);\n                int error = 0;\n\n                if (delim != std::string::npos)\n                {\n                    name = line.substr(0, delim);\n                    value = line.substr(delim + 1);\n                    iter = find(nameStrings.begin(), nameStrings.end(), name);\n                    if(iter != nameStrings.end())\n                    {\n                        // matched string\n                        switch(iter - nameStrings.begin())\n                        {\n                            case 0: // header_template\n                            {\n                                std::shared_ptr<TTXLine> header(new TTXLine(\"        \"+value)); // use to process escape codes\n                                SetHeaderTemplate(header);\n                                break;\n                            }\n                            case 1: // initial_teletext_page\n                            {\n                                if (value.size() >= 3)\n                                {\n                                    size_t idx;\n                                    int magpage;\n                                    int subcode;\n                                    try\n                                    {\n                                        magpage = stoi(std::string(value, 0, 3), &idx, 16);\n                                    }\n                                    catch (const std::invalid_argument& ia)\n                                    {\n                                        error = 1;\n                                        break;\n                                    }\n                                    if (magpage < 0x100 || magpage > 0x8FF || (magpage & 0xFF) == 0xFF)\n                                    {\n                                        error = 1;\n                                        break;\n                                    }\n                                    if (value.size() > 3)\n                                    {\n                                        if ((value.size() != 8) || (value.at(idx) != ':'))\n                                        {\n                                            error = 1;\n                                            break;\n                                        }\n                                        try\n                                        {\n                                            subcode = stoi(std::string(value, 4, 4), &idx, 16);\n                                        }\n                                        catch (const std::invalid_argument& ia)\n                                        {\n                                            error = 1;\n                                            break;\n                                        }\n                                        if (subcode < 0x0000 || subcode > 0x3F7F || subcode & 0xC080)\n                                        {\n                                            error = 1;\n                                            break;\n                                        }\n                                        _initialSubcode = subcode;\n                                    }\n                                    _initialMag = magpage / 0x100;\n                                    _initialPage = magpage % 0x100;\n                                    break;\n                                }\n                                error = 1;\n                                break;\n                            }\n                            case 2: // row_adaptive_mode\n                            {\n                                if (!value.compare(\"true\"))\n                                {\n                                    _rowAdaptive = true;\n                                }\n                                else if (!value.compare(\"false\"))\n                                {\n                                    _rowAdaptive = false;\n                                }\n                                else\n                                {\n                                    error = 1;\n                                }\n                                break;\n                            }\n                            case 3: // \"network_identification_code\" - four character hex. eg. FA6F\n                            {\n                                if (value.size() == 4)\n                                {\n                                    size_t idx;\n                                    try\n                                    {\n                                        _NetworkIdentificationCode = stoi(std::string(value, 0, 4), &idx, 16);\n                                    }\n                                    catch (const std::invalid_argument& ia)\n                                    {\n                                        error = 1;\n                                        break;\n                                    }\n                                }\n                                else\n                                {\n                                    error = 1;\n                                }\n                                break;\n                            }\n                            case 4: // \"country_network_identification\" - four character hex. eg. 2C2F\n                            {\n                                if (value.size() == 4)\n                                {\n                                    size_t idx;\n                                    try\n                                    {\n                                        _CountryNetworkIdentificationCode = stoi(std::string(value, 0, 4), &idx, 16);\n                                    }\n                                    catch (const std::invalid_argument& ia)\n                                    {\n                                        error = 1;\n                                        break;\n                                    }\n                                }\n                                else\n                                {\n                                    error = 1;\n                                }\n                                break;\n                            }\n                            case 5: // \"full_field\"\n                            {\n                                break;\n                            }\n                            case 6: // \"status_display\"\n                            {\n                                SetServiceStatusString(value);\n                                break;\n                            }\n                            case 7: // \"lines_per_field\"\n                            {\n                                if (value.size() > 0 && value.size() < 4)\n                                {\n                                    try\n                                    {\n                                        _linesPerField = stoi(std::string(value, 0, 3));\n                                    }\n                                    catch (const std::invalid_argument& ia)\n                                    {\n                                        error = 1;\n                                        break;\n                                    }\n                                }\n                                else\n                                {\n                                    error = 1;\n                                }\n                                break;\n                            }\n                            case 8: // datacast_lines\n                            {\n                                if (value.size() > 0 && value.size() < 4)\n                                {\n                                    try\n                                    {\n                                        _datacastLines = stoi(std::string(value, 0, 3));\n                                    }\n                                    catch (const std::invalid_argument& ia)\n                                    {\n                                        error = 1;\n                                        break;\n                                    }\n                                }\n                                else\n                                {\n                                    error = 1;\n                                }\n                                break;\n                            }\n                            case 9: // \"magazine_priority\"\n                            {\n                                std::stringstream ss(value);\n                                std::string temps;\n                                int tmp[8];\n                                int i;\n                                for (i=0; i<8; i++)\n                                {\n                                    if (std::getline(ss, temps, ','))\n                                    {\n                                        try\n                                        {\n                                            tmp[i] = stoi(temps);\n                                        }\n                                        catch (const std::invalid_argument& ia)\n                                        {\n                                            error = 1;\n                                            break;\n                                        }\n                                        if (!(tmp[i] > 0 && tmp[i] < 10)) // must be 1-9\n                                        {\n                                            error = 1;\n                                            break;\n                                        }\n                                    }\n                                    else\n                                    {\n                                        error = 1;\n                                        break;\n                                    }\n                                }\n                                for (i=0; i<8; i++)\n                                    _magazinePriority[i] = tmp[i];\n                                break;\n                            }\n                        }\n                    }\n                    else\n                    {\n                        error = 1; // unrecognised config line\n                    }\n                }\n                if (error)\n                {\n                    _debug->Log(Debug::LogLevels::logERROR,\"[Configure::LoadConfigFile] invalid config line: \" + line);\n                }\n            }\n        }\n        filein.close();\n        return 0;\n    }\n    else\n    {\n        _debug->Log(Debug::LogLevels::logWARN,\"[Configure::LoadConfigFile] open failed\");\n        return -1;\n    }\n}\n"
  },
  {
    "path": "configure.h",
    "content": "#ifndef _CONFIGURE_H_\n#define _CONFIGURE_H_\n/** Configure processes settings related to the teletext service and vbit2 command line options.\n */\n\n#include <iostream>\n#include <fstream>\n#include <sstream>\n#include <stdint.h>\n#include <cstring>\n#include <sys/stat.h>\n#include <vector>\n#include <array>\n#include <algorithm>\n#include <stdexcept>\n#include <memory>\n\n#include \"debug.h\"\n#include \"ttxline.h\"\n\n#define CONFIGFILE \"vbit.conf\" // default config file name\n\nnamespace vbit\n\n{\nclass Configure\n{\n    public:\n        enum OutputFormat\n        {\n            None,\n            T42,\n            Raw,\n            TS,\n            TSNPTS\n        };\n        \n        //Configure();\n        /** Constructor can take overrides from the command line\n         */\n        Configure(Debug *debug, int argc=0, char** argv=NULL);\n        ~Configure();\n        \n        inline std::string GetPageDirectory(){return _pageDir;};\n        \n        std::string GetHeaderTemplate(){return _headerTemplate;}\n        void SetHeaderTemplate(std::shared_ptr<TTXLine> line);\n        bool GetRowAdaptive(){return _rowAdaptive;}\n        void SetRowAdaptive(bool flag){_rowAdaptive = flag;}\n        std::string GetServiceStatusString(){return _serviceStatusString;}\n        void SetServiceStatusString(std::string status){status.resize(20,' '); _serviceStatusString = status;}\n        bool GetMultiplexedSignalFlag(){return _multiplexedSignalFlag;}\n        uint16_t GetNetworkIdentificationCode(){return _NetworkIdentificationCode;}\n        std::array<uint8_t, 4> GetReservedBytes(){return _reservedBytes;}\n        void SetReservedBytes(std::array<uint8_t, 4> reserved){_reservedBytes = reserved;}\n        uint8_t GetInitialMag(){return _initialMag;}\n        uint8_t GetInitialPage(){return _initialPage;}\n        uint16_t GetInitialSubcode(){return _initialSubcode;}\n        uint16_t GetLinesPerField(){return _linesPerField;}\n        uint16_t GetDatacastLines(){return _datacastLines;}\n        bool GetReverseFlag(){return _reverseBits;}\n        int GetMagazinePriority(uint8_t mag){return _magazinePriority[mag];}\n        \n        OutputFormat GetOutputFormat(){return _OutputFormat;}\n        uint16_t GetTSPID(){return _PID;}\n        \n        uint16_t GetPacketServerPort(){return _packetServerPort;}\n        bool GetPacketServerEnabled(){return _packetServerPort != 0;}\n        uint16_t GetPacketServerMaxClients(){return _packetServerMaxClients;}\n        \n        uint16_t GetInterfaceServerPort(){return _interfaceServerPort;}\n        bool GetInterfaceServerEnabled(){return _interfaceServerPort != 0;}\n        uint16_t GetInterfaceServerMaxClients(){return _interfaceServerMaxClients;}\n        \n    private:\n        Debug* _debug;\n        int DirExists(std::string *path);\n        \n        int LoadConfigFile(std::string filename);\n        \n        // template string for generating header packets\n        std::string _headerTemplate;\n        \n        bool _rowAdaptive;\n        uint16_t _linesPerField;\n        uint16_t _datacastLines;\n        \n        // settings for generation of packet 8/30\n        bool _multiplexedSignalFlag; // false indicates teletext is multiplexed with video, true means full frame teletext.\n        int _magazinePriority[8];\n        uint8_t _initialMag;\n        uint8_t _initialPage;\n        uint16_t _initialSubcode;\n        uint16_t _NetworkIdentificationCode;\n        uint16_t _CountryNetworkIdentificationCode;\n        std::array<uint8_t, 4> _reservedBytes; // four bytes which the teletext specification marks reserved\n        std::string _serviceStatusString; /// 20 characters\n        \n        std::string _configFile; /// Configuration file name --config\n        std::string _pageDir; /// Configuration file name --dir\n        bool _reverseBits;\n        \n        OutputFormat _OutputFormat;\n        uint16_t _PID;\n        \n        uint16_t _packetServerPort;\n        uint16_t _packetServerMaxClients;\n        uint16_t _interfaceServerPort;\n        uint16_t _interfaceServerMaxClients;\n    };\n}\n\n#endif\n"
  },
  {
    "path": "debug.cpp",
    "content": "#include <debug.h>\n\nusing namespace vbit;\n\nDebug::Debug() :\n    _debugLevel(logNONE)\n{\n    //ctor\n    _magDurations.fill(-1);\n    _magSizes.fill(0);\n}\n\nDebug::~Debug()\n{\n    //dtor\n}\n\nvoid Debug::Log(LogLevels level, std::string str)\n{\n    if (_debugLevel >= level){\n        std::cerr << str + \"\\n\"; // keep sending messages to stderr for now\n    }\n}\n\nvoid Debug::SetMagCycleDuration(int mag, int duration)\n{\n    if (mag >= 0 && mag < 8)\n    {\n        _magDurations[mag] = duration;\n        Log(logDEBUG, \"[Debug::SetMagCycleDuration] Magazine \" + std::to_string(mag) + \" cycle duration: \" + std::to_string(duration / 50) + \" seconds, \" + std::to_string(duration % 50) + \" fields\");\n    }\n}\n\nvoid Debug::SetMagazineSize(int mag, int size)\n{\n    if (mag >= 0 && mag < 8)\n    {\n        _magSizes[mag] = (size<255)?size:255; // clamp erroneous sizes to 255\n        Log(logDEBUG, \"[Debug::SetMagazineSize] Magazine \" + std::to_string(mag) + \" size: \" + std::to_string(size) + \" pages\");\n    }\n}\n"
  },
  {
    "path": "debug.h",
    "content": "/** debugging class\n */\n#ifndef _DEBUG_H_\n#define _DEBUG_H_\n\n#include <iostream>\n#include <array>\n\nnamespace vbit\n{\n    class Debug\n    {\n        public:\n            enum LogLevels\n            {\n                logNONE,\n                logERROR,\n                logWARN,\n                logINFO,\n                logDEBUG\n            };\n            \n            /** Default constructor */\n            Debug();\n            /** Default destructor */\n            virtual ~Debug();\n            \n            void Log(LogLevels level, std::string str);\n            void SetDebugLevel(LogLevels level){ _debugLevel = level; };\n            LogLevels GetDebugLevel(){ return _debugLevel; };\n            void SetMagCycleDuration(int mag, int duration);\n            std::array<int,8> GetMagCycleDurations(){ return _magDurations; };\n            void SetMagazineSize(int mag, int size);\n            std::array<int,8> GetMagSizes(){ return _magSizes; };\n            \n        protected:\n\n        private:\n            LogLevels _debugLevel;\n            std::array<int, 8> _magDurations;\n            std::array<int, 8> _magSizes;\n    };\n}\n\n#endif // _DEBUG_H_\n"
  },
  {
    "path": "example-vbit.conf",
    "content": "; example vbit config file\n; lines beginning with a semicolon are comment lines\n; settings are in the form name=value and there must be no whitespace.\n\n;--------------------------------- HEADER ROW ---------------------------------\n; header template value must be 32 teletext characters long.\n; both 8-bit control codes and 7-bit tti escape sequences are supported.\n; %%# is replaced by the page number\n; time and date use a subset of posix strftime conversion specifier characters\n; pre-padded with % symbols to the appropriate length.\n; day name = %%a\n; month name = %%b\n; day number with leading zero = %d\n; day number without leading zero = %e\n; month number = %m\n; 2 digit year = %y\n; hour = %H\n; minute = %M\n; second = %S\n; here are some historic examples of header rows:\n;header_template=CEEFAX %%#  %%a %e %%b \u001bC%H:%M/%S\n;header_template=CEEFAX 1 %%# %%a %d %%b\u001bC%H:%M/%S\n;header_template=ORACLE %%# %%a%e %%b ITV\u001bB%H%M:%S\n;header_template=ORACLE %%# %%a%e %%b\u001bCITV %H%M:%S\n;header_template=Teletext on 4 %%# %%b%d\u001bC%H:%M:%S\n;header_template=\u001bD\u001b]\u001bCTeletext \u001bG\u001b\\%%# %%b%d\u001bC%H:%M:%S\n\n; TEEFAX header\nheader_template=TEEFAX %%#  %%a %e %%b \u001bC%H:%M/%S\n\n;---------------------------- INITIAL TELETEXT PAGE ---------------------------\n; magazine, page, and subcode to transmit within broadcast data service packets\n; magazine and page number are specified as you would on a teletext set.\n; i.e. magazine 1-8 followed by two hex digits for example 100, 888, 19F, etc.\n; the initial subcode can optionally be appended, separated by a colon.\n;initial_teletext_page=100\n;initial_teletext_page=100:3F7F\n\n;------------------------------ SERVICE SETTINGS ------------------------------\n; omit blank rows to increase transmission efficiency (defaults to false)\n;row_adaptive_mode=false\n\n; specify number of VBI lines per video field\n;lines_per_field=16\n\n; set the priority of each magazine. 1=highest priority, 9=lowest.\n; eight comma separated values for magazines 8,1,2,3,4,5,6,7.\n;magazine_priority=9,3,3,6,3,3,5,6\n\n; 20 character status message for broadcast service data packet\nstatus_display=TEEFAX\n\n; 16 bit hexadecimal code assigned to network in ETSI TR 101 231\n; defaults to 0000 (no NI code assigned)\n;network_identification_code=0000\n\n; 16 bit hexadecimal code assigned for PDC\n;country_network_identification=0000\n"
  },
  {
    "path": "filemonitor.cpp",
    "content": "#include \"filemonitor.h\"\n\nusing namespace vbit;\n\nFile::File(std::string filename) :\n    _page(new TTXPageStream()),\n    _filename(filename),\n    _fileStatus(NEW)\n{\n    LoadFile(filename);\n}\n\nvoid File::LoadFile(std::string filename)\n{\n    _loaded = false;\n    if (filename.size() >= 4)\n    {\n        std::string ext = filename.substr(filename.size() - 4); // get last four characters of string\n        \n        if (ext == \".tti\")\n        {\n            _loaded = LoadTTI(filename);\n        }\n        // else other types of file we might want to load in future\n    }\n}\n\nbool File::LoadTTI(std::string filename)\n{\n    const std::string cmd[]={\"DS\",\"SP\",\"DE\",\"CT\",\"PN\",\"SC\",\"PS\",\"MS\",\"OL\",\"FL\",\"RD\",\"RE\",\"PF\"};\n    const int cmdCount = 13; // There are 13 possible commands, maybe DT and RT too on really old files\n    unsigned int lineNumber;\n    int lines=0;\n    // Open the file\n    std::ifstream filein(filename.c_str());\n    _page->ClearPage(); // reset to blank page\n    char * ptr;\n    unsigned int subcode;\n    int pageNumber = 0;\n    int pagestatus = 0;\n    char timedmode = false;\n    int cycletime = 1;\n    int region = 0;\n    char m;\n    \n    /* We're going to create subpages in parallel with the old system for a moment... */\n    std::shared_ptr<Subpage> s = nullptr;\n    \n    for (std::string line; std::getline(filein, line, ','); )\n    {\n        // This shows the command code:\n        bool found=false;\n        for (int i=0;i<cmdCount && !found; i++)\n        {\n            if (!line.compare(cmd[i]))\n            {\n                found=true;\n                switch (i)\n                {\n                    case 0 : // \"DS\"\n                    case 1 : // \"SP\"\n                    case 2 : // \"DE\"\n                    case 7 : // \"MS\" - Mask\n                    case 10 : // \"RD\" - not sure!\n                    {\n                        std::getline(filein, line); // consume line\n                        break;\n                    }\n                    case 3 : // \"CT\" - Cycle time (seconds)\n                    {\n                        // CT,8,T\n                        std::getline(filein, line, ',');\n                        cycletime = (atoi(line.c_str()));\n                        if (cycletime < 1)\n                            cycletime = 1;\n                        else if (cycletime > 255)\n                            cycletime = 255;\n                        \n                        std::getline(filein, line);\n                        timedmode = (line[0]=='T'?true:false);\n                        \n                        if (s != nullptr)\n                        {\n                            s->SetTimedMode(timedmode);\n                            s->SetCycleTime(cycletime);\n                        }\n                        \n                        break;\n                    }\n                    case 4 : // \"PN\" - Page Number mppss\n                    {\n                        // Where m=1..8\n                        // pp=00 to ff (hex)\n                        // ss=00 to 99 (decimal)\n                        // PN,10000\n                        \n                        std::getline(filein, line);\n                        if (!pageNumber) // use the first page number we see\n                        {\n                            if (line.length()<3) // Must have at least three characters for a page number\n                                break;\n                            m=line[0];\n                            if (m<'1' || m>'8') // Magazine must be 1 to 8\n                                break;\n                            pageNumber=std::strtol(line.c_str(), &ptr, 16);\n                            if (line.length()<5 && pageNumber<=0x8ff) // Page number without subpage? Shouldn't happen but you never know.\n                            {\n                                // leave it alone and hope for the best\n                            }\n                            else   // Normally has subpage digits, we don't care\n                            {\n                                pageNumber=(pageNumber & 0xfff00) >> 8;\n                            }\n                            \n                            _page->SetPageNumber(pageNumber);\n                        }\n                        \n                        s = std::shared_ptr<Subpage>(new Subpage()); // create a new subpage\n                        // inherit settings from previous subpage\n                        s->SetTimedMode(timedmode);\n                        s->SetCycleTime(cycletime);\n                        s->SetSubpageStatus(pagestatus);\n                        s->SetRegion(region);\n                        _page->AppendSubpage(s); // add it to the page\n\n                        break;\n                    }\n                    case 5 : // \"SC\" - Subcode\n                    {\n                        // SC,0000\n                        std::getline(filein, line);\n                        subcode=std::strtol(line.c_str(), &ptr, 16);\n                        \n                        if (s != nullptr)\n                            s->SetSubCode(subcode); // set subcode explicitly\n                        \n                        break;\n                    }\n                    case 6 : // \"PS\" - Page status flags\n                    {\n                        // PS,8000\n                        std::getline(filein, line);\n                        pagestatus = std::strtol(line.c_str(), &ptr, 16);\n                        if (s != nullptr)\n                            s->SetSubpageStatus(pagestatus);\n                        \n                        break;\n                    }\n                    case 8 : // \"OL\" - Output line\n                    {\n                        std::getline(filein, line, ',');\n                        lineNumber=atoi(line.c_str());\n                        std::getline(filein, line);\n                        if (lineNumber>MAXROW) break;\n                        if (s != nullptr)\n                        {\n                            std::shared_ptr<TTXLine> ttxline(new TTXLine(line));\n                            s->SetRow(lineNumber,ttxline);\n                            lines++;\n                        }\n                        \n                        // check for and decode OL,28 page function and coding\n                        if (lineNumber == 28 && line.length() >= 40)\n                        {\n                            uint8_t dc = line.at(0) & 0x0F;\n                            if (dc == 0 || dc == 2 || dc == 3 || dc == 4)\n                            {\n                                // packet is X/28/0, X/28/2, X/28/3, or X/28/4\n                                int triplet = line.at(1) & 0x3F;\n                                triplet |= (line.at(2) & 0x3F) << 6;\n                                triplet |= (line.at(3) & 0x3F) << 12; // first triplet contains page function and coding\n                                \n                                // Page function and coding override previous values\n                                _page->SetPageFunctionInt(triplet & 0x0F);\n                                _page->SetPageCodingInt((triplet & 0x70) >> 4);\n                            }\n                        }\n                        \n                        break;\n                    }\n                    case 9 : // \"FL\"; - Fastext links\n                    {\n                        std::array<FastextLink,6> links;\n                        \n                        for (int fli=0;fli<6;fli++)\n                        {\n                            if (fli<5)\n                                std::getline(filein, line, ',');\n                            else\n                                std::getline(filein, line); // Last parameter no comma\n                            \n                            links[fli].page = std::strtol(line.c_str(), &ptr, 16);\n                            links[fli].subpage = 0x3f7f;\n                        }\n                        \n                        if (s != nullptr)\n                        {\n                            s->SetFastext(links);\n                        }\n                        break;\n                    }\n                    case 11 : // \"RE\"; - Set page region code 0..f\n                    {\n                        std::getline(filein, line);\n                        int region = std::strtol(line.c_str(), &ptr, 16);\n                        if (s != nullptr)\n                            s->SetRegion(region);\n                        break;\n                    }\n                    case 12 : // \"PF\"; - not in the tti spec, page function and coding\n                    {\n                        std::getline(filein, line);\n                        if (line.length()<3)\n                        {\n                            // invalid page function/coding\n                        }\n                        else\n                        {\n                            _page->SetPageFunctionInt(std::strtol(line.substr(0,1).c_str(), &ptr, 16));\n                            _page->SetPageCodingInt(std::strtol(line.substr(2,1).c_str(), &ptr, 16));\n                        }\n                        break;\n                    }\n                    default:\n                    {\n                        // line not understood\n                    }\n                } // switch\n            } // if matched command\n            // If the command was not found then skip the rest of the line\n        } // seek command\n        if (!found) std::getline(filein, line);\n    }\n    filein.close(); // Not sure that we need to close it\n    _page->RenumberSubpages();\n    return (lines>0);\n}\n\nFileMonitor::FileMonitor(Configure *configure, Debug *debug, PageList *pageList) :\n    _configure(configure),\n    _debug(debug),\n    _pageList(pageList)\n{\n    //ctor\n}\n\nFileMonitor::FileMonitor()\n    : _pageList(nullptr)\n{\n    //ctor\n}\n\nFileMonitor::~FileMonitor()\n{\n    //dtor\n}\n\nvoid FileMonitor::run()\n{\n    std::string path=_configure->GetPageDirectory() ;\n    _debug->Log(Debug::LogLevels::logINFO,\"[FileMonitor::run] Monitoring \" + path);\n    \n    readDirectory(path, true);\n\n    while (true)\n    {\n        ClearFlags(); // Assume that no files exist\n        \n        readDirectory(path);\n        \n        // Delete pages that no longer exist (this blocks the thread until the pages are removed)\n        DeleteOldPages();\n\n        // Wait 5 seconds to avoid hogging cpu\n        // Sounds like a job for a mutex.\n        struct timespec rec;\n        int ms;\n\n        ms=5000;\n        rec.tv_sec = ms / 1000;\n        rec.tv_nsec=(ms % 1000) *1000000;\n        nanosleep(&rec,nullptr);\n    }\n} // run\n\nint FileMonitor::readDirectory(std::string path, bool firstrun)\n{\n    struct dirent *dirp;\n    struct stat attrib;\n    \n    DIR *dp;\n\n    // Open the directory\n    if ( (dp = opendir(path.c_str())) == NULL)\n    {\n        _debug->Log(Debug::LogLevels::logERROR,\"Error(\" + std::to_string(errno) + \") opening \" + path);\n        return errno;\n    }\n    \n    // Load the filenames into a list\n    while ((dirp = readdir(dp)) != NULL)\n    {\n        std::string name;\n        name=path;\n        name+=\"/\";\n        name+=dirp->d_name;\n        if (stat(name.c_str(), &attrib) == -1) // get the attributes of the file\n            continue; // skip file on failure\n        \n        if (attrib.st_mode & S_IFDIR)\n        {\n            // directory entry is another directory\n            if (dirp->d_name[0] != '.') // ignore anything beginning with .\n            {\n                if (readDirectory(name)) // recurse into directory\n                {\n                    _debug->Log(Debug::LogLevels::logERROR,\"Error(\" + std::to_string(errno) + \") recursing into \" + name);\n                }\n            }\n            continue;\n        }\n        \n        const std::vector<std::string> filetypes{\".tti\"}; // the filetypes we will attempt to load\n        if (name.size() >= 4)\n        {\n            std::string ext = name.substr(name.size() - 4); // get last four characters of string\n            if (find(filetypes.begin(), filetypes.end(), ext) != filetypes.end())\n            {\n                // Now we want to process changes\n                // 1) Is it a new page? Then add it.\n                std::shared_ptr<File> f = Locate(name);\n                if (f) // File was found\n                {\n                    f->SetState(File::FOUND); // Mark this page as existing on the drive\n                    std::shared_ptr<TTXPageStream> page = f->GetPage();\n                    if (attrib.st_mtime!=f->GetModifiedTime()) // File exists. Has it changed?\n                    {\n                        if (page->GetIsMarked()) // file is mid-deletion\n                        {\n                            f->SetState(File::NOTFOUND); // let this file object get deleted and reloaded\n                        }\n                        else\n                        {\n                            // We just load the new page and update the modified time\n                            if (page->GetLock()) // try to lock page\n                            {\n                                int curnum = page->GetPageNumber();\n                                f->LoadFile(name);\n                                if (page->GetPageNumber() != curnum)\n                                {\n                                    // page number changed\n                                    page->MarkForDeletion(); // mark page for deletion from service\n                                    f->SetState(File::NOTFOUND); // let this file object get deleted and reloaded\n                                }\n                                else\n                                {\n                                    if (f->Loaded())\n                                    {\n                                        if (page->GetOneShotFlag())\n                                        {\n                                            // file load clears oneshot status\n                                            page->SetOneShotFlag(false);\n                                            _debug->Log(Debug::LogLevels::logINFO,\"[FileMonitor::run] Reloading page from \" + std::string(dirp->d_name));\n                                        }\n                                        \n                                        page->IncrementUpdateCount();\n                                        int update = false;\n                                        \n                                        page->StepFirstSubpage(); // Only check update flag on first subpage. Carousels don't get pushed out anyway\n                                        if (std::shared_ptr<Subpage> s = page->GetSubpage())\n                                            update = (s->GetSubpageStatus() & PAGESTATUS_C8_UPDATE);\n                                        \n                                        if (!(_pageList->Contains(page)))\n                                        {\n                                            // this page is not currently in pagelist\n                                            _pageList->AddPage(page, !update); // only transmit immediate update if update flag is set\n                                        }\n                                        else\n                                        {\n                                            _pageList->UpdatePageLists(page, !update); // only transmit immediate update if update flag is set\n                                        }\n                                        \n                                        page->StepLastSubpage(); // prepare for page to roll to first subpage\n                                    }\n                                    else\n                                    {\n                                        _debug->Log(Debug::LogLevels::logWARN,\"[FileMonitor::run] Failed to load \" + std::string(dirp->d_name));\n                                        page->MarkForDeletion(); // mark page for deletion from service\n                                    }\n                                    \n                                    _pageList->CheckForPacket29OrCustomHeader(page);\n                                    \n                                    f->SetModifiedTime(attrib.st_mtime);\n                                }\n                                page->FreeLock(); // must unlock or everything will grind to a halt\n                            }\n                        }\n                    }\n                }\n                else\n                {\n                    if (!firstrun){ // suppress logspam on first run\n                        _debug->Log(Debug::LogLevels::logINFO,\"[FileMonitor::run] Adding a new page \" + std::string(dirp->d_name));\n                    }\n                    // A new file. Create the page object and add it to the page list.\n                    \n                    std::shared_ptr<File> f(new File(name));\n                    f->SetModifiedTime(attrib.st_mtime); // set timestamp\n                    if (f->Loaded())\n                    {\n                        std::shared_ptr<TTXPageStream> page = f->GetPage();\n                        \n                        // don't add to updated pages list if this is the initial startup\n                        int update = false;\n                        if (std::shared_ptr<Subpage> s = page->GetSubpage())\n                            update = (s->GetSubpageStatus() & PAGESTATUS_C8_UPDATE) && !page->IsCarousel(); // only check update flag of single subpages\n                        _pageList->AddPage(page, firstrun | !update); // only transmit immediate update if update flag is set and vbit2 isn't starting up\n                        _pageList->CheckForPacket29OrCustomHeader(page);\n                    }\n                    else\n                    {\n                        _debug->Log(Debug::LogLevels::logWARN,\"[FileMonitor::run] Failed to load \" + std::string(dirp->d_name));\n                    }\n                    _FilesList.push_back(f);\n                }\n            }\n        }\n    }\n    closedir(dp);\n    \n    return 0;\n}\n\n\n// Find a file by filename\nstd::shared_ptr<File> FileMonitor::Locate(std::string filename)\n{\n    for (std::list<std::shared_ptr<File>>::iterator p=_FilesList.begin();p!=_FilesList.end();++p)\n    {\n        std::shared_ptr<File> ptr = *p;\n        if (filename==ptr->GetFilename())\n            return ptr;\n    }\n    \n    return NULL; // @todo placeholder What should we do here?\n}\n\n// Detect pages that have been deleted from the drive\n// Do this by first clearing all the \"exists\" flags\n// As we scan through the list, set the \"exists\" flag as we match up the drive to the loaded page\nvoid FileMonitor::ClearFlags()\n{\n    for (std::list<std::shared_ptr<File>>::iterator p=_FilesList.begin();p!=_FilesList.end();++p)\n    {\n        std::shared_ptr<File> ptr = *p;\n\n        ptr->SetState(File::NOTFOUND);\n    }\n}\n\nvoid FileMonitor::DeleteOldPages()\n{\n    for (std::list<std::shared_ptr<File>>::iterator p=_FilesList.begin();p!=_FilesList.end();++p)\n    {\n        std::shared_ptr<File> ptr = *p;\n        if (ptr->GetStatusFlag()==File::NOTFOUND)\n        {\n            ptr->GetPage()->MarkForDeletion(); // mark page for deletion from service\n            _debug->Log(Debug::LogLevels::logINFO,\"[FileMonitor::DeleteOldPages] Deleted \" + ptr->GetFilename());\n                \n            Delete29AndHeader(ptr->GetPage());\n            _FilesList.remove(*p--); // remove file from filelist\n        }\n    }\n}\n\nvoid FileMonitor::Delete29AndHeader(std::shared_ptr<TTXPageStream> page)\n{\n    int mag=(page->GetPageNumber() >> 8) & 0x7;\n    if (page->GetPacket29Flag())\n    {\n        // Packet 29 was loaded from this page, so remove it.\n        _pageList->GetMagazines()[mag]->DeletePacket29();\n        _debug->Log(Debug::LogLevels::logINFO,\"[PageList::DeleteOldPages] Removing packet 29 from magazine \" + std::to_string((mag == 0)?8:mag));\n    }\n    if (page->GetCustomHeaderFlag())\n    {\n        // Custom header was loaded from this page, so remove it.\n        _pageList->GetMagazines()[mag]->DeleteCustomHeader();\n    }\n}\n"
  },
  {
    "path": "filemonitor.h",
    "content": "#ifndef _FILEMONITOR_H_\n#define _FILEMONITOR_H_\n\n#include <iostream>\n#include <sstream>\n#include <thread>\n#include <list>\n#include <strings.h>\n#include <sys/stat.h>\n#include <array>\n\n#include \"configure.h\"\n#include \"pagelist.h\"\n#include \"packetmag.h\"\n#include \"ttxpagestream.h\"\n\nnamespace vbit\n{\n    class File\n    {\n        public:\n            enum Status\n            {\n              NEW,      // Just created\n              NOTFOUND, // Not found yet\n              FOUND     // Matched on drive\n            };\n            \n            File(std::string filename);\n            std::shared_ptr<TTXPageStream> GetPage(){return _page;};\n            \n            // The time that the file was modified.\n            time_t GetModifiedTime(){return _modifiedTime;};\n            void SetModifiedTime(time_t timeVal){_modifiedTime=timeVal;};\n            \n            void SetState(Status state){_fileStatus=state;};\n            Status GetStatusFlag(){return _fileStatus;};\n            \n            std::string GetFilename() const {return _filename;}\n            \n            void LoadFile(std::string filename);\n            bool Loaded(){return _loaded;}\n            \n        private:\n            std::shared_ptr<TTXPageStream> _page; // the page loaded from this file\n            std::string _filename;\n            time_t _modifiedTime;   /// Poll this in case the source file changes (Used to detect updates)\n            Status _fileStatus; /// Used to mark if we found the file. (Used to detect deletions)\n            bool LoadTTI(std::string filename);\n            bool _loaded;\n    };\n    \n    /**\n     * Watches for changes to teletext page files and adds them to the page list or marks them for removal\n     */\n    class FileMonitor\n    {\n        public:\n            /** Default constructor */\n            FileMonitor();\n            FileMonitor(Configure *configure, Debug *debug, PageList *pageList);\n            /** Default destructor */\n            virtual ~FileMonitor();\n\n            /**\n             * Runs the monitoring thread and does not terminate (at least for now)\n             * @return Nothing useful yet. Perhaps return an error status if something goes wrong\n             */\n            void run();\n\n        protected:\n\n        private:\n            Configure* _configure; /// Member reference to the configuration settings\n            Debug* _debug;\n            PageList* _pageList;\n            std::list<std::shared_ptr<File>> _FilesList;\n            int readDirectory(std::string path, bool firstrun=false);\n            \n            std::shared_ptr<File> Locate(std::string filename);\n            void ClearFlags();\n            void DeleteOldPages();\n            void Delete29AndHeader(std::shared_ptr<TTXPageStream> page);\n    };\n}\n\n#endif // _FILEMONITOR_H_\n"
  },
  {
    "path": "getvbit2",
    "content": "#!/bin/bash\n\nif [ ! -f /etc/rpi-issue ]; then\n    echo \"This installer is intended for Raspberry Pi OS only\"\n    exit 1\nfi\n\n# install required packages\nsudo apt update\nsudo apt -y install git subversion dialog python3-dialog\n\nif (( $(lsb_release -r | tr -dc '0-9') > 11 )); then\n    # bookworm\n    sudo apt -y install libraspberrypi-dev\n    \n    # Disable the KMS graphics driver\n    sudo sed -i s/dtoverlay=vc4-kms-v3d/#dtoverlay=vc4-kms-v3d/ /boot/firmware/config.txt\n    \n    if ! grep -q \"sdtv_mode\" /boot/firmware/config.txt; then\n        sudo sed -i -e $'$a\\\\\\nsdtv_mode=2' /boot/firmware/config.txt\n    fi\n    \n    if ! grep -q \"enable_tvout\" /boot/firmware/config.txt; then\n        sudo sed -i -e $'$a\\\\\\nenable_tvout=1' /boot/firmware/config.txt\n    fi\nelse\n    # System sets SD video on bootup \n    sudo sed -i s/#sdtv_mode/sdtv_mode/ /boot/config.txt\n\n    # Disable the new KMS graphics driver in bullseye\n    sudo sed -i s/dtoverlay=vc4-kms-v3d/#dtoverlay=vc4-kms-v3d/ /boot/config.txt\nfi\n\n# download the raspi-teletext git repository and compile it\ngit clone https://github.com/ali1234/raspi-teletext.git $HOME/raspi-teletext\ncd $HOME/raspi-teletext\nmake\n\n# download the vbit2 git repository\ngit clone https://github.com/peterkvt80/vbit2.git $HOME/vbit2\ncd $HOME/vbit2\n\n# switch to the latest release branch and compile it\nlatestTag=`curl --silent \"https://api.github.com/repos/peterkvt80/vbit2/releases/latest\" | grep -Po '\"tag_name\": \"\\K.*?(?=\")'`\ngit checkout $latestTag\nmake\n\n# create links\nmkdir -p $HOME/.local/bin\nln -s -f `pwd`/vbit2 $HOME/.local/bin/\nln -s -f `pwd`/scripts/runvbit2.py $HOME/.local/bin/runvbit2\nln -s -f `pwd`/scripts/teletext-update.py $HOME/.local/bin/teletext-update\nln -s -f `pwd`/scripts/vbit-config.py $HOME/.local/bin/vbit-config\n\n# install systemd user scripts\nmkdir -p $HOME/.local/share/systemd/user\ncp vbit2.service $HOME/.local/share/systemd/user\ncp teletext-update.timer $HOME/.local/share/systemd/user\ncp teletext-update.service $HOME/.local/share/systemd/user\n\nmkdir -p $HOME/.config/systemd/user/\nsystemctl --user daemon-reload\nloginctl enable-linger\n\nif [[ ! $PATH =~ \"$HOME/.local/bin\" ]]; then\n  PATH=\"$HOME/.local/bin:$PATH\"\nfi\n\nvbit-config\n"
  },
  {
    "path": "interfaceServer.cpp",
    "content": "/** A TCP server for various configuration and teletext input interfaces\n    A network interface for the injection of databroadcast packets, inserter configuration, and\n    page configuration, for multiple simultaneous clients.\n    \n    This is a binary message interface where the first byte of any message holds the message\n    length, and the second byte contains either a command number (for messages to the server) or an\n    error code (for server replies). A client should transmit a command and then read the server's\n    response to get the error state and any returned data.\n    \n    The command numbers and error codes are defined in interfaceServer.h\n    \n    The client must select which channel subsequent commands relate to using the SETCHAN command.\n    Databroadcast packet commands must be sent to any of channels 1-15 (corresponding to the packet\n    addresses 1/30 to 7/31), and only a single client may select each of these datacast channels at\n    one time.\n    Channel 0 is used for non databroadcast commands and may have multiple simultaneous users.\n    \n    The server currently implements:\n    * A databroadcast interface, which can encode IDL format A datacast, or inject raw packet data.\n    * A configuration interface, which can set and retrieve certain vbit2 configuration options.\n    * A page data API, capable of creating and deleting pages, modifying settings and row data.\n*/\n\n#include \"interfaceServer.h\"\n\nusing namespace vbit;\n\nInterfaceServer::InterfaceServer(Configure *configure, Debug *debug, PageList *pageList) :\n    _configure(configure),\n    _debug(debug),\n    _pageList(pageList),\n    _portNumber(configure->GetInterfaceServerPort()),\n    _maxClients(configure->GetInterfaceServerMaxClients()),\n    _isActive(false)\n{\n    /* initialise sockets */\n    _serverSock = -1;\n    \n    _datachannel[0]=nullptr; // do not create PacketDatacast for reserved data channel 0\n    for (int dc=1; dc<16; dc++)\n    {\n        _datachannel[dc] = new PacketDatacast(dc, configure); // initialise remaining 15 datacast channels\n    }\n}\n\nInterfaceServer::~InterfaceServer()\n{\n}\n\nvoid InterfaceServer::SocketError(std::string errorMessage)\n{\n    if (_serverSock >= 0)\n    {\n        #ifdef WIN32\n            closesocket(_serverSock);\n        #else\n            close(_serverSock);\n        #endif\n    }\n    \n    for(std::list<ClientState>::iterator it = _clients.begin(); it != _clients.end();)\n    {\n        ClientState client = *it;\n        \n        if (client.socket >= 0)\n            CloseClient(&client);\n        \n        it = _clients.erase(it);\n    }\n    \n    _debug->Log(Debug::LogLevels::logERROR,errorMessage);\n}\n\nvoid InterfaceServer::CloseClient(ClientState *client)\n{\n    // close socket\n    #ifdef WIN32\n        closesocket(client->socket);\n    #else\n        close(client->socket);\n    #endif\n    \n    if (client->page)\n    {\n        client->page->FreeLock(); // unlock page\n    }\n}\n\nvoid InterfaceServer::run()\n{\n    _debug->Log(Debug::LogLevels::logINFO,\"[InterfaceServer::run] Datacast server thread started for \"+(_maxClients?\"max \"+std::to_string(_maxClients):\"unlimited\")+\" connections\");\n    \n    int newSock;\n    struct sockaddr_in address;\n    char readBuffer[256];\n    fd_set readfds;\n    \n#ifdef WIN32\n    int addrlen;\n    WSADATA wsaData;\n    int iResult;\n\n    // Initialize Winsock\n    iResult = WSAStartup(MAKEWORD(2,2), &wsaData);\n    if (iResult != 0)\n    {\n        SocketError(\"[InterfaceServer::run] WSAStartup failed\\n\");\n        return;\n    }\n#else\n    unsigned int addrlen;\n#endif\n    \n    /* Create socket */\n    if ((_serverSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)\n    {\n        SocketError(\"[InterfaceServer::run] socket() failed\\n\");\n        return;\n    }\n    \n    int reuse = true;\n    \n    /* Allow multiple connnections */\n    if(setsockopt(_serverSock, SOL_SOCKET, SO_REUSEADDR, (const char *)&reuse, sizeof(reuse)) < 0)\n    {\n        SocketError(\"[InterfaceServer::run] setsockopt() SO_REUSEADDR failed\\n\");\n        return;\n    }\n    \n    address.sin_family = AF_INET;\n    address.sin_addr.s_addr = htonl(INADDR_ANY);\n    address.sin_port = htons(_portNumber);\n    \n    /* bind socked */\n    if (bind(_serverSock, (struct sockaddr *) &address, sizeof(address)) < 0)\n    {\n        SocketError(\"[InterfaceServer::run] bind() failed\\n\");\n        return;\n    }\n    \n    /* Listen for incoming connections */\n    if (listen(_serverSock, MAXPENDING) < 0)\n    {\n        SocketError(\"[InterfaceServer::run] listen() failed\\n\");\n        return;\n    }\n    \n    addrlen = sizeof(address);\n    \n    while(true)\n    {\n        FD_ZERO(&readfds);\n        FD_SET(_serverSock, &readfds);\n        \n        for(std::list<ClientState>::iterator it = _clients.begin(); it != _clients.end(); ++it)\n        {\n            if ((*it).socket > -1)\n                FD_SET((*it).socket , &readfds);\n        }\n        _isActive = !(_clients.empty());\n        \n        /* wait for activity on any socket */\n        if ((select(FD_SETSIZE, &readfds, NULL, NULL, NULL) < 0) && (errno!=EINTR))\n            SocketError(\"[InterfaceServer::run] select() failed\");\n        \n        if (FD_ISSET(_serverSock, &readfds))\n        {\n            /* incoming connection to server */\n            if ((newSock = accept(_serverSock, (struct sockaddr *)&address, &addrlen))<0)\n            {\n                SocketError(\"[InterfaceServer::run] accept() failed\");\n                return;\n            }\n            \n            #ifdef WIN32\n                u_long ul = 1;\n                if (ioctlsocket(newSock, FIONBIO, &ul) < 0)\n                {\n                    SocketError(\"[InterfaceServer::run] ioctlsocket() failed\");\n                    return;\n                }\n            #else\n                if (fcntl(newSock, F_SETFL, fcntl(newSock, F_GETFL, 0) | O_NONBLOCK) < 0)\n                {\n                    SocketError(\"[InterfaceServer::run] fcntl() failed\");\n                    return;\n                }\n            #endif\n            \n            if (_maxClients > 0 && _maxClients == _clients.size())\n            {\n                /* no more client slots so reject */\n                #ifdef WIN32\n                    closesocket(newSock);\n                #else\n                    close(newSock);\n                #endif\n                _debug->Log(Debug::LogLevels::logWARN,\"[InterfaceServer::run] reject new connection from \" + std::string(inet_ntoa(address.sin_addr)) + \" (too many connections)\");\n                continue;\n            }\n            \n            ClientState newClient;\n            newClient.socket = newSock;\n            _clients.push_back(newClient);\n            \n            _debug->Log(Debug::LogLevels::logINFO,\"[InterfaceServer::run] new connection from \" + std::string(inet_ntoa(address.sin_addr)) + \":\" + std::to_string(ntohs(address.sin_port)) + \" as socket \" + std::to_string(newSock));\n        }\n        else\n        {\n            /* a client socket has activity */\n            for(std::list<ClientState>::iterator it = _clients.begin(); it != _clients.end();)\n            {\n                ClientState* client = &(*it);\n                \n                if (client->socket >= 0 && FD_ISSET(client->socket , &readfds))\n                {\n                    /* socket has activity */\n                    getpeername(client->socket, (struct sockaddr*)&address, &addrlen);\n                    \n                    int n = recv(client->socket, readBuffer, 1, MSG_PEEK); // peek at first byte of message\n                    if (n == 1)\n                    {\n                        // byte 0 of message is message length\n                        int len = (uint8_t)readBuffer[0];\n                        if (len)\n                        {\n                            n = recv(client->socket, readBuffer, len, 0); // try to read whole message\n                            if (n == len)\n                            {\n                                std::vector<uint8_t> res = {CMDOK}; // create \"OK\" response\n                                \n                                // byte 1 of message is interface server command number\n                                switch ((uint8_t)readBuffer[1]){\n                                    case SETCHAN: // set interface channel\n                                    {\n                                        int ch = (uint8_t)readBuffer[2];\n                                        if (n == 3 && ch >= 0 && ch <= 15)\n                                        {\n                                            client->channel = -1; // release current channel\n                                            \n                                            \n                                            if (ch) // allows multiple connections for channel 0\n                                            {\n                                                // check if another client is using desired datachannel\n                                                for(std::list<ClientState>::iterator it = _clients.begin(); it != _clients.end(); ++it)\n                                                {\n                                                    if ((*it).channel == ch)\n                                                    {\n                                                        res[0] = CMDBUSY;\n                                                        goto SETCHANError; // jump out to return error\n                                                    }\n                                                }\n                                            }\n                                            \n                                            client->channel = ch; // use requested channel\n                                            \n                                            std::stringstream ss;\n                                            ss << \"[InterfaceServer::run] Client \" << std::string(inet_ntoa(address.sin_addr)) << \":\" << std::to_string(ntohs(address.sin_port)) << \": SETCHAN \" << ch;\n                                            _debug->Log(Debug::LogLevels::logDEBUG,ss.str());\n                                            break;\n                                        }\n                                        \n                                        res[0] = CMDERR;\n                                        SETCHANError:\n                                        break;\n                                    }\n                                    \n                                    case GETAPIVER:\n                                    {\n                                        /* get API version number */\n                                        if (n == 2)\n                                        {\n                                            res.push_back(APIVERSION[0]); // major version\n                                            res.push_back(APIVERSION[1]); // minor version\n                                            res.push_back(APIVERSION[2]); // patch\n                                        }\n                                        else\n                                            res[0] = CMDERR;\n                                        break;\n                                    }\n                                    \n                                    case DBCASTAPI:\n                                    {\n                                        /* databroadcast API */\n                                        if (client->channel > 0 && n > 2) // databroadcast commands only on channels 1-15\n                                        {\n                                            switch((uint8_t)readBuffer[2]) // byte 2 is databroadcast command number\n                                            {\n                                                case DCRAW: // push raw datacast packet to buffer\n                                                {\n                                                    if (n == 43) // 40 bytes of packet data\n                                                    {\n                                                        std::vector<uint8_t> data(readBuffer+3, readBuffer+n);\n                                                        \n                                                        if(_datachannel[client->channel]->PushRaw(&data))\n                                                        {\n                                                            res[0] = CMDBUSY; // buffer full\n                                                        }\n                                                    }\n                                                    else\n                                                    {\n                                                        res[0] = CMDERR;\n                                                    }\n                                                    break;\n                                                }\n                                                \n                                                case DCFORMATA: // encode and push a format A datacast payload to buffer\n                                                {\n                                                    /* Does _not_ automate repeats, continuity indicator, etc.\n                                                       Format is:\n                                                         byte 3: bits 4-7 IAL, bits 1-3 flags RI,CI,DL\n                                                         byte 4-6: 24 bit Service Packet Address (little endian)\n                                                         byte 7: Repeat indicator\n                                                         byte 8: Continuity indicator\n                                                         byte 9+: payload data\n                                                    */\n                                                    if (n > 9)\n                                                    {\n                                                        uint8_t flags = (uint8_t)readBuffer[3] & 0xe;\n                                                        uint8_t ial = (uint8_t)readBuffer[3] >> 4;\n                                                        uint32_t spa = (uint8_t)readBuffer[4] | ((uint8_t)readBuffer[5] << 8) | ((uint8_t)readBuffer[6] << 16);\n                                                        uint8_t ri = (uint8_t)readBuffer[7];\n                                                        uint8_t ci = (uint8_t)readBuffer[8];\n                                                        \n                                                        std::vector<uint8_t> data(readBuffer+9, readBuffer+n);\n                                                        \n                                                        int bytes = _datachannel[client->channel]->PushIDLA(flags, ial, spa, ri, ci, &data);\n                                                        \n                                                        if (bytes == 0) // buffer full\n                                                            res[0] = CMDBUSY;\n                                                        else if (bytes < n-9) // payload didn't fit\n                                                            res[0] = CMDTRUNC; // warn of truncation\n                                                        \n                                                        res.push_back(bytes); // return number of bytes written\n                                                        \n                                                        break;\n                                                    }\n                                                    else\n                                                    {\n                                                        res[0] = CMDERR;\n                                                        res.push_back(0); // no bytes written\n                                                    }\n                                                    break;\n                                                }\n                                                \n                                                default: // unknown datacast command\n                                                    res[0] = CMDERR;\n                                            }\n                                        }\n                                        else\n                                        {\n                                            res[0] = CMDERR;\n                                        }\n                                        break;\n                                    }\n                                    \n                                    case CONFIGAPI:\n                                    {\n                                        /* vbit2 configuration API */\n                                        if (client->channel == 0 && n > 2) // allow VBIT2 configuration commands on channel 0 only\n                                        {\n                                            switch((uint8_t)readBuffer[2]){ // byte 2 is configuration command number\n                                                case CONFRAFLAG: /* get/set row adaptive flag */\n                                                {\n                                                    if (n == 4)\n                                                    {\n                                                        _configure->SetRowAdaptive((uint8_t)readBuffer[3]&1);\n                                                        std::stringstream ss;\n                                                        ss << \"[InterfaceServer::run] Client \" << std::string(inet_ntoa(address.sin_addr)) << \":\" << std::to_string(ntohs(address.sin_port)) << \": CONFRAFLAG \" << ((readBuffer[3] & 1)?\"ON\":\"OFF\");\n                                                        _debug->Log(Debug::LogLevels::logDEBUG,ss.str());\n                                                    }\n                                                    else if (n != 3)\n                                                    {\n                                                        res[0] = CMDERR;\n                                                    }\n                                                    res.push_back(_configure->GetRowAdaptive()?1:0);\n                                                    \n                                                    break;\n                                                }\n                                                \n                                                case CONFRBYTES: /* get/set BSDP reserved bytes */\n                                                {\n                                                    if (n == 7) // set new bytes\n                                                    {\n                                                        _configure->SetReservedBytes(std::array<uint8_t, 4>({(uint8_t)readBuffer[3],(uint8_t)readBuffer[4],(uint8_t)readBuffer[5],(uint8_t)readBuffer[6]}));\n                                                        std::stringstream ss;\n                                                        ss << \"[InterfaceServer::run] Client \" << std::string(inet_ntoa(address.sin_addr)) << \":\" << std::to_string(ntohs(address.sin_port)) << \": CONFRBYTES set\";\n                                                        _debug->Log(Debug::LogLevels::logDEBUG,ss.str());\n                                                    }\n                                                    else if (n != 3)\n                                                    {\n                                                        res[0] = CMDERR;\n                                                    }\n                                                    \n                                                    std::array<uint8_t, 4> bytes = _configure->GetReservedBytes();\n                                                    res.push_back(bytes[0]);\n                                                    res.push_back(bytes[1]);\n                                                    res.push_back(bytes[2]);\n                                                    res.push_back(bytes[3]); // read back reserved bytes\n                                                    break;\n                                                }\n                                                \n                                                case CONFSTATUS: /* get/set BSDP status message */\n                                                {\n                                                    if (n == 23) // set new status\n                                                    {\n                                                        std::ostringstream tmp;\n                                                        for (int i = 3; i < 23; i++)\n                                                        {\n                                                            tmp << (char)(readBuffer[i] & 0x7f);\n                                                        }\n                                                        \n                                                        _configure->SetServiceStatusString(tmp.str());\n                                                        std::stringstream ss;\n                                                        ss << \"[InterfaceServer::run] Client \" << std::string(inet_ntoa(address.sin_addr)) << \":\" << std::to_string(ntohs(address.sin_port)) << \": CONFSTATUS set\";\n                                                        _debug->Log(Debug::LogLevels::logDEBUG,ss.str());\n                                                    }\n                                                    else if (n != 3)\n                                                    {\n                                                        res[0] = CMDERR;\n                                                    }\n                                                    \n                                                    for(char& c : _configure->GetServiceStatusString()) {\n                                                        res.push_back((uint8_t)c & 0x7f);\n                                                    }\n                                                    \n                                                    break;\n                                                }\n                                                \n                                                case CONFHEADER: /* get/set header template */\n                                                {\n                                                    if (n == 35 || n == 36) // set new header template\n                                                    {\n                                                        std::array<uint8_t, 40> tmp;\n                                                        for (int i = 0; i < 32; i++)\n                                                            tmp[8+i] = readBuffer[(n-32)+i];\n                                                        std::shared_ptr<TTXLine> line(new TTXLine(tmp));\n                                                        \n                                                        std::stringstream ss;\n                                                        if (n==35)\n                                                        {\n                                                            _configure->SetHeaderTemplate(line);\n                                                            ss << \"[InterfaceServer::run] Client \" << std::string(inet_ntoa(address.sin_addr)) << \":\" << std::to_string(ntohs(address.sin_port)) << \": CONFHEADER set\";\n                                                        }\n                                                        else\n                                                        {\n                                                            uint8_t mag = (uint8_t)readBuffer[3] & 0x7;\n                                                            _pageList->GetMagazines()[mag]->SetCustomHeader(line);\n                                                            ss << \"[InterfaceServer::run] Client \" << std::string(inet_ntoa(address.sin_addr)) << \":\" << std::to_string(ntohs(address.sin_port)) << \": CONFHEADER set on magazine \" << (int)mag;\n                                                        }\n                                                        _debug->Log(Debug::LogLevels::logDEBUG,ss.str());\n                                                    }\n                                                    else if (n == 3)\n                                                    {\n                                                        for(char& c : _configure->GetHeaderTemplate()) {\n                                                            res.push_back((uint8_t)c);\n                                                        }\n                                                    }\n                                                    else if (n == 4)\n                                                    {\n                                                        uint8_t mag = (uint8_t)readBuffer[3];\n                                                        if (mag & 0x80)\n                                                        {\n                                                            // delete flag\n                                                            _pageList->GetMagazines()[mag & 0x7]->DeleteCustomHeader();\n                                                        }\n                                                        else\n                                                        {\n                                                            std::string tmp = _pageList->GetMagazines()[mag & 0x7]->GetCustomHeader();\n                                                            if (tmp.length() == 32)\n                                                            {\n                                                                for(char& c : tmp) {\n                                                                    res.push_back((uint8_t)c);\n                                                                }\n                                                            }\n                                                            else\n                                                            {\n                                                                // no custom header set for magazine\n                                                                res[0] = CMDNOENT;\n                                                            }\n                                                        }\n                                                    }\n                                                    else\n                                                    {\n                                                        res[0] = CMDERR;\n                                                    }\n                                                    \n                                                    break;\n                                                }\n                                                \n                                                case CONFENHANC:\n                                                {\n                                                    res[0] = CMDERR; // default to an returning an error unless we have success\n                                                    if (n > 3)\n                                                    {\n                                                        int mag = (uint8_t)readBuffer[3] & 0x1F;\n                                                        int deleteFlag = (uint8_t)readBuffer[3]&0x80;\n                                                        if (mag < 8)\n                                                        {\n                                                            if (!deleteFlag)\n                                                            {\n                                                                if (n == 44)\n                                                                {\n                                                                    // write row data\n                                                                    std::array<uint8_t, 40> tmp;\n                                                                    for (int i = 0; i < 40; i++)\n                                                                        tmp[i] = (uint8_t)readBuffer[4+i];\n                                                                    std::shared_ptr<TTXLine> line(new TTXLine(tmp));\n                                                                    \n                                                                    _pageList->GetMagazines()[mag]->SetPacket29(line);\n                                                                    \n                                                                    res[0] = CMDOK;\n                                                                }\n                                                                else if (n == 5)\n                                                                {\n                                                                    // read row data\n                                                                    std::shared_ptr<TTXLine> line = _pageList->GetMagazines()[mag]->GetPacket29();\n                                                                    if (line!=nullptr)\n                                                                        line = line->LocateLine((uint8_t)readBuffer[4]&0xF);\n                                                                    \n                                                                    if (line != nullptr)\n                                                                    {\n                                                                        std::array<uint8_t, 40> tmp = line->GetLine();\n                                                                        res.insert (res.end(), tmp.data(), tmp.data()+tmp.size());\n                                                                        res[0] = CMDOK;\n                                                                    }\n                                                                    else\n                                                                        res[0] = CMDNOENT; // row doesn't exist\n                                                                }\n                                                            }\n                                                            else\n                                                            {\n                                                                if (n==4)\n                                                                {\n                                                                    // delete all packets\n                                                                    _pageList->GetMagazines()[mag]->DeletePacket29();\n                                                                }\n                                                                else if (n==5)\n                                                                {\n                                                                    // delete dc\n                                                                    _pageList->GetMagazines()[mag]->DeletePacket29((uint8_t)readBuffer[4]&0xF);\n                                                                }\n                                                                res[0] = CMDOK; // didn't check if line existed, just returns OK\n                                                            }\n                                                        }\n                                                    }\n                                                    break;\n                                                }\n                                                \n                                                default: // unknown configuration command\n                                                    res[0] = CMDERR;\n                                            }\n                                        }\n                                        else\n                                        {\n                                            res[0] = CMDERR;\n                                        }\n                                        break;\n                                    }\n                                    \n                                    case PAGESAPI:\n                                    {\n                                        /* page data API */\n                                        \n                                        if (client->channel == 0 && n > 2) // allow page data commands on channel 0 only\n                                        {\n                                            int cmd = (uint8_t)readBuffer[2]; // byte 2 is page data API command number\n                                            \n                                            if (cmd <= PAGEDELSUB) // all commands that start with a page/subpage number\n                                            {\n                                                if (cmd <= PAGEOPEN && client->page)\n                                                {\n                                                    // implicitly close page when issuing other page delete/open commands\n                                                    client->page->FreeLock();\n                                                    client->page = nullptr;\n                                                    client->subpage = nullptr;\n                                                }\n                                                \n                                                if (n >= 5)\n                                                {\n                                                    int num = ((uint8_t)readBuffer[3] << 8) | (uint8_t)readBuffer[4];\n                                                    if (cmd == PAGEDELETE)\n                                                    {\n                                                        if (n == 5)\n                                                        {\n                                                            std::stringstream ss;\n                                                            ss << \"[InterfaceServer::run] Client \" << std::string(inet_ntoa(address.sin_addr)) << \":\" << std::to_string(ntohs(address.sin_port)) << \": PAGEDELETE \" << std::hex << num;\n                                                            _debug->Log(Debug::LogLevels::logDEBUG,ss.str());\n                                                            \n                                                            std::shared_ptr<TTXPageStream> p = _pageList->Locate(num);\n                                                            if (p != nullptr)\n                                                            {\n                                                                p->SetOneShotFlag(false);\n                                                                p->MarkForDeletion();\n                                                            }\n                                                            else\n                                                            {\n                                                                res[0] = CMDNOENT;\n                                                            }\n                                                        }\n                                                        else\n                                                            res[0] = CMDERR;\n                                                    }\n                                                    else if (cmd == PAGEOPEN)\n                                                    {\n                                                        bool OneShot = false;\n                                                        if (n > 5)\n                                                            OneShot = (readBuffer[5] & 1);\n                                                        \n                                                        if (n < 7)\n                                                        {\n                                                            std::stringstream ss;\n                                                            ss << \"[InterfaceServer::run] Client \" << std::string(inet_ntoa(address.sin_addr)) << \":\" << std::to_string(ntohs(address.sin_port)) << \": PAGEOPEN \" << std::hex << num << (OneShot?\" as OneShot\":\"\");\n                                                            _debug->Log(Debug::LogLevels::logDEBUG,ss.str());\n                                                            if ((uint8_t)readBuffer[3] > 0 && (uint8_t)readBuffer[3] <= 8 && (uint8_t)readBuffer[4] < 0xff)\n                                                            {\n                                                                std::shared_ptr<TTXPageStream> p = _pageList->Locate(num);\n                                                                if (p == nullptr || p->GetIsMarked())\n                                                                {\n                                                                    p = std::shared_ptr<TTXPageStream>(new TTXPageStream()); // create new page\n                                                                    std::stringstream ss;\n                                                                    ss << \"[InterfaceServer::run] Created new page \" << std::hex << num;\n                                                                    _debug->Log(Debug::LogLevels::logINFO,ss.str());\n                                                                    if (p->GetLock()) // if this fails we have a real problem!\n                                                                    {\n                                                                        p->SetPageNumber(num);\n                                                                        p->SetOneShotFlag(OneShot);\n                                                                        _pageList->AddPage(p, true); // put it in the page lists\n                                                                        \n                                                                        // at this stage it has no subpages!\n                                                                        client->page = p;\n                                                                    }\n                                                                    else\n                                                                    {\n                                                                        res[0] = CMDBUSY;\n                                                                    }\n                                                                }\n                                                                else\n                                                                {\n                                                                    res[0] = CMDBUSY; // overwritten if successful\n                                                                    if (p->GetOneShotFlag() && p->GetUpdatedFlag())\n                                                                    {\n                                                                        // previous oneshot hasn't yet sent\n                                                                    }\n                                                                    else if (p->GetLock()) // try to lock page\n                                                                    {\n                                                                        if (OneShot || (p->GetOneShotFlag() != OneShot)) // oneshot or oneshot changed\n                                                                        {\n                                                                            p->SetOneShotFlag(OneShot);\n                                                                            _pageList->UpdatePageLists(p);\n                                                                        }\n                                                                        \n                                                                        client->page = p;\n                                                                        res[0] = CMDOK;\n                                                                    }\n                                                                }\n                                                            }\n                                                        }\n                                                        else\n                                                            res[0] = CMDERR;\n                                                    }\n                                                    else if (cmd == PAGESETSUB || cmd == PAGEDELSUB)\n                                                    {\n                                                        client->subpage = nullptr; // invalidate previous subpage\n                                                        if (n == 5 && client->page)\n                                                        {\n                                                            std::stringstream ss;\n                                                            ss << \"[InterfaceServer::run] Client \" << std::string(inet_ntoa(address.sin_addr)) << \":\" << std::to_string(ntohs(address.sin_port)) << \": \" << ((cmd==PAGESETSUB)?\"PAGESETSUB \":\"PAGEDELSUB \") << std::hex << num;\n                                                            _debug->Log(Debug::LogLevels::logDEBUG,ss.str());\n                                                            \n                                                            if ((num & 0xc080) || num >= 0x3f7f) // reject invalid subpage numbers\n                                                            {\n                                                                res[0] = CMDERR;\n                                                            }\n                                                            else\n                                                            {\n                                                                client->subpage = client->page->LocateSubpage(num);\n                                                                if (client->subpage == nullptr) // subpage not found\n                                                                {\n                                                                    if (cmd == PAGESETSUB)\n                                                                    {\n                                                                        client->subpage = std::shared_ptr<Subpage>(new Subpage()); // create new subpage\n                                                                        client->subpage->SetSubCode(num); // set subcode first\n                                                                        client->page->InsertSubpage(client->subpage); // add to page\n                                                                        _pageList->UpdatePageLists(client->page);\n                                                                        client->subpage->SetSubpageStatus(PAGESTATUS_TRANSMITPAGE);\n                                                                        \n                                                                        if (client->page->GetOneShotFlag()) // page is a oneshot\n                                                                            client->page->SetSubpage(num); // put this subpage on air\n                                                                    }\n                                                                    else // PAGEDELSUB\n                                                                    {\n                                                                        res[0] = CMDNOENT;\n                                                                    }\n                                                                }\n                                                                else\n                                                                {\n                                                                    if (cmd == PAGESETSUB)\n                                                                    {\n                                                                        if (client->page->GetOneShotFlag()) // page is a oneshot\n                                                                            client->page->SetSubpage(num); // put this subpage on air\n                                                                    }\n                                                                    else // PAGEDELSUB\n                                                                    {\n                                                                        client->page->RemoveSubpage(client->subpage);\n                                                                    }\n                                                                }\n                                                                unsigned int count = client->page->GetSubpageCount();\n                                                                res.push_back((count >> 8) & 0xff);\n                                                                res.push_back(count & 0xff); // return subpage count (big endian)\n                                                            }\n                                                        }\n                                                        else\n                                                            res[0] = CMDERR;\n                                                    }\n                                                }\n                                                else\n                                                    res[0] = CMDERR;\n                                            }\n                                            else if (cmd == PAGECLOSE)\n                                            {\n                                                std::stringstream ss;\n                                                ss << \"[InterfaceServer::run] Client \" << std::string(inet_ntoa(address.sin_addr)) << \":\" << std::to_string(ntohs(address.sin_port)) << \": PAGECLOSE\";\n                                                _debug->Log(Debug::LogLevels::logDEBUG,ss.str());\n                                                if (n==3)\n                                                {\n                                                    if (client->page)\n                                                        client->page->FreeLock();\n                                                    else\n                                                        res[0] = CMDNOENT;\n                                                    client->page = nullptr;\n                                                    client->subpage = nullptr;\n                                                    \n                                                    \n                                                }\n                                                else\n                                                {\n                                                    res[0] = CMDERR;\n                                                }\n                                            }\n                                            else if (cmd == PAGEFANDC)\n                                            {\n                                                if (client->page)\n                                                {\n                                                    std::stringstream ss;\n                                                    ss << \"[InterfaceServer::run] Client \" << std::string(inet_ntoa(address.sin_addr)) << \":\" << std::to_string(ntohs(address.sin_port)) << \": PAGEFANDC\";\n                                                    _debug->Log(Debug::LogLevels::logDEBUG,ss.str());\n                                                    if (n == 5) // write\n                                                    {\n                                                        client->page->SetPageFunctionInt((uint8_t)readBuffer[3]);\n                                                        client->page->SetPageCodingInt((uint8_t)readBuffer[4]);\n                                                        _pageList->UpdatePageLists(client->page);\n                                                    }\n                                                    else if (n != 3)\n                                                    {\n                                                        res[0] = CMDERR;\n                                                    }\n                                                    res.push_back(client->page->GetPageFunction());\n                                                    res.push_back(client->page->GetPageCoding());\n                                                }\n                                                else\n                                                {\n                                                    res[0] = CMDERR;\n                                                }\n                                            }\n                                            else if (cmd == PAGEOPTNS)\n                                            {\n                                                if (client->subpage)\n                                                {\n                                                    std::stringstream ss;\n                                                    ss << \"[InterfaceServer::run] Client \" << std::string(inet_ntoa(address.sin_addr)) << \":\" << std::to_string(ntohs(address.sin_port)) << \": PAGEOPTNS\";\n                                                    _debug->Log(Debug::LogLevels::logDEBUG,ss.str());\n                                                    \n                                                    if (n == 8) // write\n                                                    {\n                                                        client->subpage->SetSubpageStatus(((uint8_t)readBuffer[3] << 8) | (uint8_t)readBuffer[4]);\n                                                        client->subpage->SetRegion((uint8_t)readBuffer[5]);\n                                                        client->subpage->SetCycleTime((uint8_t)readBuffer[6]);\n                                                        client->subpage->SetTimedMode((uint8_t)readBuffer[7] & 1);\n                                                    }\n                                                    else if (n != 3)\n                                                    {\n                                                        res[0] = CMDERR;\n                                                    }\n                                                    uint16_t status = client->subpage->GetSubpageStatus();\n                                                    res.push_back((status >> 8) & 0xff);\n                                                    res.push_back(status & 0xff);\n                                                    res.push_back(client->subpage->GetRegion());\n                                                    res.push_back(client->subpage->GetCycleTime());\n                                                    res.push_back(client->subpage->GetTimedMode()?1:0);\n                                                }\n                                                else\n                                                {\n                                                    res[0] = CMDERR;\n                                                }\n                                            }\n                                            else if (cmd == PAGEROW)\n                                            {\n                                                res[0] = CMDERR; // default to an returning an error unless we have success\n                                                if (client->subpage)\n                                                {\n                                                    std::stringstream ss;\n                                                    ss << \"[InterfaceServer::run] Client \" << std::string(inet_ntoa(address.sin_addr)) << \":\" << std::to_string(ntohs(address.sin_port)) << \": PAGEROW\";\n                                                    _debug->Log(Debug::LogLevels::logDEBUG,ss.str());\n                                                    \n                                                    if (n > 3)\n                                                    {\n                                                        int num = (uint8_t)readBuffer[3] & 0x1F;\n                                                        int deleteFlag = (uint8_t)readBuffer[3]&0x80;\n                                                        if (num > 0 && num < 29)\n                                                        {\n                                                            if (!deleteFlag)\n                                                            {\n                                                                if (n == 44)\n                                                                {\n                                                                    // write row data\n                                                                    std::array<uint8_t, 40> tmp;\n                                                                    for (int i = 0; i < 40; i++)\n                                                                        tmp[i] = (uint8_t)readBuffer[4+i];\n                                                                    std::shared_ptr<TTXLine> line(new TTXLine(tmp));\n                                                                    \n                                                                    client->subpage->SetRow(num, line);\n                                                                    \n                                                                    res[0] = CMDOK;\n                                                                }\n                                                                else if ((num < 26 && n==4) || (num > 25 && n==5))\n                                                                {\n                                                                    // read row data\n                                                                    std::shared_ptr<TTXLine> line = client->subpage->GetRow(num);\n                                                                    if (n==5 && line!=nullptr)\n                                                                        line = line->LocateLine((uint8_t)readBuffer[4]&0xF);\n                                                                    \n                                                                    if (line != nullptr)\n                                                                    {\n                                                                        std::array<uint8_t, 40> tmp = line->GetLine();\n                                                                        res.insert (res.end(), tmp.data(), tmp.data()+tmp.size());\n                                                                        res[0] = CMDOK;\n                                                                    }\n                                                                    else\n                                                                        res[0] = CMDNOENT; // row doesn't exist\n                                                                }\n                                                            }\n                                                            else\n                                                            {\n                                                                if (n==4)\n                                                                {\n                                                                    // delete row data\n                                                                    client->subpage->DeleteRow(num);\n                                                                }\n                                                                else if (num > 25 && n==5)\n                                                                {\n                                                                    // delete dc\n                                                                    client->subpage->DeleteRow(num, (uint8_t)readBuffer[4]&0xF);\n                                                                }\n                                                                res[0] = CMDOK; // didn't check if line existed, just returns OK\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            }\n                                            else if (cmd == PAGELINKS)\n                                            {\n                                                if (client->subpage)\n                                                {\n                                                    std::stringstream ss;\n                                                    ss << \"[InterfaceServer::run] Client \" << std::string(inet_ntoa(address.sin_addr)) << \":\" << std::to_string(ntohs(address.sin_port)) << \": PAGELINKS\";\n                                                    _debug->Log(Debug::LogLevels::logDEBUG,ss.str());\n                                                    \n                                                    std::array<FastextLink, 6> links;\n                                                    \n                                                    if (n == 15 || n == 27)\n                                                    {\n                                                        for (int l=0; l<6; l++)\n                                                        {\n                                                            links[l].page = (((uint8_t)readBuffer[3+(l*2)] & 0x7) << 8) | (uint8_t)readBuffer[4+(l*2)];\n                                                            \n                                                            if (n == 27)\n                                                            {\n                                                                links[l].subpage = (((uint8_t)readBuffer[15+(l*2)] << 8) | (uint8_t)readBuffer[16+(l*2)]) & 0x3f7f;\n                                                            }\n                                                            else\n                                                            {\n                                                                links[l].subpage = 0x3f7f;\n                                                            }\n                                                        }\n                                                        client->subpage->SetFastext(links);\n                                                    }\n                                                    else if (n != 3)\n                                                    {\n                                                        res[0] = CMDERR;\n                                                    }\n                                                    \n                                                    if (res[0] == CMDOK)\n                                                    {\n                                                        if (client->subpage->GetFastext(&links))\n                                                        {\n                                                            for (int l = 0; l < 6; l++)\n                                                            {\n                                                                res.push_back((links[l].page >> 8) & 7);\n                                                                res.push_back(links[l].page & 0xff);\n                                                            }\n                                                            for (int l = 0; l < 6; l++)\n                                                            {\n                                                                res.push_back((links[l].subpage >> 8) & 0x3f);\n                                                                res.push_back(links[l].subpage & 0x7f);\n                                                            }\n                                                        }\n                                                        else\n                                                        {\n                                                            res[0] = CMDNOENT;\n                                                        }\n                                                    }\n                                                }\n                                                else\n                                                {\n                                                    res[0] = CMDERR;\n                                                }\n                                            }\n                                            else if (cmd > PAGELINKS) // last defined command number\n                                            {\n                                                std::stringstream ss;\n                                                ss << \"[InterfaceServer::run] Client \" << std::string(inet_ntoa(address.sin_addr)) << \":\" << std::to_string(ntohs(address.sin_port)) << \": Unknown PAGESAPI command received \" << std::hex << cmd;\n                                                _debug->Log(Debug::LogLevels::logDEBUG,ss.str());\n                                            }\n                                        }\n                                        break;\n                                    }\n                                    \n                                    default: // unknown command\n                                    {\n                                        res[0] = CMDERR;\n                                        break;\n                                    }\n                                }\n                                \n                                if (res.size() > 254)\n                                {\n                                    _debug->Log(Debug::LogLevels::logERROR,\"[InterfaceServer::run] Response too long\");\n                                    res.resize(254); // truncate!\n                                }\n                                \n                                res.insert(res.begin(), res.size()+1); // prepend message size\n                                \n                                unsigned int n = send(client->socket, (char*)res.data(), res.size(), 0); // send response\n                                \n                                if (n == res.size()) // fail if only partial response can be sent\n                                {\n                                    ++it;\n                                    continue; // next socket in loop\n                                }\n                            }\n                            // else fall through to error handling\n                        }\n                    }\n                    // couldn't read/write all bytes\n                    if (n == 0)\n                    {\n                        /* client disconnected */\n                        _debug->Log(Debug::LogLevels::logINFO,\"[InterfaceServer::run] closing connection from \" + std::string(inet_ntoa(address.sin_addr)) + \":\" + std::to_string(ntohs(address.sin_port)) + \" on socket \" + std::to_string(client->socket));\n                    }\n                    else\n                    {\n                        #ifdef WIN32\n                            int e = WSAGetLastError();\n                        #else\n                            int e = errno;\n                        #endif\n                        \n                        _debug->Log(Debug::LogLevels::logWARN,\"[InterfaceServer::run] closing connection from \" + std::string(inet_ntoa(address.sin_addr)) + \":\" + std::to_string(ntohs(address.sin_port)) + \" recv error \" + std::to_string(e) + \" on socket \" + std::to_string(client->socket));\n                    }\n                    \n                    /* close the socket when any error occurs */\n                    CloseClient(client);\n                    it = _clients.erase(it);\n                }\n                else\n                {\n                    ++it;\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "interfaceServer.h",
    "content": "#ifndef _INTERFACESERVER_H_\n#define _INTERFACESERVER_H_\n\n#include \"configure.h\"\n#include \"debug.h\"\n#include \"pagelist.h\"\n#include \"packetmag.h\"\n#include \"ttxpagestream.h\"\n#include \"packet.h\"\n#include \"packetDatacast.h\"\n\n#ifdef WIN32\n#include <winsock2.h>\n#else\n#include <fcntl.h>\n#include <sys/socket.h> /* for socket(), bind(), and connect() */\n#include <sys/select.h> /* for fd_set() */\n#include <arpa/inet.h>  /* for sockaddr_in and inet_ntoa() */\n#include <unistd.h>     /* for close() */\n#endif\n\n/* interface server command numbers */\n#define SETCHAN     0x00    /* set channel number for subsequent commands */\n#define DBCASTAPI   0x01    /* databroadcast API command */\n#define CONFIGAPI   0x02    /* vbit2 configuration API command */\n#define PAGESAPI    0x03    /* page data API */\n#define GETAPIVER   0x04    /* get API version number */\n\n/* interface server response codes */\n#define CMDOK       0x00    /* command successful */\n#define CMDTRUNC    0xfc    /* command completed but data was truncated */\n#define CMDNOENT    0xfd    /* command failed because entity doesn't exist */\n#define CMDBUSY     0xfe    /* command failed because operation is blocked */\n#define CMDERR      0xff    /* command failed */\n\n/* command numbers for databroadcast API */\n#define DCRAW       0x00    /* push raw packet data to datacast buffer */\n#define DCFORMATA   0x01    /* push format A packet to datacast buffer */\n#define DCFORMATB   0x02    /* placeholder - may never implement */\n\n/* command numbers for vbit2 configuration API */\n#define CONFRAFLAG  0x00    /* get/set row adaptive flag */\n#define CONFRBYTES  0x01    /* get/set BSDP reserved bytes */\n#define CONFSTATUS  0x02    /* get/set 20 byte BSDP status message */\n#define CONFHEADER  0x03    /* get/set 32 byte header template */\n#define CONFENHANC  0x04    /* Get/Set/Delete magazine enhancements */\n\n/* command numbers for page data API */\n#define PAGEDELETE  0x00    /* remove a page from the service */\n#define PAGEOPEN    0x01    /* open a page for updating */\n#define PAGESETSUB  0x02    /* select subpage */\n#define PAGEDELSUB  0x03    /* delete subpage */\n#define PAGECLOSE   0x04    /* close updated page */\n#define PAGEFANDC   0x05    /* get/set page function and coding */\n#define PAGEOPTNS   0x06    /* get/set subpage options */\n#define PAGEROW     0x07    /* read/write/delete subpage row data */\n#define PAGELINKS   0x08    /* get/set fastext link values */\n\nnamespace vbit\n\n{\n    class ClientState\n    {\n        public:\n            int socket = -1;\n            int channel = -1;\n            std::shared_ptr<TTXPageStream> page = nullptr;\n            std::shared_ptr<Subpage> subpage = nullptr;\n    };\n    class InterfaceServer\n    {\n        public:\n            InterfaceServer(Configure *configure, Debug *debug, PageList *pageList);\n            ~InterfaceServer();\n            \n            void run();\n            bool GetIsActive(){return _isActive;}; /* is interface server in use? */\n            \n            \n            PacketDatacast** GetDatachannels() { PacketDatacast **channels=_datachannel; return channels; };\n            \n        private:\n            const uint8_t APIVERSION[3] = {1,0,0}; // Version number for interface API.\n            \n            Configure* _configure;\n            Debug* _debug;\n            PageList* _pageList;\n            PacketDatacast* _datachannel[16]; /* array of datacast sources */\n            \n            static const uint16_t MAXPENDING=5;\n            \n            int _portNumber;\n            int _serverSock;\n            \n            std::list<ClientState> _clients;\n            uint16_t _maxClients;\n            \n            bool _isActive;\n            \n            void SocketError(std::string errorMessage); // handle fatal socket errors\n            void CloseClient(ClientState *client); // clean up after a connected client\n    };\n}\n\n#endif\n"
  },
  {
    "path": "masterClock.h",
    "content": "#ifndef _MASTERCLOCK_H_\n#define _MASTERCLOCK_H_\n\n#include <cstdint>\n#include <ctime>\n\nnamespace vbit\n{\n    class MasterClock {\n        public:\n            struct timeStruct {\n                time_t seconds;\n                uint8_t fields;\n            };\n            \n            static MasterClock *Instance(){\n                if (!instance)\n                    instance = new MasterClock;\n                return instance;\n            }\n            \n            void SetMasterClock(timeStruct t){ _masterClock = t; }\n            timeStruct GetMasterClock(){ return _masterClock; }\n            \n        private:\n            MasterClock(){ _masterClock = {time(NULL)-1, 0}; }; // initialise master clock to system time - 1\n            static MasterClock *instance;\n            timeStruct _masterClock;\n    };\n}\n\n#endif // _MASTERCLOCK_H_\n"
  },
  {
    "path": "normalpages.cpp",
    "content": "\n#include \"normalpages.h\"\n\nusing namespace vbit;\n\nNormalPages::NormalPages(int mag, PageList *pageList, Debug *debug) :\n    _mag(mag),\n    _pageList(pageList),\n    _debug(debug)\n{\n    _iter=_NormalPagesList.begin();\n    _page=nullptr;\n}\n\nNormalPages::~NormalPages()\n{\n\n}\n\nvoid NormalPages::addPage(std::shared_ptr<TTXPageStream> p)\n{\n    p->SetNormalFlag(true);\n    \n    for (std::list<std::shared_ptr<TTXPageStream>>::iterator it=_NormalPagesList.begin();it!=_NormalPagesList.end();++it)\n    {\n        // find first page with a higher number\n        std::shared_ptr<TTXPageStream> ptr = *it;\n        if (ptr->GetPageNumber() > p->GetPageNumber())\n        {\n            _NormalPagesList.insert(it,p);\n            return;\n        }\n    }\n    // if we are here we ran to the end of the list without a match\n    _NormalPagesList.push_back(p);\n}\n\nstd::shared_ptr<TTXPageStream> NormalPages::NextPage()\n{\n    if (_page == nullptr)\n    {\n        _iter=_NormalPagesList.begin();\n        _page = *_iter;\n    }\n    else\n    {\n        ++_iter;\n        _page = *_iter;\n    }\n\n    while(true)\n    {\n        if (_iter == _NormalPagesList.end())\n        {\n            _page = nullptr;\n            return _page;\n        }\n        \n        if (_page)\n        {\n            if (_page->GetOneShotFlag())\n            {\n                _page->SetNormalFlag(false);\n                _iter = _NormalPagesList.erase(_iter); // remove oneshot pages from the page list\n                _page = *_iter;\n                continue;\n            }\n            \n            if (_page->GetLock()) // try to lock this page against changes\n            {\n                /* remove pointers from this list if the pages are marked for deletion */\n                if (_page->GetIsMarked() && _page->GetNormalFlag()) // only remove it once\n                {\n                    std::stringstream ss;\n                    ss << \"[NormalPages::NextPage] Deleted \" << std::hex << (_page->GetPageNumber());\n                    _debug->Log(Debug::LogLevels::logINFO,ss.str());\n                    _iter = _NormalPagesList.erase(_iter);\n                    _page->SetNormalFlag(false);\n                    _pageList->RemovePage(_page); // try to remove it from the pagelist immediately - will free the lock\n                    _page = *_iter;\n                    continue; // jump back to loop\n                }\n                else if (_page->Special())\n                {\n                    std::stringstream ss;\n                    ss << \"[NormalPages::NextPage] page became Special \"  << std::hex << (_page->GetPageNumber());\n                    _debug->Log(Debug::LogLevels::logINFO,ss.str());\n                    _iter = _NormalPagesList.erase(_iter);\n                    _page->SetNormalFlag(false);\n                }\n                else if ((_page->GetPageNumber() & 0xFF) == 0xFF) // never return page mFF from the page list\n                {\n                    ++_iter;\n                }\n                else if (_page->GetSubpageCount() == 0) // skip pages with no subpages\n                {\n                    ++_iter;\n                }\n                else\n                {\n                    return _page; // return page locked\n                }\n                \n                _page->FreeLock(); // must unlock page again\n                _page = *_iter;\n            }\n            else\n            {\n                // skip page\n                ++_iter;\n                _page = *_iter;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "normalpages.h",
    "content": "#ifndef _NORMALPAGES_H\n#define _NORMALPAGES_H\n\n#include <list>\n#include <mutex>\n\n#include \"debug.h\"\n#include \"ttxpagestream.h\"\n#include \"pagelist.h\"\n\n// list of normal pages\n\nnamespace vbit\n{\n\nclass NormalPages\n{\n    public:\n        /** Default constructor */\n        NormalPages(int mag, PageList *pageList, Debug *debug);\n        /** Default destructor */\n        virtual ~NormalPages();\n\n        std::shared_ptr<TTXPageStream> NextPage();\n\n        void addPage(std::shared_ptr<TTXPageStream> p);\n\n    protected:\n\n    private:\n        int _mag;\n        PageList* _pageList;\n        Debug* _debug;\n        std::list<std::shared_ptr<TTXPageStream>> _NormalPagesList;\n        std::list<std::shared_ptr<TTXPageStream>>::iterator _iter;\n        std::shared_ptr<TTXPageStream> _page;\n};\n\n}\n\n#endif // _NORMALPAGES_H\n"
  },
  {
    "path": "packet.cpp",
    "content": "#include \"packet.h\"\n#include \"version.h\"\n\nusing namespace vbit;\n\nPacket::Packet(int mag, int row) : _isHeader(false), _coding(CODING_7BIT_TEXT)\n{\n    //ctor\n    _packet.fill(0x20); // fill with spaces\n    SetMRAG(mag, row); // overwrite front 5 bytes\n    assert(_row!=0); // Use Header for row 0\n}\n\nPacket::~Packet()\n{\n    //dtor\n}\n\nvoid Packet::SetRow(int mag, int row, std::array<uint8_t, 40> val, PageCoding coding)\n{\n    SetMRAG(mag, row);\n    \n    _isHeader=false; // Because it can't be a header\n    std::copy(val.begin(), val.end(), _packet.begin() + 5);\n    \n    _coding = coding;\n    \n    switch(coding)\n    {\n        case CODING_PER_PACKET:\n        {\n            _coding = Page::ReturnPageCoding(_packet[5] & 0xF); // set packet coding based on first byte of packet\n            /* fallthrough */\n            [[gnu::fallthrough]];\n        }\n        case CODING_13_TRIPLETS:\n        case CODING_HAMMING_8_4:\n        case CODING_HAMMING_7BIT_GROUPS:\n        {\n            _packet[5] = Hamming8EncodeTable[_packet[5] & 0x0F]; // first byte is hamming 8/4 coded\n            break;\n        }\n        case CODING_7BIT_TEXT:\n        {\n            _packet[5] = OddParityTable[_packet[5] & 0x7f]; // set parity on first byte\n        }\n        default:\n        {\n            break;\n        }\n    }\n    \n    switch(_coding)\n    {\n        default: // treat an invalid coding as 7-bit text\n        case CODING_7BIT_TEXT:\n        {\n            // Perform substitution of version number string\n            // %%%%%V version number eg. v2.0.0\n            int off = Packet::GetOffsetOfSubstition(\"%%%%%V\");\n            if (off > -1)\n            {\n                // allow this substitution to overflow the template\n                int len = strlen(VBIT2_VERSION);\n                if (off + len > 45)\n                    len = 45 - off; // but clamp to the end of the packet\n                std::copy_n(VBIT2_VERSION,len,_packet.begin() + off);\n                if (len < 6)\n                {\n                    for (int i = len; i < 6; i++)\n                        _packet[off+i] = 0x20; // insert spaces if version string is too short\n                }\n            }\n            \n            // first byte parity already set by first switch statement\n            Parity(6);\n            break;\n        }\n        case CODING_13_TRIPLETS:\n        {\n            // Special handler to allow stuffing enhancement packets in as OL rows\n            // Each 18 bits of data for a triplet is coded in the input line as\n            // three bytes least significant first where each byte contains 6 data\n            // bits in b0-b5.\n            // designation code is 8/4 hamming coded by first switch statement\n            /* 0x0a and 0x00 in the hammed output is causing a problem so disable this until they are fixed (output will be gibberish) */\n            int triplet;\n            for (int i = 1; i<=13; i++)\n            {\n                triplet = _packet[i*3+3] & 0x3F;\n                triplet |= (_packet[i*3+4] & 0x3F) << 6;\n                triplet |= (_packet[i*3+5] & 0x3F) << 12;\n                Hamming24EncodeTriplet(i, triplet);\n            }\n            break;\n        }\n        case CODING_HAMMING_8_4:\n        {\n            // first byte already hamming 8/4 coded by first switch statement\n            for (int i = 1; i<40; i++)\n            {\n                _packet[5+i] = Hamming8EncodeTable[_packet[5+i] & 0x0F];\n            }\n            break;\n        }\n        case CODING_HAMMING_7BIT_GROUPS:\n        {\n            // first byte already hamming 8/4 coded by first switch statement\n            for (int i = 1; i<8; i++)\n            {\n                _packet[5+i] = Hamming8EncodeTable[_packet[5+i] & 0x0F];\n            }\n            for (int i = 8; i<20; i++)\n            {\n                _packet[5+i] = OddParityTable[(uint8_t)(_packet[5+i]&0x7f)];\n            }\n            for (int i = 20; i<28; i++)\n            {\n                _packet[5+i] = Hamming8EncodeTable[_packet[5+i] & 0x0F];\n            }\n            for (int i = 28; i<40; i++)\n            {\n                _packet[5+i] = OddParityTable[(uint8_t)(_packet[5+i]&0x7f)];\n            }\n            break;\n        }\n        case CODING_8BIT_DATA:\n        {\n            // do nothing to 8-bit data\n            break;\n        }\n    }\n}\n\nvoid Packet::SetX27CRC(uint16_t crc)\n{\n    if (Hamming8DecodeTable[_packet[5]] == 0) // only set CRC bytes for packet X/27/0\n    {\n        _packet[43]=crc >> 8;\n        _packet[44]=crc & 0xFF;\n    }\n}\n\nvoid Packet::SetPacketRaw(std::vector<uint8_t> data)\n{\n    data.resize(40, 0x00); // ensure correct length\n    std::copy(data.begin(), data.end(), _packet.begin() + 5);\n    _coding = CODING_8BIT_DATA; // don't allow this to be re-processed with parity etc\n}\n\n// Set CRI and MRAG. Leave the rest of the packet alone\nvoid Packet::SetMRAG(uint8_t mag, uint8_t row)\n{\n    _packet[0]=0x55; // clock run in\n    _packet[1]=0x55; // clock run in\n    _packet[2]=0x27; // framing code\n    \n    _packet[3]=Hamming8EncodeTable[mag%8+((row%2)<<3)]; // mag + bit 3 is the lowest bit of row\n    _packet[4]=Hamming8EncodeTable[((row>>1)&0x0f)];\n    _isHeader=row==0;\n    _row=row;\n    _mag=mag;\n}\n\n/** get_offset_time.\n * Given a parameter of say %t+02\n * where str[2] is + or -\n * str[4:3] is a two digit of half hour offsets from UTC\n * @return local time at offset from UTC\n */\nbool Packet::get_offset_time(time_t t, uint8_t* str)\n{\n    char strTime[6];\n    time_t rawtime = t;\n    struct tm *tmGMT;\n\n    // What is our offset in seconds?\n    int offset=((str[3]-'0')*10+str[4]-'0')*30*60; // @todo We really ought to validate this\n\n    // Is it negative (west of us?)\n    if (str[2]=='-')\n        offset=-offset;\n    else\n        if (str[2]!='+') return false; // Must be + or -\n\n    // Add the offset to the time value\n    rawtime+=offset;\n\n    tmGMT = gmtime(&rawtime);\n\n    strftime(strTime, 21, \"%H:%M\", tmGMT);\n    std::copy_n(strTime,5,str);\n    return true;\n}\n\nint Packet::GetOffsetOfSubstition(std::string string)\n{\n    auto it = std::search(_packet.begin()+1, _packet.end(), string.begin(), string.end());\n    if (it != _packet.end())\n        return std::distance(_packet.begin(), it);\n    else\n        return -1;\n}\n\n/* Perform translations on packet.\n * return pointer to 45 byte packet data vector\n *\n * *** Any substitutions applied by this function will break the checksum that has already been calculated and broadcast ***\n */\nstd::array<uint8_t, PACKETSIZE>* Packet::tx()\n{\n    // get master clock singleton\n    MasterClock *mc = mc->Instance();\n    time_t t = mc->GetMasterClock().seconds;\n    \n    // Get local time\n    struct tm * timeinfo;\n    timeinfo=localtime(&t);\n    \n    char tmpstr[21];\n    int off;\n    \n    if (_isHeader)\n    {\n        // substitutions already done in HeaderText\n    }\n    else if (_row < 26 && _coding == CODING_7BIT_TEXT) // Other text rows\n    {\n        for (int i=5;i<45;i++) _packet[i] &= 0x7f; // strip parity bits off\n        // ======= TEMPERATURE ========\n        off = Packet::GetOffsetOfSubstition(\"%%%T\");\n        \n        if (off > -1)\n        {\n            #ifdef RASPBIAN\n            get_temp(tmpstr);\n            std::copy_n(tmpstr,4,_packet.begin() + off);\n            #else\n            std::copy_n(\"err \",4,_packet.begin() + off);\n            #endif\n        }\n\n        // ======= WORLD TIME ========\n        // Special case for world time. Put %t<+|-><hh> to get local time HH:MM offset by +/- half hours\n        for (;;)\n        {\n            off = Packet::GetOffsetOfSubstition(\"%t+\");\n            if (off == -1)\n            {\n                off = Packet::GetOffsetOfSubstition(\"%t-\");\n            }\n            if (off > -1)\n            {\n                //std::cout << \"[test 1]\" << _packet << std::endl;\n                get_offset_time(t, _packet.data() + off); // TODO: something with return value\n                //exit(4);\n            }\n            else\n                break;\n        }\n        // ======= NETWORK ========\n        // Special case for network address. Put %%%%%%%%%%%%%%n to get network address in form xxx.yyy.zzz.aaa with trailing spaces (15 characters total)\n        off = Packet::GetOffsetOfSubstition(\"%%%%%%%%%%%%%%n\");\n        if (off > -1)\n        {\n            #ifndef WIN32\n            get_net(tmpstr);\n            std::copy_n(tmpstr,15,_packet.begin() + off);\n            #else\n            std::copy_n(\"not implemented\",15,_packet.begin() + off);\n            #endif\n        }\n        // ======= TIME AND DATE ========\n        // Special case for system time. Put %%%%%%%%%%%%timedate to get time and date\n        off = Packet::GetOffsetOfSubstition(\"%%%%%%%%%%%%timedate\");\n        if (off > -1)\n        {\n            int num = strftime(tmpstr, 21, \"\\x02%a %d %b\\x03%H:%M/%S\", timeinfo);\n            std::copy_n(tmpstr,num,_packet.begin() + off);\n        }\n        \n        Parity(5); // redo the parity because substitutions will need processing\n    }\n    \n    return &_packet;\n}\n\n/** A header has mag, row=0, page, flags, caption and time\n */\nvoid Packet::Header(uint8_t mag, uint8_t page, uint16_t subcode, uint16_t control, std::string text)\n{\n    uint8_t cbit;\n    SetMRAG(mag,0);\n    _packet[5]=Hamming8EncodeTable[page%0x10];\n    _packet[6]=Hamming8EncodeTable[page/0x10];\n    _packet[7]=Hamming8EncodeTable[(subcode&0x0f)];         // S1 four bits\n    subcode>>=4;\n    // Map the page settings control bits from MiniTED to actual teletext packet.\n    // To find the MiniTED settings look at the tti format document.\n    // To find the target bit position these are in reverse order to tx and not hammed.\n    // So for each bit in ETSI document, just divide the bit number by 2 to find the target location.\n    // Where ETSI says bit 8,6,4,2 this maps to 4,3,2,1 (where the bits are numbered 1 to 8)\n    cbit=0;\n    if (control & 0x4000) cbit=0x08;                        // C4 Erase page\n    _packet[8]=Hamming8EncodeTable[(subcode&0x07) | cbit];  // S2 (3 bits) add C4\n    subcode>>=4;\n    _packet[9]=Hamming8EncodeTable[(subcode&0x0f)];         // S3 four bits\n    subcode>>=4;\n    \n    cbit=0;\n    if (control & 0x0001) cbit=0x04;                        // C5 Newsflash\n    if (control & 0x0002) cbit|=0x08;                       // C6 Subtitle\n    _packet[10]=Hamming8EncodeTable[(subcode&0x03) | cbit]; // S4 C6, C5\n    \n    cbit=0;\n    if (control & 0x0004)  cbit=0x01;                       // C7 Suppress Header\n    if (control & 0x0008) cbit|=0x02;                       // C8 Update\n    if (control & 0x0010) cbit|=0x04;                       // C9 Interrupted sequence\n    if (control & 0x0020) cbit|=0x08;                       // C10 Inhibit display\n    _packet[11]=Hamming8EncodeTable[cbit];                  // C7 to C10\n    \n    cbit=(control & 0x0380) >> 6;                           // Shift the language bits C12,C13,C14.\n    \n    // if (control & 0x0040) cbit|=0x01;                    // C11 serial/parallel *** We only work in parallel mode, Serial would mean a different packet ordering.\n    _packet[12]=Hamming8EncodeTable[cbit];                  // C11 to C14 (C11=0 is parallel, C12,C13,C14 language)\n\n    _isHeader=true; // Because it must be a header\n    text.resize(32);\n    std::copy_n(text.begin(),32,_packet.begin() + 13);\n    \n    // perform the header template substitutions for page number, date, etc.\n    \n    // get master clock singleton\n    MasterClock *mc = mc->Instance();\n    time_t t = mc->GetMasterClock().seconds;\n    \n    // Get local time\n    struct tm * timeinfo;\n    timeinfo=localtime(&t);\n    \n    char tmpstr[4];\n    int off;\n    \n    // mpp page number - %%#\n    off = Packet::GetOffsetOfSubstition(\"%%#\");\n    if (off > -1)\n    {\n        if (_mag==0)\n            _packet[off]='8';\n        else\n            _packet[off]=_mag+'0';\n        _packet[off+1]=page/0x10+'0';\n        if (_packet[off+1]>'9')\n            _packet[off+1]=_packet[off+1]-'0'-10+'A'; // Particularly poor hex conversion algorithm\n\n        _packet[off+2]=page%0x10+'0';\n        if (_packet[off+2]>'9')\n            _packet[off+2]=_packet[off+2]-'0'-10+'A'; // Particularly poor hex conversion algorithm\n    }\n    \n    // day name - %%a\n    off = Packet::GetOffsetOfSubstition(\"%%a\");\n    if (off > -1)\n    {\n        int num = strftime(tmpstr,4,\"%a\",timeinfo);\n        if (num){\n            _packet[off]=tmpstr[0];\n            _packet[off+1]=(num > 1)?tmpstr[1]:' ';\n            _packet[off+2]=(num > 2)?tmpstr[2]:' ';\n        }\n    }\n\n    // month name - %%b\n    off = Packet::GetOffsetOfSubstition(\"%%b\");\n    if (off > -1)\n    {\n        int num = strftime(tmpstr,4,\"%b\",timeinfo);\n        if (num){\n            _packet[off]=tmpstr[0];\n            _packet[off+1]=(num > 1)?tmpstr[1]:' ';\n            _packet[off+2]=(num > 2)?tmpstr[2]:' ';\n        }\n    }\n    \n    // day of month with leading zero - %d\n    off = Packet::GetOffsetOfSubstition(\"%d\");\n    if (off > -1)\n    {\n        strftime(tmpstr,3,\"%d\",timeinfo);\n        _packet[off]=tmpstr[0];\n        _packet[off+1]=tmpstr[1];\n    }\n    \n    // day of month with no leading zero - %e\n    off = Packet::GetOffsetOfSubstition(\"%e\");\n    if (off > -1)\n    {\n        // windows doesn't support %e so just use %d and blank leading zero\n        strftime(tmpstr,3,\"%d\",timeinfo);\n        if (tmpstr[0] == '0')\n            _packet[off]=' ';\n        else\n            _packet[off]=tmpstr[0];\n        _packet[off+1]=tmpstr[1];\n    }\n    \n    // month number with leading 0 - %m\n    off = Packet::GetOffsetOfSubstition(\"%m\");\n    if (off > -1)\n    {\n        strftime(tmpstr,10,\"%m\",timeinfo);\n        _packet[off]=tmpstr[0];\n        _packet[off+1]=tmpstr[1];\n    }\n    \n    // 2 digit year - %y\n    off = Packet::GetOffsetOfSubstition(\"%y\");\n    if (off > -1)\n    {\n        strftime(tmpstr,10,\"%y\",timeinfo);\n        _packet[off]=tmpstr[0];\n        _packet[off+1]=tmpstr[1];\n    }\n    \n    // hours - %H\n    off = Packet::GetOffsetOfSubstition(\"%H\");\n    if (off > -1)\n    {\n        strftime(tmpstr,10,\"%H\",timeinfo);\n        _packet[off]=tmpstr[0];\n        _packet[off+1]=tmpstr[1];\n    }\n    \n    // minutes - %M\n    off = Packet::GetOffsetOfSubstition(\"%M\");\n    if (off > -1)\n    {\n        strftime(tmpstr,10,\"%M\",timeinfo);\n        _packet[off]=tmpstr[0];\n        _packet[off+1]=tmpstr[1];\n    }\n    \n    // seconds - %S\n    off = Packet::GetOffsetOfSubstition(\"%S\");\n    if (off > -1)\n    {\n        strftime(tmpstr,10,\"%S\",timeinfo);\n        _packet[off]=tmpstr[0];\n        _packet[off+1]=tmpstr[1];\n    }\n    \n    Parity(13); // apply parity to the text of the header\n}\n\n/**\n * @brief Set parity bits.\n * \\param Offset is normally 5 for text rows, 13 for header\n */\nvoid Packet::Parity(uint8_t offset)\n{\n    int i;\n    //uint8_t c;\n    for (i=offset;i<PACKETSIZE;i++)\n    {\n        _packet[i]=OddParityTable[_packet[i] & 0x7f];\n    }\n}\n\nvoid Packet::Fastext(std::array<FastextLink, 6> links, int mag)\n{\n    uint16_t lp, ls;\n    uint8_t p=5;\n    \n    _packet[p++]=Hamming8EncodeTable[0]; // Designation code 0\n    mag&=0x07; // Mask the mag just in case. Keep it valid\n\n    // add the link control byte. This will allow row 24 to show.\n    _packet[42]=Hamming8EncodeTable[0x0f];\n\n    // and a blank page CRC - this is set later by Packet::SetX27CRC\n    _packet[43]=0x00;\n    _packet[44]=0x00;\n\n    // for each of the six links\n    for (uint8_t i=0; i<6; i++)\n    {\n        lp=links[i].page;\n        if (lp == 0) lp = 0x8ff; // turn zero into 8FF to be ignored\n        ls=links[i].subpage;\n\n        uint8_t m=(lp/0x100 ^ mag); // calculate the relative magazine\n        _packet[p++]=Hamming8EncodeTable[lp & 0xF];             // page units\n        _packet[p++]=Hamming8EncodeTable[(lp & 0xF0) >> 4];     // page tens\n        _packet[p++]=Hamming8EncodeTable[ls & 0xF];             // S1\n        _packet[p++]=Hamming8EncodeTable[((m & 1) << 3) | ((ls >> 4) & 0xF)]; // S2 + M1\n        _packet[p++]=Hamming8EncodeTable[((ls >> 8) & 0xF)];    // S3\n        _packet[p++]=Hamming8EncodeTable[((m & 6) << 1) | ((ls >> 4) & 0x3)]; // S4 + M2, M3\n    }\n}\n\nint Packet::IDLA(uint8_t datachannel, uint8_t flags, uint8_t ial, uint32_t spa, uint8_t ri, uint8_t ci, std::vector<uint8_t> data)\n{\n    _coding = CODING_8BIT_DATA; // don't allow this to be re-processed with parity etc\n    \n    SetMRAG(datachannel & 0x7,((datachannel & 8) >> 3) + 30);\n    \n    _packet[5]=Hamming8EncodeTable[flags & 0xe]; // Format Type\n    _packet[6]=Hamming8EncodeTable[ial&0xf]; // Interpretation and Address Length\n    \n    uint8_t p = 7;\n    \n    for (uint8_t i = 0; i < (ial&0x7) && i < 7; i++)\n    {\n        _packet[p++] = Hamming8EncodeTable[(spa >> (4 * i)) & 0xf]; // variable number of Service Packet Address nibbles\n    }\n    \n    if (flags & IDLA_RI)\n        _packet[p++]=ri; // Repeat Indicator\n    \n    uint8_t startOfCRC = p; // where the scope of CRC begins\n    \n    int sameCount = 0;\n    \n    if (flags & IDLA_CI)\n    {\n        _packet[p++]=ci; // explicit Continuity Indicator\n        sameCount = (ci == 0x00 || ci == 0xff) ? 1 : 0;\n    }\n    \n    uint8_t DLoffset = p; // store this position in case it needs to be updated later\n    if (flags & IDLA_DL)\n    {\n        _packet[p++] = 0; // initialise Data Length as zero and update it as we add bytes\n        sameCount = 0; // ignore this first zero as any amount of data that would result in a dummy byte would mean DL is > 0\n    }\n    \n    // remaining space is 45 - p - 2 crc bytes\n    \n    unsigned int bytesSent = 0; // count how much of the payload we fit in packet\n    while (p < 43)\n    {\n        if (bytesSent < data.size())\n        {\n            _packet[p] = data[bytesSent++];\n            if (flags & IDLA_DL)\n                _packet[DLoffset]++;\n            \n            if ((_packet[p] == _packet[p-1]) && ((uint8_t)(_packet[p]) == 0xff || (uint8_t)(_packet[p]) == 0x00))\n            {\n                sameCount++;\n                \n                if ((uint8_t)(_packet[p]) == (uint8_t)(_packet[p-1]))\n                {\n                    if (sameCount >= 7 && p < 42)\n                    {\n                        sameCount = 0;\n                        _packet[++p] = 0xaa; // add a dummy byte\n                        \n                        if (flags & IDLA_DL)\n                            _packet[DLoffset]++;\n                    }\n                }\n            }\n            else\n                sameCount = 0; // reset the counter for dummy bytes\n            \n            p++;\n        }\n        else\n        {\n            _packet[p++] = 0xaa; // fill unused part of packet with dummy bytes\n        }\n    }\n    \n    uint16_t crc = 0;\n    \n    for (uint8_t i = startOfCRC; i < 43; i++)\n    {\n        IDLcrc(&crc, _packet[i]); // calculate CRC for user data\n    }\n    \n    if (!(flags & IDLA_CI)) // implicit Continuity Indicator\n    {\n        // modify the CRC so that both bytes are equal to ci\n        \n        uint16_t tmpcrc = crc; // stash the crc\n        \n        crc = (ci << 8) | ci; // initialise crc with ci value in both bytes\n        \n        ReverseCRC(&crc, tmpcrc>>8); // reverse the crc from desired value with previously calculated crc as the input\n        ReverseCRC(&crc, tmpcrc&0xff);\n    }\n    \n    _packet[43]=crc&0xff; // store modified crc in packet\n    _packet[44]=crc>>8;\n    \n    return bytesSent;\n}\n\nvoid Packet::IDLcrc(uint16_t *crc, uint8_t data)\n{\n    // Perform the IDL A crc\n    *crc ^= data;\n    \n    for (uint8_t i = 0; i < 8; i++)\n    {\n        *crc = (*crc & 1) ? (*crc >> 1) ^ 0x8940 : (*crc >> 1);\n    }\n}\n\nvoid Packet::ReverseCRC(uint16_t *crc, uint8_t byte)\n{\n    /* reverse the IDL A crc */\n    uint8_t bit;\n    for (uint8_t i = 0; i < 8; i++)\n    {\n        bit =  (byte >> (7-i)) & 1;\n        *crc = (*crc & 0x8000) ? (((*crc << 1) | bit) ^ 0x1281) : ((*crc << 1) | bit);\n    }\n}\n\n#ifdef RASPBIAN\n/** get_temp\n *  Pinched from raspi-teletext demo.c\n * @return Four character temperature in degrees C eg. \"45.7\"\n */\nbool Packet::get_temp(char* str)\n{\n    FILE *fp;\n    char *pch;\n    char tmp[100];\n\n    fp = popen(\"/usr/bin/vcgencmd measure_temp\", \"r\");\n    fgets(tmp, 99, fp);\n    pclose(fp);\n    pch = strtok (tmp,\"=\\n\");\n    pch = strtok (NULL,\"=\\n\");\n    strncpy(str,pch,5);\n    return true; // @todo\n}\n#endif\n\n#ifndef WIN32\n/** get_net\n *  Pinched from raspi-teletext demo.c\n * @return network address as 20 characters\n * Sample response\n * 3: wlan0    inet 192.168.1.14/24 brd 192.168.1.255 scope global wlan0\\       valid_lft forever preferred_lft forever\n */\nbool Packet::get_net(char* str)\n{\n    FILE *fp;\n    char *pch;\n\n    int n;\n    char temp[100];\n    fp = popen(\"/sbin/ip -o -f inet addr show scope global\", \"r\");\n    fgets(temp, 99, fp);\n    pclose(fp);\n    pch = strtok (temp,\" \\n/\");\n    for (n=1; n<4; n++)\n    {\n        pch = strtok (NULL, \" \\n/\");\n    }\n    // If we don't have a connection established, try not to crash\n    if (pch==NULL)\n    {\n        strcpy(str,\"IP address????\");\n        return false;\n    }\n    strncpy(str,pch,15);\n    return true; // @todo\n}\n#endif\n\nvoid Packet::Hamming24EncodeTriplet(uint8_t index, uint32_t triplet)\n{\n    if (index<1) return;\n    \n    uint8_t D5_D11;\n    uint8_t D12_D18;\n    uint8_t P5, P6;\n    uint8_t Byte_0;\n\n    Byte_0 = (Hamming24EncodeTable0[(triplet >> 0) & 0xFF] ^ Hamming24EncodeTable1[(triplet >> 8) & 0xFF] ^ Hamming24EncodeTable2[(triplet >> 16) & 0x03]);\n    _packet[index*3+3] = Byte_0;\n\n    D5_D11 = (triplet >> 4) & 0x7F;\n    D12_D18 = (triplet >> 11) & 0x7F;\n\n    P5 = 0x80 & ~(Hamming24ParityTable[0][D12_D18] << 2);\n    _packet[index*3+4] = D5_D11 | P5;\n\n    P6 = 0x80 & ((Hamming24ParityTable[0][Byte_0] ^ Hamming24ParityTable[0][D5_D11]) << 2);\n    _packet[index*3+5] = D12_D18 | P6;\n}\n\nuint16_t Packet::PacketCRC(uint16_t crc)\n{\n    int i;\n    uint16_t tempcrc = crc;\n    \n    if (_isHeader)\n    {\n        for (i=13; i<37; i++)\n            PageCRC(&tempcrc, _packet[i]); // calculate CRC for header text\n    }\n    else if (_row < 26)\n    {\n        for (i=5; i<45; i++)\n            PageCRC(&tempcrc, _packet[i]); // calculate CRC for text rows\n    }\n    \n    return tempcrc;\n}\n\nvoid Packet::PageCRC(uint16_t *crc, uint8_t byte)\n{\n    // perform the teletext page CRC\n    uint8_t b;\n    \n    for (int i = 0; i < 8; i++)\n    {\n        b = ((byte >> (7-i)) & 1) ^ ((*crc>>6) & 1) ^ ((*crc>>8) & 1) ^ ((*crc>>11) & 1) ^ ((*crc>>15) & 1);\n        *crc = b | ((*crc&0x7FFF)<<1);\n    }\n}\n"
  },
  {
    "path": "packet.h",
    "content": "#ifndef _PACKET_H_\n#define _PACKET_H_\n\n#include <cstdint>\n#include <algorithm>\n#include <iostream>\n#include <vector>\n#include <array>\n#include <iomanip>\n#include <cstring>\n#include <ctime>\n#include \"tables.h\"\n#include <cassert>\n#include \"page.h\"\n#include \"masterClock.h\"\n\n/**\n * Teletext packet.\n * Deals with teletext packets including header, text rows, enhanced packets, service packets and parity\n */\n\n#define PACKETSIZE 45\nnamespace vbit\n{\n    class Packet\n    {\n        public:\n            /** row constructor */\n            Packet(int mag, int row);\n\n            /** Default destructor */\n            virtual ~Packet();\n\n            inline std::array<uint8_t, PACKETSIZE> Get_packet() { return _packet; }\n            \n            /** SetPacketRaw\n             * Copy the supplied raw binary data into the last 40 bytes of packet\n             * \\param data New 40 byte binary packet data\n             */\n            void SetPacketRaw(std::vector<uint8_t> data);\n            \n            /** tx\n             * @return pointer to packet data vector\n             * We create transmission ready packets of 45 bytes.\n             */\n            std::array<uint8_t, PACKETSIZE>* tx();\n\n            /** SetMRAG\n             * Sets the first five bytes of the packet\n             * Namely Two clock run in, one framing code, and two for magazine/row address group\n             * CRI|CRI|FC|MRAG\n             */\n            void SetMRAG(uint8_t mag, uint8_t row);\n\n            /** Header\n             * @param mag 0..7 (where 0 is mag 8)\n             * @param page number 00..ff\n             * @param subcode (16 bit hex code as in tti file)\n             * @param control C bits\n             * @param text header template\n             */\n            void Header(uint8_t mag, uint8_t page, uint16_t subcode, uint16_t control, std::string text);\n\n            /** Parity\n             * Sets the parity of the bytes starting from offset\n             * @param offset 5 (default) for normal text rows, 13 for headers\n             */\n            void Parity(uint8_t offset=5);\n\n            /** IsHeader\n             * Transmission rule: After a header packet, wait at least one field before transmitting rows.\n             * @return true if the last packet out was a header packet.\n             */\n            bool IsHeader(){return _isHeader;};\n\n            /** Create a Fastext packet\n             * Requires a list of six links\n             * @param links Array of six link values (0x100 to 0x8FF)\n             * @param mag - Magazine number\n             *\n             */\n            void Fastext(std::array<FastextLink, 6>links, int mag);\n            \n            enum IDLAFormatFlags : uint8_t\n            {\n                IDLA_NONE   = 0x0,\n                IDLA_RI     = 0x2,\n                IDLA_CI     = 0x4,\n                IDLA_DL     = 0x8\n            };\n            \n            /**\n             * Independent data line format A, returns the number of payload bytes which were transmitted.\n             * @param datachannel - channel from 0-15 per EN 300 708 6.4.2\n             * @param flags - controls which options are in use\n             * @param ial - Interpretation and Address Length\n             * @param spa - Service Packet Address (0 to 24 bits)\n             * @param ri - Repeat Indicator\n             * @param ci - Continuity Indicator\n             * @param data - payload\n             */\n            int IDLA(uint8_t datachannel, uint8_t flags, uint8_t ial, uint32_t spa, uint8_t ri, uint8_t ci, std::vector<uint8_t> data);\n            \n            /**\n             * @brief Same as the row constructor, except it doesn't construct\n             * @param mag - Magazine number 0..7 where 0 is magazine 8\n             * @param row - Row 0..31\n             * @param val - The contents of the row text (40 characters)\n             * @param coding -\n             */\n            void SetRow(int mag, int row, std::array<uint8_t, 40> val, PageCoding coding);\n            \n            /** PacketCRC\n             * Set the 16 byte CRC in X/27/0 packets\n             * @param crc intial crc value\n             */\n            void SetX27CRC(uint16_t crc);\n            \n            /** PacketCRC\n             * @return result of applying teletext page CRC to packet\n             * @param crc intial crc value\n             */\n            uint16_t PacketCRC(uint16_t crc);\n\n        protected:\n        \n        private:\n            std::array<uint8_t, PACKETSIZE> _packet; // 45 byte packet\n            bool _isHeader; //<! True if the packet is a header\n            uint8_t _mag;//<! The magazine number this packet belongs to 0..7 where 0 is maazine 8\n            uint8_t _row; //<! Row number 0 to 31\n            PageCoding _coding; // packet coding\n            \n            int GetOffsetOfSubstition(std::string string);\n            \n            void IDLcrc(uint16_t *crc, uint8_t data); // calculate a CRC checksum for one byte\n            void ReverseCRC(uint16_t *crc, uint8_t byte);\n\n            bool get_offset_time(time_t t, uint8_t* str);\n            bool get_net(char* str);\n            \n            /** Hamming 24/18\n             * Hamming 24/18 encode a triplet and place at appropriate index in packet\n             * The incoming triplet should be packed 18 bits of an uint32_t representing D1..D18\n             * The triplet is repacked with parity bits\n             */\n            void Hamming24EncodeTriplet(uint8_t index, uint32_t triplet);\n            \n            void PageCRC(uint16_t *crc, uint8_t byte); // calculate a CRC checksum for one byte\n            \n            #ifdef RASPBIAN\n            bool get_temp(char* str);\n            #endif\n    };\n}\n\n#endif // _PACKET_H_\n"
  },
  {
    "path": "packet830.cpp",
    "content": "#include <packet830.h>\n\nusing namespace vbit;\n\nPacket830::Packet830(Configure *configure) :\n    _configure(configure)\n{\n    //ctor\n    ClearEvent(EVENT_P830_FORMAT_1);\n    ClearEvent(EVENT_P830_FORMAT_2_LABEL_0 );\n    ClearEvent(EVENT_P830_FORMAT_2_LABEL_1 );\n    ClearEvent(EVENT_P830_FORMAT_2_LABEL_2 );\n    ClearEvent(EVENT_P830_FORMAT_2_LABEL_3 );\n}\n\nPacket830::~Packet830()\n{\n    //dtor\n}\n\nPacket* Packet830::GetPacket(Packet* p)\n{\n    MasterClock *mc = mc->Instance();\n    time_t timeRaw = mc->GetMasterClock().seconds;\n    time_t timeLocal;\n    struct tm *tmLocal;\n    struct tm *tmGMT;\n    int offsetHalfHours, year, month, day, hour, minute, second;\n    uint32_t modifiedJulianDay;\n\n    std::vector<uint8_t> data(40, 0x15); // 40 bytes filled with hamming coded 0\n\n    p->SetMRAG(8, 30); // Packet 8/30\n\n    uint8_t muxed = _configure->GetMultiplexedSignalFlag();\n\n    uint8_t m = _configure->GetInitialMag();\n    uint8_t pn = _configure->GetInitialPage();\n    uint16_t sc = _configure->GetInitialSubcode();\n    data.at(1) = Hamming8EncodeTable[pn & 0xF];\n    data.at(2) = Hamming8EncodeTable[(pn & 0xF0) >> 4];\n    data.at(3) = Hamming8EncodeTable[sc & 0xF];\n    data.at(4) = Hamming8EncodeTable[((sc & 0xF0) >> 4) | ((m & 1) << 3)];\n    data.at(5) = Hamming8EncodeTable[(sc & 0xF00) >> 8];\n    data.at(6) = Hamming8EncodeTable[((sc & 0xF000) >> 12) | ((m & 6) << 1)];\n\n    std::copy_n(_configure->GetServiceStatusString().begin(), 20, data.begin() + 20); // copy status display from std::string into packet data\n\n    if (GetEvent(EVENT_P830_FORMAT_1))\n    {\n        ClearEvent(EVENT_P830_FORMAT_1);\n        data.at(0) = Hamming8EncodeTable[muxed]; // Format 1 designation code\n        \n        uint16_t nic = _configure->GetNetworkIdentificationCode();\n        data.at(7) = ReverseByteTab[(nic & 0xFF00) >> 8];\n        data.at(8) = ReverseByteTab[nic & 0xFF];\n        \n        /* calculate number of seconds local time is offset from UTC */\n        tmLocal = localtime(&timeRaw);\n        \n        /* convert tmLocal into a timestamp without correcting for timezones and summertime */\n        #ifdef WIN32\n        timeLocal = _mkgmtime(tmLocal);\n        #else\n        timeLocal = timegm(tmLocal);\n        #endif\n        \n        offsetHalfHours = difftime(timeLocal, timeRaw) / 1800;\n        // time offset code -bits 2-6 half hours offset from UTC, bit 7 sign bit\n        // bits 0 and 7 reserved - set to 1\n        data.at(9) = ((offsetHalfHours < 0) ? 0xC1 : 0x81) | ((abs(offsetHalfHours) & 0x1F) << 1);\n        \n        // get the time current UTC time into separate variables\n        tmGMT = gmtime(&timeRaw);\n        year = tmGMT->tm_year + 1900;\n        month = tmGMT->tm_mon + 1;\n        day = tmGMT->tm_mday;\n        hour = tmGMT->tm_hour;\n        minute = tmGMT->tm_min;\n        second = tmGMT->tm_sec;\n        \n        modifiedJulianDay = calculateMJD(year, month, day);\n        // generate five decimal digits of modified julian date decimal digits and increment each one.\n        data.at(10) = (modifiedJulianDay % 100000 / 10000 + 1);\n        data.at(11) = ((modifiedJulianDay % 10000 / 1000 + 1) << 4) | (modifiedJulianDay % 1000 / 100 + 1);\n        data.at(12) = ((modifiedJulianDay % 100 / 10 + 1) << 4) | (modifiedJulianDay % 10 + 1);\n        \n        // generate six decimal digits of UTC time and increment each one before transmission\n        data.at(13) = (((hour / 10) + 1) << 4) | ((hour % 10) + 1);\n        data.at(14) = (((minute / 10) + 1) << 4) | ((minute % 10) + 1);\n        data.at(15) = (((second / 10) + 1) << 4) | ((second % 10) + 1);\n        \n        // bytes 22-25 of the packet are marked reserved in the spec. Different broadcasters fill them with different values\n        std::copy_n(_configure->GetReservedBytes().begin(), 4, data.begin() + 16); // copy from configuration\n        \n        p->SetPacketRaw(data);\n        p->Parity(25); // set correct parity for status display\n        return p;\n    }\n\n    // PDC labels 8/30/1\n    if (GetEvent(EVENT_P830_FORMAT_2_LABEL_0))\n    {\n        ClearEvent(EVENT_P830_FORMAT_2_LABEL_0);\n        data.at(0) = Hamming8EncodeTable[muxed | 2]; // Format 2 designation code\n        \n        //@todo\n    }\n    if (GetEvent(EVENT_P830_FORMAT_2_LABEL_1))\n    {\n        ClearEvent(EVENT_P830_FORMAT_2_LABEL_1);\n        data.at(0) = Hamming8EncodeTable[muxed | 2]; // Format 2 designation code\n        \n        //@todo\n    }\n    if (GetEvent(EVENT_P830_FORMAT_2_LABEL_2))\n    {\n        ClearEvent(EVENT_P830_FORMAT_2_LABEL_2);\n        data.at(0) = Hamming8EncodeTable[muxed | 2]; // Format 2 designation code\n        \n        //@todo\n    }\n    if (GetEvent(EVENT_P830_FORMAT_2_LABEL_3))\n    {\n        ClearEvent(EVENT_P830_FORMAT_2_LABEL_3 );\n        data.at(0) = Hamming8EncodeTable[muxed | 2]; // Format 2 designation code\n        \n        //@todo\n    }\n    return nullptr;\n}\n\nbool Packet830::IsReady(bool force)\n{\n    // We will be waiting for 10 fields between becoming true\n    // 8/30/1 should go out on the system clock seconds interval.\n    (void)force; // silence error about unused parameter\n    bool result = false;\n    \n    if (GetEvent(EVENT_P830_FORMAT_1) || // always ready to generate a format 1 packet\n        (GetEvent(EVENT_P830_FORMAT_2_LABEL_0) && _label0) ||\n        (GetEvent(EVENT_P830_FORMAT_2_LABEL_1) && _label1) ||\n        (GetEvent(EVENT_P830_FORMAT_2_LABEL_2) && _label2) ||\n        (GetEvent(EVENT_P830_FORMAT_2_LABEL_3) && _label3)\n       )\n    {\n        result = true;\n    }\n    return result;\n}\n\nlong Packet830::calculateMJD(int year, int month, int day)\n{\n    // calculate modified julian day number\n    int a, b, c, d;\n    a = (month - 14) / 12;\n    b = day - 32075 + (1461 * (year + 4800 + a) / 4);\n    c = (367 * (month - 2 - 12 * a) / 12);\n    d = 3 * (((year + 4900 + a) / 100) / 4);\n    return b + c - d - 2400001;\n}\n"
  },
  {
    "path": "packet830.h",
    "content": "/** Packet source for 8/30/1 and 8/30/2\n */\n#ifndef _PACKET830_H_\n#define _PACKET830_H_\n\n#include \"packetsource.h\"\n#include \"configure.h\"\n\nnamespace vbit\n{\n\n\nclass Packet830 : public PacketSource\n{\n  public:\n    /** Default constructor */\n    Packet830(Configure *configure);\n    /** Default destructor */\n    virtual ~Packet830();\n\n    /** @todo Routines for cni, nic, MJD and station ident\n     *  @todo Routines for PDC flag management\n     */\n\n    // overrides\n    Packet* GetPacket(Packet* p) override;\n\n    /**\n     * Packet 830 must always wait for the correct field.\n     * @param force is ignored\n     */\n    bool IsReady(bool force=false);\n\n  protected:\n\n  private:\n    Configure* _configure;\n    long calculateMJD(int year, int month, int day);\n    \n    // TODO: some temporary flags\n    bool _label0 = false;\n    bool _label1 = false;\n    bool _label2 = false;\n    bool _label3 = false;\n};\n\n}\n\n#endif // _PACKET830_H_\n"
  },
  {
    "path": "packetDatacast.cpp",
    "content": "/* Packet source for datacast channels */\n\n#include \"packetDatacast.h\"\n\nusing namespace vbit;\n\nPacketDatacast::PacketDatacast(uint8_t datachannel, Configure* configure) :\n    _datachannel(datachannel)\n{\n    uint16_t datacastLines = configure->GetDatacastLines();\n    if (datacastLines == 0 || datacastLines > 4)\n        datacastLines = 4; /* cap at 4 lines */\n    _bufferSize = datacastLines*4; /* assign space for around 4 fields */\n    \n    for (int i=0; i<_bufferSize; i++){\n        // build packet buffer\n        _packetBuffer.push_back(new Packet(8,25));\n    }\n    // set head and tail indices\n    _head = 0;\n    _tail = 0;\n}\n\nPacketDatacast::~PacketDatacast()\n{\n    \n}\n\nint PacketDatacast::PushRaw(std::vector<uint8_t> *data)\n{\n    /* push 40 bytes of raw packet data into the buffer */\n    Packet* p = GetFreeBuffer();\n    if (p == nullptr)\n        return -1;\n    \n    p->SetPacketRaw(*data); // copy data into buffer packet\n    _head = (_head + 1) % _bufferSize; // advance head on circular buffer\n    \n    return 0;\n}\n\nint PacketDatacast::PushIDLA(uint8_t flags, uint8_t ial, uint32_t spa, uint8_t ri, uint8_t ci, std::vector<uint8_t> *data)\n{\n    /* push a format A datacast packet into the buffer */\n    int bytes = 0;\n    Packet* p = GetFreeBuffer();\n    if (p != nullptr)\n    {\n        bytes = p->IDLA(_datachannel, flags, ial, spa, ri, ci, *data);\n        _head = (_head + 1) % _bufferSize; // advance head on circular buffer\n    }\n    \n    return bytes;\n}\n\nPacket* PacketDatacast::GetFreeBuffer()\n{\n    /* gets a pointer to the next free buffer packet or a null pointer if buffer is full\n       Does NOT actually advance the head to avoid a race condition */\n    \n    if ((_head + 1) % _bufferSize == _tail)\n        return nullptr; // buffer full\n    else\n        return _packetBuffer[_head];\n}\n\nPacket* PacketDatacast::GetPacket(Packet* p)\n{\n    if (_tail == _head)\n    {\n        // generate some hardcoded datacast filler\n        std::string str = \"VBIT2 Datacast Service       \";\n        std::vector<uint8_t> data(str.begin(), str.end());\n        p->IDLA(8, Packet::IDLA_DL, 6, 0xfffffe, 0, 0, data);\n        // TODO: make channel, address, and content of filler configurable\n    }\n    else\n    {\n        // copy data from buffer to packet\n        p->SetMRAG(_datachannel & 0x7,((_datachannel & 8) >> 3) + 30);\n        p->SetPacketRaw(std::vector<uint8_t>(_packetBuffer[_tail]->Get_packet().begin()+5, _packetBuffer[_tail]->Get_packet().end()));\n        \n        _tail = (_tail + 1) % _bufferSize; // advance tail on circular buffer\n    }\n    \n    return p;\n}\n\nbool PacketDatacast::IsReady(bool force)\n{\n    bool result=false;\n    \n    if (GetEvent(EVENT_DATABROADCAST))\n    {\n        // Don't clear event, Service::_updateEvents explicitly turns it off for non datacast lines\n        \n        if (_tail != _head)\n            result = true;\n        else\n            result = force;\n    }\n    return result;\n}\n"
  },
  {
    "path": "packetDatacast.h",
    "content": "#ifndef PACKETDATACAST_H\n#define PACKETDATACAST_H\n\n#include \"packetsource.h\"\n#include \"configure.h\"\n\nnamespace vbit\n{\n    class PacketDatacast : public PacketSource\n    {\n        public:\n            PacketDatacast(uint8_t datachannel, Configure* configure);\n            virtual ~PacketDatacast();\n            \n            Packet* GetPacket(Packet* p) override;\n            bool IsReady(bool force=false);\n            \n            int PushRaw(std::vector<uint8_t> *data);\n            int PushIDLA(uint8_t flags, uint8_t ial, uint32_t spa, uint8_t ri, uint8_t ci, std::vector<uint8_t> *data);\n            \n        protected:\n        private:\n            uint8_t _datachannel;\n            \n            std::vector<Packet*> _packetBuffer;\n            uint8_t _bufferSize;\n            uint8_t _head;\n            uint8_t _tail;\n            \n            Packet* GetFreeBuffer();\n    };\n}\n\n#endif // PACKETDATACAST_H\n"
  },
  {
    "path": "packetDebug.cpp",
    "content": "#include \"packetDebug.h\"\n\nusing namespace vbit;\n\nPacketDebug::PacketDebug(Configure* configure, Debug* debug) :\n    _configure(configure),\n    _debug(debug),\n    _debugPacketCI(0), // continuity counter for debug datacast packets\n    _startupTime(time(NULL)),\n    _masterClockSeconds(0),\n    _masterClockFields(0),\n    _systemClock(0),\n    _debugType(FORMAT1),\n    _clockFlags(CFLAGRESYNC),\n    _magFlags(0)\n{\n    //ctor\n    \n    // TODO: make channel and address configurable\n    _datachannel = 8; \n    _servicePacketAddress = 0xffffff;\n}\n\nPacketDebug::~PacketDebug()\n{\n    //dtor\n}\n\nPacket* PacketDebug::GetPacket(Packet* p)\n{\n    /* Generate simple datacast packets for timing measurement and magazine monitoring.\n       The user data payload for each format type is padded to a fixed 26 bytes long.\n       \n       Format types are documented below.\n       The byte offsets listed are the position within the datacast user data payload.\n       These offsets are only valid after discarding any dummy bytes per EN 300 708 6.5.7.1!\n    */\n    \n    std::vector<uint8_t> data;\n    int mag;\n    \n    data.push_back(_debugType); // format type in this packet\n    \n    // all types start with the current master clock\n    data.push_back(_masterClockSeconds >> 24);\n    data.push_back(_masterClockSeconds >> 16);\n    data.push_back(_masterClockSeconds >> 8);\n    data.push_back(_masterClockSeconds);\n    data.push_back(_masterClockFields);\n    \n    // 6 of 26 bytes used\n    \n    switch(_debugType)\n    {\n        default:\n        case NONE: // format type 0\n        {\n            /* Bytes  Description\n                   0: format type\n                1-25: reserved\n            */\n            break;\n        }\n        case FORMAT1: // format type 1, system time\n        {\n            /* Bytes  Description\n                   0: format type\n                 1-4: current master clock seconds\n                   5: current master clock field\n                   6: debug stream data format version\n                   7: clock flags\n                        b0: master clock resynchronisation\n                8-11: current system clock seconds\n                  12: current system clock field\n               13-16: program startup timestamp\n               17-25: reserved\n            */\n            \n            _debugType = FORMAT2; // cycle to format 2 on next packet\n            \n            data.push_back(VBIT2_DEBUG_VERSION); // Debug stream format version\n            \n            // clock flags\n            data.push_back(_clockFlags);\n            _clockFlags = 0; // clear flags\n            \n            // current system clock\n            data.push_back(_systemClock >> 24);\n            data.push_back(_systemClock >> 16);\n            data.push_back(_systemClock >> 8);\n            data.push_back(_systemClock);\n            data.push_back(_systemClockFields);\n            \n            // vbit2 startup timestamp\n            data.push_back(_startupTime >> 24);\n            data.push_back(_startupTime >> 16);\n            data.push_back(_startupTime >> 8);\n            data.push_back(_startupTime);\n            \n            // 17 of 26 bytes used\n            \n            break;\n        }\n        case FORMAT2: // format type 2, magazine data\n        {\n            /* Bytes  Description\n                   0: format type\n                 1-4: current master clock seconds\n                   5: current master clock field\n                   6: magazine flags\n                        b0: magazine cycle durations changed\n                        b1: magazine page counts changed\n                7-14: magazine (0-7) cycle durations in seconds\n               15-22: magazine (0-7) page counts\n               23-25: reserved\n            */\n            \n            _debugType = FORMAT1; // cycle to format 1 on next packet\n            \n            // magazine flags\n            data.push_back(_magFlags);\n            _magFlags = 0; // clear flags\n            \n            for (mag=0; mag<8; mag++){\n                data.push_back(_magDurations[mag]);\n            }\n            // these 8 bytes may result in a dummy byte!\n            \n            for (mag=0; mag<8; mag++){\n                data.push_back(_magSizes[mag]);\n            }\n            // these 8 bytes may result in a dummy byte!\n            \n            // 22 of 26 bytes used\n            \n            break;\n        }\n    }\n    \n    data.insert(data.end(), 26 - data.size(), 0); // pad payload data to 26 bytes long (leave three bytes free for dummy bytes)\n    \n    // generate format A datacast packet with explicit data length, and implicit continuity indicator\n    p->IDLA(_datachannel, Packet::IDLA_DL, 6, _servicePacketAddress, 0, _debugPacketCI++, data); \n    \n    return p;\n}\n\nbool PacketDebug::IsReady(bool force)\n{\n    bool result=false;\n    \n    if (GetEvent(EVENT_DATABROADCAST) && GetEvent(EVENT_FIELD))\n    {\n        // Don't clear event, Service::_updateEvents explicitly turns it off for non datacast lines\n        if (_debugType > FORMAT1) // we are in the middle of sending debug data already\n        {\n            result = true;\n        }\n        else\n        {\n            // update magazine data\n            int mag;\n            std::array<int, 8> magDurations = _debug->GetMagCycleDurations(); // get magazine durations in number of fields\n            for (mag=0; mag<8; mag++){\n                if(magDurations[mag] < 0 || magDurations[mag] / 50 > 255)\n                    magDurations[mag] = 255; // clamp to 255 seconds\n                else\n                    magDurations[mag] = magDurations[mag] / 50; // truncate to whole seconds\n                \n                if (_magDurations[mag] != magDurations[mag])\n                {\n                    _magFlags |= MFLAGDURATION; // mag durations have changed!\n                    _magDurations[mag] = magDurations[mag]; // update our internal variable\n                }\n            }\n            \n            std::array<int, 8> magSizes = _debug->GetMagSizes(); // get number of pages in each magazine\n            for (mag=0; mag<8; mag++){\n                if (_magSizes[mag] != magSizes[mag])\n                {\n                    _magFlags |= MFLAGPAGES; // mag sizes have changed!\n                    _magSizes[mag] = magSizes[mag]; // update our internal variable\n                }\n            }\n            \n            if (_clockFlags || _magFlags || force) // generate packets if any of the content has changed, or if we are forcing output to use this source as datacast filler\n                result = true;\n        }\n        \n        ClearEvent(EVENT_FIELD); // don't jam all datacast lines in a field\n    }\n    \n    return result;\n}\n\nvoid PacketDebug::TimeAndField(MasterClock::timeStruct masterClock, time_t systemClock, uint8_t systemClockFields, bool resync)\n{\n    // update the clocks in _debugData struct - called once per field by Service::_updateEvents()\n    _masterClockSeconds = masterClock.seconds;\n    _masterClockFields = masterClock.fields;\n    _systemClock = systemClock;\n    _systemClockFields = systemClockFields;\n    if (resync)\n        _clockFlags |= CFLAGRESYNC; // set flag\n}\n"
  },
  {
    "path": "packetDebug.h",
    "content": "/** Packet source for debugging output\n */\n#ifndef _PACKETDEBUG_H_\n#define _PACKETDEBUG_H_\n\n#include <list>\n#include \"packetsource.h\"\n#include \"configure.h\"\n#include \"debug.h\"\n\n#define VBIT2_DEBUG_VERSION 0x02   // Debug packet version\n\nnamespace vbit\n{\n    class PacketDebug : public PacketSource\n    {\n        public:\n            /** Default constructor */\n            PacketDebug(Configure* configure, Debug* debug);\n            /** Default destructor */\n            virtual ~PacketDebug();\n\n            // overrides\n            Packet* GetPacket(Packet* p) override;\n            \n            void TimeAndField(MasterClock::timeStruct masterClock, time_t systemClock, uint8_t systemClockFields, bool resync);\n            \n            bool IsReady(bool force=false);\n\n        protected:\n\n        private:\n            Configure* _configure;\n            Debug* _debug;\n            \n            uint8_t _datachannel;\n            uint32_t _servicePacketAddress;\n            uint8_t _debugPacketCI; // continuity indicator for databroadcast stream\n            \n            const time_t _startupTime;\n            time_t _masterClockSeconds;\n            uint8_t _masterClockFields;\n            time_t _systemClock;\n            uint8_t _systemClockFields;\n            \n            std::array<uint8_t, 8> _magDurations;\n            std::array<uint8_t, 8> _magSizes;\n            \n            enum DebugTypes {NONE, FORMAT1, FORMAT2};\n            DebugTypes _debugType;\n            \n            enum ClockFlags {CFLAGRESYNC = 1};\n            uint8_t _clockFlags;\n            \n            enum MagFlags {MFLAGDURATION=1, MFLAGPAGES=2};\n            uint8_t _magFlags;\n    };\n}\n\n#endif // _PACKETDEBUG_H_\n"
  },
  {
    "path": "packetServer.cpp",
    "content": "/* Provide a TCP packet server which sends a whole frame of t42 data at a time to all connected clients */\n\n#include \"packetServer.h\"\n\nusing namespace vbit;\n\nPacketServer::PacketServer(Configure *configure, Debug *debug) :\n    _debug(debug),\n    _portNumber(configure->GetPacketServerPort()),\n    _maxClients(configure->GetPacketServerMaxClients()),\n    _isActive(false)\n{\n    /* initialise sockets */\n    _serverSock = -1;\n}\n\nPacketServer::~PacketServer()\n{\n}\n\nvoid PacketServer::DieWithError(std::string errorMessage)\n{\n    if (_serverSock >= 0)\n    {\n        #ifdef WIN32\n            closesocket(_serverSock);\n        #else\n            close(_serverSock);\n        #endif\n    }\n    \n    for(std::list<int>::iterator it = _clientSocks.begin(); it != _clientSocks.end();)\n    {\n        int port = *it;\n        it = _clientSocks.erase(it);\n        #ifdef WIN32\n            closesocket(port);\n        #else\n            close(port);\n        #endif\n    }\n    \n    perror(errorMessage.c_str());\n    exit(1);\n}\n\nvoid PacketServer::SendField(std::vector<std::vector<uint8_t>> FrameBuffer)\n{\n    std::array<uint8_t, 42*32> RawFrameBuffer;\n    RawFrameBuffer.fill(0x00); // clear two frames\n    unsigned int i, j;\n    \n    for (i = 0; i < FrameBuffer.size(); i++)\n    {\n        int field = FrameBuffer[i][0]&1;\n        int line = (FrameBuffer[i][1]<<8) + FrameBuffer[i][2];\n        if (line < 16) // full field lines not supported in this output\n        {\n            for (j = 0; j < 42; j++)\n            {\n                RawFrameBuffer[(field*42*16)+(line*42)+j] = FrameBuffer[i][j+3];\n            }\n        }\n    }\n    \n    int sock;\n    int ret;\n    \n    _mtx.lock();\n    for(std::list<int>::iterator it = _clientSocks.begin(); it != _clientSocks.end(); ++it)\n    {\n        sock = *it;\n        if (sock >= 0)\n        {\n            ret = send(sock, (char*)RawFrameBuffer.data(), RawFrameBuffer.size(), 0);\n            if (ret != RawFrameBuffer.size())\n            {\n                /*\n                    We were unable to send a whole frame to the client. We either sent a partial frame or the send failed entirely.\n                    This probably means that either there are network issues, or the client is not consuming data fast enough.\n                    Either way trying to handle this adds a lot of complexity and risks getting the client desynchronised or blocking the thread trying to sort it out, so the best thing to do is probably just boot the client off and let it reconnect.\n                */\n                \n                #ifdef WIN32\n                    int e = WSAGetLastError();\n                #else\n                    int e = errno;\n                #endif\n                \n                _debug->Log(Debug::LogLevels::logWARN,\"[PacketServer::SendField] send() failed. Closing socket \" + std::to_string(sock) + \" send error \" + std::to_string(e));\n                \n                #ifdef WIN32\n                    closesocket(sock);\n                #else\n                    close(sock);\n                #endif\n                *it = -1; // mark this closed so it gets removed in the server thread\n            }\n        }\n    }\n    _mtx.unlock();\n}\n\nvoid PacketServer::run()\n{\n    _debug->Log(Debug::LogLevels::logINFO,\"[PacketServer::run] TCP packet server thread started for \"+(_maxClients?\"max \"+std::to_string(_maxClients):\"unlimited\")+\" connections\");\n    \n    int newSock;\n    int sock;\n    struct sockaddr_in address;\n    unsigned short servPort;\n    \n    char readBuffer[BUFFLEN];\n    \n    fd_set readfds;\n    \n#ifdef WIN32\n    int addrlen;\n    WSADATA wsaData;\n    int iResult;\n\n    // Initialize Winsock\n    iResult = WSAStartup(MAKEWORD(2,2), &wsaData);\n    if (iResult != 0)\n    {\n        DieWithError(\"[PacketServer::run] WSAStartup failed\");\n    }\n#else\n    unsigned int addrlen;\n#endif\n    \n    servPort = _portNumber;\n    \n    /* Create socket */\n    if ((_serverSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)\n        DieWithError(\"[PacketServer::run] socket() failed\\n\");\n    \n    int reuse = true;\n    \n    /* Allow multiple connnections */\n    if(setsockopt(_serverSock, SOL_SOCKET, SO_REUSEADDR, (const char *)&reuse, sizeof(reuse)) < 0)\n        DieWithError(\"[PacketServer::run] setsockopt() SO_REUSEADDR failed\");\n    \n    address.sin_family = AF_INET;\n    address.sin_addr.s_addr = htonl(INADDR_ANY);\n    address.sin_port = htons(servPort);\n    \n    /* bind socked */\n    if (bind(_serverSock, (struct sockaddr *) &address, sizeof(address)) < 0)\n        DieWithError(\"[PacketServer::run] bind() failed\");\n    \n    /* Listen for incoming connections */\n    if (listen(_serverSock, MAXPENDING) < 0)\n        DieWithError(\"[PacketServer::run] listen() failed\");\n    \n    addrlen = sizeof(address);\n    \n    int iopt = 42*32*25; /* 25 frames worth of t42 */\n    \n    while(true)\n    {\n        FD_ZERO(&readfds);\n        FD_SET(_serverSock, &readfds);\n        \n        for(std::list<int>::iterator it = _clientSocks.begin(); it != _clientSocks.end(); ++it)\n        {\n            if (*it > -1)\n                FD_SET(*it , &readfds);\n        }\n        _isActive = !(_clientSocks.empty());\n        \n        /* wait for activity on any socket */\n        if ((select(FD_SETSIZE, &readfds, NULL, NULL, NULL) < 0) && (errno!=EINTR))\n            DieWithError(\"[PacketServer::run] select() failed\");\n        \n        if (FD_ISSET(_serverSock, &readfds))\n        {\n            /* incoming connection to server */\n            if ((newSock = accept(_serverSock, (struct sockaddr *)&address, &addrlen))<0)\n                DieWithError(\"[PacketServer::run] accept() failed\");\n            \n            #ifdef WIN32\n                u_long ul = 1;\n                if (ioctlsocket(newSock, FIONBIO, &ul) < 0)\n                    DieWithError(\"[PacketServer::run] ioctlsocket() failed\");\n            #else\n                if (fcntl(newSock, F_SETFL, fcntl(newSock, F_GETFL, 0) | O_NONBLOCK) < 0)\n                    DieWithError(\"[PacketServer::run] fcntl() failed\");\n            #endif\n            \n            if (setsockopt(newSock, SOL_SOCKET, SO_SNDBUF, (char *) &iopt, sizeof(iopt)) < 0 )\n                DieWithError(\"[PacketServer::run] setsockopt() failed\");\n            \n            if (_maxClients > 0 && _maxClients == _clientSocks.size())\n            {\n                /* no more client slots so reject */\n                #ifdef WIN32\n                    closesocket(newSock);\n                #else\n                    close(newSock);\n                #endif\n                _debug->Log(Debug::LogLevels::logWARN,\"[PacketServer::run] reject new connection from \" + std::string(inet_ntoa(address.sin_addr)) + \" (too many connections)\");\n                break;\n            }\n                \n            /* add to active sockets */\n            _clientSocks.push_back(newSock);\n            _debug->Log(Debug::LogLevels::logINFO,\"[PacketServer::run] new connection from \" + std::string(inet_ntoa(address.sin_addr)) + \":\" + std::to_string(ntohs(address.sin_port)) + \" as socket \" + std::to_string(newSock));\n        }\n        else\n        {\n            /* a client socket has activity */\n            for(std::list<int>::iterator it = _clientSocks.begin(); it != _clientSocks.end();)\n            {\n                sock = *it;\n                \n                if (sock >= 0 && FD_ISSET(sock , &readfds))\n                {\n                    /* socket has activity */\n                    \n                    int n = recv(sock, readBuffer, BUFFLEN, 0);\n                    if (n == 0)\n                    {\n                        /* client disconnected */\n                        getpeername(sock, (struct sockaddr*)&address, &addrlen);\n                        \n                        _debug->Log(Debug::LogLevels::logINFO,\"[PacketServer::run] closing connection from \" + std::string(inet_ntoa(address.sin_addr)) + \":\" + std::to_string(ntohs(address.sin_port)) + \" on socket \" + std::to_string(sock));\n\n                        _mtx.lock();\n                        it = _clientSocks.erase(it);\n                        #ifdef WIN32\n                            closesocket(sock);\n                        #else\n                            close(sock);\n                        #endif\n                        _mtx.unlock();\n                        \n                    }\n                    else if (n > 0)\n                    {\n                        // don't care what client sent right now\n                        ++it;\n                    }\n                    else /* n < 0 */\n                    {\n                        #ifdef WIN32\n                            int e = WSAGetLastError();\n                        #else\n                            int e = errno;\n                        #endif\n                        \n                        getpeername(sock, (struct sockaddr*)&address, &addrlen);\n                        \n                        _debug->Log(Debug::LogLevels::logWARN,\"[PacketServer::run] closing connection from \" + std::string(inet_ntoa(address.sin_addr)) + \":\" + std::to_string(ntohs(address.sin_port)) + \" recv error \" + std::to_string(e) + \" on socket \" + std::to_string(sock));\n                        \n                        /* close the socket when any error occurs */\n                        \n                        _mtx.lock();\n                        it = _clientSocks.erase(it);\n                        #ifdef WIN32\n                            closesocket(sock);\n                        #else\n                            close(sock);\n                        #endif\n                        _mtx.unlock();\n                    }\n                }\n                else\n                {\n                    if (sock < 0)\n                    {\n                        // socket was closed in main thread\n                        _mtx.lock();\n                        it = _clientSocks.erase(it);\n                        _mtx.unlock();\n                    }\n                    else\n                        ++it;\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "packetServer.h",
    "content": "#ifndef _PACKETSERVER_H_\n#define _PACKETSERVER_H_\n\n#include \"configure.h\"\n#include \"debug.h\"\n#include <mutex>\n#include <list>\n\n#ifdef WIN32\n#include <winsock2.h>\n#else\n#include <fcntl.h>\n#include <sys/socket.h> /* for socket(), bind(), and connect() */\n#include <sys/select.h> /* for fd_set() */\n#include <arpa/inet.h>  /* for sockaddr_in and inet_ntoa() */\n#include <unistd.h>     /* for close() */\n#endif\n\nnamespace vbit\n\n{\n    class PacketServer\n    {\n        public:\n            PacketServer(Configure *configure, Debug *debug);\n            ~PacketServer();\n            \n            void run();\n            bool GetIsActive(){return _isActive;}; /* is the packet server running? */\n            void SendField(std::vector<std::vector<uint8_t>> FrameBuffer);\n            \n        private:\n            Debug* _debug;\n            static const uint16_t MAXPENDING=5;\n            static const uint16_t BUFFLEN=256;\n            \n            int _portNumber;\n            int _serverSock;\n            \n            std::mutex _mtx;\n            std::list<int> _clientSocks;\n            uint16_t _maxClients;\n            \n            bool _isActive;\n            \n            void DieWithError(std::string errorMessage); // handle fatal socket errors\n    };\n}\n\n#endif\n"
  },
  {
    "path": "packetmag.cpp",
    "content": "/** Implements a packet source for magazines\n */\n\n#include \"packetmag.h\"\n\nusing namespace vbit;\n\nPacketMag::PacketMag(uint8_t mag, PageList *pageList, Configure *configure, Debug *debug, uint8_t priority) :\n    _pageList(pageList),\n    _configure(configure),\n    _debug(debug),\n    _page(nullptr),\n    _subpage(nullptr),\n    _magNumber(mag),\n    _priority(priority),\n    _priorityCount(priority),\n    _state(PACKETSTATE_HEADER),\n    _thisRow(0),\n    _lastTxt(nullptr),\n    _packet29(nullptr),\n    _nextPacket29(nullptr),\n    _hasCustomHeader(false),\n    _magRegion(0),\n    _specialPagesFlipFlop(false),\n    _waitingForField(false),\n    _waitingForSecond(false),\n    _cycleDuration(-1)\n{\n    //ctor\n    _lastCycleTimestamp = {0,0};\n    \n    _carousel=new Carousel(_magNumber, _pageList, _debug);\n    _specialPages=new SpecialPages(_magNumber, _pageList, _debug);\n    _normalPages=new NormalPages(_magNumber, _pageList, _debug);\n    _updatedPages=new UpdatedPages(_magNumber, _pageList, _debug);\n}\n\nPacketMag::~PacketMag()\n{\n    //dtor\n    delete _carousel;\n    delete _specialPages;\n    delete _normalPages;\n    delete _updatedPages;\n}\n\nPacket* PacketMag::GetPacket(Packet* p)\n{\n    unsigned int thisSubcode;\n    bool updatedFlag=false;\n\n    // We should only call GetPacket if IsReady has returned true\n\n    // no pages\n    if (_pageList->GetSize(_magNumber)<1)\n    {\n        return nullptr;\n    }\n\nloopback: // jump back point to avoid returning null packets when we could send something\n    switch (_state)\n    {\n        case PACKETSTATE_HEADER: // Start to send out a new page, which may be a simple page or one of a carousel\n        {\n            _waitingForField = true; // enforce 20ms page erasure interval\n            if (GetEvent(EVENT_PACKET_29) && _packet29 != nullptr)\n            {\n                if (_mtx.try_lock()) // skip if unable to get lock\n                {\n                    if (_nextPacket29 == nullptr)\n                    {\n                        _nextPacket29 = _packet29;\n                        ClearEvent(EVENT_PACKET_29);\n                    }\n                    else\n                    {\n                        p->SetRow(_magNumber, 29, _nextPacket29->GetLine(), CODING_13_TRIPLETS);\n                        _nextPacket29 = _nextPacket29->GetNextLine();\n                        _mtx.unlock(); // unlock before we return!\n                        return p;\n                    }\n                    _mtx.unlock(); // we got a lock so unlock again\n                }\n            }\n            _specialPagesFlipFlop = !_specialPagesFlipFlop; // toggle the flag so that we interleave special pages and regular pages during the special pages event so that rolling headers aren't stopped completely\n            if (GetEvent(EVENT_SPECIAL_PAGES) && _specialPagesFlipFlop)\n            {\n                _page=_specialPages->NextPage();\n                \n                if (_page)\n                {\n                    // got a special page\n                    \n                    if (_page->GetPageFunction() != MIP)\n                    {\n                        // presentation enhancement pages\n                        _waitingForField = false; // don't need a page erasure interval\n                    }\n                    \n                    _subpage = _page->GetSubpage();\n                    if (_subpage == nullptr) // page is empty\n                    {\n                        _page->FreeLock(); // Must free the lock or we can never use this page again!\n                        goto loopback;\n                    }\n                    \n                    _status = _subpage->GetSubpageStatus() & PAGESTATUS_TRANSMITPAGE; // get transmit flag\n                    _region = _subpage->GetRegion();\n                    thisSubcode = (_subpage->GetSubCode() & 0x000F) | (_subpage->GetLastPacket() << 8);\n                    \n                    thisSubcode |= _page->GetUpdateCount() << 4;\n                    \n                    /* rules for the control bits are complicated. There are rules to allow the page to be sent as fragments. Since we aren't doing that, all the flags are left clear except for C9 (interrupted sequence) to keep special pages out of rolling headers */\n                    _status |= 0x0010;\n                    /* rules for the subcode are really complicated. The S1 nibble should be the sub page number, S2 is a counter that increments when the page is updated, S3 and S4 hold the last row number */\n                }\n                else\n                {\n                    // got to the end of the special pages\n                    ClearEvent(EVENT_SPECIAL_PAGES);\n                    goto loopback;\n                }\n            }\n            else\n            {\n                _page=_updatedPages->NextPage(); // Get the next updated page (if there is one)\n                if (_page)\n                {\n                    // updated page\n                    updatedFlag=true; // use our own flag because the pagestream's _isUpdated flag gets cleared by NextPage\n                }\n                else\n                {\n                    // no updated pages\n                    _page=_carousel->nextCarousel(); // Get the next carousel page (if there is one)\n                    if (_page)\n                    {\n                        // carousel with hard timings\n                    }\n                    else\n                    {\n                        // no urgent carousels\n                        _page=_normalPages->NextPage();  // Get the next normal page (if there is one)\n                    }\n                }\n                \n                if (_page == nullptr)\n                {\n                    // reached the end of a magazine cycle\n                    // get master clock singleton\n                    MasterClock *mc = mc->Instance();\n                    MasterClock::timeStruct t = mc->GetMasterClock();\n                    if (_lastCycleTimestamp.seconds){ // wait for real timestamps\n                        // calculate time since magazine cycle started\n                        int diffSeconds = difftime(t.seconds, _lastCycleTimestamp.seconds); // truncates double to int\n                        _cycleDuration = ((diffSeconds * 50) - _lastCycleTimestamp.fields) + t.fields;\n                        if (_cycleDuration < 50)\n                        {\n                            // hold magazine cycle start until next second tick\n                            _waitingForSecond = true;\n                        }\n                        else\n                        {\n                            // otherwise update cycle duration\n                            _debug->SetMagCycleDuration(_magNumber, _cycleDuration);\n                        }\n                    }\n                    _lastCycleTimestamp = t; // update timestamp\n                    \n                    // couldn't get a page to send so sent a time filling header\n                    p->Header(_magNumber,0xFF,0x0000,0x8010,_hasCustomHeader?_customHeaderTemplate:_configure->GetHeaderTemplate());\n                    return p;\n                }\n                \n                _thisRow=0;\n                \n                if (_page->IsCarousel() && !_page->GetOneShotFlag()) // don't cycle oneshot pages\n                {\n                    if (_page->Expired(true))\n                    {\n                        // cycle if timer has expired\n                        _page->StepNextSubpage();\n                        _subpage = _page->GetSubpage();\n                        if (_subpage == nullptr) // page is empty\n                        {\n                            _page->FreeLock(); // Must free the lock or we can never use this page again!\n                            goto loopback;\n                        }\n                        _page->SetTransitionTime(_subpage->GetCycleTime());\n                        _status=_subpage->GetSubpageStatus();\n                    }\n                    else\n                    {\n                        _subpage = _page->GetSubpage();\n                        if (_subpage == nullptr) // page is empty\n                        {\n                            _page->FreeLock(); // Must free the lock or we can never use this page again!\n                            goto loopback;\n                        }\n                        // clear any ERASE bit if page hasn't cycled to minimise flicker, and the interrupted status bit\n                        _status=_subpage->GetSubpageStatus() & ~(PAGESTATUS_C4_ERASEPAGE | PAGESTATUS_C9_INTERRUPTED);\n                    }\n                    \n                    thisSubcode=_subpage->GetSubCode();\n                    _region=_subpage->GetRegion();\n                }\n                else\n                {\n                    _subpage = _page->GetSubpage();\n                    if (_subpage == nullptr) // page is empty\n                    {\n                        _page->FreeLock(); // Must free the lock or we can never use this page again!\n                        goto loopback;\n                    }\n                    \n                    thisSubcode=_subpage->GetSubCode();\n                    _status=_subpage->GetSubpageStatus();\n                    _region=_subpage->GetRegion();\n                }\n                \n                // Handle pages with update bit set in a useful way.\n                // This isn't defined by the specification.\n                if (_status & PAGESTATUS_C8_UPDATE)\n                {\n                    // Clear update bit in stored page so that update flag is only transmitted once\n                    _subpage->SetSubpageStatus(_subpage->GetSubpageStatus() & ~PAGESTATUS_C8_UPDATE);\n                    \n                    // Also set the erase flag in output. This will allow left over rows in adaptive transmission to be cleared without leaving the erase flag set causing flickering.\n                    _status|=PAGESTATUS_C4_ERASEPAGE;\n                }\n                \n                if (updatedFlag)\n                {\n                    // page is updated set interrupted sequence flag\n                    _status|=PAGESTATUS_C9_INTERRUPTED;\n                }\n            }\n            \n            if (!(_status & PAGESTATUS_TRANSMITPAGE))\n            {\n                _page->FreeLock(); // Must free the lock or we can never use this page again!\n                goto loopback;\n            }\n            \n            // clear a flag we use to prevent duplicated X/28/0 packets\n            _hasX28Region = false;\n            p->Header(_magNumber,_page->GetPageNumber(),thisSubcode,_status,_hasCustomHeader?_customHeaderTemplate:_configure->GetHeaderTemplate());\n            \n            uint16_t tempCRC = p->PacketCRC(0); // calculate the crc of the new header\n            \n            bool headerChanged = _subpage->HasHeaderChanged(tempCRC);\n            bool pageChanged = _subpage->HasSubpageChanged();\n            if (headerChanged || pageChanged)\n            {\n                // the content of the header has changed or the page has been reloaded\n                // we must now CRC the whole page\n                Packet TempPacket(8,25); // a temporary packet for checksum calculation\n                for (int i=1; i<26; i++)\n                {\n                    TempPacket.SetRow(_magNumber, _thisRow, _subpage->GetRow(i)->GetLine(), _subpage->GetRow(i)->IsBlank()?CODING_7BIT_TEXT:_page->GetPageCoding());\n                    tempCRC = TempPacket.PacketCRC(tempCRC);\n                }\n                \n                _subpage->SetSubpageCRC(tempCRC);\n                \n                // TODO: the page content may get modified by substitutions in Packet::tx() which will result in an invalid checksum\n            }\n            \n            assert(p!=NULL);\n            \n            _lastTxt=_page->GetTxRow(27); // Get _lastTxt ready for packet 27 processing\n            _state=PACKETSTATE_PACKET27;\n            break;\n        }\n        case PACKETSTATE_PACKET27:\n        {\n            if (_lastTxt)\n            {\n                if ((_lastTxt->GetCharAt(0) & 0xF) > 3) // designation codes > 3\n                    p->SetRow(_magNumber, 27, _lastTxt->GetLine(), CODING_13_TRIPLETS); // enhancement linking\n                else if ((_lastTxt->GetCharAt(1) & _lastTxt->GetCharAt(2) & _lastTxt->GetCharAt(7) & _lastTxt->GetCharAt(8) &\n                         _lastTxt->GetCharAt(13) & _lastTxt->GetCharAt(14) & _lastTxt->GetCharAt(19) & _lastTxt->GetCharAt(20) &\n                         _lastTxt->GetCharAt(25) & _lastTxt->GetCharAt(26) & _lastTxt->GetCharAt(31) & _lastTxt->GetCharAt(32)) != 0xf)\n                         // don't generate packet if all page links are 0xFF\n                {\n                    p->SetRow(_magNumber, 27, _lastTxt->GetLine(), CODING_HAMMING_8_4); // navigation packets\n                    if ((_lastTxt->GetCharAt(0) & 0xF) == 0) // only designation code 0 has CRC\n                        p->SetX27CRC(_subpage->GetSubpageCRC());\n                }\n                _lastTxt=_lastTxt->GetNextLine();\n                break;\n            }\n            _lastTxt=_page->GetTxRow(28); // Get _lastTxt ready for packet 28 processing\n            _state=PACKETSTATE_PACKET28; //  // Intentional fall through to PACKETSTATE_PACKET28\n            /* fallthrough */\n            [[gnu::fallthrough]];\n        }\n        case PACKETSTATE_PACKET28:\n        {\n            if (_lastTxt)\n            {\n                p->SetRow(_magNumber, 28, _lastTxt->GetLine(), CODING_13_TRIPLETS);\n                if ((_lastTxt->GetCharAt(0) & 0xF) == 0 || (_lastTxt->GetCharAt(0) & 0xF) == 4)\n                    _hasX28Region = true; // don't generate an X/28/0 for a RE line\n                _lastTxt=_lastTxt->GetNextLine();\n                break;\n            }\n            else if (!(_hasX28Region) && (_region != _magRegion))\n            {\n                // create X/28/0 packet for pages which have a region set with RE in file\n                // this could almost certainly be done more efficiently but it's quite confusing and this is more readable for when it all goes wrong.\n                \n                std::array<uint8_t, 40> val{\n                    0x40, 0x40, 0x40, 0x74, 0x47, 0x70, 0x43, 0x75, 0x57, 0x40, 0x74, 0x77,\n                    0x77, 0x43, 0x70, 0x52, 0x41, 0x60, 0x55, 0x42, 0x57, 0x77, 0x44, 0x73,\n                    0x57, 0x77, 0x75, 0x77, 0x77, 0x77, 0x55, 0x77, 0x57, 0x77, 0x75, 0x57,\n                    0x77, 0x45, 0x40, 0x40\n                }; // default X/28/0 packet in pre Hamming24EncodeTriplet form (i.e. tti OL format)\n                int NOS = (_status & 0x380) >> 7;\n                int language = NOS | (_region << 3);\n                int triplet = 0x3C000 | (language << 7); // construct triplet 1\n                val[1] = (triplet & 0x3F) | 0x40;\n                val[2] = ((triplet & 0xFC0) >> 6) | 0x40;\n                val[3] = ((triplet & 0x3F000) >> 12) | 0x40;\n                p->SetRow(_magNumber, 28, val, CODING_13_TRIPLETS);\n                _lastTxt=_page->GetTxRow(26); // Get _lastTxt ready for packet 26 processing\n                _state=PACKETSTATE_PACKET26;\n                break;\n            }\n            else if (_page->GetPageCoding() == CODING_7BIT_TEXT)\n            {\n                // X/26 packets next in normal pages\n                _lastTxt=_page->GetTxRow(26); // Get _lastTxt ready for packet 26 processing\n                _state=PACKETSTATE_PACKET26; // Intentional fall through to PACKETSTATE_PACKET26\n            }\n            else\n            {\n                // do X/1 to X/25 first and go back to X/26 after\n                _state=PACKETSTATE_TEXTROW;\n                goto loopback;\n            }\n            /* fallthrough */\n            [[gnu::fallthrough]];\n        }\n        case PACKETSTATE_PACKET26:\n        {\n            if (_lastTxt)\n            {\n                p->SetRow(_magNumber, 26, _lastTxt->GetLine(), CODING_13_TRIPLETS);\n                // Do we have another line?\n                _lastTxt=_lastTxt->GetNextLine();\n                break;\n            }\n            if (_page->GetPageCoding() == CODING_7BIT_TEXT)\n            {\n                _state=PACKETSTATE_TEXTROW; // Intentional fall through to PACKETSTATE_TEXTROW\n            }\n            else\n            {\n                // otherwise we end the page here\n                _state=PACKETSTATE_HEADER;\n                _thisRow=0;\n                _pageList->RemovePage(_page); // remove from page list if no longer in any type lists - will free the lock\n                // this is to handle the case where an UpdatedPages is the only copy of a page left and must be removed now it's been transmitted.\n                goto loopback;\n            }\n            /* fallthrough */\n            [[gnu::fallthrough]];\n        }\n        case PACKETSTATE_TEXTROW:\n        {\n            // Find the next row that isn't NULL\n            for (_thisRow++;_thisRow<26;_thisRow++)\n            {\n                _lastTxt=_page->GetTxRow(_thisRow);\n                if (_lastTxt!=NULL)\n                    break;\n            }\n\n            // Didn't find? End of this page.\n            if (_thisRow>25 || _lastTxt==NULL)\n            {\n                if(_page->GetPageCoding() == CODING_7BIT_TEXT)\n                {\n                    // if this is a normal page we've finished\n                    _state=PACKETSTATE_HEADER;\n                    _thisRow=0;\n                    _page->FreeLock(); // Must free the lock or we can never use this page again!\n                }\n                else\n                {\n                    // otherwise go on to X/26\n                    _lastTxt=_page->GetTxRow(26);\n                    _state=PACKETSTATE_PACKET26;\n                }\n                goto loopback;\n            }\n            else\n            {\n            //_outp(\"J\");\n                if (_lastTxt->IsBlank() && (_configure->GetRowAdaptive() || _thisRow == 25 || _page->GetPageFunction() != LOP)) // If a row is empty then skip it if row adaptive mode on, or not a level 1 page\n                {\n                    goto loopback;\n                }\n                else\n                {\n                    // Assemble the packet\n                    p->SetRow(_magNumber, _thisRow, _lastTxt->GetLine(), _page->GetPageCoding());\n                    assert(p->IsHeader()!=true);\n                }\n            }\n            break;\n        }\n        default:\n        {\n            _state=PACKETSTATE_HEADER;// For now, do the next page\n            _page->FreeLock(); // Must free the lock or we can never use this page again!\n            return nullptr;\n        }\n    }\n\n    return p; //\n}\n\n/** Is there a packet ready to go?\n *  If the ready flag is set\n *  and the priority count allows a packet to go out\n *  @param force - If true AND if the next packet is being held back due to priority, send the packet anyway\n */\nbool PacketMag::IsReady(bool force)\n{\n    bool result=false;\n    // We can always send something unless\n    // 1) We have just sent out a header and are waiting on a new field\n    // 2) There are no pages\n    // 3) The magazine cycle is less than 1 second\n    if (GetEvent(EVENT_FIELD))\n    {\n        ClearEvent(EVENT_FIELD);\n        if (_waitingForField)\n        {\n            _waitingForField = false;\n        }\n    }\n    \n    if (GetEvent(EVENT_P830_FORMAT_1))\n    {\n        ClearEvent(EVENT_P830_FORMAT_1);\n        \n        if (_waitingForSecond)\n        {\n            _waitingForSecond = false;\n            // get master clock singleton\n            MasterClock *mc = mc->Instance();\n            MasterClock::timeStruct t = mc->GetMasterClock();\n            // calculate time since last time filling header and add to cycle time measured there\n            int diffSeconds = difftime(t.seconds, _lastCycleTimestamp.seconds); // truncates double to int\n            _cycleDuration += ((diffSeconds * 50) - _lastCycleTimestamp.fields) + t.fields;\n            // should clamp to 1 second\n            _debug->SetMagCycleDuration(_magNumber, _cycleDuration);\n            _lastCycleTimestamp = t; // update timestamp so that true cycle time can be measured\n        }\n    }\n    \n    if (_state==PACKETSTATE_HEADER && _waitingForSecond && !_updatedPages->waiting()) // force if there are updated pages waiting\n        return false; // limit output\n    \n    if (!_waitingForField)\n    {\n        _priorityCount--;\n        if (_priorityCount==0 || force || _updatedPages->waiting())\n        {\n            _priorityCount=_priority;\n            result=true;\n        }\n    }\n    \n    if (_pageList->GetSize(_magNumber)>0)\n    {\n        return result;\n    }\n    else\n    {\n        return false;\n    }\n};\n\nvoid PacketMag::SetPacket29(std::shared_ptr<TTXLine> line)\n{\n    _packet29 = line;\n    _nextPacket29 = _packet29;\n    do\n    {\n        uint8_t dc = _nextPacket29->GetCharAt(0) & 0xf;\n        if (dc == 0 || dc == 4)\n        {\n            _magRegion = ((_nextPacket29->GetCharAt(2) & 0x30) >> 4) | ((_nextPacket29->GetCharAt(3) & 0x3) << 2);\n        }\n        _nextPacket29 = _nextPacket29->GetNextLine();\n    } while (_nextPacket29 != nullptr);\n    _nextPacket29 = _packet29;\n}\n\nvoid PacketMag::DeletePacket29(int designationCode)\n{\n    _mtx.lock();\n    if (designationCode < 0 || designationCode > 15)\n    {\n        // delete all\n        _packet29 = nullptr;\n        _nextPacket29 == nullptr;\n    }\n    else\n    {\n        _packet29 = _packet29->RemoveLine(designationCode); // delete specific dc\n        _nextPacket29 = _packet29; // reset to first dc\n    }\n    _mtx.unlock();\n}\n\nvoid PacketMag::SetCustomHeader(std::shared_ptr<TTXLine> line)\n{\n    _hasCustomHeader = true;\n    std::string str = \"\";\n    for (int i=8; i<40; i++)\n        str += line->GetCharAt(i);\n    _customHeaderTemplate.assign(str);\n}\n\nvoid PacketMag::DeleteCustomHeader()\n{\n    _hasCustomHeader = false;\n    _debug->Log(Debug::LogLevels::logINFO,\"[PacketMag::DeleteCustomHeader] Removing custom header from magazine \" + std::to_string(_magNumber));\n}\n"
  },
  {
    "path": "packetmag.h",
    "content": "#ifndef PACKETMAG_H\n#define PACKETMAG_H\n#include <list>\n#include <mutex>\n#include <memory>\n#include <packetsource.h>\n#include \"ttxpagestream.h\"\n#include \"carousel.h\"\n#include \"specialpages.h\"\n#include \"normalpages.h\"\n#include \"updatedpages.h\"\n#include \"configure.h\"\n#include \"pagelist.h\"\n#include \"debug.h\"\n#include \"masterClock.h\"\n\nnamespace vbit\n{\n    class PacketMag : public PacketSource\n    {\n        public:\n            /** Default constructor */\n            PacketMag(uint8_t mag, PageList *pageList, Configure *configure, Debug *debug, uint8_t priority);\n            /** Default destructor */\n            virtual ~PacketMag();\n\n            Carousel* GetCarousel() { return _carousel; }\n            SpecialPages* GetSpecialPages() { return _specialPages; }\n            NormalPages* GetNormalPages() { return _normalPages; }\n            UpdatedPages* GetUpdatedPages() { return _updatedPages; }\n\n            /** Get the next packet\n             *  @return The next packet OR if IsReady() would return false then a filler packet\n             */\n            Packet* GetPacket(Packet* p) override;\n\n            void SetPriority(uint8_t priority) { _priority = priority; }\n\n            bool IsReady(bool force=false);\n\n            void SetPacket29(std::shared_ptr<TTXLine> line);\n            std::shared_ptr<TTXLine> GetPacket29() { return _packet29; }\n            void DeletePacket29(int designationCode=-1);\n            \n            void SetCustomHeader(std::shared_ptr<TTXLine> line);\n            bool GetCustomHeaderFlag() { return _hasCustomHeader; };\n            std::string GetCustomHeader() { return _hasCustomHeader?_customHeaderTemplate:\"\";}\n            void DeleteCustomHeader();\n            \n            void InvalidateCycleTimestamp() { _lastCycleTimestamp = {0,0}; }; // reset cycle duration calculation\n            int GetCycleDuration() { return _cycleDuration; };\n\n        protected:\n\n        private:\n            enum PacketState {PACKETSTATE_HEADER, PACKETSTATE_PACKET26, PACKETSTATE_PACKET27, PACKETSTATE_PACKET28, PACKETSTATE_TEXTROW};\n            \n            PageList* _pageList;\n            Configure* _configure;\n            Debug* _debug;\n            std::shared_ptr<TTXPageStream> _page; //!< The current page being output\n            std::shared_ptr<Subpage> _subpage; // pointer to the actual subpage\n            int _magNumber; //!< The number of this magazine. (where 0 is mag 8)\n            uint8_t _priority; //!< Priority of transmission where 1 is highest\n\n            std::list<TTXPageStream>::iterator _it;\n            Carousel* _carousel;\n            SpecialPages* _specialPages;\n            NormalPages* _normalPages;\n            UpdatedPages* _updatedPages;\n            uint8_t _priorityCount; /// Controls transmission priority\n            PacketState _state; /// State machine to sequence packet types\n            uint8_t _thisRow; // The current line that we are outputting\n            std::shared_ptr<TTXLine> _lastTxt; // The text of the last row that we fetched. Used for enhanced packets\n\n            std::shared_ptr<TTXLine> _packet29; // magazine related enhancement packets\n            std::shared_ptr<TTXLine> _nextPacket29;\n            std::mutex _mtx; // Mutex to interlock packet 29 from filemonitor.\n            \n            std::string _customHeaderTemplate;\n            bool _hasCustomHeader;\n\n            int _magRegion;\n            int _status;\n            int _region;\n            bool _hasX28Region;\n            bool _specialPagesFlipFlop; // toggle to alternate between special pages and normal pages\n            bool _waitingForField;\n            bool _waitingForSecond;\n            \n            MasterClock::timeStruct _lastCycleTimestamp;\n            int _cycleDuration; // magazine cycle time in fields\n    };\n}\n\n#endif // PACKETMAG_H\n"
  },
  {
    "path": "packetsource.cpp",
    "content": "#include \"packetsource.h\"\n\nusing namespace vbit;\n\nPacketSource::PacketSource()\n{\n    //ctor\n    // This could be in the initializer list BUT does not work in Visual C++\n    for (int i=0;i<EVENT_NUMBER_ITEMS;i++)\n    {\n    _eventList[i]=false;\n    }\n}\n\nPacketSource::~PacketSource()\n{\n    //dtor. Probably nothing to do here\n}\n\nvoid PacketSource::SetEvent(Event event)\n{\n    // An event is recorded by setting the corresponding flag\n    // GetPacket() will clear any event flags when it needs to wait.\n    _eventList[event]=true;\n}\n"
  },
  {
    "path": "packetsource.h",
    "content": "/**\n * Anything that generates teletext packets is derived from this interface.\n * Functions defined by this interface are\n * Constructor - sets up the data required by a particular packet source.\n * GetPacket - Gets the next packet in the stream.\n * SetEvent - Registers an even with the packet source\n**/\n#ifndef _PACKETSOURCE_H_\n#define _PACKETSOURCE_H_\n\n#include <packet.h>\n\nnamespace vbit\n{\n\n/** @brief Events are used to trigger packet sources so that they may proceed\n *  @description Different packet sources use different timing schemes.\n *  Packet sources may put themselves into a waiting state.\n *  Events are used to start them up again.\n *  Magazines stop after a header and wait for the next field for normal pages.\n *  Packet 830 waits for a multiple of 10 fields.\n *  \"Special pages\" and M/29 packets are triggered every few seconds.\n *  Databroadcast varies depending on the configuration of the service.\n */\nenum Event\n{\n  EVENT_FIELD,\n  EVENT_P830_FORMAT_1,\n  EVENT_P830_FORMAT_2_LABEL_0,\n  EVENT_P830_FORMAT_2_LABEL_1,\n  EVENT_P830_FORMAT_2_LABEL_2,\n  EVENT_P830_FORMAT_2_LABEL_3,\n  EVENT_DATABROADCAST,\n  EVENT_SPECIAL_PAGES,\n  EVENT_PACKET_29,\n  EVENT_NUMBER_ITEMS\n}  ;\n\n\nclass PacketSource\n{\n  public:\n    /** Default constructor */\n    PacketSource();\n    /** Default destructor */\n    virtual ~PacketSource();\n\n    /** Get the next packet\n     *  @return The next packet OR if IsReady() would return false then a filler packet\n     */\n    virtual Packet* GetPacket(Packet* p)=0;\n\n    /** Is there a packet ready to go?\n     *  @param force - If true and the next packet's priority is holding it, then allow the packet to go anyway. Default false.\n     *  @return true if there is a packet ready to go.\n     */\n    virtual bool IsReady(bool force=false)=0;\n\n    /** Report that an event happened */\n    void SetEvent(Event event); // All packet sources can use the same code\n    void ClearEvent(Event event){_eventList[event]=false;}; // All packet sources can use the same code\n    bool GetEvent(Event event){return _eventList[event];};\n\n  private:\n     bool _eventList[EVENT_NUMBER_ITEMS];\n};\n\n} // vbit namespace\n\n#endif // _PACKETSOURCE_H_\n"
  },
  {
    "path": "page.cpp",
    "content": " #include \"page.h\"\n\nusing namespace vbit;\n\nPage::Page() :\n    _carouselPage(nullptr)\n{\n    ClearPage(); // initialises variables\n}\n\nPage::~Page()\n{\n    //std::cerr << \"Page dtor\\n\";\n}\n\nvoid Page::AppendSubpage(std::shared_ptr<Subpage> s)\n{\n    s->SetMagazine(_pageNumber >> 8); // tell subpage what magazine it is in for fastext\n    \n    _subpages.push_back(s);\n    if (_carouselPage == nullptr)\n        StepFirstSubpage();\n}\n\nvoid Page::InsertSubpage(std::shared_ptr<Subpage> s)\n{\n    s->SetMagazine(_pageNumber >> 8); // tell subpage what magazine it is in for fastext\n    \n    for (std::list<std::shared_ptr<Subpage>>::iterator it=_subpages.begin();it!=_subpages.end();++it)\n    {\n        // find first subpage with a higher subcode\n        std::shared_ptr<Subpage> ptr = *it;\n        if (ptr->GetSubCode() > s->GetSubCode())\n        {\n            _subpages.insert(it,s);\n            return;\n        }\n    }\n    \n    // if we are here we ran to the end of the list without a match\n    _subpages.push_back(s);\n    if (_carouselPage == nullptr)\n        StepFirstSubpage();\n}\n\nvoid Page::RemoveSubpage(std::shared_ptr<Subpage> s)\n{\n    _subpages.remove(s);\n    \n    if (_subpages.empty())\n        _carouselPage = nullptr;\n    else\n    {\n        _iter=_subpages.begin();\n        _carouselPage = *_iter;\n    }\n}\n\nvoid Page::ClearPage()\n{\n    _pageNumber = 0; // an invalid page number\n    _pageCoding=CODING_7BIT_TEXT;\n    _pageFunction=LOP;\n    _carouselPage=nullptr;\n    \n    _subpages.clear(); // empty subpage list\n    _iter=_subpages.begin(); // reset iterator\n}\n\nvoid Page::RenumberSubpages()\n{\n    int count=0;\n    unsigned int subcode;\n    int code[4];\n    if (_subpages.size() == 1)\n    {\n        // A single page\n        \n        // Annex A.1 states that pages with no sub-pages should be coded Mxx-0000. This is the default when no subcode is specified in tti file.\n        // Annex E.2 states that the subcode may be used to transmit a BCD time code, e.g for an alarm clock. Where a non zero subcode is specified in the tti file keep it.\n        \n        if (Special())\n        {\n            // \"Special\" pages (e.g. MOT, POP, GPOP, DRCS, GDRCS, MIP) should be coded sequentially in hexadecimal 0000-000F\n            _subpages.front()->SetSubCode(0);\n        }\n    }\n    else if (_subpages.size() > 1)\n    {\n        // Page has subpages. Renumber according to Annex A.1.\n        for (int i=0;i<4;i++) code[i]=0;\n        std::list<std::shared_ptr<Subpage>>::iterator it;\n        for (it = _subpages.begin(); it != _subpages.end(); ++it)\n        {\n            if (Special())\n            {\n                // \"Special\" pages (e.g. MOT, POP, GPOP, DRCS, GDRCS, MIP) should be coded sequentially in hexadecimal 0000-000F\n                subcode = count;\n            }\n            else\n            {\n                // Pages intended for display with sub-pages should have sub-pages coded sequentially from Mxx-0001 to\n                // Mxx-0009 and then Mxx-0010 to Mxx-0019 and similarly using the decimal values of sub-code nibbles, from subcode 0001 up to 0079 as per ETS-300-706 Annex A.1.\n                // Pages intended for display shouldn't have more than 79 subpages, however we should handle it sensibly if they do, therefore above 0079 the numbers jump to 01nn to 09nn, then 1nnn to 3nnn.\n\n                // Increment the subcode is a baroque way\n                code[3]++; // increment units\n                if (code[3]>9) // if units > 9\n                {\n                    code[3]=0; // units = 0\n                    code[2]++; // increment tens\n                    if (code[2]>7) // if tens > 7\n                    {\n                        code[2]=0; // tens = 0\n                        code[1]++; // increment 'hundreds'\n                        if (code[1]>9) // if 'hundreds' > 9\n                        {\n                            code[1]=0; // 'hundreds' = 0\n                            code[0]++; // increment 'thousands'\n                            if (code[0]>3) // if 'thousands' > 3\n                            {\n                                code[0]=0; // overflow subcode\n                                code[1]=0;\n                                code[2]=0;\n                                code[3]=0;\n                            }\n                        }\n                    }\n                }\n                subcode=(code[0]<<12) + (code[1]<<8) + (code[2]<<4) + code[3];\n            }\n            \n            (*it)->SetSubCode(subcode); // modify the subcode\n            count++;\n        }\n    }\n}\n\nvoid Page::SetPageNumber(int page)\n{\n    if ((page<0x100) || (page>0x8ff))\n    {\n        page = 0x8FF;\n    }\n    _pageNumber=page;\n}\n\nvoid Page::SetPageFunctionInt(int pageFunction)\n{\n    switch (pageFunction)\n    {\n        default: // treat page functions we don't know as level one pages\n        case 0:\n        {\n            _pageFunction = LOP;\n            break;\n        }\n        case 2:\n        {\n            _pageFunction = GPOP;\n            break;\n        }\n        case 3:\n        {\n            _pageFunction = POP;\n            break;\n        }\n        case 4:\n        {\n            _pageFunction = GDRCS;\n            break;\n        }\n        case 5:\n        {\n            _pageFunction = DRCS;\n            break;\n        }\n        case 6:\n        {\n            _pageFunction = MOT;\n            break;\n        }\n        case 7:\n        {\n            _pageFunction = MIP;\n            break;\n        }\n        case 8:\n        {\n            _pageFunction = BTT;\n            break;\n        }\n        case 9:\n        {\n            _pageFunction = AIT;\n            break;\n        }\n        case 10:\n        {\n            _pageFunction = MPT;\n            break;\n        }\n        case 11:\n        {\n            _pageFunction = MPT_EX;\n            break;\n        }\n    }\n}\n\nvoid Page::SetPageCodingInt(int pageCoding)\n{\n    if (pageCoding != _pageCoding)\n    {\n        _pageCoding = ReturnPageCoding(pageCoding);\n        for (auto it = _subpages.begin(); it != _subpages.end(); ++it)\n        {\n            (*it)->SetSubpageChanged(); // page coding changed so CRC needs recalculating for each subpage\n        }\n    }\n}\n\nPageCoding Page::ReturnPageCoding(int pageCoding)\n{\n    switch (pageCoding)\n    {\n        default: // treat codings we don't know yet as normal text.\n        case 0:\n            return CODING_7BIT_TEXT;\n        case 1:\n            return CODING_8BIT_DATA;\n        case 2:\n            return CODING_13_TRIPLETS;\n        case 3:\n            return CODING_HAMMING_8_4;\n        case 4:\n            return CODING_HAMMING_7BIT_GROUPS;\n        case 5:\n            return CODING_PER_PACKET;\n    }\n}\n\nbool Page::IsCarousel()\n{\n    if (_subpages.size() > 1) // has multiple subpages\n    {\n        return true;\n    }\n    \n    if (std::shared_ptr<Subpage> s = GetSubpage())\n    {\n        if (s->GetTimedMode() && s->GetSubpageStatus() & PAGESTATUS_C9_INTERRUPTED)\n        {\n            // interrupted sequence flag is set, and page is in timed mode, so treat as a 1 page carousel\n            return true;\n        }\n    }\n    \n    return false;\n}\n\nvoid Page::StepFirstSubpage()\n{\n    if (_subpages.empty())\n    {\n        _carouselPage = nullptr;\n    }\n    else\n    {\n        _iter=_subpages.begin();\n        _carouselPage = *_iter;\n    }\n}\n\nvoid Page::StepLastSubpage()\n{\n    if (_subpages.empty())\n    {\n        _carouselPage = nullptr;\n    }\n    else\n    {\n        _iter=_subpages.end();\n        _carouselPage = *--_iter;\n    }\n}\n\nvoid Page::StepNextSubpageNoLoop()\n{\n    if (_subpages.empty())\n    {\n        _carouselPage = nullptr;\n    }\n    else\n    {\n        if (_carouselPage==nullptr)\n        {\n            _iter=_subpages.begin();\n        }\n        else\n        {\n            ++_iter;\n        }\n        \n        if (_iter == _subpages.end())\n        {\n            _carouselPage = nullptr;\n        }\n        else\n        {\n            _carouselPage = *_iter;\n        }\n        \n        if (_carouselPage != nullptr)\n        {\n            if (!(_carouselPage->GetSubpageStatus() & PAGESTATUS_TRANSMITPAGE))\n                StepNextSubpageNoLoop(); // skip over subpages if transmit flag not set\n        }\n    }\n}\n\nvoid Page::StepNextSubpage()\n{\n    if (_subpages.empty())\n    {\n        _carouselPage = nullptr;\n    }\n    else\n    {\n        if (_carouselPage==nullptr)\n        {\n            _iter=_subpages.begin();\n            _carouselPage = *_iter;\n        }\n        else\n        {\n            if (_iter == _subpages.end())\n                _iter = _subpages.begin();\n            if (++_iter == _subpages.end())\n                _iter = _subpages.begin();\n            _carouselPage = *_iter;\n        }\n        \n        if (!(_carouselPage->GetSubpageStatus() & PAGESTATUS_TRANSMITPAGE))\n        {\n            StepNextSubpageNoLoop(); // skip over subpages if transmit flag not set\n        }\n    }\n}\n\n// Find a subpage by subcode - Warning: this will only find the first match so don't let multiples into the list!\nstd::shared_ptr<Subpage> Page::LocateSubpage(uint16_t SubpageNumber)\n{\n    for (std::list<std::shared_ptr<Subpage>>::iterator s=_subpages.begin();s!=_subpages.end();++s)\n    {\n        std::shared_ptr<Subpage> ptr = *s;\n        if (SubpageNumber==ptr->GetSubCode())\n            return ptr;\n    }\n    return nullptr;\n}\n\n// attempt to set current subpage by number\nvoid Page::SetSubpage(uint16_t SubpageNumber)\n{\n    if (std::shared_ptr<Subpage> s = LocateSubpage(SubpageNumber))\n        _carouselPage = s;\n    // no warning on failure\n}\n\nstd::shared_ptr<TTXLine> Page::GetTxRow(uint8_t row)\n{\n    // Return a line or nullptr if the row does not exist\n    std::shared_ptr<TTXLine> line=nullptr;\n    \n    if (_carouselPage)\n        line=_carouselPage->GetRow(row);\n    \n    if (line!=nullptr) // Found a line\n    {\n        return line;\n    }\n    // No more lines? return NULL.\n    return nullptr;\n}\n\nSubpage::Subpage() :\n    _subcode(0),\n    _status(0),\n    _cycleTime(1),\n    _timedMode(false),\n    _region(0),\n    _mag(0),\n    _lastPacket(0),\n    _subpageChanged(true),\n    _headerCRC(0),\n    _subpageCRC(0)\n{\n    for (int i=0;i<=MAXROW;i++)\n    {\n        _lines[i]=nullptr; // delete rows\n    }\n}\n\nSubpage::~Subpage()\n{\n    //std::cerr << \"Subpage dtor\\n\";\n}\n\nstd::shared_ptr<TTXLine> Subpage::GetRow(unsigned int row)\n{\n    if (row>MAXROW)\n    {\n        return nullptr;\n    }\n    \n    if (_lines[row]==nullptr && row>0 && row<26)\n    {\n        _lines[row].reset(new TTXLine()); // return a blank row for X/1-X/25\n    }\n    return _lines[row];\n}\n\nvoid Subpage::SetRow(unsigned int rownumber, std::shared_ptr<TTXLine> line)\n{\n    unsigned int dc;\n    \n    // assert(rownumber<=MAXROW);\n    if (rownumber>MAXROW) return;\n    \n    if (rownumber == 26)\n    {\n        dc = line->GetCharAt(0) & 0x0F;\n        if ((dc + 26) > _lastPacket)\n            _lastPacket = dc + 26;\n    }\n    else if (rownumber < 26)\n    {\n        _subpageChanged = true; // page content within scope of CRC was changed\n        \n        if (rownumber > _lastPacket)\n            _lastPacket = rownumber;\n    }\n\n    if (_lines[rownumber]==nullptr)\n    {\n        _lines[rownumber] = line; // Didn't exist before\n    }\n    else\n    {\n        if (rownumber<26) // Ordinary line\n        {\n            if (line->IsBlank())\n            {\n                // don't store blank lines - they will be generated on the fly if necessary\n                _lines[rownumber] = nullptr;\n            }\n            else\n            {\n                _lines[rownumber] = line;\n            }\n        }\n        else // Enhanced packet\n        {\n            // If the line already exists we want to add to linked list of different designation codes\n            _lines[rownumber]->AppendLine(line);\n        }\n    }\n}\n\nvoid Subpage::DeleteRow(unsigned int rownumber, int designationCode)\n{\n    if (rownumber < 26)\n        _subpageChanged = true; // page content within scope of CRC was changed\n    \n    if (rownumber < 26 || designationCode < 0 || designationCode > 15)\n    {\n        _lines[rownumber] = nullptr; // delete entire row\n    }\n    else\n    {\n        if (_lines[rownumber])\n            _lines[rownumber] = _lines[rownumber]->RemoveLine(designationCode); // delete specific dc\n    }\n}\n\nvoid Subpage::SetFastext(std::array<FastextLink, 6> links)\n{\n    std::array<uint8_t, 40> line; // 40 bytes of packet data in CODING_HAMMING_8_4 form\n    \n    uint16_t lp, ls;\n    uint8_t p=0;\n    line[p++] = 0x0; // designation code 0\n    line[37] = 0xf; // link control set\n    line[38] = 0;\n    line[39] = 0; // last two bytes get overwritten with page CRC by packetmag\n    for (uint8_t i=0; i<6; i++)\n    {\n        lp=links[i].page;\n        if (lp == 0) lp = 0x8ff; // turn zero into 8FF to be ignored\n        ls=links[i].subpage;\n\n        uint8_t m=(lp/0x100 ^ _mag);     // calculate the relative magazine\n        line[p++]=lp & 0xF;              // page units\n        line[p++]=(lp & 0xF0) >> 4;      // page tens\n        line[p++]=ls & 0xF;              // S1\n        line[p++]=((m & 1) << 3) | ((ls >> 4) & 0xF); // S2 + M1\n        line[p++]=((ls >> 8) & 0xF);     // S3\n        line[p++]=((m & 6) << 1) | ((ls >> 4) & 0x3); // S4 + M2, M3\n    }\n    \n    std::shared_ptr<TTXLine> ttxline(new TTXLine(line));\n    SetRow(27,ttxline);\n}\n\nbool Subpage::GetFastext(std::array<FastextLink, 6> *links)\n{\n    std::shared_ptr<TTXLine> line = _lines[27];\n    if (line == nullptr)\n        return false; // no X/27\n    \n    line = line->LocateLine(0);\n    if (line == nullptr)\n        return false; // no X/27/0\n    \n    uint8_t p=1;\n    for (uint8_t i=0; i<6; i++)\n    {\n        uint8_t m;\n        uint16_t lp, ls;\n        lp = line->GetCharAt(p++) & 0xf;            // page units\n        lp |= (line->GetCharAt(p++) & 0xf) << 4;    // page tens\n        ls = line->GetCharAt(p++) & 0xf;            // S1\n        m = (line->GetCharAt(p) & 0x8) >> 3;        // M1\n        ls |= (line->GetCharAt(p++) & 0x7) << 4;    // S2\n        ls |= (line->GetCharAt(p++) & 0xf) << 8;    // S3\n        m |= (line->GetCharAt(p) & 0xc) >> 1;       // M2 + M3\n        ls |= (line->GetCharAt(p++) & 0x3) << 12;   // S4\n        links->at(i).page = ((m ^ _mag) << 8) | lp;\n        links->at(i).subpage = ls;\n    }\n    return true;\n}\n\nbool Subpage::HasHeaderChanged(uint16_t crc)\n{\n    if (_headerCRC != crc)\n    {\n        // update stored CRC and signal change\n        _headerCRC = crc;\n        return true;\n    }\n    \n    return false; // no change\n}\n"
  },
  {
    "path": "page.h",
    "content": "#ifndef PAGE_H\n#define PAGE_H\n#include <stdlib.h>\n#include \"string.h\"\n#include <iostream>\n#include <sstream>\n#include <memory>\n#include <fstream>\n#include <string>\n#include <list>\n#include <array>\n\n#include <cstdint>\n#include <cstdlib>\n#include <iomanip>\n\n#include <assert.h>\n\n#include \"ttxline.h\"\n\n// TTI format Page Status word\n#define PAGESTATUS_C4_ERASEPAGE     0x4000\n#define PAGESTATUS_C5_NEWSFLASH     0x0001\n#define PAGESTATUS_C6_SUBTITLE      0x0002\n#define PAGESTATUS_C7_SUPPRESSHDR   0x0004\n#define PAGESTATUS_C8_UPDATE        0x0008\n#define PAGESTATUS_C9_INTERRUPTED   0x0010\n#define PAGESTATUS_C10_INHIBIT      0x0020\n#define PAGESTATUS_TRANSMITPAGE     0x8000\n#define PAGESTATUS_SUBSTITUTEPAGE   0x0800\n#define PAGESTATUS_C11_SERIALMAG    0x0040\n\n// Allow for enhancement packets\n#define MAXROW 29\n\nenum PageCoding {CODING_7BIT_TEXT,CODING_8BIT_DATA,CODING_13_TRIPLETS,CODING_HAMMING_8_4,CODING_HAMMING_7BIT_GROUPS,CODING_PER_PACKET};\nenum PageFunction {LOP, DATABROADCAST, GPOP, POP, GDRCS, DRCS, MOT, MIP, BTT, AIT, MPT, MPT_EX};\n\nnamespace vbit\n{\nclass FastextLink\n{\n    public:\n        uint16_t page;\n        uint16_t subpage;\n};\n\nclass Subpage\n{\n    public:\n        Subpage();\n        virtual ~Subpage();\n        \n        void SetMagazine(uint8_t mag){_mag=mag&7;} // messing with this will break fastext\n        \n        uint16_t GetSubCode() {return _subcode;}\n        void SetSubCode(uint16_t subcode){_subcode=subcode;}\n        \n        uint16_t GetSubpageStatus() {return _status;}\n        void SetSubpageStatus(uint16_t ps){_status=ps;}\n        \n        uint8_t GetCycleTime() {return _cycleTime;}\n        void SetCycleTime(uint8_t time){_cycleTime=time;}\n        \n        bool GetTimedMode() {return _timedMode;}\n        void SetTimedMode(bool mode){_timedMode=mode;}\n        \n        uint8_t GetRegion(){return _region;}\n        void SetRegion(uint8_t region){_region=region;}\n        \n        std::shared_ptr<TTXLine> GetRow(unsigned int rowNumber);\n        void SetRow(unsigned int rownumber, std::shared_ptr<TTXLine> line);\n        void DeleteRow(unsigned int rownumber, int designationCode=-1);\n        \n        void SetFastext(std::array<FastextLink, 6> links);\n        bool GetFastext(std::array<FastextLink, 6> *links);\n        \n        unsigned int GetLastPacket() {return _lastPacket;};\n        \n        void SetSubpageChanged(){_subpageChanged = true;}; // mark subpage changed to cause CRC to be recalculated\n        bool HasSubpageChanged(){bool t = _subpageChanged; _subpageChanged = false; return t; }; // clears the flag for this subpage\n        \n        bool HasHeaderChanged(uint16_t crc); // updates the header crc for this subpage\n        \n        void SetSubpageCRC(uint16_t crc){_subpageCRC = crc;}; // update the stored crc\n        uint16_t GetSubpageCRC(){return _subpageCRC;}; // retrieve the stored crc\n        \n    private:\n        uint16_t _subcode;\n        uint16_t _status;\n        uint8_t _cycleTime;     // number of page cycles or seconds before subpage cycles\n        bool _timedMode;        // cycle subpage based on time in seconds\n        uint8_t _region;\n        \n        uint8_t _mag;           // this is used in fastext link calculation\n        \n        std::shared_ptr<TTXLine> _lines[MAXROW+1];\n        unsigned int _lastPacket;\n        \n        bool _subpageChanged;   // page was reloaded\n        uint16_t _headerCRC;    // holds the last calculated CRC of the page header\n        uint16_t _subpageCRC;   // holds the last calculated CRC of the page\n};\n\nclass Page\n{\n    public:\n        /** Default constructor */\n        Page();\n\n        /** Default destructor */\n        virtual ~Page();\n        \n        void AppendSubpage(std::shared_ptr<Subpage> s);\n        void InsertSubpage(std::shared_ptr<Subpage> s);\n        void RemoveSubpage(std::shared_ptr<Subpage> s);\n        unsigned int GetSubpageCount() {return _subpages.size();};\n        \n        int GetPageNumber() const {return _pageNumber;};\n        void SetPageNumber(int page);\n        \n        // get the function or coding of a page as the enum\n        PageCoding GetPageCoding() {return _pageCoding;};\n        PageFunction GetPageFunction() {return _pageFunction;};\n        \n        // set the page function or coding based on their integer representations in ETS 300 706 section 9.4.2.1\n        static PageCoding ReturnPageCoding(int pageCoding);\n        void SetPageFunctionInt(int pageFunction);\n        void SetPageCodingInt(int pageCoding);\n\n        bool Special() {return (_pageFunction == GPOP || _pageFunction == POP || _pageFunction == GDRCS || _pageFunction == DRCS || _pageFunction == MOT || _pageFunction == MIP);} // more convenient way to tell if a page is 'special'.\n        \n        void ClearPage();\n        void RenumberSubpages();\n        \n        bool IsCarousel();\n\n        void StepFirstSubpage();\n        void StepLastSubpage();\n        void StepNextSubpage();\n        void StepNextSubpageNoLoop();\n        \n        std::shared_ptr<Subpage> GetSubpage(){return _carouselPage;};\n        void SetSubpage(uint16_t SubpageNumber);\n        std::shared_ptr<Subpage> LocateSubpage(uint16_t SubpageNumber);\n        \n        std::shared_ptr<TTXLine> GetTxRow(uint8_t row);\n        \n    protected:\n        \n    private:\n        int _pageNumber;\n        PageCoding _pageCoding;\n        PageFunction _pageFunction;\n        bool _pageChanged; // page was reloaded\n        \n        std::list<std::shared_ptr<Subpage>> _subpages; // list of subpages\n        std::list<std::shared_ptr<Subpage>>::iterator _iter;\n        std::shared_ptr<Subpage> _carouselPage;\n};\n};\n#endif // PAGE_H\n"
  },
  {
    "path": "pagelist.cpp",
    "content": "/** PageList\n */\n#include \"pagelist.h\"\n#include \"packetmag.h\"\n\nusing namespace vbit;\n\nPageList::PageList(Configure *configure, Debug *debug) :\n    _configure(configure),\n    _debug(debug)\n{\n    for (int i=0;i<8;i++)\n    {\n        _mag[i]=nullptr;\n    }\n    if (_configure==nullptr)\n    {\n        _debug->Log(Debug::LogLevels::logERROR,\"NULL configuration object\");\n        return;\n    }\n    \n    for (int i=0;i<8;i++)\n    {\n        _mag[i]=new PacketMag(i, this, _configure, _debug, 9); // this creates the eight PacketMags that Service will use. Priority will be set in Service later\n    }\n}\n\nPageList::~PageList()\n{\n}\n\nvoid PageList::AddPage(std::shared_ptr<TTXPageStream> page, bool noupdate)\n{\n    int num = page->GetPageNumber();\n    int mag = (num >> 8) & 7;\n    \n    if ((num & 0xFF) != 0xFF)\n    {\n        // never load page mFF into page lists\n        std::shared_ptr<TTXPageStream> q = Locate(num);\n        if (q)\n        {\n            _pageList[mag].remove(q);\n            q->MarkForDeletion();\n            \n            std::stringstream ss;\n            ss << \"[PageList::AddPage] Replacing page \" << std::hex << num;\n            _debug->Log(Debug::LogLevels::logERROR,ss.str());\n        }\n\n        _pageList[mag].push_back(page);\n        UpdatePageLists(page, noupdate);\n    \n        _debug->SetMagazineSize(mag, _pageList[mag].size());\n    }\n}\n\nvoid PageList::UpdatePageLists(std::shared_ptr<TTXPageStream> page, bool noupdate)\n{\n    int mag=(page->GetPageNumber() >> 8) & 0x7;\n    \n    if (page->Special() && !page->GetOneShotFlag()) // OneShot pages can't be special\n    {\n        // Page is 'special'\n        if (!(page->GetSpecialFlag()))\n        {\n            if (page->GetNormalFlag())\n            {\n                std::stringstream ss;\n                ss << \"[PageList::UpdatePageLists] page was normal, is now special \" << std::hex << (page->GetPageNumber());\n                _debug->Log(Debug::LogLevels::logINFO,ss.str());\n            }\n            _mag[mag]->GetSpecialPages()->addPage(page);\n        }\n    }\n    else\n    {\n        // Page is 'normal'\n        if (!(page->GetNormalFlag()))\n        {\n            if (page->GetSpecialFlag() && !page->GetOneShotFlag())\n            {\n                std::stringstream ss;\n                ss << \"[PageList::UpdatePageLists] page was special, is now normal \" << std::hex << (page->GetPageNumber());\n                _debug->Log(Debug::LogLevels::logINFO,ss.str());\n            }\n            \n            _mag[mag]->GetNormalPages()->addPage(page);\n        }\n        \n        if (page->IsCarousel() && !page->GetOneShotFlag()) // don't cycle OneShot pages\n        {\n            // Page is also a 'carousel'\n            if (!(page->GetCarouselFlag()))\n            {\n                std::stringstream ss;\n                ss << \"[PageList::UpdatePageLists] page is now a carousel \" << std::hex << (page->GetPageNumber());\n                _debug->Log(Debug::LogLevels::logINFO,ss.str());\n                if (page->GetSubpage() == nullptr)\n                    page->StepFirstSubpage(); // ensure we're pointing at a subpage\n                _mag[mag]->GetCarousel()->addPage(page);\n            }\n        }\n        else\n        {\n            // add normal, non carousel pages to updatedPages list\n            if ((!(page->GetUpdatedFlag())) && !noupdate)\n            {\n                _mag[mag]->GetUpdatedPages()->addPage(page);\n            }\n            else\n            {\n            }\n        }\n    }\n}\n\nvoid PageList::RemovePage(std::shared_ptr<TTXPageStream> page)\n{\n    if (!(page->GetCarouselFlag() || page->GetNormalFlag() || page->GetSpecialFlag() || page->GetUpdatedFlag()))\n    {\n        // page has been removed from all of the page type lists\n        \n        int mag=(page->GetPageNumber() >> 8) & 0x7;\n        _pageList[mag].remove(page);\n        _debug->SetMagazineSize(mag, _pageList[mag].size());\n        \n        std::stringstream ss;\n        ss << \"[PageList::RemovePage] Deleted \" << std::hex << (page->GetPageNumber());\n        _debug->Log(Debug::LogLevels::logINFO,ss.str());\n    }\n    page->FreeLock(); // free the lock on page\n}\n\nvoid PageList::CheckForPacket29OrCustomHeader(std::shared_ptr<TTXPageStream> page)\n{\n    if (page->IsCarousel()) // page mFF should never be a carousel and this code leads to a crash if it is so bail out now\n        return;\n    \n    int mag=(page->GetPageNumber() >> 8) & 0x7;\n    if ((page->GetPageNumber() & 0xFF) == 0xFF) // Only read from page mFF\n    {\n        /* attempt to load custom header from the page */\n        if ((!_mag[mag]->GetCustomHeaderFlag()) || page->GetCustomHeaderFlag()) // Only allow one file to set header per magazine\n        {\n            if (page->GetTxRow(0)){\n                _mag[mag]->SetCustomHeader(page->GetTxRow(0)); // set custom headers\n                \n                if (!page->GetCustomHeaderFlag())\n                    _debug->Log(Debug::LogLevels::logINFO,\"[PageList::CheckForPacket29OrCustomHeader] Added custom header for magazine \" + std::to_string((mag == 0)?8:mag));\n                page->SetCustomHeaderFlag(true); // mark the page\n            }\n            else if (page->GetCustomHeaderFlag()) // page previously had custom header\n            {\n                _mag[mag]->DeleteCustomHeader();\n                page->SetCustomHeaderFlag(false);\n            }\n        }\n        \n        /* attempt to load packet M/29 data from the page */\n        if ((_mag[mag]->GetPacket29() == nullptr) || page->GetPacket29Flag()) // Only allow one file to set packet29 per magazine\n        {\n            bool Packet29Flag = false;\n            \n            if (page->GetPacket29Flag())\n                _mag[mag]->DeletePacket29(); // clear previous packet 29\n            \n            std::shared_ptr<TTXLine> tempLine = page->GetTxRow(29);\n            \n            if (tempLine != nullptr)\n            {\n                Packet29Flag = true;\n                _mag[mag]->SetPacket29(std::shared_ptr<TTXLine>(new TTXLine(tempLine))); // create a deep copy\n            }\n            \n            if (Packet29Flag && !page->GetPacket29Flag())\n                _debug->Log(Debug::LogLevels::logINFO,\"[PageList::CheckForPacket29OrCustomHeader] Added packet 29 for magazine \"+ std::to_string((mag == 0)?8:mag));\n            \n            page->SetPacket29Flag(Packet29Flag); // mark the page\n        }\n    }\n}\n\n// Find a page by number - Warning: this will only find the first match so don't let multiples into the list!\nstd::shared_ptr<TTXPageStream> PageList::Locate(int PageNumber)\n{\n    // This is called from the FileMonitor thread\n    int mag = (PageNumber >> 8) & 7;\n    for (std::list<std::shared_ptr<TTXPageStream>>::iterator p=_pageList[mag].begin();p!=_pageList[mag].end();++p)\n    {\n        std::shared_ptr<TTXPageStream> ptr = *p;\n        if (PageNumber==ptr->GetPageNumber())\n            return ptr;\n    }\n    return nullptr;\n}\n\n// Does the page list contain a particular TTXPageStream\nbool PageList::Contains(std::shared_ptr<TTXPageStream> page)\n{\n    // This is called from the FileMonitor thread\n    int mag = (page->GetPageNumber() >> 8) & 7;\n    for (std::list<std::shared_ptr<TTXPageStream>>::iterator p=_pageList[mag].begin();p!=_pageList[mag].end();++p)\n    {\n        if (*p==page)\n            return true;\n    }\n    return false;\n}\n\nint PageList::GetSize(int mag)\n{\n    if (mag < 8 && mag >= 0)\n        return _pageList[mag].size();\n    else\n        return 0;\n}\n"
  },
  {
    "path": "pagelist.h",
    "content": "#ifndef _PAGELIST_H_\n#define _PAGELIST_H_\n\n#include <iostream>\n#include <string>\n#include <dirent.h>\n#include <errno.h>\n#include <vector>\n#include <list>\n\n#include \"configure.h\"\n#include \"debug.h\"\n#include \"ttxpagestream.h\"\n\nnamespace vbit\n{\n    class PacketMag; // forward declaration\n\n    /** @brief A PageList maintains the set of all teletext pages in a teletext service\n     *  Internally each magazine has its own list of pages.\n     */\n    class PageList\n    {\n        public:\n            /** @brief Create am empty page list\n             */\n            PageList(Configure *configure, Debug *debug);\n            ~PageList();\n\n            PacketMag **GetMagazines(){PacketMag **p=_mag;return p;};\n\n            /** Return the page object with the specified page number */\n            std::shared_ptr<TTXPageStream> Locate(int PageNumber);\n            \n            bool Contains(std::shared_ptr<TTXPageStream> page);\n\n            /** Add a teletext page to the proper magazine */\n            void AddPage(std::shared_ptr<TTXPageStream> page, bool noupdate=false);\n            \n            void RemovePage(std::shared_ptr<TTXPageStream> page);\n            \n            /** Add a teletext page to the correct list for its type */\n            void UpdatePageLists(std::shared_ptr<TTXPageStream> page, bool noupdate=false);\n\n            void CheckForPacket29OrCustomHeader(std::shared_ptr<TTXPageStream> page);\n            \n            int GetSize(int mag);\n\n        private:\n            Configure* _configure; // The configuration object\n            Debug* _debug;\n            std::list<std::shared_ptr<TTXPageStream>> _pageList[8]; /// The list of Pages in this service. One list per magazine\n            PacketMag* _mag[8];\n    };\n}\n\n#endif\n"
  },
  {
    "path": "postupdate.sh",
    "content": "#!/bin/bash\n\n# Perform any script actions which need to happen after switching to the latest\n# tagged release\n\nmain(){\n  # ensure we are in the vbit2 source directory\n  cd `dirname \"$(readlink -f \"$0\")\"`\n  \n  # if it's an old install without auto deps we should do a complete recompile\n  if [ ! -f vbit2.d ]; then\n    # hope that presence of vbit2.d means all dep files are present\n    make clean\n  fi\n\n  # offer to upgrade old installs to new scripts (runs getvbit2)\n  migrate\n  \n  # recompile vbit2\n  make\n  \n  sudo apt -qq -y install python3-dialog\n  \n  # remove old symlink\n  rm $HOME/.local/bin/runvbit2.sh 2>/dev/null\n  \n  # create links\n  mkdir -p $HOME/.local/bin\n  ln -s -f `pwd`/vbit2 $HOME/.local/bin/\n  ln -s -f `pwd`/scripts/runvbit2.py $HOME/.local/bin/runvbit2\n  ln -s -f `pwd`/scripts/teletext-update.py $HOME/.local/bin/teletext-update\n  ln -s -f `pwd`/scripts/vbit-config.py $HOME/.local/bin/vbit-config\n  \n  # install systemd user scripts\n  mkdir -p $HOME/.local/share/systemd/user\n  cp vbit2.service $HOME/.local/share/systemd/user\n  cp teletext-update.timer $HOME/.local/share/systemd/user\n  cp teletext-update.service $HOME/.local/share/systemd/user\n  \n  systemctl --user daemon-reload\n  \n  cleanoldunits\n  \n  # warn about removing old services\n  migratejson\n  \n  # restart vbit if service is active\n  if [[ `systemctl --user is-active vbit2.service` == \"active\" ]]; then\n    systemctl --user restart vbit2.service\n  fi\n}\n\ncleanoldunits(){\n  # clean up older systemd files\n  if [ -f $HOME/.config/systemd/user/vbit2.service ]; then\n    systemctl --user disable vbit2.service --now # removes old link\n  fi\n\n  if [ -f $HOME/.config/systemd/user/teletext-update.service ]; then\n    systemctl --user disable teletext-update.service --now # removes old link\n  fi\n\n  if [ -f $HOME/.config/systemd/user/teletext-update.timer ]; then\n    systemctl --user disable teletext-update.timer --now # removes old link\n  fi\n}\n\nmigrate(){\n  FOUND=()\n  if [ -f $HOME/vb2 ]; then FOUND+=(\"$HOME/vb2\"); fi\n  if [ -f $HOME/vbit2.sh ]; then FOUND+=(\"$HOME/vbit2.sh\"); fi\n  if [ -f $HOME/updatePages.sh ]; then FOUND+=(\"$HOME/updatePages.sh\"); fi\n  if [ -d $HOME/raspi-teletext-master ]; then FOUND+=(\"$HOME/raspi-teletext-master\"); fi\n  if [ -f /etc/systemd/system/vbit2.service ]; then FOUND+=(\"/etc/systemd/system/vbit2.service\"); fi\n  if [ ! ${#FOUND[@]} -eq 0 ]; then\n    printf 'The following files were found which relate to an old version of vbit2:' | fold -s -w `tput cols`\n    printf '\\n%s' \"${FOUND[@]}\" | fold -s -w `tput cols`\n    printf '\\n\\nIt is recommended to upgrade to the new system which includes the interactive vbit-config utility.\\nDo you wish to attempt to reinstall vbit2 automatically?\\n\\033[1mCaution: this will remove the files and directories listed above losing any local changes you have made.\\033[0m\\n' | fold -s -w `tput cols`\n    read -p \"(y)es (n)o\" -n 1 -s\n    echo\n    if [[ ! $REPLY =~ ^[Yy]$ ]]; then\n      printf \"leaving start scripts unchanged.\\n\"\n    else\n      # Here Be Dragons!\n      # remove any updatePages.sh cron job\n      sudo crontab -l | grep -v 'updatePages.sh' | sudo crontab -\n      crontab -l | grep -v 'updatePages.sh' | crontab -\n      # delete the old files and directories\n      sudo systemctl disable vbit2 --now\n      sudo rm -rf ${FOUND[@]}\n      # run the new installer\n      ./getvbit2\n      \n      if [ -d $HOME/Pages ]; then\n        printf 'The directory %s is no longer required.\\n' \"$HOME/Pages\"\n      fi\n      \n      if [ -d $HOME/teletext ]; then\n        printf 'The directory %s is no longer required.\\n' \"$HOME/teletext\"\n      fi\n    fi\n    exit\n  fi\n}\n\nmigratejson(){\n  if [ -f $HOME/.teletext-services/config ]; then\n    systemctl --user stop vbit2.service\n    \n    printf 'This version of VBIT2 uses a new config format and directory scheme.\\nServices will not be automatically migrated so must be reinstalled.\\nThe following services were found:\\n'\n    ls -d1 ~/.teletext-services/*/\n    \n    read -n 1 -s -r -p \"Do you wish to create a backup of these services? (y/N)\"\n    \n    if [[ $REPLY =~ ^[Yy]$ ]]; then\n      printf '\\nbacking up old services to %s\\n' \"$HOME/teletext-services.bak\"\n      mv $HOME/.teletext-services $HOME/teletext-services.bak\n    else\n      rm -rf $HOME/.teletext-services/ 2>/dev/null\n    fi\n    \n    vbit-config\n  fi\n}\n\nmain; exit\n"
  },
  {
    "path": "scripts/config.py",
    "content": "import os\nimport json\nimport subprocess\nimport shutil\n\n# absolute path of installed services\nSERVICESDIR = os.path.join(os.getenv('HOME'), \".teletext-services\")\n\ndef load():\n    if not os.path.exists(SERVICESDIR):\n        os.makedirs(SERVICESDIR)\n        with open(os.path.join(SERVICESDIR,\"IMPORTANT\"), 'w') as importantFile:\n            importantFile.write(\"IMPORTANT:\\nThese directories were created by vbit-config.\\nIf a service is uninstalled the directory will be deleted.\")\n    \n    try:\n        with open(os.path.join(SERVICESDIR,\"config.json\")) as configFile:\n            configData = json.load(configFile)\n    except:\n        # opening file failed\n        configData = {}\n    \n    if not configData.get(\"installed\"):\n        configData[\"installed\"] = []\n        \n    if not configData.get(\"settings\"):\n        configData[\"settings\"] = {}\n    \n    # Default to raspi-teletext output\n    if not configData[\"settings\"].get(\"output\"):\n        configData[\"settings\"][\"output\"] = \"raspi-teletext\"\n    \n    if not configData[\"settings\"].get(\"packetServer\"):\n        configData[\"settings\"][\"packetServer\"] = False\n    \n    if not configData[\"settings\"].get(\"packetServerPort\"):\n        configData[\"settings\"][\"packetServerPort\"] = 19761\n    \n    if not configData[\"settings\"].get(\"interfaceServer\"):\n        configData[\"settings\"][\"interfaceServer\"] = False\n    \n    if not configData[\"settings\"].get(\"interfaceServerPort\"):\n        configData[\"settings\"][\"interfaceServerPort\"] = 1992\n    \n    return configData\n\ndef save(configData):\n    try:\n        with open(os.path.join(SERVICESDIR,\"config.json\"), 'w') as configFile:\n            json.dump(configData, configFile, indent=2)\n    except:\n        raise RuntimeError(\"updating config file failed\")\n\ndef getInstalledServices():\n    configData = load()\n    return configData[\"installed\"]\n\ndef getSelectedService():\n    configData = load()\n    selected = configData[\"settings\"].get(\"selected\")\n    if not selected:\n        return {}\n    \n    for service in configData[\"installed\"]:\n        name = service.get(\"name\")\n        path = service.get(\"path\")\n        if name == selected:\n            return service\n        \n    return {}\n\ndef selectService(name):\n    configData = load()\n    configData[\"settings\"][\"selected\"] = name\n    save(configData)\n    \n    if subprocess.run([\"systemctl\", \"--user\", \"is-active\", \"vbit2.service\"], capture_output=True, text=True).stdout == \"active\\n\":\n        # service is running so restart it\n        subprocess.run([\"systemctl\", \"--user\", \"restart\", \"vbit2.service\"])\n\ndef uninstallService(name):\n    configData = load()\n    \n    for service in configData[\"installed\"]:\n        if service.get(\"name\") == name:\n            type = service.get(\"type\")\n            \n            if dict(configData[\"settings\"]).get(\"selected\") == name:\n                # stop vbit2 before deleting an active service\n                subprocess.run([\"systemctl\", \"--user\", \"stop\", \"vbit2.service\"])\n                # deselect the service\n                configData[\"settings\"].pop(\"selected\")\n            if os.path.commonpath([os.path.abspath(SERVICESDIR)]) == os.path.commonpath([os.path.abspath(SERVICESDIR), os.path.abspath(service[\"path\"])]) and type != \"dir\":\n                # only delete service if installed under SERVICESDIR and not custom dir type\n                shutil.rmtree(service[\"path\"], ignore_errors=True) # delete directory\n            \n            configData[\"installed\"].remove(service) # remove from installed list\n            save(configData)\n            break\n\ndef installService(service):\n    configData = load()\n    \n    if not service.get(\"name\") or not service.get(\"type\") or not service.get(\"path\"):\n        raise RuntimeError(\"invalid service configuration data\\n{0}\".format(service))\n    \n    installedNames = []\n    for s in configData.get(\"installed\"):\n        installedNames += [s.get(\"name\")]\n    \n    if service[\"name\"] in installedNames:\n        raise RuntimeError(\"Service name already in use\")\n    \n    if service[\"type\"] == \"dir\":\n        # service in an arbitrary directory\n        if not os.path.exists(service[\"path\"]):\n            raise RuntimeError(\"Directory does not exist\")\n    else:\n        if not service.get(\"url\"):\n            raise RuntimeError(\"invalid service configuration data\\n{0}\".format(service))\n        doServiceInstall(service[\"type\"], service[\"path\"], service[\"url\"])\n    \n    serviceConfigObject = {\"name\":service[\"name\"], \"type\":service[\"type\"], \"path\":service[\"path\"]}\n    \n    subservices = service.get(\"subservices\")\n    \n    try:\n        if subservices:\n            serviceConfigObject[\"subservices\"] = []\n            for subservice in subservices:\n                if not subservice.get(\"type\") or not subservice.get(\"path\") or not subservice.get(\"url\"):\n                    raise RuntimeError(\"invalid subservice configuration data\\n{0}\".format(subservice))\n                subservicePath = os.path.join(service[\"path\"],subservice.get(\"path\"))\n                doServiceInstall(subservice.get(\"type\"), subservicePath, subservice.get(\"url\"))\n                serviceConfigObject[\"subservices\"] += [{\"name\":subservice[\"name\"], \"type\":subservice[\"type\"], \"path\":subservicePath, \"required\":subservice.get(\"required\") or False}]\n        \n        configData[\"installed\"] += [serviceConfigObject]\n        configData[\"installed\"].sort(key=lambda x: x[\"name\"])\n        # if no service is selected, select it\n        if not configData[\"settings\"].get(\"selected\"):\n            configData[\"settings\"][\"selected\"] = service[\"name\"]\n        \n        save(configData)\n    except Exception as e:\n        shutil.rmtree(service[\"path\"], ignore_errors=True) # delete directory\n        raise Exception(e)\n\ndef doServiceInstall(type, path, url):\n    if type == \"git\" or type == \"svn\":\n        if os.path.commonpath([os.path.abspath(SERVICESDIR)]) != os.path.commonpath([os.path.abspath(SERVICESDIR), os.path.abspath(path)]):\n            raise RuntimeError(\"Tried to install outside SERVICESDIR\")\n            \n        os.mkdir(path)\n        \n        if type == \"svn\":\n            process = subprocess.run([\"svn\", \"checkout\", \"--quiet\", url, path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)\n        \n            if process.returncode:\n                text = \"Subversion checkout failed with the following error:\\n\"\n                text += process.stdout.decode(\"utf8\")\n                raise RuntimeError(text)\n                \n        elif type == \"git\":\n            process = subprocess.run([\"git\", \"clone\", \"--quiet\", \"--depth\", \"1\", url, path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)\n            \n            if process.returncode:\n                text = \"Git clone failed with the following error:\\n\"\n                text += process.stdout.decode(\"utf8\")\n                raise RuntimeError(text)\n    else:\n        raise RuntimeError(\"unknown service type\")\n"
  },
  {
    "path": "scripts/known_services.json",
    "content": "{\n    \"services\":[\n        {\"name\":\"Artfax\",\"type\":\"git\",\"url\":\"https://github.com/teletexx/service-artfax\",\"path\":\"Artfax\"},\n        {\"name\":\"Ceefax\",\"type\":\"group\",\"services\":[\n            {\"name\":\"Ceefax (East)\",\"type\":\"svn\",\"url\":\"https://internal.nathanmediaservices.co.uk/svn/ceefax/East\",\"path\":\"CeefaxEast\"},\n            {\"name\":\"Ceefax (East Midlands)\",\"type\":\"svn\",\"url\":\"https://internal.nathanmediaservices.co.uk/svn/ceefax/EastMidlands\",\"path\":\"CeefaxEastMids\"},\n            {\"name\":\"Ceefax (London)\",\"type\":\"svn\",\"url\":\"https://internal.nathanmediaservices.co.uk/svn/ceefax/London\",\"path\":\"CeefaxLondon\"},\n            {\"name\":\"Ceefax (Northern Ireland)\",\"type\":\"svn\",\"url\":\"https://internal.nathanmediaservices.co.uk/svn/ceefax/NorthernIreland\",\"path\":\"CeefaxNI\"},\n            {\"name\":\"Ceefax (Scotland)\",\"type\":\"svn\",\"url\":\"https://internal.nathanmediaservices.co.uk/svn/ceefax/Scotland\",\"path\":\"CeefaxScotland\"},\n            {\"name\":\"Ceefax (South)\",\"type\":\"svn\",\"url\":\"https://internal.nathanmediaservices.co.uk/svn/ceefax/South\",\"path\":\"CeefaxSouth\"},\n            {\"name\":\"Ceefax (South West)\",\"type\":\"svn\",\"url\":\"https://internal.nathanmediaservices.co.uk/svn/ceefax/SouthWest\",\"path\":\"CeefaxSouthWest\"},\n            {\"name\":\"Ceefax (Wales)\",\"type\":\"svn\",\"url\":\"https://internal.nathanmediaservices.co.uk/svn/ceefax/Wales\",\"path\":\"CeefaxWales\"},\n            {\"name\":\"Ceefax (West)\",\"type\":\"svn\",\"url\":\"https://internal.nathanmediaservices.co.uk/svn/ceefax/West\",\"path\":\"CeefaxWest\"},\n            {\"name\":\"Ceefax (Worldwide)\",\"type\":\"svn\",\"url\":\"https://internal.nathanmediaservices.co.uk/svn/ceefax/Worldwide\",\"path\":\"CeefaxWorldwide\"},\n            {\"name\":\"Ceefax (Yorks and Lincs)\",\"type\":\"svn\",\"url\":\"https://internal.nathanmediaservices.co.uk/svn/ceefax/Yorks&Lincs\",\"path\":\"CeefaxYorksAndLincs\"}\n        ]},\n        {\"name\":\"Chunkytext\",\"type\":\"git\",\"url\":\"https://zxnet.co.uk/git/cf.git\",\"path\":\"Chunkytext\"},\n        {\"name\":\"Nemetext\",\"type\":\"git\",\"url\":\"https://github.com/JamieNemeth/nemetext.git\",\"path\":\"Nemetext\"},\n        {\"name\":\"SPARK\",\"type\":\"git\",\"url\":\"https://github.com/ZXGuesser/spark-teletext.git\",\"path\":\"SPARK\"},\n        {\"name\":\"Teefax\",\"type\":\"svn\",\"url\":\"http://teastop.plus.com/svn/teletext/\",\"path\":\"Teefax\"},\n        {\"name\":\"Webfax\",\"type\":\"group\",\"services\":[\n            {\"name\":\"Webfax 1\",\"type\":\"git\",\"url\":\"https://github.com/Webfax-Teletext/Webfax-Teletext.git\",\"path\":\"Webfax1\"},\n            {\"name\":\"Webfax 2\",\"type\":\"git\",\"url\":\"https://github.com/Webfax-Teletext/Webfax2-Teletext.git\",\"path\":\"Webfax2\"}\n        ]}\n    ]\n}\n"
  },
  {
    "path": "scripts/runvbit2.py",
    "content": "#!/usr/bin/env python3\n# run vbit2 using configuration from config.json\n\nimport config\nimport os\nimport subprocess\nimport signal\n\ndef signalHandler(_signo, _stack_frame):\n    # send TERM signal to vbit2 process\n    vbit.terminate()\n\nconfigData = config.load()\n\nservice = config.getSelectedService()\n\nif not service:\n    print(\"No service selected\")\n    quit()\n\nlinesPerField = 16 # vbit2 defaults to 16 lpf\n\n# try to get lines_per_field setting from config files\nfor conffile in [\"vbit.conf\", \"vbit.conf.override\"]:\n    confpath = os.path.join(service[\"path\"], conffile)\n    if os.path.exists(confpath):\n        with open(confpath, \"r\") as conf:\n            for line in conf:\n                if line.startswith(\"lines_per_field=\"):\n                    try:\n                        linesPerField = int(line[16:])\n                        if linesPerField < 1:\n                            raise ValueError\n                    except ValueError:\n                        print(\"invalid lines_per_field in \"+conffile)\n\n# could check for overrides in the json config here too if vbit2 gains a command argument to override lines per field\n\ncmdline = [os.path.join(os.getenv('HOME'), \".local/bin/vbit2\"), \"--dir\", service[\"path\"]]\nprerun = []\npostrun = []\n\noutput = configData[\"settings\"].get(\"output\")\nif output == \"none\":\n    # disables piped output!\n    cmdline.append(\"--format\")\n    cmdline.append(\"none\")\nelif output == \"raspi-teletext\":\n    prerun = [\"sudo\", os.path.join(os.getenv('HOME'), \"raspi-teletext/tvctl\"), \"on\"]\n    postrun = [\"sudo\", os.path.join(os.getenv('HOME'), \"raspi-teletext/tvctl\"), \"off\"]\n    \n    if linesPerField > 16:\n        print(\"full field operation not currently supported\")\n        quit()\n    \n    mask = 0xffff\n    for i in range(linesPerField):\n        mask = (mask << 1) & 0xffff\n    \n    destproc = [os.path.join(os.getenv('HOME'), \"raspi-teletext/teletext\"), \"-m\", \"0x{:04x}\".format(mask), \"-l\", \"66\", \"-\"]\n\nif prerun:\n    subprocess.run(prerun)\n\npacketServerPort = configData[\"settings\"].get(\"packetServerPort\")\nif configData[\"settings\"].get(\"packetServer\") and packetServerPort:\n    cmdline.append(\"--packetserver\")\n    cmdline.append(str(packetServerPort))\n\ninterfaceServerPort = configData[\"settings\"].get(\"interfaceServerPort\")\nif configData[\"settings\"].get(\"interfaceServer\") and interfaceServerPort:\n    cmdline.append(\"--interface\")\n    cmdline.append(str(interfaceServerPort))\n\nvbit = subprocess.Popen(cmdline, stdout=subprocess.PIPE)\n\nsignal.signal(signal.SIGTERM, signalHandler)\nsignal.signal(signal.SIGINT, signalHandler)\n\nif not output == \"none\":\n    subprocess.Popen(destproc, stdin=vbit.stdout)\n\nvbit.wait()\n\nif postrun:\n    subprocess.run(postrun)\n"
  },
  {
    "path": "scripts/teletext-update.py",
    "content": "#!/usr/bin/env python3\n# Update the active service and any ancillary sub-services\n\nimport config\nimport os\nimport subprocess\n\ntry:\n    service = config.getSelectedService()\nexcept Exception as e:\n    print(e)\n    quit()\n\ndef updateService(service):\n    # update the service\n    if service[\"type\"] == \"svn\":\n        process = subprocess.run([\"svn\", \"up\", service[\"path\"]], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)\n        \n        if process.returncode:\n            print(\"svn update failed with: \"+process.stdout.decode(\"utf8\"))\n        \n        process = subprocess.run([\"svn\", \"cleanup\", service[\"path\"]], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)\n        if process.returncode:\n            print(\"svn cleanup failed with: \"+process.stdout.decode(\"utf8\"))\n        \n        \n    elif service[\"type\"] == \"git\":\n        process = subprocess.run([\"git\", \"-C\", service[\"path\"], \"pull\"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)\n        \n        if process.returncode:\n            print(\"git pull failed with: \"+process.stdout.decode(\"utf8\"))\n    \n    subservices = service.get(\"subservices\")\n    if subservices:\n        for subservice in subservices:\n            updateService(subservice) # recurse into subservices\n\nupdateService(service)\n\n"
  },
  {
    "path": "scripts/vbit-config.py",
    "content": "#!/usr/bin/env python3\n\nimport config\nimport json\nimport sys\nimport os\nimport shutil\nimport subprocess\nimport re\nfrom dialog import Dialog\nfrom dialog import DialogBackendVersion\n\n# get absolute path of vbit2 scripts directory\nSCRIPTDIR = os.path.dirname(os.path.realpath(__file__))\n\n# absolute path of known services file\nKNOWNSERVICES = os.path.join(SCRIPTDIR,\"known_services.json\")\n\n# absolute path of installed services\nSERVICESDIR = os.path.join(os.getenv('HOME'), \".teletext-services\")\n\nd = Dialog(dialog=\"dialog\", autowidgetsize=True)\nd.set_background_title(\"VBIT2 Config\")\n\ndef selectServiceMenu():\n    choices = []\n    i = 0\n    \n    for service in config.getInstalledServices():\n        choices += [(str(i), service[\"name\"])]\n        i += 1\n    \n    code, tag = d.menu(\"Choose service to generate\",choices=choices,title=\"Select\",cancel_label=\"Back\")\n    \n    if code == \"ok\":\n        config.selectService(dict(choices)[tag])\n\ndef installServiceMenu(servicesData, isGroup=False):\n    installable = []\n    choices = []\n    i = 0\n    for service in sorted(servicesData,key=lambda x: x[\"name\"]):\n        installable += [service]\n        choices += [(str(i), service[\"name\"])]\n        i += 1\n    \n    if not isGroup:\n        choices += [(\"C\", \"Custom service\")]\n    \n    code, tag = d.menu(\"Choose service to install\",choices=choices,title=\"Install\",cancel_label=\"Back\")\n    \n    if code == \"ok\":\n        if tag == \"C\":\n            # custom service\n            customServiceMenu()\n        \n        else:\n            service = installable[int(tag)]\n            if service[\"type\"] == \"group\":\n                # iterate into subgroup with new menu\n                installServiceMenu(service[\"services\"], True)\n            else:\n                name = service.get(\"name\")\n                path = service.get(\"path\")\n                \n                # check if already installed\n                installedNames = []\n                for s in config.getInstalledServices():\n                    installedNames += [s[\"name\"]]\n                if service[\"name\"] in installedNames:\n                    n = 2\n                    while service[\"name\"]+\"-\"+str(n) in installedNames:\n                        n += 1\n                    \n                    name += \"-\"+str(n)\n                    path += \"-\"+str(n)\n                    \n                    text = \"Service is already installed.\\nInstall anyway as \"+name+\"?\\n\"\n                    code = d.yesno(text)\n                    if code != \"ok\":\n                        return\n                \n                serviceConfigObject = {\"name\":name, \"type\":service.get(\"type\"), \"path\":os.path.join(SERVICESDIR,path), \"url\":service.get(\"url\")}\n                \n                if service.get(\"subservices\"):\n                    selectedSubservices = chooseSubservicesMenu(service[\"subservices\"])\n                    if selectedSubservices:\n                        serviceConfigObject[\"subservices\"] = selectedSubservices\n                \n                try:\n                    # selected a name so add service to config\n                    d.infobox(\"Installing. Please wait...\")\n                    config.installService(serviceConfigObject)\n                except Exception as e:\n                    d.msgbox(\"Installation failed: {0}\".format(e), cancel_label=\"Back\")\n\ndef chooseSubservicesMenu(subservices):\n    selectedSubservices = []\n    optionalSubservices = []\n    choices = []\n    i = 0\n    for subservice in subservices:\n        if subservice.get(\"required\"):\n            selectedSubservices += [subservice]\n        else:\n            # optional ancillary services\n            optionalSubservices += [subservice]\n            choices += [(str(i), subservice[\"name\"], \"off\",)]\n            i += 1\n    \n    if choices:\n        choices = [(\"none\", \"None\", \"on\",)] + choices\n        code, tag = d.radiolist(\"\",choices=choices, title=\"Select optional sub-service to install\", no_tags=True, no_cancel=True)\n        \n        if code == \"ok\" and tag != \"none\":\n            selectedSubservices += [optionalSubservices[int(tag)]]\n    \n    return selectedSubservices\n\ndef customServiceMenu():\n    choices = [(\"S\", \"Subversion repository\"),(\"G\", \"Git repository\"),(\"D\", \"Directory\")]\n    \n    code, tag = d.menu(\"Select service type\",choices=choices,title=\"Install custom service\",cancel_label=\"Back\")\n    \n    if code == \"ok\":\n        if tag == \"D\":\n            # local directory\n            if DialogBackendVersion.fromstring(d.backend_version()) < DialogBackendVersion.fromstring(\"1.3-20201126\"):\n                h = 10\n            else:\n                h = 20\n\n            while True: # select directory loop\n                code, string = d.dselect(os.getenv('HOME')+\"/\", title=\"Enter teletext service directory:\", height=h)\n                if code == \"ok\":\n                    path = os.path.normpath(string)\n                    code = d.yesno(\"Selected directory \"+string, yes_label=\"OK\", no_label=\"Back\")\n                    if code == \"ok\":\n                        # confirmed directory\n                        while True: # name input loop\n                            code, string = d.inputbox(\"Enter service name:\", cancel_label=\"Back\")\n                            if code == \"ok\":\n                                try:\n                                    # selected a name so add service to config\n                                    config.installService({\"name\":string, \"type\":\"dir\", \"path\":path})\n                                except Exception as e:\n                                    d.msgbox(\"Installation failed: {0}\".format(e), cancel_label=\"Back\")\n                                break\n                        break\n                else:\n                    break # aborted so break out of select directory loop\n        \n        else:\n            # remote service\n            if tag == \"S\":\n                # subversion\n                type = \"svn\"\n                \n            if tag == \"G\":\n                # git\n                type = \"git\"\n            \n            code, string = d.inputbox(\"Enter repository URL:\")\n            \n            if code == \"ok\":\n                url = string\n                \n                while True: # name input loop\n                    code, string = d.inputbox(\"Enter service name:\")\n                    if code == \"ok\":\n                        # strip whitespace and illegal characters\n                        name = re.sub('(^\\s*)|([./\\\\\\\"\\'])|(\\s*$)', '', string)\n                        \n                        installedNames = []\n                        for s in config.getInstalledServices():\n                            installedNames += [s[\"name\"]]\n                            \n                        if name in installedNames:\n                            d.msgbox(\"Service name already in use\", cancel_label=\"Back\")\n                            \n                        else:\n                            if not os.path.exists(os.path.join(SERVICESDIR, \"custom_services\")):\n                                os.makedirs(os.path.join(SERVICESDIR, \"custom_services\"))\n                            \n                            fullpath = os.path.join(SERVICESDIR, \"custom_services\", name) # create a path from the service name\n                            \n                            try:\n                                # selected a name so add service to config\n                                d.infobox(\"Installing. Please wait...\")\n                                config.installService({\"name\":name, \"type\":type, \"path\":fullpath, \"url\":url})\n                            except Exception as e:\n                                d.msgbox(\"Installation failed: {0}\".format(e), cancel_label=\"Back\")\n                            break\n                    else:\n                        break\n\ndef uninstallService():\n    choices = []\n    i = 0\n    \n    for service in config.getInstalledServices():\n        choices += [(str(i), service[\"name\"])]\n        i += 1\n    \n    code, tag = d.menu(\"Choose service to uninstall\",choices=choices,title=\"Uninstall\",cancel_label=\"Back\")\n    \n    if code == \"ok\":\n        name = dict(choices)[tag]\n        text = \"Are you sure you want to remove this service:\\n \"+name\n        code = d.yesno(text)\n        \n        if code == \"ok\":\n            d.infobox(\"Uninstalling. Please wait...\")\n            \n            config.uninstallService(name)\ndef optionsMenu():\n    configData = config.load()\n    updateEnabled = subprocess.run([\"systemctl\", \"--user\", \"is-enabled\", \"teletext-update.timer\"], capture_output=True, text=True).stdout == \"enabled\\n\"\n    bootEnabled = subprocess.run([\"systemctl\", \"--user\", \"is-enabled\", \"vbit2.service\"], capture_output=True, text=True).stdout == \"enabled\\n\"\n    serverEnabled = configData[\"settings\"].get(\"packetServer\")\n    interfaceEnabled = configData[\"settings\"].get(\"interfaceServer\")\n    \n    options = [(\"U\", \"Automatically update selected service\")]\n    \n    if updateEnabled:\n        options[0] += (\"on\",)\n    else:\n        options[0] += (\"off\",)\n    \n    options += [(\"B\", \"Run VBIT2 automatically at boot\")]\n    \n    if bootEnabled:\n        options[1] += (\"on\",)\n    else:\n        options[1] += (\"off\",)\n    \n    options += [(\"S\", \"Enable teletext packet server\")]\n    \n    if serverEnabled:\n        options[2] += (\"on\",)\n    else:\n        options[2] += (\"off\",)\n    \n    options += [(\"I\", \"Enable control interface server\")]\n    \n    if interfaceEnabled:\n        options[3] += (\"on\",)\n    else:\n        options[3] += (\"off\",)\n    \n    code, tags = d.checklist(\"\",choices=options, title=\"Options\", no_tags=True, no_cancel=True)\n\n    if code == \"ok\":\n        if not \"U\" in tags and updateEnabled:\n            # update was enabled, now clear\n            subprocess.run([\"systemctl\", \"--user\", \"disable\", \"teletext-update.timer\", \"--now\"], stderr=subprocess.DEVNULL)\n        if \"U\" in tags and not updateEnabled:\n            # update was clear, now enabled\n            subprocess.run([\"systemctl\", \"--user\", \"enable\", \"teletext-update.timer\", \"--now\"], stderr=subprocess.DEVNULL)\n        if not \"B\" in tags and bootEnabled:\n            # run at boot was enabled, now clear\n            subprocess.run([\"systemctl\", \"--user\", \"disable\", \"vbit2.service\"], stderr=subprocess.DEVNULL)\n        if \"B\" in tags and not bootEnabled:\n            # run at boot was clear, now enabled\n            subprocess.run([\"systemctl\", \"--user\", \"enable\", \"vbit2.service\"], stderr=subprocess.DEVNULL)\n        if not \"S\" in tags and serverEnabled:\n            # server was enabled, now clear\n            configData[\"settings\"][\"packetServer\"] = False\n            config.save(configData)\n        if \"S\" in tags and not serverEnabled:\n            # server was clear, now enabled\n            configData[\"settings\"][\"packetServer\"] = True\n            config.save(configData)\n        if not \"I\" in tags and interfaceEnabled:\n            # server was enabled, now clear\n            configData[\"settings\"][\"interfaceServer\"] = False\n            config.save(configData)\n        if \"I\" in tags and not interfaceEnabled:\n            # server was clear, now enabled\n            configData[\"settings\"][\"interfaceServer\"] = True\n            config.save(configData)\n        \n\ndef mainMenu():\n    while True:\n        options = [(\"I\",\"Install service\")]\n        \n        if config.getInstalledServices():\n            options += [(\"S\",\"Select service\"), (\"R\",\"Remove service\"), (\"O\",\"Options\"), (\"U\",\"Update VBIT2\")]\n        \n            if subprocess.run([\"systemctl\", \"--user\", \"is-active\", \"vbit2.service\"], capture_output=True, text=True).stdout == \"active\\n\":\n                options += [(\"V\",\"Stop VBIT2\")]\n                command = \"stop\"\n            elif config.getSelectedService():\n                options += [(\"V\",\"Start VBIT2\")]\n                command = \"start\"\n        \n        code, tag = d.menu(\"\",choices=options,title=\"Main menu\",cancel_label=\"Exit\")\n        \n        if code == \"ok\":\n            if tag == \"S\":\n                selectServiceMenu()\n            elif tag == \"I\":\n                # reload known services\n                with open(KNOWNSERVICES) as servicesFile:\n                    data = json.load(servicesFile)\n                    servicesData = data[\"services\"]\n                \n                for service in servicesData:\n                    if not service.get(\"name\") or not service.get(\"type\"):\n                        d.msgbox(\"Fatal error: invalid service configuration data in {0}\\n{1}\".format(KNOWNSERVICES, service), cancel_label=\"Back\")\n                installServiceMenu(servicesData)\n            elif tag == \"R\":\n                uninstallService()\n            elif tag == \"O\":\n                optionsMenu()\n            elif tag == \"V\":\n                subprocess.run([\"systemctl\", \"--user\", command, \"vbit2.service\"])\n            elif tag == \"U\":\n                os.system('clear')\n                subprocess.run(os.path.join(SCRIPTDIR,\"../update.sh\")) # run the update script\n                break # exit vbit-config\n        \n        else:\n            break\n\nif __name__ == \"__main__\":\n    mainMenu()\n    os.system('clear')\n"
  },
  {
    "path": "service.cpp",
    "content": "/** Service\n */\n#include \"service.h\"\n\nusing namespace vbit;\n\nService::Service(Configure *configure, Debug *debug, PageList *pageList, PacketServer *packetServer, InterfaceServer *interfaceServer) :\n    _configure(configure),\n    _debug(debug),\n    _pageList(pageList),\n    _packetServer(packetServer),\n    _interfaceServer(interfaceServer),\n    _fieldCounter(49) // roll over immediately\n{\n    _magList=_pageList->GetMagazines();\n    // Register all the magazine packet sources\n    for (uint8_t mag=0;mag<8;mag++)\n    {\n        PacketMag* m=_magList[mag];\n        m->SetPriority(_configure->GetMagazinePriority(mag)); // set the mags to the desired priorities\n        _register(&_magazineSources, m); // use the PacketMags created in pageList rather than duplicating them\n    }\n    \n    // register datacast sources\n    _register(&_datacastSources, _packetDebug = new PacketDebug(_configure, _debug));\n    \n    PacketDatacast** channels = _interfaceServer->GetDatachannels();\n    for (int dc=1; dc<16; dc++)\n    {\n        _register(&_datacastSources, channels[dc]);\n    }\n    \n    // don't register BSDP source\n    _packet830 = new Packet830(_configure);\n    \n    _linesPerField = _configure->GetLinesPerField();\n    _datacastLines = _configure->GetDatacastLines();\n    \n    _lineCounter = _linesPerField - 1; // roll over immediately\n    \n    _OutputFormat = _configure->GetOutputFormat();\n    _PTS = 0;\n    _PTSFlag = true;\n    _PID = _configure->GetTSPID();\n    _tscontinuity = 0;\n}\n\nService::~Service()\n{\n}\n\nvoid Service::_register(std::list<PacketSource*> *list, PacketSource *src)\n{\n    list->push_front(src);\n}\n\nint Service::run()\n{\n    _debug->Log(Debug::LogLevels::logDEBUG,\"[Service::run] This is the worker process\");\n    \n    std::list<PacketSource*>::const_iterator magIterator=_magazineSources.begin(); // Iterator for magazine packet sources\n    std::list<PacketSource*>::const_iterator dcIterator=_datacastSources.begin(); // Iterator for datacast sources\n\n    Packet* pkt=new Packet(8,25);  // This just allocates storage.\n\n    static Packet* filler=new Packet(8,25);  // A pre-prepared quiet packet to avoid eating the heap\n\n    _debug->Log(Debug::LogLevels::logINFO,\"[Service::run] Lines per field: \" + std::to_string((int)_linesPerField));\n    _debug->Log(Debug::LogLevels::logINFO,\"[Service::run] Dedicated datacast lines: \" + std::to_string((int)_datacastLines));\n    while(1)\n    {\n        // Send ONLY one packet per loop\n        _updateEvents();\n        \n        // special case for BSDP. Ensures it will always get a vbi line\n        if (_packet830->IsReady())\n        {\n            if (_packet830->GetPacket(pkt) != nullptr)\n            {\n                _packetOutput(pkt);\n            }\n            else\n            {\n                _packetOutput(filler);\n            }\n        }\n        // Special case for debug. Ensures it can steal lines from other sources during DATABROADCAST event\n        else if (_packetDebug->IsReady(_debug->GetDebugLevel() == Debug::LogLevels::logDEBUG)) // force if log level is DEBUG\n        {\n            _packetDebug->GetPacket(pkt);\n            _packetOutput(pkt);\n        }\n        else\n        {\n            // Iterate through the packet sources until we get a packet to transmit\n            PacketSource* p=nullptr;\n            \n            if (_datacastLines)\n            {\n                // try datacast sources first\n                for (unsigned int count = 0; count < _datacastSources.size()-1; count++) // iterate over sources once\n                {\n                    // Get the packet source\n                    p=(*dcIterator);\n                    \n                    if (++dcIterator==--_datacastSources.end()) // loop skipping last source (_packetDebug)\n                    {\n                        dcIterator=_datacastSources.begin();\n                    }\n                    \n                    if (p->IsReady(count==_datacastSources.size()-2)) // force if the last datacast source\n                        break; // a datacast buffer has packets to go\n                    else\n                        p=nullptr;\n                }\n                if (p)\n                {\n                    p->GetPacket(pkt);\n                    _packetOutput(pkt);\n                    continue; // main while loop\n                }\n                // else fall through to magazine sources\n            }\n            \n            // now try magazine sources\n            uint8_t sourceCount=0;\n            uint8_t listSize=_magazineSources.size();\n            bool force=false;\n            do\n            {\n                // Loop back to the first source\n                if (magIterator==_magazineSources.end())\n                {\n                    magIterator=_magazineSources.begin();\n                }\n\n                // If we have tried all sources with and without force, then break out with a filler to prevent a deadlock\n                if (sourceCount>listSize*2)\n                {\n                    p=nullptr;\n                    // If we get a lot of this maybe there is a problem?\n                    break;\n                }\n\n                // If we have gone around once and got nothing, then force sources to go if possible.\n                if (sourceCount>listSize)\n                {\n                    force=true;\n                }\n\n                // Get the packet source\n                p=(*magIterator);\n                ++magIterator;\n\n                sourceCount++; // Count how many sources we tried.\n            }\n            while (!p->IsReady(force));\n            \n            // Did we find a packet?\n            if (p)\n            {\n                // GetPacket returns nullptr if the pkt isn't valid - if it's null go round again.\n                if (p->GetPacket(pkt) != nullptr)\n                {\n                    _packetOutput(pkt);\n                    continue; // main while loop\n                }\n                // else fall through to filler\n            }\n            \n            if (!_datacastLines)\n            {\n                // output datacast in place of filler\n                for (unsigned int count = 0; count < _datacastSources.size()-1; count++) // iterate over sources once\n                {\n                    // Get the packet source\n                    p=(*dcIterator);\n                    \n                    if (++dcIterator==--_datacastSources.end()) // loop skipping last source (_packetDebug)\n                    {\n                        dcIterator=_datacastSources.begin();\n                    }\n                    \n                    if (p->IsReady())\n                    {\n                        p->GetPacket(pkt);\n                        break;\n                    }\n                    else\n                        p=nullptr;\n                }\n                \n                if (p)\n                {\n                    _packetOutput(pkt);\n                    continue; // main while loop\n                }\n                // else fall through to filler\n            }\n            \n            _packetOutput(filler);\n        }\n\n    } // while forever\n    return 99; // can't return but this keeps the compiler happy\n} // worker\n\nvoid Service::_updateEvents()\n{\n    MasterClock *mc = mc->Instance();\n    MasterClock::timeStruct masterClock = mc->GetMasterClock();\n    \n    // Step the counters\n    _lineCounter = (_lineCounter + 1) % _linesPerField;\n    \n    auto t1 = std::chrono::system_clock::now();\n    auto duration = t1.time_since_epoch();\n    int64_t fields = std::chrono::duration_cast<std::chrono::milliseconds>(duration).count() / 20;\n    \n    time_t now = fields / 50;\n    \n    if (((int64_t)masterClock.seconds * 50 + masterClock.fields) > fields)\n        std::this_thread::sleep_for(std::chrono::milliseconds(40)); // back off for ≈2 fields to limit output to (less than) 50 fields per second\n    \n    if (_lineCounter == 0) // new field\n    {\n        _fieldCounter = (_fieldCounter + 1) % 50;\n        \n        if (_fieldCounter == 0)\n        {\n            masterClock.seconds++; // step the master clock before updating debug packet\n        }\n        \n        masterClock.fields = _fieldCounter;\n        \n        _packetDebug->TimeAndField(masterClock, now, fields%50, false); // update the clocks in debugPacket.\n        \n        if (_fieldCounter == 0)\n        {\n            // if internal master clock is behind real time, or more than 1 second ahead, resynchronise it.\n            if (masterClock.seconds < now || masterClock.seconds > now + 1)\n            {\n                masterClock.seconds = now;\n                \n                _debug->Log(Debug::LogLevels::logWARN,\"[Service::_updateEvents] Resynchronising master clock\");\n                \n                for (int i=0;i<8;i++)\n                    _magList[i]->InvalidateCycleTimestamp(); // reset magazine cycle duration calculations\n                \n                _packetDebug->TimeAndField(masterClock, now, fields%50, true); // update the clocks in debugPacket.\n            }\n            \n            if (masterClock.seconds%15==0) // TODO: how often do we want to trigger sending special packets?\n            {\n                for (std::list<PacketSource*>::const_iterator iterator = _magazineSources.begin(), end = _magazineSources.end(); iterator != end; ++iterator)\n                {\n                    (*iterator)->SetEvent(EVENT_SPECIAL_PAGES);\n                    (*iterator)->SetEvent(EVENT_PACKET_29);\n                }\n            }\n        }\n        \n        mc->SetMasterClock(masterClock); // update the master clock singleton\n        \n        // New field, so set the FIELD event in all the registered magazine sources.\n        for (std::list<PacketSource*>::const_iterator iterator = _magazineSources.begin(), end = _magazineSources.end(); iterator != end; ++iterator)\n        {\n            (*iterator)->SetEvent(EVENT_FIELD);\n            \n            if (_fieldCounter%50==0)\n                (*iterator)->SetEvent(EVENT_P830_FORMAT_1); // let magazines see the second tick\n        }\n        \n        _packetDebug->SetEvent(EVENT_FIELD);\n        \n        if (_fieldCounter%10==0) // Packet 830 happens every 200ms.\n        {\n            Event ev=EVENT_P830_FORMAT_1;\n            switch (_fieldCounter/10)\n            {\n                case 0:\n                {\n                    ev=EVENT_P830_FORMAT_1;\n                    break;\n                }\n                case 1:\n                {\n                    ev=EVENT_P830_FORMAT_2_LABEL_0;\n                    break;\n                }\n                case 2:\n                {\n                    ev=EVENT_P830_FORMAT_2_LABEL_1;\n                    break;\n                }\n                case 3:\n                {\n                    ev=EVENT_P830_FORMAT_2_LABEL_2;\n                    break;\n                }\n                case 4:\n                {\n                    ev=EVENT_P830_FORMAT_2_LABEL_3;\n                    break;\n                }\n            }\n            _packet830->SetEvent(ev); // only BSDP source needs to know about these events\n        }\n    }\n    \n    \n    for (std::list<PacketSource*>::const_iterator iterator = _datacastSources.begin(), end = _datacastSources.end(); iterator != end; ++iterator)\n    {\n        // if no dedicated datacast lines are assigned, allow datacast on all lines\n        if ((_datacastLines == 0) || (_lineCounter >= _linesPerField - _datacastLines))\n        {\n            (*iterator)->SetEvent(EVENT_DATABROADCAST);\n        }\n        else\n        {\n            (*iterator)->ClearEvent(EVENT_DATABROADCAST);\n        }\n    }\n}\n\nvoid Service::_packetOutput(Packet* pkt)\n{\n    std::array<uint8_t, PACKETSIZE> *p = pkt->tx();\n    \n    switch (_OutputFormat)\n    {\n        case Configure::OutputFormat::None:\n        {\n            /* disable stdout */\n            break;\n        }\n        \n        case Configure::OutputFormat::T42:\n        {\n            /* t42 output */\n            if (_configure->GetReverseFlag())\n            {\n                static std::array<uint8_t, PACKETSIZE> tmp;\n                for (unsigned int i=0;i<(p->size());i++)\n                {\n                    tmp[i]=ReverseByteTab[p->at(i)];\n                }\n                p = &tmp;\n            }\n            \n            std::cout.write((char*)p->data()+3, 42); // have to cast the pointer to char for cout.write()\n            \n            break;\n        }\n        \n        case Configure::OutputFormat::Raw:\n        {\n            /* full 45 byte teletext packets */\n            std::cout.write((char*)p->data(), 45); // have to cast the pointer to char for cout.write()\n            \n            break;\n        }\n        \n        case Configure::OutputFormat::TSNPTS:\n            _PTSFlag = false; // Don't generate PCR and PTS in output stream\n            /* fallthrough */\n            [[gnu::fallthrough]];\n        case Configure::OutputFormat::TS:\n        {\n            /* MPEG-2 transport stream holding a DVB-TXT Packetized Elementary Stream */\n            \n            if (_lineCounter == 0 && !(_fieldCounter&1))\n            {\n                // a new frame has started - transmit data for previous frame if there is any\n                if (!(_PESBuffer.empty()))\n                {\n                    std::array<uint8_t, 46> padding;\n                    padding.fill(0xff);\n                    \n                    std::array<uint8_t, 188> ts;\n                    ts.fill(0xff);\n                    \n                    ts[0] = 0x47;\n                    ts[1] = (uint8_t)(_PID >> 8);\n                    ts[2] = (uint8_t)(_PID & 0xFF);\n                    \n                    if (_PTSFlag){\n                        ts[3] = 0x20 | _tscontinuity; // adaption field no payload\n                        ts[4] = 0x07; // 7 bytes in adaption field\n                        ts[5] = 0x10; // PCR flag\n                        // make PCR from our PTS\n                        ts[6] = _PTS >> 25;\n                        ts[7] = (_PTS >> 17) & 0xFF;\n                        ts[8] = (_PTS >> 9) & 0xFF;\n                        ts[9] = (_PTS >> 1) & 0xFF;\n                        ts[10] = (_PTS & 1) << 7;\n                        ts[11] = 0x00;\n                        std::cout.write((char*)ts.data(), 188); // write out transport stream packet\n                    }\n                    \n                    std::vector<uint8_t> header = {0x00, 0x00, 0x01, 0xBD}; // PES start code\n                    \n                    int numBlocks = _PESBuffer.size() + 1; // header and N lines\n                    int numTSPackets = ((numBlocks * 46) + 183) / 184; // round up\n                    int packetLength = (numTSPackets * 184) - 6;\n                    \n                    header.push_back(packetLength >> 8);\n                    header.push_back(packetLength & 0xff);\n                    \n                    /* bits | 7 | 6 |  5   | 4   |     3    |     2     |     1     |     0    |\n                            | 1 | 0 | Scrambling | Priority | Alignment | Copyright | Original | */\n                    header.push_back(0x85); // Align, Original\n                    \n                    /* bits |  7 | 6  |   5  |    4    |     3     |     2     |    1    |       0       |\n                            | PTS DTS | ESCR | ES rate | DSM trick | copy info | PES CRC | PES extension |*/\n                    header.push_back(_PTSFlag?0x80:0x00); // if _PTSFlag, PTS no DTS follows\n                    \n                    header.push_back(0x24); // PES header data length\n                    \n                    if (_PTSFlag)\n                    {\n                        // append PTS\n                        header.push_back(0x21 | ((_PTS & 0x1C0000000) >> 29));\n                        header.push_back((_PTS & 0x3FC00000) >> 22);\n                        header.push_back(0x01 | ((_PTS & 0x3F8000) >> 14));\n                        header.push_back((_PTS & 0x7F80) >> 7);\n                        header.push_back(0x01 | ((_PTS & 0x7F) << 1));\n                        \n                        _PTS += 3600;\n                        if (_PTS >= 0x200000000)\n                            _PTS = 0;\n                    }\n                    \n                    header.resize(0x2D, 0xff); // make PES header up to 45 bytes long with stuffing bytes.\n                    \n                    header.push_back(0x10); // append PES data identifier (EBU data)\n                    \n                    ts[1] = (uint8_t)((_PID >> 8) | 0x40);\n                    \n                    _tscontinuity = (_tscontinuity+1)&0xf;\n                    ts[3] = 0x10 | _tscontinuity; // no adaption field payload only\n                    \n                    std::cout.write((char*)ts.data(), 4); // transport stream header\n                    \n                    std::cout.write((char*)header.data(), header.size()); // output PES header and data_identifier\n                    \n                    for (unsigned int i = 0; i < _PESBuffer.size(); i++)\n                    {\n                        if (((i % 184) % 4) == 3) // new ts packet\n                        {\n                            ts[1] = (uint8_t)(_PID >> 8);\n                            _tscontinuity = (_tscontinuity+1)&0xf;\n                            ts[3] = 0x10 | _tscontinuity;\n                            std::cout.write((char*)ts.data(), 4); // transport stream header\n                        }\n                        std::cout.write((char*)_PESBuffer[i].data(), 46);\n                    }\n                    \n                    for (int i = numBlocks; i < numTSPackets * 4; i++)\n                    {\n                        std::cout.write((char*)padding.data(), 46); // pad out remainder of PES packet\n                    }\n                    \n                    _PESBuffer.clear(); // empty buffer ready for next field's packets\n                }\n            }\n            \n            std::vector<uint8_t> data = {0x02, 0x2c}; // data_unit_id and data_unit_length (EBU teletext non-subtitle, 44 bytes)\n            \n            if (_lineCounter > 15)\n            {\n                data.push_back(((_fieldCounter&1)^1) << 5); //field parity, line number undefined\n            }\n            else\n            {\n                data.push_back((((_fieldCounter&1)^1) << 5) | (_lineCounter + 7)); // field parity and line number\n            }\n            \n            for (int i = 2; i < 45; i++)\n            {\n                data.push_back(ReverseByteTab[p->at(i)]); // bits are reversed in PES stream\n            }\n            \n            _PESBuffer.push_back(data);\n            \n            break;\n        }\n    }\n    \n    if (_packetServer->GetIsActive())\n    {\n        // packet server needs feeding\n        \n        if (_lineCounter == 0 && !(_fieldCounter&1))\n        {\n            // a new field has started \n            \n            _packetServer->SendField(_FrameBuffer);\n            \n            _FrameBuffer.clear(); // empty buffer ready for next frame's packets\n        }\n        \n        /* internal format only: 45 bytes in the form field count, line high byte, line low byte, 42 payload bytes */\n        std::vector<uint8_t> data = {_fieldCounter, (uint8_t)(_lineCounter >> 8), (uint8_t)(_lineCounter & 0xFF)};\n        \n        for (int i = 3; i < 45; i++)\n        {\n            data.push_back(p->at(i));\n        }\n        \n        _FrameBuffer.push_back(data);\n    }\n}\n"
  },
  {
    "path": "service.h",
    "content": "#ifndef _SERVICE_H_\n#define _SERVICE_H_\n\n#include <iostream>\n#include <iomanip>\n#include <thread>\n#include <ctime>\n#include <list>\n\n#include \"configure.h\"\n#include \"debug.h\"\n#include \"pagelist.h\"\n#include \"packetServer.h\"\n#include \"interfaceServer.h\"\n#include \"packet.h\"\n#include \"packetsource.h\"\n#include \"packetmag.h\"\n#include \"packet830.h\"\n#include \"packetDebug.h\"\n#include \"masterClock.h\"\n\nnamespace vbit\n{\n    /** A Service creates a teletext stream from packet sources.\n     *  Packet sources are magazines, Packet 8/30, databroadcast, and debugging.\n     *  Service:\n     *    Instances the packet sources\n     *    Sends them timing events (cues for field timing etc.)\n     *    Polls the packet sources for packets to send\n     *    Sends the packets.\n     */\n    class Service\n    {\n        public:\n            /**\n             * @param configure A Configure object with all the settings\n             * @param pageList A pageList object already loaded with pages\n             */\n            Service(Configure* configure, Debug* debug, PageList* pageList, PacketServer* packetServer, InterfaceServer *interfaceServer);\n            \n            ~Service();\n            \n            /**\n             * Creates a worker thread and does not terminate (at least for now)\n             * @return Nothing useful yet. Perhaps return an error status if something goes wrong\n             */\n            int run();\n\n        private:\n            // Member variables that define the service\n            Configure* _configure; /// Member reference to the configuration settings\n            Debug* _debug;\n            PageList* _pageList; /// Member reference to the pages list\n            PacketMag** _magList;\n            PacketServer* _packetServer;\n            InterfaceServer* _interfaceServer;\n\n            // Member variables for event management\n            uint16_t _linesPerField;\n            uint16_t _datacastLines;\n            uint16_t _lineCounter; // Which VBI line are we on? Used to signal a new field.\n            uint8_t _fieldCounter; // Which field? Used to time packet 8/30\n            \n            uint64_t _PTS; // presentation timestamp counter\n            bool _PTSFlag; // generate PCR and PTS\n            \n            std::list<PacketSource*> _magazineSources; // A list of packet sources for magazine data\n            std::list<PacketSource*> _datacastSources; // A list of sources for independent data line packets\n\n            Packet830* _packet830; // BSDP packet source\n            PacketDebug* _packetDebug; // Debug packet source\n\n            // Member functions\n            void _register(std::list<PacketSource*> *list, PacketSource *src); // Register packet sources\n\n            /**\n             * @brief Check if anything changed, and if so signal the event to the packet sources.\n             * Must be called once per transmitted row so that it can maintain a field count\n             */\n            void _updateEvents();\n            \n            /* output a packet in the desired format */\n            void _packetOutput(Packet* pkt);\n            \n            /* queue up packets for outputting as a Packetised Elementary Stream */\n            std::vector<std::vector<uint8_t>> _PESBuffer;\n            \n            Configure::OutputFormat _OutputFormat;\n            uint16_t _PID;\n            uint8_t _tscontinuity;\n            \n            /* queue up a frame of packets for the packet server */\n            std::vector<std::vector<uint8_t>> _FrameBuffer;\n    };\n}\n\n#endif\n"
  },
  {
    "path": "specialpages.cpp",
    "content": "\n#include \"specialpages.h\"\n\nusing namespace vbit;\n\nSpecialPages::SpecialPages(int mag, PageList *pageList, Debug *debug) :\n    _mag(mag),\n    _pageList(pageList),\n    _debug(debug)\n{\n    ResetIter();\n}\n\nSpecialPages::~SpecialPages()\n{\n\n}\n\nvoid SpecialPages::addPage(std::shared_ptr<TTXPageStream> p)\n{\n    p->SetSpecialFlag(true);\n    for (std::list<std::shared_ptr<TTXPageStream>>::iterator it=_specialPagesList.begin();it!=_specialPagesList.end();++it)\n    {\n        // find first page with a higher number\n        std::shared_ptr<TTXPageStream> ptr = *it;\n        if (ptr->GetPageNumber() > p->GetPageNumber())\n        {\n            _specialPagesList.insert(it,p);\n            return;\n        }\n    }\n    // if we are here we ran to the end of the list without a match\n    _specialPagesList.push_back(p);\n}\n\nstd::shared_ptr<TTXPageStream> SpecialPages::NextPage()\n{\n    if (_page == nullptr)\n    {\n        ++_iter;\n        _page = *_iter;\n    }\n    else\n    {\n        if (!_page->GetOneShotFlag()) // don't cycle oneshot pages\n        {\n            if (_page->GetLock()) // try to lock this page against changes\n            {\n                _page->StepNextSubpageNoLoop(); // next subpage if a carousel\n                if (_page->GetSubpage() != nullptr) // make sure there is a subpage\n                {\n                    return _page; // return page locked\n                }\n                _page->FreeLock(); // must unlock page again\n            }\n            ++_iter;\n            _page = *_iter;\n        }\n    }\n\n    while(true)\n    {\n        if (_iter == _specialPagesList.end())\n        {\n            _page = nullptr;\n            return _page;\n        }\n        \n        if (_page)\n        {\n            if (_page->GetOneShotFlag())\n            {\n                _page->SetSpecialFlag(false);\n                _iter = _specialPagesList.erase(_iter); // remove oneshot pages from the page list\n                continue;\n            }\n            \n            if (_page->GetLock()) // try to lock this page against changes\n            {\n                if (_page->GetSubpage() == nullptr && !(_page->GetOneShotFlag()))\n                {\n                    _page->StepNextSubpageNoLoop(); // step to first subpage\n                }\n                \n                if (_page->GetIsMarked() && _page->GetSpecialFlag()) // only remove it once\n                {\n                    std::stringstream ss;\n                    ss << \"[SpecialPages::NextPage] Deleted \" << std::hex << _page->GetPageNumber();\n                    _debug->Log(Debug::LogLevels::logINFO,ss.str());\n                    _iter = _specialPagesList.erase(_iter);\n                    _page->SetSpecialFlag(false);\n                    _pageList->RemovePage(_page); // try to remove it from the pagelist immediately - will free the lock\n                    _page = *_iter;\n                    continue; // jump back to loop\n                }\n                else if (!(_page->Special()))\n                {\n                    std::stringstream ss;\n                    ss << \"[SpecialPages::NextPage()] no longer special \" << std::hex << _page->GetPageNumber();\n                    _debug->Log(Debug::LogLevels::logINFO,ss.str());\n                    _iter = _specialPagesList.erase(_iter);\n                    _page->SetSpecialFlag(false);\n                }\n                else if ((_page->GetPageNumber() & 0xFF) == 0xFF) // never return page mFF from the page list\n                {\n                    ++_iter;\n                }\n                else if (_page->GetSubpageCount() == 0) // skip pages with no subpages\n                {\n                    ++_iter;\n                }\n                else\n                {\n                    return _page; // return page locked\n                }\n                \n                _page->FreeLock(); // must unlock page again\n                _page = *_iter;\n            }\n            else\n            {\n                // skip page\n                ++_iter;\n                _page = *_iter;\n            }\n        }\n    }\n}\n\nvoid SpecialPages::ResetIter()\n{\n    _iter=_specialPagesList.begin();\n    _page=nullptr;\n}\n"
  },
  {
    "path": "specialpages.h",
    "content": "#ifndef _SPECIALPAGES_H\n#define _SPECIALPAGES_H\n\n#include <list>\n\n#include \"debug.h\"\n#include \"ttxpagestream.h\"\n#include \"pagelist.h\"\n\n// list of special pages\n\nnamespace vbit\n{\n\nclass SpecialPages\n{\n    public:\n        /** Default constructor */\n        SpecialPages(int mag, PageList *pageList, Debug *debug);\n        /** Default destructor */\n        virtual ~SpecialPages();\n\n        std::shared_ptr<TTXPageStream> NextPage();\n        \n        void ResetIter();\n\n        void addPage(std::shared_ptr<TTXPageStream> p);\n\n    protected:\n\n    private:\n        int _mag;\n        PageList* _pageList;\n        Debug* _debug;\n        std::list<std::shared_ptr<TTXPageStream>> _specialPagesList;\n        std::list<std::shared_ptr<TTXPageStream>>::iterator _iter;\n        std::shared_ptr<TTXPageStream> _page;\n};\n\n}\n\n#endif // _SPECIALPAGES_H\n"
  },
  {
    "path": "tables.cpp",
    "content": "#include \"tables.h\"\n\n/*-------------------------------------------\n* Reverse bytes\n*/\nconst uint8_t ReverseByteTab[256] = {\n    0x00,0x80,0x40,0xc0,0x20,0xa0,0x60,0xe0,\n    0x10,0x90,0x50,0xd0,0x30,0xb0,0x70,0xf0,\n    0x08,0x88,0x48,0xc8,0x28,0xa8,0x68,0xe8,\n    0x18,0x98,0x58,0xd8,0x38,0xb8,0x78,0xf8,\n    0x04,0x84,0x44,0xc4,0x24,0xa4,0x64,0xe4,\n    0x14,0x94,0x54,0xd4,0x34,0xb4,0x74,0xf4,\n    0x0c,0x8c,0x4c,0xcc,0x2c,0xac,0x6c,0xec,\n    0x1c,0x9c,0x5c,0xdc,0x3c,0xbc,0x7c,0xfc,\n    0x02,0x82,0x42,0xc2,0x22,0xa2,0x62,0xe2,\n    0x12,0x92,0x52,0xd2,0x32,0xb2,0x72,0xf2,\n    0x0a,0x8a,0x4a,0xca,0x2a,0xaa,0x6a,0xea,\n    0x1a,0x9a,0x5a,0xda,0x3a,0xba,0x7a,0xfa,\n    0x06,0x86,0x46,0xc6,0x26,0xa6,0x66,0xe6,\n    0x16,0x96,0x56,0xd6,0x36,0xb6,0x76,0xf6,\n    0x0e,0x8e,0x4e,0xce,0x2e,0xae,0x6e,0xee,\n    0x1e,0x9e,0x5e,0xde,0x3e,0xbe,0x7e,0xfe,\n    0x01,0x81,0x41,0xc1,0x21,0xa1,0x61,0xe1,\n    0x11,0x91,0x51,0xd1,0x31,0xb1,0x71,0xf1,\n    0x09,0x89,0x49,0xc9,0x29,0xa9,0x69,0xe9,\n    0x19,0x99,0x59,0xd9,0x39,0xb9,0x79,0xf9,\n    0x05,0x85,0x45,0xc5,0x25,0xa5,0x65,0xe5,\n    0x15,0x95,0x55,0xd5,0x35,0xb5,0x75,0xf5,\n    0x0d,0x8d,0x4d,0xcd,0x2d,0xad,0x6d,0xed,\n    0x1d,0x9d,0x5d,0xdd,0x3d,0xbd,0x7d,0xfd,\n    0x03,0x83,0x43,0xc3,0x23,0xa3,0x63,0xe3,\n    0x13,0x93,0x53,0xd3,0x33,0xb3,0x73,0xf3,\n    0x0b,0x8b,0x4b,0xcb,0x2b,0xab,0x6b,0xeb,\n    0x1b,0x9b,0x5b,0xdb,0x3b,0xbb,0x7b,0xfb,\n    0x07,0x87,0x47,0xc7,0x27,0xa7,0x67,0xe7,\n    0x17,0x97,0x57,0xd7,0x37,0xb7,0x77,0xf7,\n    0x0f,0x8f,0x4f,0xcf,0x2f,0xaf,0x6f,0xef,\n    0x1f,0x9f,0x5f,0xdf,0x3f,0xbf,0x7f,0xff\n};\n\n/*-------------------------------------------\n* Odd parity table\n*/\nconst uint8_t OddParityTable[128] = {\n    0x80,0x01,0x02,0x83,0x04,0x85,0x86,0x07,\n    0x08,0x89,0x8A,0x0B,0x8C,0x0D,0x0E,0x8F,\n    0x10,0x91,0x92,0x13,0x94,0x15,0x16,0x97,\n    0x98,0x19,0x1A,0x9B,0x1C,0x9D,0x9E,0x1F,\n    0x20,0xA1,0xA2,0x23,0xA4,0x25,0x26,0xA7,\n    0xA8,0x29,0x2A,0xAB,0x2C,0xAD,0xAE,0x2F,\n    0xB0,0x31,0x32,0xB3,0x34,0xB5,0xB6,0x37,\n    0x38,0xB9,0xBA,0x3B,0xBC,0x3D,0x3E,0xBF,\n    0x40,0xC1,0xC2,0x43,0xC4,0x45,0x46,0xC7,\n    0xC8,0x49,0x4A,0xCB,0x4C,0xCD,0xCE,0x4F,\n    0xD0,0x51,0x52,0xD3,0x54,0xD5,0xD6,0x57,\n    0x58,0xD9,0xDA,0x5B,0xDC,0x5D,0x5E,0xDF,\n    0xE0,0x61,0x62,0xE3,0x64,0xE5,0xE6,0x67,\n    0x68,0xE9,0xEA,0x6B,0xEC,0x6D,0x6E,0xEF,\n    0x70,0xF1,0xF2,0x73,0xF4,0x75,0x76,0xF7,\n    0xF8,0x79,0x7A,0xFB,0x7C,0xFD,0xFE,0x7F};\n\n/*-------------------------------------------\n* Hamming 8/4 encode table\n*      0    1    2    3    4    5    6    7    8    9    A    B    C    D    E    F */\nconst uint8_t Hamming8EncodeTable[16] = {\n    0x15,0x02,0x49,0x5E,0x64,0x73,0x38,0x2F,0xD0,0xC7,0x8C,0x9B,0xA1,0xB6,0xFD,0xEA};\n\n/*-------------------------------------------\n* Hamming 8/4 decode table\n*/\nconst uint8_t Hamming8DecodeTable[256] = {\n    0x01,0x80,0x01,0x01,0x80,0x00,0x01,0x80,\n    0x80,0x02,0x01,0x80,0x0A,0x80,0x80,0x07,\n    0x80,0x00,0x01,0x80,0x00,0x00,0x80,0x00,\n    0x06,0x80,0x80,0x0B,0x80,0x00,0x03,0x80,\n    0x80,0x0C,0x01,0x80,0x04,0x80,0x80,0x07,\n    0x06,0x80,0x80,0x07,0x80,0x07,0x07,0x07,\n    0x06,0x80,0x80,0x05,0x80,0x00,0x0D,0x80,\n    0x06,0x06,0x06,0x80,0x06,0x80,0x80,0x07,\n    0x80,0x02,0x01,0x80,0x04,0x80,0x80,0x09,\n    0x02,0x02,0x80,0x02,0x80,0x02,0x03,0x80,\n    0x08,0x80,0x80,0x05,0x80,0x00,0x03,0x80,\n    0x80,0x02,0x03,0x80,0x03,0x80,0x03,0x03,\n    0x04,0x80,0x80,0x05,0x04,0x04,0x04,0x80,\n    0x80,0x02,0x0F,0x80,0x04,0x80,0x80,0x07,\n    0x80,0x05,0x05,0x05,0x04,0x80,0x80,0x05,\n    0x06,0x80,0x80,0x05,0x80,0x0E,0x03,0x80,\n    0x80,0x0C,0x01,0x80,0x0A,0x80,0x80,0x09,\n    0x0A,0x80,0x80,0x0B,0x0A,0x0A,0x0A,0x80,\n    0x08,0x80,0x80,0x0B,0x80,0x00,0x0D,0x80,\n    0x80,0x0B,0x0B,0x0B,0x0A,0x80,0x80,0x0B,\n    0x0C,0x0C,0x80,0x0C,0x80,0x0C,0x0D,0x80,\n    0x80,0x0C,0x0F,0x80,0x0A,0x80,0x80,0x07,\n    0x80,0x0C,0x0D,0x80,0x0D,0x80,0x0D,0x0D,\n    0x06,0x80,0x80,0x0B,0x80,0x0E,0x0D,0x80,\n    0x08,0x80,0x80,0x09,0x80,0x09,0x09,0x09,\n    0x80,0x02,0x0F,0x80,0x0A,0x80,0x80,0x09,\n    0x08,0x08,0x08,0x80,0x08,0x80,0x80,0x09,\n    0x08,0x80,0x80,0x0B,0x80,0x0E,0x03,0x80,\n    0x80,0x0C,0x0F,0x80,0x04,0x80,0x80,0x09,\n    0x0F,0x80,0x0F,0x0F,0x80,0x0E,0x0F,0x80,\n    0x08,0x80,0x80,0x05,0x80,0x0E,0x0D,0x80,\n    0x80,0x0E,0x0F,0x80,0x0E,0x0E,0x80,0x0E\n};\n\n/*-------------------------------------------\n* Hamming 24/18 encode tables\n*/\nconst uint8_t Hamming24EncodeTable0[256] = {\n    0x8b,0x8c,0x92,0x95,0xa1,0xa6,0xb8,0xbf,\n    0xc0,0xc7,0xd9,0xde,0xea,0xed,0xf3,0xf4,\n    0x0a,0x0d,0x13,0x14,0x20,0x27,0x39,0x3e,\n    0x41,0x46,0x58,0x5f,0x6b,0x6c,0x72,0x75,\n    0x09,0x0e,0x10,0x17,0x23,0x24,0x3a,0x3d,\n    0x42,0x45,0x5b,0x5c,0x68,0x6f,0x71,0x76,\n    0x88,0x8f,0x91,0x96,0xa2,0xa5,0xbb,0xbc,\n    0xc3,0xc4,0xda,0xdd,0xe9,0xee,0xf0,0xf7,\n    0x08,0x0f,0x11,0x16,0x22,0x25,0x3b,0x3c,\n    0x43,0x44,0x5a,0x5d,0x69,0x6e,0x70,0x77,\n    0x89,0x8e,0x90,0x97,0xa3,0xa4,0xba,0xbd,\n    0xc2,0xc5,0xdb,0xdc,0xe8,0xef,0xf1,0xf6,\n    0x8a,0x8d,0x93,0x94,0xa0,0xa7,0xb9,0xbe,\n    0xc1,0xc6,0xd8,0xdf,0xeb,0xec,0xf2,0xf5,\n    0x0b,0x0c,0x12,0x15,0x21,0x26,0x38,0x3f,\n    0x40,0x47,0x59,0x5e,0x6a,0x6d,0x73,0x74,\n    0x03,0x04,0x1a,0x1d,0x29,0x2e,0x30,0x37,\n    0x48,0x4f,0x51,0x56,0x62,0x65,0x7b,0x7c,\n    0x82,0x85,0x9b,0x9c,0xa8,0xaf,0xb1,0xb6,\n    0xc9,0xce,0xd0,0xd7,0xe3,0xe4,0xfa,0xfd,\n    0x81,0x86,0x98,0x9f,0xab,0xac,0xb2,0xb5,\n    0xca,0xcd,0xd3,0xd4,0xe0,0xe7,0xf9,0xfe,\n    0x00,0x07,0x19,0x1e,0x2a,0x2d,0x33,0x34,\n    0x4b,0x4c,0x52,0x55,0x61,0x66,0x78,0x7f,\n    0x80,0x87,0x99,0x9e,0xaa,0xad,0xb3,0xb4,\n    0xcb,0xcc,0xd2,0xd5,0xe1,0xe6,0xf8,0xff,\n    0x01,0x06,0x18,0x1f,0x2b,0x2c,0x32,0x35,\n    0x4a,0x4d,0x53,0x54,0x60,0x67,0x79,0x7e,\n    0x02,0x05,0x1b,0x1c,0x28,0x2f,0x31,0x36,\n    0x49,0x4e,0x50,0x57,0x63,0x64,0x7a,0x7d,\n    0x83,0x84,0x9a,0x9d,0xa9,0xae,0xb0,0xb7,\n    0xc8,0xcf,0xd1,0xd6,0xe2,0xe5,0xfb,0xfc\n};\n\nconst uint8_t Hamming24EncodeTable1[256] = {\n    0x00,0x89,0x8a,0x03,0x8b,0x02,0x01,0x88,\n    0x01,0x88,0x8b,0x02,0x8a,0x03,0x00,0x89,\n    0x02,0x8b,0x88,0x01,0x89,0x00,0x03,0x8a,\n    0x03,0x8a,0x89,0x00,0x88,0x01,0x02,0x8b,\n    0x03,0x8a,0x89,0x00,0x88,0x01,0x02,0x8b,\n    0x02,0x8b,0x88,0x01,0x89,0x00,0x03,0x8a,\n    0x01,0x88,0x8b,0x02,0x8a,0x03,0x00,0x89,\n    0x00,0x89,0x8a,0x03,0x8b,0x02,0x01,0x88,\n    0x08,0x81,0x82,0x0b,0x83,0x0a,0x09,0x80,\n    0x09,0x80,0x83,0x0a,0x82,0x0b,0x08,0x81,\n    0x0a,0x83,0x80,0x09,0x81,0x08,0x0b,0x82,\n    0x0b,0x82,0x81,0x08,0x80,0x09,0x0a,0x83,\n    0x0b,0x82,0x81,0x08,0x80,0x09,0x0a,0x83,\n    0x0a,0x83,0x80,0x09,0x81,0x08,0x0b,0x82,\n    0x09,0x80,0x83,0x0a,0x82,0x0b,0x08,0x81,\n    0x08,0x81,0x82,0x0b,0x83,0x0a,0x09,0x80,\n    0x09,0x80,0x83,0x0a,0x82,0x0b,0x08,0x81,\n    0x08,0x81,0x82,0x0b,0x83,0x0a,0x09,0x80,\n    0x0b,0x82,0x81,0x08,0x80,0x09,0x0a,0x83,\n    0x0a,0x83,0x80,0x09,0x81,0x08,0x0b,0x82,\n    0x0a,0x83,0x80,0x09,0x81,0x08,0x0b,0x82,\n    0x0b,0x82,0x81,0x08,0x80,0x09,0x0a,0x83,\n    0x08,0x81,0x82,0x0b,0x83,0x0a,0x09,0x80,\n    0x09,0x80,0x83,0x0a,0x82,0x0b,0x08,0x81,\n    0x01,0x88,0x8b,0x02,0x8a,0x03,0x00,0x89,\n    0x00,0x89,0x8a,0x03,0x8b,0x02,0x01,0x88,\n    0x03,0x8a,0x89,0x00,0x88,0x01,0x02,0x8b,\n    0x02,0x8b,0x88,0x01,0x89,0x00,0x03,0x8a,\n    0x02,0x8b,0x88,0x01,0x89,0x00,0x03,0x8a,\n    0x03,0x8a,0x89,0x00,0x88,0x01,0x02,0x8b,\n    0x00,0x89,0x8a,0x03,0x8b,0x02,0x01,0x88,\n    0x01,0x88,0x8b,0x02,0x8a,0x03,0x00,0x89\n};\n\nconst uint8_t Hamming24EncodeTable2[4] = {\n    0x00,0x0a,0x0b,0x01\n};\n\nconst uint8_t Hamming24ParityTable[3][256] = {\n    {\n        0x00,0x21,0x22,0x03,0x23,0x02,0x01,0x20,\n        0x24,0x05,0x06,0x27,0x07,0x26,0x25,0x04,\n        0x25,0x04,0x07,0x26,0x06,0x27,0x24,0x05,\n        0x01,0x20,0x23,0x02,0x22,0x03,0x00,0x21,\n        0x26,0x07,0x04,0x25,0x05,0x24,0x27,0x06,\n        0x02,0x23,0x20,0x01,0x21,0x00,0x03,0x22,\n        0x03,0x22,0x21,0x00,0x20,0x01,0x02,0x23,\n        0x27,0x06,0x05,0x24,0x04,0x25,0x26,0x07,\n        0x27,0x06,0x05,0x24,0x04,0x25,0x26,0x07,\n        0x03,0x22,0x21,0x00,0x20,0x01,0x02,0x23,\n        0x02,0x23,0x20,0x01,0x21,0x00,0x03,0x22,\n        0x26,0x07,0x04,0x25,0x05,0x24,0x27,0x06,\n        0x01,0x20,0x23,0x02,0x22,0x03,0x00,0x21,\n        0x25,0x04,0x07,0x26,0x06,0x27,0x24,0x05,\n        0x24,0x05,0x06,0x27,0x07,0x26,0x25,0x04,\n        0x00,0x21,0x22,0x03,0x23,0x02,0x01,0x20,\n        0x28,0x09,0x0a,0x2b,0x0b,0x2a,0x29,0x08,\n        0x0c,0x2d,0x2e,0x0f,0x2f,0x0e,0x0d,0x2c,\n        0x0d,0x2c,0x2f,0x0e,0x2e,0x0f,0x0c,0x2d,\n        0x29,0x08,0x0b,0x2a,0x0a,0x2b,0x28,0x09,\n        0x0e,0x2f,0x2c,0x0d,0x2d,0x0c,0x0f,0x2e,\n        0x2a,0x0b,0x08,0x29,0x09,0x28,0x2b,0x0a,\n        0x2b,0x0a,0x09,0x28,0x08,0x29,0x2a,0x0b,\n        0x0f,0x2e,0x2d,0x0c,0x2c,0x0d,0x0e,0x2f,\n        0x0f,0x2e,0x2d,0x0c,0x2c,0x0d,0x0e,0x2f,\n        0x2b,0x0a,0x09,0x28,0x08,0x29,0x2a,0x0b,\n        0x2a,0x0b,0x08,0x29,0x09,0x28,0x2b,0x0a,\n        0x0e,0x2f,0x2c,0x0d,0x2d,0x0c,0x0f,0x2e,\n        0x29,0x08,0x0b,0x2a,0x0a,0x2b,0x28,0x09,\n        0x0d,0x2c,0x2f,0x0e,0x2e,0x0f,0x0c,0x2d,\n        0x0c,0x2d,0x2e,0x0f,0x2f,0x0e,0x0d,0x2c,\n        0x28,0x09,0x0a,0x2b,0x0b,0x2a,0x29,0x08\n    }, {\n        0x00,0x29,0x2a,0x03,0x2b,0x02,0x01,0x28,\n        0x2c,0x05,0x06,0x2f,0x07,0x2e,0x2d,0x04,\n        0x2d,0x04,0x07,0x2e,0x06,0x2f,0x2c,0x05,\n        0x01,0x28,0x2b,0x02,0x2a,0x03,0x00,0x29,\n        0x2e,0x07,0x04,0x2d,0x05,0x2c,0x2f,0x06,\n        0x02,0x2b,0x28,0x01,0x29,0x00,0x03,0x2a,\n        0x03,0x2a,0x29,0x00,0x28,0x01,0x02,0x2b,\n        0x2f,0x06,0x05,0x2c,0x04,0x2d,0x2e,0x07,\n        0x2f,0x06,0x05,0x2c,0x04,0x2d,0x2e,0x07,\n        0x03,0x2a,0x29,0x00,0x28,0x01,0x02,0x2b,\n        0x02,0x2b,0x28,0x01,0x29,0x00,0x03,0x2a,\n        0x2e,0x07,0x04,0x2d,0x05,0x2c,0x2f,0x06,\n        0x01,0x28,0x2b,0x02,0x2a,0x03,0x00,0x29,\n        0x2d,0x04,0x07,0x2e,0x06,0x2f,0x2c,0x05,\n        0x2c,0x05,0x06,0x2f,0x07,0x2e,0x2d,0x04,\n        0x00,0x29,0x2a,0x03,0x2b,0x02,0x01,0x28,\n        0x30,0x19,0x1a,0x33,0x1b,0x32,0x31,0x18,\n        0x1c,0x35,0x36,0x1f,0x37,0x1e,0x1d,0x34,\n        0x1d,0x34,0x37,0x1e,0x36,0x1f,0x1c,0x35,\n        0x31,0x18,0x1b,0x32,0x1a,0x33,0x30,0x19,\n        0x1e,0x37,0x34,0x1d,0x35,0x1c,0x1f,0x36,\n        0x32,0x1b,0x18,0x31,0x19,0x30,0x33,0x1a,\n        0x33,0x1a,0x19,0x30,0x18,0x31,0x32,0x1b,\n        0x1f,0x36,0x35,0x1c,0x34,0x1d,0x1e,0x37,\n        0x1f,0x36,0x35,0x1c,0x34,0x1d,0x1e,0x37,\n        0x33,0x1a,0x19,0x30,0x18,0x31,0x32,0x1b,\n        0x32,0x1b,0x18,0x31,0x19,0x30,0x33,0x1a,\n        0x1e,0x37,0x34,0x1d,0x35,0x1c,0x1f,0x36,\n        0x31,0x18,0x1b,0x32,0x1a,0x33,0x30,0x19,\n        0x1d,0x34,0x37,0x1e,0x36,0x1f,0x1c,0x35,\n        0x1c,0x35,0x36,0x1f,0x37,0x1e,0x1d,0x34,\n        0x30,0x19,0x1a,0x33,0x1b,0x32,0x31,0x18\n    }, {\n        0x3f,0x0e,0x0d,0x3c,0x0c,0x3d,0x3e,0x0f,\n        0x0b,0x3a,0x39,0x08,0x38,0x09,0x0a,0x3b,\n        0x0a,0x3b,0x38,0x09,0x39,0x08,0x0b,0x3a,\n        0x3e,0x0f,0x0c,0x3d,0x0d,0x3c,0x3f,0x0e,\n        0x09,0x38,0x3b,0x0a,0x3a,0x0b,0x08,0x39,\n        0x3d,0x0c,0x0f,0x3e,0x0e,0x3f,0x3c,0x0d,\n        0x3c,0x0d,0x0e,0x3f,0x0f,0x3e,0x3d,0x0c,\n        0x08,0x39,0x3a,0x0b,0x3b,0x0a,0x09,0x38,\n        0x08,0x39,0x3a,0x0b,0x3b,0x0a,0x09,0x38,\n        0x3c,0x0d,0x0e,0x3f,0x0f,0x3e,0x3d,0x0c,\n        0x3d,0x0c,0x0f,0x3e,0x0e,0x3f,0x3c,0x0d,\n        0x09,0x38,0x3b,0x0a,0x3a,0x0b,0x08,0x39,\n        0x3e,0x0f,0x0c,0x3d,0x0d,0x3c,0x3f,0x0e,\n        0x0a,0x3b,0x38,0x09,0x39,0x08,0x0b,0x3a,\n        0x0b,0x3a,0x39,0x08,0x38,0x09,0x0a,0x3b,\n        0x3f,0x0e,0x0d,0x3c,0x0c,0x3d,0x3e,0x0f,\n        0x1f,0x2e,0x2d,0x1c,0x2c,0x1d,0x1e,0x2f,\n        0x2b,0x1a,0x19,0x28,0x18,0x29,0x2a,0x1b,\n        0x2a,0x1b,0x18,0x29,0x19,0x28,0x2b,0x1a,\n        0x1e,0x2f,0x2c,0x1d,0x2d,0x1c,0x1f,0x2e,\n        0x29,0x18,0x1b,0x2a,0x1a,0x2b,0x28,0x19,\n        0x1d,0x2c,0x2f,0x1e,0x2e,0x1f,0x1c,0x2d,\n        0x1c,0x2d,0x2e,0x1f,0x2f,0x1e,0x1d,0x2c,\n        0x28,0x19,0x1a,0x2b,0x1b,0x2a,0x29,0x18,\n        0x28,0x19,0x1a,0x2b,0x1b,0x2a,0x29,0x18,\n        0x1c,0x2d,0x2e,0x1f,0x2f,0x1e,0x1d,0x2c,\n        0x1d,0x2c,0x2f,0x1e,0x2e,0x1f,0x1c,0x2d,\n        0x29,0x18,0x1b,0x2a,0x1a,0x2b,0x28,0x19,\n        0x1e,0x2f,0x2c,0x1d,0x2d,0x1c,0x1f,0x2e,\n        0x2a,0x1b,0x18,0x29,0x19,0x28,0x2b,0x1a,\n        0x2b,0x1a,0x19,0x28,0x18,0x29,0x2a,0x1b,\n        0x1f,0x2e,0x2d,0x1c,0x2c,0x1d,0x1e,0x2f\n    }\n};\n"
  },
  {
    "path": "tables.h",
    "content": "#ifndef _TABLES_H_\n#define _TABLES_H_\n#include <cstdint>\n\nextern const uint8_t ReverseByteTab[256];\n\nextern const uint8_t OddParityTable[128];\n\nextern const uint8_t Hamming8EncodeTable[16];\n\nextern const uint8_t Hamming8DecodeTable[256];\n\nextern const uint8_t Hamming24EncodeTable0[256];\nextern const uint8_t Hamming24EncodeTable1[256];\nextern const uint8_t Hamming24EncodeTable2[4];\nextern const uint8_t Hamming24ParityTable[3][256];\n\n#endif\n"
  },
  {
    "path": "teletext-update.service",
    "content": "[Unit]\nDescription=Automatically check active teletext service for updates\n\n[Service]\nType=oneshot\nExecStart=%h/.local/bin/teletext-update\n"
  },
  {
    "path": "teletext-update.timer",
    "content": "[Unit]\nDescription=Run update-teletext.service every 5 minutes\n\n[Timer]\nOnUnitActiveSec=5min\nOnBootSec=10s\n\n[Install]\nWantedBy=timers.target\n"
  },
  {
    "path": "ttxline.cpp",
    "content": "#include \"ttxline.h\"\n\n// create a blank line\nTTXLine::TTXLine():\n    _nextLine(nullptr)\n{\n    _line.fill(0x20); // initialise lines with all spaces\n}\n\n// create a new line from a 40 byte array\nTTXLine::TTXLine(std::array<uint8_t, 40> line):\n    _nextLine(nullptr)\n{\n    _line = line;\n}\n\nTTXLine::TTXLine(std::string const& line):\n    _nextLine(nullptr)\n{\n    // convert text string to 40 byte ttxline representation, expanding escape codes etc\n    char ch;\n    int j=0;\n    _line.fill(0x20); // spaces\n    for (unsigned int i=0;i<line.length() && j<40;i++)\n    {\n        ch = line[i] & 0x7f; // 7-bit\n        if (line[i] == 0x1b) // ascii escape\n        {\n            i++;\n            ch = line[i] & 0x3f;\n        }\n        else if ((uint8_t)line[i] < 0x20) // other ascii control code\n        {\n            break;\n        }\n\n        _line[j++]=ch;\n    }\n}\n\n// create a copy of an existing line\nTTXLine::TTXLine(std::shared_ptr<TTXLine> line):\n    _nextLine(nullptr)\n{\n    _line = line->GetLine();\n    \n    // recurse down appended lines\n    if (line->GetNextLine())\n        _nextLine = std::shared_ptr<TTXLine>(new TTXLine(line->GetNextLine()));\n}\n\nTTXLine::~TTXLine()\n{\n    //std::cerr << \"TTXLine dtor \" << m_textline << std::endl;\n}\n\nbool TTXLine::IsBlank()\n{\n    for (unsigned int i=0;i<_line.size();i++)\n    {\n        if (_line[i]!=0x20)\n        {\n            return false;\n        }\n    }\n    return true; // Yes, the line is blank\n}\n\nvoid TTXLine::AppendLine(std::shared_ptr<TTXLine> line)\n{\n    // Seek through list\n    std::shared_ptr<TTXLine> p=this->getptr();\n    for (p=this->getptr();;p=p->_nextLine)\n    {\n        if ((p->GetCharAt(0)&0xF) == (line->GetCharAt(0)&0xF))\n        {\n            // line with same designation code already exists\n            p->_line = line->_line; // overwrite it with our new line data but leave _nextLine unmodified\n            return;\n        }\n        else if ((p->GetCharAt(0)&0xF) > (line->GetCharAt(0)&0xF))\n        {\n            // existing line has higher designation code than we are trying to add\n            std::array<uint8_t, 40> tmp = p->_line; // preserve original line data\n            line->_nextLine = p->_nextLine; // move original _nextLine pointer to our new line\n            p->_line = line->_line; // move our new line data to original line\n            line->_line = tmp; // move original line data into our new line\n            p->_nextLine = line; // finally update _nextLine pointer to our line which now contains the original line\n        }\n        \n        if (p->_nextLine == nullptr) // reached the end of the list\n            break;\n    }\n    \n    p->_nextLine = line; // append line to end of chain\n}\n\nstd::shared_ptr<TTXLine> TTXLine::RemoveLine(uint8_t designationCode)\n{\n    // returns new pointer to linked list for this line\n    if ((_line[0]&0xF) == (designationCode&0xF)) // the root line matches the designation code\n    {\n        if (_nextLine == nullptr) // this is the only line\n            return nullptr;\n        else\n            return _nextLine; // return the next line as the new root\n    }\n    \n    // seek through list for a specific designation code\n    std::shared_ptr<TTXLine> p=this->getptr();\n    for (p=this->getptr();p->_nextLine!=nullptr;p=p->_nextLine)\n    {\n        if ((p->_nextLine->GetCharAt(0)&0xF) == (designationCode&0xF))\n        {\n            // next line matches designationCode\n            p->_nextLine = p->_nextLine->_nextLine; // cut line out of list\n            \n            if (p->_nextLine == nullptr)\n                break; // this is now the end of the list so break out of loop\n        }\n    }\n    \n    return this->getptr(); // give back the same list\n}\n\nstd::shared_ptr<TTXLine> TTXLine::LocateLine(uint8_t designationCode)\n{\n    // seek through list for a specific designation code\n    std::shared_ptr<TTXLine> p=this->getptr();\n    for (p=this->getptr();;p=p->_nextLine)\n    {\n        if ((p->GetCharAt(0)&0xF) == (designationCode&0xF))\n        {\n            return p; // return the line\n        }\n        \n        if (p->_nextLine == nullptr) // reached the end of the list\n            break;\n    }\n    return nullptr;\n}\n"
  },
  {
    "path": "ttxline.h",
    "content": "#ifndef TTXLINE_H\n#define TTXLINE_H\n#include <cstdint>\n#include <iostream>\n#include <iomanip>\n#include <string>\n#include <memory>\n#include <array>\n\n/** TTXLine - a single line of teletext\n *  The line is always stored in 40 bytes in transmission ready format\n * (but with the parity bit set to 0).\n */\n\nclass TTXLine : public std::enable_shared_from_this<TTXLine>\n{\n    public:\n        /** Constructors */\n        TTXLine();\n        TTXLine(std::array<uint8_t, 40> line);\n        TTXLine(std::string const& line);\n        TTXLine(std::shared_ptr<TTXLine> line);\n        \n        /** Default destructor */\n        virtual ~TTXLine();\n\n        std::array<uint8_t, 40> GetLine(){return _line;};\n        bool IsBlank();\n        uint8_t GetCharAt(int index){return _line[index];};\n\n        /** Adds line to a linked list\n         *  This is used for enhanced packets which might require multiples of the same row\n         */\n        void AppendLine(std::shared_ptr<TTXLine> line);\n        \n        std::shared_ptr<TTXLine> RemoveLine(uint8_t designationCode); // remove line by designation code\n        \n        std::shared_ptr<TTXLine> LocateLine(uint8_t designationCode); // get line by designation code\n\n        std::shared_ptr<TTXLine> GetNextLine(){return _nextLine;}\n\n    protected:\n    private:\n        std::shared_ptr<TTXLine> getptr()\n        {\n            return shared_from_this();\n        }\n        \n        std::array<uint8_t, 40> _line; // 40 byte line\n        std::shared_ptr<TTXLine> _nextLine;\n};\n\n#endif // TTXLINE_H\n"
  },
  {
    "path": "ttxpagestream.cpp",
    "content": "#include \"ttxpagestream.h\"\n\nusing namespace vbit;\n\nTTXPageStream::TTXPageStream() :\n    _transitionTime(0),\n    _loadedPacket29(false),\n    _loadedCustomHeader(false),\n    _isCarousel(false),\n    _isSpecial(false),\n    _isNormal(false),\n    _isUpdated(false),\n    _updateCount(0),\n    _deleteFlag(false),\n    _isOneShot(false)\n{\n    //ctor\n    _mtx.reset(new std::mutex());\n}\n\nTTXPageStream::~TTXPageStream()\n{\n    //dtor\n    //std::cerr << \"TTXPageStream dtor\\n\";\n}\n\nbool TTXPageStream::GetLock()\n{\n    if (_mtx->try_lock())\n        return true; // ok\n    return false; // couldn't get mutex\n}\n\nvoid TTXPageStream::FreeLock()\n{\n    _mtx->unlock();\n}\n\nvoid TTXPageStream::IncrementUpdateCount()\n{\n    _updateCount = (_updateCount + 1) % 8;\n}\n\nvoid TTXPageStream::SetTransitionTime(uint8_t cycleTime)\n{\n    if (std::shared_ptr<Subpage> s = GetSubpage())\n    {\n        if (s->GetTimedMode())\n        {\n            MasterClock *mc = mc->Instance();\n            _transitionTime = mc->GetMasterClock().seconds + cycleTime;\n        }\n        else\n        {\n            _cyclesRemaining=cycleTime;\n        }\n    }\n}\n\nbool TTXPageStream::Expired(bool StepCycles)\n{\n    // Has carousel timer expired\n    if (std::shared_ptr<Subpage> s = GetSubpage())\n    {\n        if (s->GetTimedMode())\n        {\n            MasterClock *mc = mc->Instance();\n            if (_transitionTime == 0)\n                return false; // catch race condition where we can check carousel before its timeout has been set\n            return _transitionTime <= mc->GetMasterClock().seconds;\n        }\n    }\n    \n    if (StepCycles > 0)\n    {\n        _cyclesRemaining--;\n    }\n    return _cyclesRemaining == 0;\n}\n"
  },
  {
    "path": "ttxpagestream.h",
    "content": "#ifndef _TTXPAGESTREAM_H_\n#define _TTXPAGESTREAM_H_\n\n#include <mutex>\n#include <sys/stat.h>\n#include <memory>\n\n#include \"page.h\"\n#include \"packet.h\"\n#include \"masterClock.h\"\n\n/** @brief Extends Page to allow Service to iterate through this page.\n *  It adds iterators to the page and also timing control if it is a carousel.\n *  It also has features to help add, remove and update pages in a service.\n */\nnamespace vbit\n{\nclass TTXPageStream : public Page\n{\n    public:\n        /** Default constructor. */\n        TTXPageStream();\n        /** Default destructor */\n        virtual ~TTXPageStream();\n        \n        void MarkForDeletion() { _deleteFlag = true; }\n        bool GetIsMarked() { return _deleteFlag; }\n        \n        void SetOneShotFlag(bool val) { _isOneShot = val; }\n        bool GetOneShotFlag() { return _isOneShot; }\n\n        bool GetCarouselFlag() { return _isCarousel; }\n        void SetCarouselFlag(bool val) { _isCarousel = val; } // must only be set by Carousel!\n\n        bool GetSpecialFlag() { return _isSpecial; }\n        void SetSpecialFlag(bool val) { _isSpecial = val; } // must only be set by SpecialPages!\n        \n        bool GetNormalFlag() { return _isNormal; }\n        void SetNormalFlag(bool val) { _isNormal = val; } // must only be set by NormalPages!\n        \n        bool GetUpdatedFlag() { return _isUpdated; }\n        void SetUpdatedFlag(bool val) { _isUpdated = val; } // must only be set by UpdatedPages!\n        \n        int GetUpdateCount() {return _updateCount;}\n        void IncrementUpdateCount();\n\n        /** Set the time when this carousel expires\n         *  which is the current time plus the cycle time\n         *  or the number of page cycles remaining\n         */\n        void SetTransitionTime(uint8_t cycleTime);\n\n        /** Used to time carousels\n         *  If StepCycles is set, decrement page cycle count\n         *  @return true if it is time to change carousel page\n         */\n        bool Expired(bool StepCycles=false);\n\n        bool LoadPage(std::string filename);\n        \n        bool GetLock();\n        void FreeLock();\n\n        /** Used to enable list->remove\n         */\n        bool operator==(const TTXPageStream& rhs) const;\n\n        // Todo: These are migrating to Page\n        void SetSelected(bool value){_Selected=value;}; /// Set the selected state to value\n        bool Selected(){return _Selected;}; /// Return the selected state\n        \n        void SetPacket29Flag(bool value){_loadedPacket29=value;}; // Used by PageList::CheckForPacket29OrCustomHeader\n        bool GetPacket29Flag(){return _loadedPacket29;}; // Used by PageList::DeleteOldPages\n        \n        void SetCustomHeaderFlag(bool value){_loadedCustomHeader=value;}; // Used by PageList::CheckForPacket29OrCustomHeader\n        bool GetCustomHeaderFlag(){return _loadedCustomHeader;};\n        \n    protected:\n        \n    private:\n        time_t _transitionTime; // Records when the next carousel transition is due\n        uint8_t _cyclesRemaining; // As above for cycle mode\n        \n        bool _loadedPacket29; // Packet 29 for magazine was loaded from this page. Should only be set on one page in each magazine.\n        \n        bool _loadedCustomHeader; // Custom header was loaded from this page. Should only be set on one page in each magazine.\n        \n        bool _Selected;   /// Marked as selected by the inserter P command\n\n        bool _isCarousel;\n        bool _isSpecial;\n        bool _isNormal;\n        bool _isUpdated;\n\n        int _updateCount; // update counter for special pages.\n        \n        bool _deleteFlag; // marks a page for deletion from the service and cannot be undone\n        \n        bool _isOneShot;\n        \n        std::shared_ptr<std::mutex> _mtx;\n};\n};\n#endif // _TTXPAGESTREAM_H_\n"
  },
  {
    "path": "update.sh",
    "content": "#!/bin/sh\nset -e\ncd `dirname \"$(readlink -f \"$0\")\"` # switch to vbit2 directory\necho Updating to latest stable version of VBIT2.\ngit fetch --tags\nlatestTag=`curl --silent \"https://api.github.com/repos/peterkvt80/vbit2/releases/latest\" | grep -Po '\"tag_name\": \"\\K.*?(?=\")'`\ngit checkout $latestTag --quiet # switch to latest \n\n# new version might need to perform tasks we don't know about, so run its postupdate script\n./postupdate.sh\n"
  },
  {
    "path": "updatedpages.cpp",
    "content": "\n#include \"updatedpages.h\"\n\nusing namespace vbit;\n\nUpdatedPages::UpdatedPages(int mag, PageList *pageList, Debug *debug) :\n    _mag(mag),\n    _pageList(pageList),\n    _debug(debug)\n{\n    _iter=_UpdatedPagesList.begin();\n    _page=nullptr;\n}\n\nUpdatedPages::~UpdatedPages()\n{\n\n}\n\nvoid UpdatedPages::addPage(std::shared_ptr<TTXPageStream> p)\n{\n    p->SetUpdatedFlag(true);\n    _UpdatedPagesList.push_back(p);\n}\n\nstd::shared_ptr<TTXPageStream> UpdatedPages::NextPage()\n{\n    if (_page == nullptr)\n    {\n        _iter=_UpdatedPagesList.begin();\n        _page = *_iter;\n    }\n    else\n    {\n        ++_iter;\n        _page = *_iter;\n    }\n\n    while(true)\n    {\n        if (_iter == _UpdatedPagesList.end())\n        {\n            _page = nullptr;\n            return _page;\n        }\n        else\n        {\n            _page = *_iter;\n        }\n        \n        if (_page)\n        {\n            if (_page->GetLock()) // try to lock this page against changes\n            {\n                // don't care if page has been marked for deletion/removal\n                // if we were put into this list, we want to get transmitted regardless\n                \n                _iter = _UpdatedPagesList.erase(_iter); // remove page from this list after transmitting it\n                _page->SetUpdatedFlag(false);\n                \n                if (_page->GetSubpage() != nullptr) // make sure there is a subpage\n                {\n                    return _page; // return page locked\n                }\n                _page->FreeLock(); // must unlock page again\n            }\n            else\n            {\n                // skip page\n                ++_iter;\n                _page = *_iter;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "updatedpages.h",
    "content": "#ifndef _UPDATEDPAGES_H\n#define _UPDATEDPAGES_H\n\n#include <list>\n\n#include \"debug.h\"\n#include \"ttxpagestream.h\"\n#include \"pagelist.h\"\n\n// list of updated pages\n\nnamespace vbit\n{\n\nclass UpdatedPages\n{\n    public:\n        /** Default constructor */\n        UpdatedPages(int mag, PageList *pageList, Debug *debug);\n        /** Default destructor */\n        virtual ~UpdatedPages();\n\n        std::shared_ptr<TTXPageStream> NextPage();\n\n        void addPage(std::shared_ptr<TTXPageStream> p);\n        \n        bool waiting(){ return _UpdatedPagesList.size() > 0; };\n\n    protected:\n\n    private:\n        int _mag;\n        PageList* _pageList;\n        Debug* _debug;\n        std::list<std::shared_ptr<TTXPageStream>> _UpdatedPagesList;\n        std::list<std::shared_ptr<TTXPageStream>>::iterator _iter;\n        std::shared_ptr<TTXPageStream> _page;\n        \n};\n\n}\n\n#endif // _UPDATEDPAGES_H\n"
  },
  {
    "path": "vbit2.cpp",
    "content": "#include \"vbit2.h\"\n\nusing namespace vbit;\n\nMasterClock *MasterClock::instance = 0; // initialise MasterClock singleton\n\n/* Options\n * --dir <path to pages>\n * Sets the pages directory and the location of vbit.conf.\n */\n\nint main(int argc, char** argv)\n{\n    #ifdef WIN32\n    _setmode(_fileno(stdout), _O_BINARY); // set stdout to binary mode stdout to avoid pesky line ending conversion\n    #endif\n    \n    Debug *debug=new Debug();\n    \n    /// @todo option of adding a non standard config path\n    Configure *configure=new Configure(debug, argc, argv);\n    \n    // attempt to use system locale for strftime\n    if (std::setlocale(LC_TIME, \"\") == nullptr)\n    {\n        debug->Log(Debug::LogLevels::logERROR,\"[main] Unable to set locale\");\n    }\n    \n    PageList *pageList=new PageList(configure, debug);\n    PacketServer *packetServer=new PacketServer(configure, debug);\n    InterfaceServer *interfaceServer=new InterfaceServer(configure, debug, pageList);\n\n    Service* svc=new Service(configure, debug, pageList, packetServer, interfaceServer); // Need to copy the subtitle packet source for Newfor\n\n    std::thread monitorThread(&FileMonitor::run, FileMonitor(configure, debug, pageList));\n    std::thread serviceThread(&Service::run, svc);\n\n    if (configure->GetPacketServerEnabled())\n    {\n        // only start packet server thread if required\n        std::thread packetServerThread(&PacketServer::run, packetServer );\n        packetServerThread.detach();\n    }\n\n    if (configure->GetInterfaceServerEnabled())\n    {\n        // only start interface server thread if required\n        std::thread interfaceServerThread(&InterfaceServer::run, interfaceServer );\n        interfaceServerThread.detach();\n    }\n\n    // The threads should never stop, but just in case...\n    monitorThread.join();\n    serviceThread.join();\n\n    return 0;\n}\n\n"
  },
  {
    "path": "vbit2.h",
    "content": "#ifndef _VBIT2_H_\n#define _VBIT2_H_\n\n#include <iostream>\n#include <thread>\n#include <clocale>\n#include \"service.h\"\n#include \"configure.h\"\n#include \"debug.h\"\n#include \"pagelist.h\"\n#include \"filemonitor.h\"\n#include \"packetServer.h\"\n#include \"interfaceServer.h\"\n#include \"masterClock.h\"\n\n#ifdef WIN32\n#include \"fcntl.h\"\n#endif\n\nnamespace vbit\n{\n    \n}\n\n#endif\n"
  },
  {
    "path": "vbit2.service",
    "content": "[Unit]\nDescription=VBIT2\n\n[Service]\nType=simple\nExecStart=%h/.local/bin/runvbit2\nRestart=always\n\n[Install]\nWantedBy=default.target\n"
  },
  {
    "path": "version.h",
    "content": "#ifndef _VERSION_H_\n#define _VERSION_H_\n\n#define VBIT2_VERSION \"2.8.2\"\n\n#endif\n"
  }
]