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