diff options
| author | Matthias P. Braendli <matthias.braendli@mpb.li> | 2023-11-21 22:12:14 +0100 | 
|---|---|---|
| committer | Matthias P. Braendli <matthias.braendli@mpb.li> | 2023-11-21 22:12:14 +0100 | 
| commit | 5fe36a627405b8fc65bdb212a6d505b9a6c8e489 (patch) | |
| tree | 8646d654e0467be8603c5ad37fb6cd89656dfd26 | |
| parent | 477ac4639a7c7f74f07a6164096fc7de102528ff (diff) | |
| parent | f84065c3cc6fff0edb771f85190f7228f4d740b6 (diff) | |
| download | dabmod-5fe36a627405b8fc65bdb212a6d505b9a6c8e489.tar.gz dabmod-5fe36a627405b8fc65bdb212a6d505b9a6c8e489.tar.bz2 dabmod-5fe36a627405b8fc65bdb212a6d505b9a6c8e489.zip | |
Merge branch 'dexter' into next
72 files changed, 4427 insertions, 1303 deletions
| @@ -15,6 +15,9 @@ config.h  config.h.in  config.status  odr-dabmod +*~ + +*.iq  __pycache__/  *.py[cod] diff --git a/Makefile.am b/Makefile.am index 39280fb..d29b530 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,7 +1,7 @@  # Copyright (C) 2007, 2008, 2009, 2010 Her Majesty the Queen in Right  # of Canada (Communications Research Center Canada)  # -#  Copyright (C) 2018 +#  Copyright (C) 2023  #  Matthias P. Braendli, matthias.braendli@mpb.li  #  #   http://opendigitalradio.org @@ -37,15 +37,17 @@ bin_PROGRAMS = odr-dabmod  odr_dabmod_CFLAGS   = -Wall -Isrc -Ilib \  					  $(GITVERSION_FLAGS) -odr_dabmod_CXXFLAGS = -Wall -Isrc -Ilib -std=c++11 \ +odr_dabmod_CXXFLAGS = -Wall -Isrc -Ilib \  					  $(GITVERSION_FLAGS) $(BOOST_CPPFLAGS) -odr_dabmod_LDADD    =  $(BOOST_LDFLAGS) $(BOOST_THREAD_LIB) +odr_dabmod_LDADD    =  $(BOOST_LDFLAGS) $(BOOST_THREAD_LIB) $(UHD_LIBS) $(LIMESDR_LIBS) $(ADDITIONAL_UHD_LIBS)  odr_dabmod_SOURCES  = src/DabMod.cpp \  					  src/PcDebug.h \  					  src/DabModulator.cpp \  					  src/DabModulator.h \  					  src/Buffer.cpp \  					  src/Buffer.h \ +					  src/CharsetTools.cpp \ +					  src/CharsetTools.h \  					  src/ConfigParser.cpp \  					  src/ConfigParser.h \  					  src/ModPlugin.cpp \ @@ -54,6 +56,10 @@ odr_dabmod_SOURCES  = src/DabMod.cpp \  					  src/EtiReader.h \  					  src/Eti.cpp \  					  src/Eti.h \ +					  src/Events.cpp \ +					  src/Events.h \ +					  src/FigParser.cpp \ +					  src/FigParser.h \  					  src/FicSource.cpp \  					  src/FicSource.h \  					  src/PuncturingRule.cpp \ @@ -75,7 +81,6 @@ odr_dabmod_SOURCES  = src/DabMod.cpp \  					  src/InputMemory.h \  					  src/InputReader.h \  					  src/InputTcpReader.cpp \ -					  src/InputZeroMQReader.cpp \  					  src/OutputFile.cpp \  					  src/OutputFile.h \  					  src/FrameMultiplexer.cpp \ @@ -99,6 +104,8 @@ odr_dabmod_SOURCES  = src/DabMod.cpp \  					  lib/RemoteControl.h \  					  lib/Log.cpp \  					  lib/Log.h \ +					  lib/Json.h \ +					  lib/Json.cpp \  					  lib/Globals.cpp \  					  lib/INIReader.h \  					  lib/crc.h \ @@ -123,10 +130,7 @@ odr_dabmod_SOURCES  = src/DabMod.cpp \  					  lib/edi/ETIDecoder.hpp \  					  lib/edi/ETIDecoder.cpp \  					  lib/edi/PFT.hpp \ -					  lib/edi/PFT.cpp - -if !COMPILE_FOR_EASYDABV3 -odr_dabmod_SOURCES += \ +					  lib/edi/PFT.cpp \  					  src/FIRFilter.cpp \  					  src/FIRFilter.h \  					  src/MemlessPoly.cpp \ @@ -138,6 +142,8 @@ odr_dabmod_SOURCES += \  					  src/output/SDR.cpp \  					  src/output/SDR.h \  					  src/output/SDRDevice.h \ +					  src/output/Dexter.cpp \ +					  src/output/Dexter.h \  					  src/output/Soapy.cpp \  					  src/output/Soapy.h \  					  src/output/UHD.cpp \ @@ -171,7 +177,5 @@ odr_dabmod_SOURCES += \  					  src/TII.cpp \  					  src/TII.h -odr_dabmod_LDADD   += $(UHD_LIBS) $(LIMESDR_LIBS) $(ADDITIONAL_UHD_LIBS) -endif  man_MANS = man/odr-dabmod.1 @@ -31,7 +31,7 @@ Features  - TII insertion  - Logging: log to file, to syslog  - EDI sources: TCP and UDP, both with and without Protection and Fragmentation Layer. -- ETI sources: ETI-over-TCP, file (Raw, Framed and Streamed) and ZeroMQ +- ETI sources: ETI-over-TCP, file (Raw, Framed and Streamed)  - A Telnet and ZeroMQ remote-control that can be used to change    some parameters during runtime and retrieve statistics.    See `doc/README-RC.md` for more information diff --git a/configure.ac b/configure.ac index 7590896..ba5d65a 100644 --- a/configure.ac +++ b/configure.ac @@ -1,7 +1,7 @@  # Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012 Her Majesty the  # Queen in Right of Canada (Communications Research Center Canada) -# Copyright (C) 2022 Matthias P. Braendli, http://opendigitalradio.org +# Copyright (C) 2023 Matthias P. Braendli, http://opendigitalradio.org  # This file is part of ODR-DabMod.  # @@ -34,7 +34,7 @@ AC_PROG_CC  AM_PROG_CC_C_O  AC_PROG_INSTALL -AX_CXX_COMPILE_STDCXX(11,noext,mandatory) +AX_CXX_COMPILE_STDCXX(17,noext,mandatory)  EXTRA=""  AC_ARG_ENABLE([prof], @@ -52,9 +52,9 @@ AC_ARG_ENABLE([zeromq],  AC_ARG_ENABLE([native],          [AS_HELP_STRING([--disable-native], [Do not compile with -march=native])],          [], [enable_native=yes]) -AC_ARG_ENABLE([easydabv3], -        [AS_HELP_STRING([--enable-easydabv3], [Build for EasyDABv3 board])], -        [], [enable_easydabv3=no]) +AC_ARG_ENABLE([dexter], +        [AS_HELP_STRING([--enable-dexter], [Build for PrecisionWave Dexter board])], +        [], [enable_dexter=no])  AC_ARG_ENABLE([limesdr],          [AS_HELP_STRING([--enable-limesdr], [Build for LimeSDR board])],          [], [enable_limesdr=no]) @@ -77,9 +77,7 @@ AX_CHECK_COMPILE_FLAG([-Wdouble-promotion], [CXXFLAGS="$CXXFLAGS -Wdouble-promot  AX_CHECK_COMPILE_FLAG(["-Wformat=2"], [CXXFLAGS="$CXXFLAGS -Wformat=2"], [], ["-Werror"])  AC_LANG_POP([C++]) - -AS_IF([test "x$enable_easydabv3" = "xno"], -    [PKG_CHECK_MODULES([FFTW], [fftw3f], [], [AC_MSG_ERROR([FFTW is required])])]) +PKG_CHECK_MODULES([FFTW], [fftw3f], [], [AC_MSG_ERROR([FFTW is required])])  echo "Checking zeromq" @@ -101,28 +99,27 @@ AS_IF([test "x$enable_trace" != "xno"],  # Define conditionals for Makefile.am  AM_CONDITIONAL([IS_GIT_REPO], [test -d '.git']) -AM_CONDITIONAL([COMPILE_FOR_EASYDABV3], [test "x$enable_easydabv3" = "xyes"])  # Defines for config.h  AX_PTHREAD([], AC_MSG_ERROR([requires pthread])) -AS_IF([test "x$enable_easydabv3" = "xno"], -    [PKG_CHECK_MODULES([SOAPYSDR], [SoapySDR], enable_soapysdr=yes, enable_soapysdr=no)]) +PKG_CHECK_MODULES([SOAPYSDR], [SoapySDR], enable_soapysdr=yes, enable_soapysdr=no)  AS_IF([test "x$enable_limesdr" = "xyes"],           [AC_CHECK_LIB([LimeSuite], [LMS_Init], [LIMESDR_LIBS="-lLimeSuite"],                         [AC_MSG_ERROR([LimeSDR LimeSuite is required])])]) +AS_IF([test "x$enable_dexter" = "xyes"], +         [AC_CHECK_LIB([iio], [iio_create_scan_context], [IIO_LIBS="-liio"], +                       [AC_MSG_ERROR([libiio is required])])]) +  AS_IF([test "x$enable_bladerf" = "xyes"],           [AC_CHECK_LIB([bladeRF], [bladerf_open], [BLADERF_LIBS="-lbladeRF"],                         [AC_MSG_ERROR([BladeRF library is required])])])  AC_SUBST([CFLAGS], ["$CFLAGS $EXTRA $FFTW_CFLAGS $SOAPYSDR_CFLAGS $PTHREAD_CFLAGS"])  AC_SUBST([CXXFLAGS], ["$CXXFLAGS $EXTRA $FFTW_CFLAGS $SOAPYSDR_CFLAGS $PTHREAD_CFLAGS"]) -AC_SUBST([LIBS], ["$FFTW_LIBS $SOAPYSDR_LIBS $PTHREAD_LIBS $ZMQ_LIBS $LIMESDR_LIBS $BLADERF_LIBS"]) - -AS_IF([test "x$enable_easydabv3" = "xyes" && test "x$enable_output_uhd" == "xyes"], -      AC_MSG_ERROR([Cannot enable both EasyDABv3 and UHD output])) +AC_SUBST([LIBS], ["$FFTW_LIBS $SOAPYSDR_LIBS $PTHREAD_LIBS $ZMQ_LIBS $LIMESDR_LIBS $IIO_LIBS $BLADERF_LIBS"])  # Checks for UHD.  AS_IF([test "x$enable_output_uhd" = "xyes"], @@ -144,12 +141,12 @@ AS_IF([test "x$enable_soapysdr" = "xyes"],  AS_IF([test "x$enable_limesdr" = "xyes"],        [AC_DEFINE(HAVE_LIMESDR, [1], [Define if LimeSDR output is enabled]) ]) +AS_IF([test "x$enable_dexter" = "xyes"], +      [AC_DEFINE(HAVE_DEXTER, [1], [Define if Dexter output is enabled])]) +  AS_IF([test "x$enable_bladerf" = "xyes"],        [AC_DEFINE(HAVE_BLADERF, [1], [Define if BladeRF output is enabled]) ]) -AS_IF([test "x$enable_easydabv3" = "xyes"], -      AC_DEFINE(BUILD_FOR_EASYDABV3, [1], [Define if we are building for EasyDABv3])) -  # Checks for header files.  AC_CHECK_HEADERS([fcntl.h limits.h memory.h netinet/in.h stdint.h stdlib.h string.h sys/time.h sys/timeb.h unistd.h]) @@ -217,7 +214,7 @@ echo "***********************************************"  echo  enabled=""  disabled="" -for feat in prof trace output_uhd zeromq soapysdr easydabv3 limesdr bladerf +for feat in prof trace output_uhd zeromq soapysdr limesdr bladerf dexter  do      eval var=\$enable_$feat      AS_IF([test "x$var" = "xyes"], diff --git a/doc/easydabv3.ini b/doc/easydabv3.ini deleted file mode 100644 index 5f0103f..0000000 --- a/doc/easydabv3.ini +++ /dev/null @@ -1,37 +0,0 @@ -; This sample configuration is useful if ODR-DabMod is compiled -; with --enable-easydabv3 - -[remotecontrol] -zmqctrl=1 -zmqctrlendpoint=tcp://127.0.0.1:9400 -; There is no telnet RC available in this build - - -[log] -syslog=0 -filelog=0 -filename=odr-dabmod.log - -[input] -transport=zeromq -source=tcp://localhost:9100 -max_frames_queued=400 - -; There are no [modulator], [cfr], [firfilter], [poly] nor [tii] sections - -[output] -output=file - -[fileoutput] -; to be confirmed -format=complexf - -filename=/dev/csdiof1 - -show_metadata=0 -; TODO add option for writing out timestamps to csdiof1 - -[delaymanagement] -synchronous=0 -mutenotimestamps=0 -offset=1.002 diff --git a/doc/example.ini b/doc/example.ini index aca7634..f009fbe 100644 --- a/doc/example.ini +++ b/doc/example.ini @@ -69,13 +69,6 @@ loop=0  ;transport=tcp  ;source=localhost:9200 -; When recieving data using ZeroMQ, the source is the URI to be used -;transport=zeromq -;source=tcp://localhost:9100 -; The option max_frames_queued defines the maximum number of ETI frames -; (frame duration: 24ms) that can be in the input queue -;max_frames_queued=100 -  [modulator]  ;   Mode 'fix' uses a fixed factor and is really not recommended. It is more  ; useful on an academic perspective for people trying to understand the DAB @@ -164,7 +157,7 @@ enabled=0  polycoeffile=polyCoefs  [output] -; choose output: possible values: uhd, file, zmq, soapysdr, limesdr, bladerf +; choose output: possible values: uhd, file, zmq, dexter, soapysdr, limesdr, bladerf  output=uhd  [fileoutput] @@ -322,6 +315,18 @@ channel=13C  ; Set to 0 to disable  ;dpd_port=50055 +[dexteroutput] +txgain=65535 + +; channel/frequency is applied to ad9957.center_frequency +;frequency=234208000 +channel=13C + +; lo offset is applied to dexter_dsp_tx.frequency0 +lo_offset=0 + +max_gps_holdover_time=3600 +  [limeoutput]  ; Lime output directly runs against the LMS device driver. It does not support SFN nor predistortion.  device= diff --git a/doc/receive_events.py b/doc/receive_events.py new file mode 100755 index 0000000..bfd6f86 --- /dev/null +++ b/doc/receive_events.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# +# This is an example program that shows +# how to receive runtime events from ODR-DabMod +# +# LICENSE: see bottom of file + +import sys +import zmq +import json +from pprint import pprint + +context = zmq.Context() +sock = context.socket(zmq.SUB) + +ep = "tcp://127.0.0.1:5556" +print(f"Receive from {ep}") +sock.connect(ep) + +# subscribe to all events +sock.setsockopt(zmq.SUBSCRIBE, bytes([])) + +while True: +    parts = sock.recv_multipart() +    if len(parts) == 2: +        print("Received event '{}'".format(parts[0].decode())) +        pprint(json.loads(parts[1].decode())) + +    else: +        print("Received strange event:") +        pprint(parts) + +    print() + + +# This is free and unencumbered software released into the public domain. +# +# Anyone is free to copy, modify, publish, use, compile, sell, or +# distribute this software, either in source code form or as a compiled +# binary, for any purpose, commercial or non-commercial, and by any +# means. +# +# In jurisdictions that recognize copyright laws, the author or authors +# of this software dedicate any and all copyright interest in the +# software to the public domain. We make this dedication for the benefit +# of the public at large and to the detriment of our heirs and +# successors. We intend this dedication to be an overt act of +# relinquishment in perpetuity of all present and future rights to this +# software under copyright law. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# For more information, please refer to <http://unlicense.org> diff --git a/doc/zmq-ctrl/json_remote_server.py b/doc/zmq-ctrl/json_remote_server.py new file mode 100755 index 0000000..728cf7c --- /dev/null +++ b/doc/zmq-ctrl/json_remote_server.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python +# +# This is an example program that illustrates +# how to interact with the zeromq remote control +# using JSON. +# +# LICENSE: see bottom of file + +import sys +import zmq +from pprint import pprint +import json +import re +from http.server import BaseHTTPRequestHandler, HTTPServer +import time + +re_url = re.compile(r"/([a-zA-Z0-9]+).json") + +ZMQ_REMOTE = "tcp://localhost:9400" +HTTP_HOSTNAME = "localhost" +HTTP_PORT = 8080 + +class DabMuxServer(BaseHTTPRequestHandler): +    def err500(self, message): +        self.send_response(500) +        self.send_header("Content-type", "application/json") +        self.end_headers() +        self.wfile.write(json.dumps({"error": message}).encode()) + +    def do_GET(self): +        m = re_url.match(self.path) +        if m: +            sock = context.socket(zmq.REQ) +            poller = zmq.Poller() +            poller.register(sock, zmq.POLLIN) +            sock.connect(ZMQ_REMOTE) + +            sock.send(b"ping") +            socks = dict(poller.poll(1000)) +            if socks: +                if socks.get(sock) == zmq.POLLIN: +                    data = sock.recv() +                    if data != b"ok": +                        print(f"Received {data} to ping!", file=sys.stderr) +                        self.err500("ping failure") +                        return +            else: +                print("ZMQ error: ping timeout", file=sys.stderr) +                self.err500("ping timeout") +                return + +            sock.send(b"showjson", flags=zmq.SNDMORE) +            sock.send(m.group(1).encode()) + +            socks = dict(poller.poll(1000)) +            if socks: +                if socks.get(sock) == zmq.POLLIN: +                    data = sock.recv_multipart() +                    print("Received: {}".format(len(data)), file=sys.stderr) +                    parts = [] +                    for i, part_data in enumerate(data): +                        part = part_data.decode() +                        print(" RX {}: {}".format(i, part.replace('\n',' ')), file=sys.stderr) + +                        if i == 0 and part != "fail": +                            self.send_response(200) +                            self.send_header("Content-type", "application/json") +                            self.end_headers() +                            self.wfile.write(part_data) +                            return +                        parts.append(part) +                    self.err500("data error " + " ".join(parts)) +                    return + +            else: +                print("ZMQ error: timeout", file=sys.stderr) +                self.err500("timeout") +                return +        else: +            self.send_response(200) +            self.send_header("Content-type", "text/html") +            self.end_headers() +            self.wfile.write("""<html><head><title>ODR-DabMod RC HTTP server</title></head>\n""".encode()) +            self.wfile.write("""<body>\n""".encode()) +            for mod in ("sdr", "tist", "modulator", "tii", "ofdm", "gain", "guardinterval"): +                self.wfile.write(f"""<p><a href="{mod}.json">{mod}.json</a></p>\n""".encode()) +            self.wfile.write("""</body></html>\n""".encode()) + + +if __name__ == "__main__": +    context = zmq.Context() + +    webServer = HTTPServer((HTTP_HOSTNAME, HTTP_PORT), DabMuxServer) +    print("Server started http://%s:%s" % (HTTP_HOSTNAME, HTTP_PORT)) + +    try: +        webServer.serve_forever() +    except KeyboardInterrupt: +        pass + +    webServer.server_close() + +    context.destroy(linger=5) + + +# This is free and unencumbered software released into the public domain. +# +# Anyone is free to copy, modify, publish, use, compile, sell, or +# distribute this software, either in source code form or as a compiled +# binary, for any purpose, commercial or non-commercial, and by any +# means. +# +# In jurisdictions that recognize copyright laws, the author or authors +# of this software dedicate any and all copyright interest in the +# software to the public domain. We make this dedication for the benefit +# of the public at large and to the detriment of our heirs and +# successors. We intend this dedication to be an overt act of +# relinquishment in perpetuity of all present and future rights to this +# software under copyright law. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# For more information, please refer to <http://unlicense.org> + + diff --git a/doc/zmq-ctrl/zmq_remote.py b/doc/zmq-ctrl/zmq_remote.py index 56465d3..7581575 100755 --- a/doc/zmq-ctrl/zmq_remote.py +++ b/doc/zmq-ctrl/zmq_remote.py @@ -16,7 +16,7 @@ poller = zmq.Poller()  poller.register(sock, zmq.POLLIN)  if len(sys.argv) < 2: -    print("Usage: program url cmd [args...]") +    print("Usage: program url cmd [args...]", file=sys.stderr)      sys.exit(1)  sock.connect(sys.argv[1]) @@ -25,7 +25,7 @@ message_parts = sys.argv[2:]  # first do a ping test -print("ping") +print("ping", file=sys.stderr)  sock.send(b"ping")  socks = dict(poller.poll(1000)) @@ -33,9 +33,9 @@ if socks:      if socks.get(sock) == zmq.POLLIN:          data = sock.recv_multipart() -        print("Received: {}".format(len(data))) +        print("Received: {}".format(len(data)), file=sys.stderr)          for i,part in enumerate(data): -            print("   {}".format(part)) +            print("   {}".format(part), file=sys.stderr)          for i, part in enumerate(message_parts):              if i == len(message_parts) - 1: @@ -43,18 +43,22 @@ if socks:              else:                  f = zmq.SNDMORE -            print("Send {}({}): '{}'".format(i, f, part)) +            print("Send {}({}): '{}'".format(i, f, part), file=sys.stderr)              sock.send(part.encode(), flags=f)          data = sock.recv_multipart() -        print("Received: {}".format(len(data))) -        for i,part in enumerate(data): -            print(" RX {}: {}".format(i, part.decode().replace('\n',' '))) +        print("Received: {}".format(len(data)), file=sys.stderr) +        for i, part in enumerate(data): +            if message_parts[0] == 'showjson': +                # This allows you to pipe the JSON into another tool +                print(part.decode()) +            else: +                print(" RX {}: {}".format(i, part.decode().replace('\n',' ')), file=sys.stderr)  else: -    print("ZMQ error: timeout") +    print("ZMQ error: timeout", file=sys.stderr)      context.destroy(linger=5)  # This is free and unencumbered software released into the public domain. diff --git a/lib/Json.cpp b/lib/Json.cpp new file mode 100644 index 0000000..4dc2f25 --- /dev/null +++ b/lib/Json.cpp @@ -0,0 +1,122 @@ +/* +   Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012 +   Her Majesty the Queen in Right of Canada (Communications Research +   Center Canada) + +   Copyright (C) 2023 +   Matthias P. Braendli, matthias.braendli@mpb.li + +    http://www.opendigitalradio.org + */ +/* +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU General Public License for more details. + +   You should have received a copy of the GNU General Public License +   along with this program.  If not, see <https://www.gnu.org/licenses/>. + */ +#include <string> +#include <iostream> +#include <sstream> +#include <iomanip> +#include <string> +#include <algorithm> + +#include "Json.h" + +namespace json { +    static std::string escape_json(const std::string &s) { +        std::ostringstream o; +        for (auto c = s.cbegin(); c != s.cend(); c++) { +            switch (*c) { +                case '"': o << "\\\""; break; +                case '\\': o << "\\\\"; break; +                case '\b': o << "\\b"; break; +                case '\f': o << "\\f"; break; +                case '\n': o << "\\n"; break; +                case '\r': o << "\\r"; break; +                case '\t': o << "\\t"; break; +                default: +                           if ('\x00' <= *c && *c <= '\x1f') { +                               o << "\\u" +                                   << std::hex << std::setw(4) << std::setfill('0') << static_cast<int>(*c); +                           } else { +                               o << *c; +                           } +            } +        } +        return o.str(); +    } + +    std::string map_to_json(const map_t& values) { +        std::ostringstream ss; +        ss << "{ "; +        size_t ix = 0; +        for (const auto& element : values) { +            if (ix > 0) { +                ss << ","; +            } + +            ss << "\"" << escape_json(element.first) << "\": "; +            ss << value_to_json(element.second); + +            ix++; +        } +        ss << " }"; + +        return ss.str(); +    } + +    std::string value_to_json(const value_t& value) +    { +        std::ostringstream ss; + +        if (std::holds_alternative<std::string>(value.v)) { +            ss << "\"" << escape_json(std::get<std::string>(value.v)) << "\""; +        } +        else if (std::holds_alternative<double>(value.v)) { +            ss << std::fixed << std::get<double>(value.v); +        } +        else if (std::holds_alternative<ssize_t>(value.v)) { +            ss << std::get<ssize_t>(value.v); +        } +        else if (std::holds_alternative<size_t>(value.v)) { +            ss << std::get<size_t>(value.v); +        } +        else if (std::holds_alternative<bool>(value.v)) { +            ss << (std::get<bool>(value.v) ? "true" : "false"); +        } +        else if (std::holds_alternative<std::nullopt_t>(value.v)) { +            ss << "null"; +        } +        else if (std::holds_alternative<std::vector<json::value_t> >(value.v)) { +            const auto& vec = std::get<std::vector<json::value_t> >(value.v); +            ss << "[ "; +            size_t list_ix = 0; +            for (const auto& list_element : vec) { +                if (list_ix > 0) { +                    ss << ","; +                } +                ss << value_to_json(list_element); +                list_ix++; +            } +            ss << "]"; +        } +        else if (std::holds_alternative<std::shared_ptr<json::map_t> >(value.v)) { +            const map_t& v = *std::get<std::shared_ptr<json::map_t> >(value.v); +            ss << map_to_json(v); +        } +        else { +            throw std::logic_error("variant alternative not handled"); +        } + +        return ss.str(); +    } +} diff --git a/lib/Json.h b/lib/Json.h new file mode 100644 index 0000000..65aa668 --- /dev/null +++ b/lib/Json.h @@ -0,0 +1,63 @@ +/* +   Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012 +   Her Majesty the Queen in Right of Canada (Communications Research +   Center Canada) + +   Copyright (C) 2023 +   Matthias P. Braendli, matthias.braendli@mpb.li + +    http://www.opendigitalradio.org + +   This module adds remote-control capability to some of the dabmux/dabmod modules. + */ +/* +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU General Public License for more details. + +   You should have received a copy of the GNU General Public License +   along with this program.  If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#ifdef HAVE_CONFIG_H +#  include "config.h" +#endif + +#include <vector> +#include <memory> +#include <optional> +#include <stdexcept> +#include <string> +#include <unordered_map> +#include <variant> + +namespace json { + +    // STL containers are not required to support incomplete types, +    // hence the shared_ptr + +    struct value_t { +        std::variant< +            std::shared_ptr<std::unordered_map<std::string, value_t>>, +            std::vector<value_t>, +            std::string, +            double, +            size_t, +            ssize_t, +            bool, +            std::nullopt_t> v; +    }; + +    using map_t = std::unordered_map<std::string, value_t>; + +    std::string map_to_json(const map_t& values); +    std::string value_to_json(const value_t& value); +} diff --git a/lib/RemoteControl.cpp b/lib/RemoteControl.cpp index 30dcb60..dca3373 100644 --- a/lib/RemoteControl.cpp +++ b/lib/RemoteControl.cpp @@ -25,6 +25,8 @@  #include <list>  #include <string>  #include <iostream> +#include <sstream> +#include <iomanip>  #include <string>  #include <algorithm> @@ -102,6 +104,18 @@ std::list< std::vector<std::string> > RemoteControllers::get_param_list_values(c      return allparams;  } + + +std::string RemoteControllers::get_showjson() { +    json::map_t root; +    for (auto &controllable : rcs.controllables) { +        root[controllable->get_rc_name()].v = +            std::make_shared<json::map_t>(controllable->get_all_values()); +    } + +    return json::map_to_json(root); +} +  std::string RemoteControllers::get_param(const std::string& name, const std::string& param) {      RemoteControllable* controllable = get_controllable_(name);      return controllable->get_parameter(param); @@ -123,7 +137,7 @@ RemoteControllable* RemoteControllers::get_controllable_(const std::string& name              [&](RemoteControllable* r) { return r->get_rc_name() == name; });      if (rc == controllables.end()) { -        throw ParameterError("Module name unknown"); +        throw ParameterError(string{"Module name '"} + name + "' unknown");      }      else {          return *rc; @@ -427,10 +441,15 @@ void RemoteControllerZmq::recv_all(zmq::socket_t& pSocket, std::vector<std::stri      bool more = true;      do {          zmq::message_t msg; -        pSocket.recv(msg); -        std::string incoming((char*)msg.data(), msg.size()); -        message.push_back(incoming); -        more = msg.more(); +        const auto zresult = pSocket.recv(msg); +        if (zresult) { +            std::string incoming((char*)msg.data(), msg.size()); +            message.push_back(incoming); +            more = msg.more(); +        } +        else { +            more = false; +        }      } while (more);  } @@ -457,6 +476,7 @@ void RemoteControllerZmq::send_fail_reply(zmq::socket_t &pSocket, const std::str  void RemoteControllerZmq::process()  {      m_fault = false; +    m_active = true;      // create zmq reply socket for receiving ctrl parameters      try { @@ -514,8 +534,21 @@ void RemoteControllerZmq::process()                          repSocket.send(zmsg, (--cohort_size > 0) ? zmq::send_flags::sndmore : zmq::send_flags::none);                      }                  } +                else if (msg.size() == 1 && command == "showjson") { +                    try { +                        std::string json = rcs.get_showjson(); + +                        zmq::message_t zmsg(json.size()); +                        memcpy(zmsg.data(), json.data(), json.size()); + +                        repSocket.send(zmsg, zmq::send_flags::none); +                    } +                    catch (const ParameterError &err) { +                        send_fail_reply(repSocket, err.what()); +                    } +                }                  else if (msg.size() == 2 && command == "show") { -                    std::string module((char*) msg[1].data(), msg[1].size()); +                    const std::string module((char*) msg[1].data(), msg[1].size());                      try {                          list< vector<string> > r = rcs.get_param_list_values(module);                          size_t r_size = r.size(); @@ -533,8 +566,8 @@ void RemoteControllerZmq::process()                      }                  }                  else if (msg.size() == 3 && command == "get") { -                    std::string module((char*) msg[1].data(), msg[1].size()); -                    std::string parameter((char*) msg[2].data(), msg[2].size()); +                    const std::string module((char*) msg[1].data(), msg[1].size()); +                    const std::string parameter((char*) msg[2].data(), msg[2].size());                      try {                          std::string value = rcs.get_param(module, parameter); @@ -547,9 +580,9 @@ void RemoteControllerZmq::process()                      }                  }                  else if (msg.size() == 4 && command == "set") { -                    std::string module((char*) msg[1].data(), msg[1].size()); -                    std::string parameter((char*) msg[2].data(), msg[2].size()); -                    std::string value((char*) msg[3].data(), msg[3].size()); +                    const std::string module((char*) msg[1].data(), msg[1].size()); +                    const std::string parameter((char*) msg[2].data(), msg[2].size()); +                    const std::string value((char*) msg[3].data(), msg[3].size());                      try {                          rcs.set_param(module, parameter, value); @@ -561,7 +594,7 @@ void RemoteControllerZmq::process()                  }                  else {                      send_fail_reply(repSocket, -                            "Unsupported command. commands: list, show, get, set"); +                            "Unsupported command. commands: list, show, get, set, showjson");                  }              }          } diff --git a/lib/RemoteControl.h b/lib/RemoteControl.h index 2358b3a..26f30d9 100644 --- a/lib/RemoteControl.h +++ b/lib/RemoteControl.h @@ -3,7 +3,7 @@     Her Majesty the Queen in Right of Canada (Communications Research     Center Canada) -   Copyright (C) 2019 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://www.opendigitalradio.org @@ -36,6 +36,8 @@  #endif  #include <list> +#include <unordered_map> +#include <variant>  #include <map>  #include <memory>  #include <string> @@ -46,6 +48,7 @@  #include "Log.h"  #include "Socket.h" +#include "Json.h"  #define RC_ADD_PARAMETER(p, desc) {   \    std::vector<std::string> p; \ @@ -113,13 +116,13 @@ class RemoteControllable {              }          /* Base function to set parameters. */ -        virtual void set_parameter( -                const std::string& parameter, -                const std::string& value) = 0; +        virtual void set_parameter(const std::string& parameter, const std::string& value) = 0;          /* Getting a parameter always returns a string. */          virtual const std::string get_parameter(const std::string& parameter) const = 0; +        virtual const json::map_t get_all_values() const = 0; +      protected:          std::string m_rc_name;          std::list< std::vector<std::string> > m_parameters; @@ -135,6 +138,7 @@ class RemoteControllers {          void check_faults();          std::list< std::vector<std::string> > get_param_list_values(const std::string& name);          std::string get_param(const std::string& name, const std::string& param); +        std::string get_showjson();          void set_param(                  const std::string& name, diff --git a/lib/Socket.cpp b/lib/Socket.cpp index 10ec1ca..b71c01e 100644 --- a/lib/Socket.cpp +++ b/lib/Socket.cpp @@ -893,7 +893,7 @@ ssize_t TCPClient::recv(void *buffer, size_t length, int flags, int timeout_ms)          return 0;      } -    return 0; +    throw std::logic_error("unreachable");  }  void TCPClient::reconnect() diff --git a/lib/ThreadsafeQueue.h b/lib/ThreadsafeQueue.h index 815dfe0..8b385d6 100644 --- a/lib/ThreadsafeQueue.h +++ b/lib/ThreadsafeQueue.h @@ -2,7 +2,7 @@     Copyright (C) 2007, 2008, 2009, 2010, 2011 Her Majesty the Queen in     Right of Canada (Communications Research Center Canada) -   Copyright (C) 2018 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li     An implementation for a threadsafe queue, depends on C++11 @@ -32,6 +32,7 @@  #include <condition_variable>  #include <queue>  #include <utility> +#include <cassert>  /* This queue is meant to be used by two threads. One producer   * that pushes elements into the queue, and one consumer that @@ -69,7 +70,6 @@ public:          }          size_t queue_size = the_queue.size();          lock.unlock(); -          the_rx_notification.notify_one();          return queue_size; @@ -93,11 +93,57 @@ public:          return queue_size;      } +    struct push_overflow_result { bool overflowed; size_t new_size; }; + +    /* Push one element into the queue, and if queue is +     * full remove one element from the other end. +     * +     * max_size == 0 is not allowed. +     * +     * returns the new queue size and a flag if overflow occurred. +     */ +    push_overflow_result push_overflow(T const& val, size_t max_size) +    { +        assert(max_size > 0); +        std::unique_lock<std::mutex> lock(the_mutex); + +        bool overflow = false; +        while (the_queue.size() >= max_size) { +            overflow = true; +            the_queue.pop(); +        } +        the_queue.push(val); +        const size_t queue_size = the_queue.size(); +        lock.unlock(); + +        the_rx_notification.notify_one(); + +        return {overflow, queue_size}; +    } + +    push_overflow_result push_overflow(T&& val, size_t max_size) +    { +        assert(max_size > 0); +        std::unique_lock<std::mutex> lock(the_mutex); + +        bool overflow = false; +        while (the_queue.size() >= max_size) { +            overflow = true; +            the_queue.pop(); +        } +        the_queue.emplace(std::move(val)); +        const size_t queue_size = the_queue.size(); +        lock.unlock(); + +        the_rx_notification.notify_one(); + +        return {overflow, queue_size}; +    } + +      /* Push one element into the queue, but wait until the       * queue size goes below the threshold.       * -     * Notify waiting thread. -     *       * returns the new queue size.       */      size_t push_wait_if_full(T const& val, size_t threshold) diff --git a/src/Buffer.cpp b/src/Buffer.cpp index 20f71f9..2c4b171 100644 --- a/src/Buffer.cpp +++ b/src/Buffer.cpp @@ -28,6 +28,7 @@  #include "Buffer.h"  #include "PcDebug.h" +#include <unistd.h>  #include <string>  #include <stdexcept>  #include <cstdlib> diff --git a/src/Buffer.h b/src/Buffer.h index d181a46..af52e93 100644 --- a/src/Buffer.h +++ b/src/Buffer.h @@ -31,7 +31,6 @@  #   include <config.h>  #endif -#include <unistd.h>  #include <vector>  #include <memory> diff --git a/src/CharsetTools.cpp b/src/CharsetTools.cpp new file mode 100644 index 0000000..d35c121 --- /dev/null +++ b/src/CharsetTools.cpp @@ -0,0 +1,143 @@ +/* +   Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty +   the Queen in Right of Canada (Communications Research Center Canada) + +   Most parts of this file are taken from dablin, +   Copyright (C) 2015-2022 Stefan Pöschel + +   Copyright (C) 2023 +   Matthias P. Braendli, matthias.braendli@mpb.li + +    http://opendigitalradio.org + */ +/* +   This file is part of ODR-DabMod. + +   ODR-DabMod is free software: you can redistribute it and/or modify +   it under the terms of the GNU General Public License as +   published by the Free Software Foundation, either version 3 of the +   License, or (at your option) any later version. + +   ODR-DabMod is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU General Public License for more details. + +   You should have received a copy of the GNU General Public License +   along with ODR-DabMod.  If not, see <http://www.gnu.org/licenses/>. + */ + +#include <vector> +#include <algorithm> +#include <stdexcept> +#include <string> +#include <ctime> +#include <cstdint> +#include <cstdlib> +#include <cstring> +#include "CharsetTools.h" + +// --- CharsetTools ----------------------------------------------------------------- +const char* CharsetTools::no_char = ""; +const char* CharsetTools::ebu_values_0x00_to_0x1F[] = { +        no_char , "\u0118", "\u012E", "\u0172", "\u0102", "\u0116", "\u010E", "\u0218", "\u021A", "\u010A", no_char , no_char , "\u0120", "\u0139" , "\u017B", "\u0143", +        "\u0105", "\u0119", "\u012F", "\u0173", "\u0103", "\u0117", "\u010F", "\u0219", "\u021B", "\u010B", "\u0147", "\u011A", "\u0121", "\u013A", "\u017C", no_char +}; +const char* CharsetTools::ebu_values_0x7B_to_0xFF[] = { +        /* starting some chars earlier than 0x80 -----> */                                                            "\u00AB", "\u016F", "\u00BB", "\u013D", "\u0126", +        "\u00E1", "\u00E0", "\u00E9", "\u00E8", "\u00ED", "\u00EC", "\u00F3", "\u00F2", "\u00FA", "\u00F9", "\u00D1", "\u00C7", "\u015E", "\u00DF", "\u00A1", "\u0178", +        "\u00E2", "\u00E4", "\u00EA", "\u00EB", "\u00EE", "\u00EF", "\u00F4", "\u00F6", "\u00FB", "\u00FC", "\u00F1", "\u00E7", "\u015F", "\u011F", "\u0131", "\u00FF", +        "\u0136", "\u0145", "\u00A9", "\u0122", "\u011E", "\u011B", "\u0148", "\u0151", "\u0150", "\u20AC", "\u00A3", "\u0024", "\u0100", "\u0112", "\u012A", "\u016A", +        "\u0137", "\u0146", "\u013B", "\u0123", "\u013C", "\u0130", "\u0144", "\u0171", "\u0170", "\u00BF", "\u013E", "\u00B0", "\u0101", "\u0113", "\u012B", "\u016B", +        "\u00C1", "\u00C0", "\u00C9", "\u00C8", "\u00CD", "\u00CC", "\u00D3", "\u00D2", "\u00DA", "\u00D9", "\u0158", "\u010C", "\u0160", "\u017D", "\u00D0", "\u013F", +        "\u00C2", "\u00C4", "\u00CA", "\u00CB", "\u00CE", "\u00CF", "\u00D4", "\u00D6", "\u00DB", "\u00DC", "\u0159", "\u010D", "\u0161", "\u017E", "\u0111", "\u0140", +        "\u00C3", "\u00C5", "\u00C6", "\u0152", "\u0177", "\u00DD", "\u00D5", "\u00D8", "\u00DE", "\u014A", "\u0154", "\u0106", "\u015A", "\u0179", "\u0164", "\u00F0", +        "\u00E3", "\u00E5", "\u00E6", "\u0153", "\u0175", "\u00FD", "\u00F5", "\u00F8", "\u00FE", "\u014B", "\u0155", "\u0107", "\u015B", "\u017A", "\u0165", "\u0127" +}; + +std::string CharsetTools::ConvertCharEBUToUTF8(const uint8_t value) { +    // convert via LUT +    if(value <= 0x1F) +        return ebu_values_0x00_to_0x1F[value]; +    if(value >= 0x7B) +        return ebu_values_0x7B_to_0xFF[value - 0x7B]; + +    // convert by hand (avoiding a LUT with mostly 1:1 mapping) +    switch(value) { +    case 0x24: +        return "\u0142"; +    case 0x5C: +        return "\u016E"; +    case 0x5E: +        return "\u0141"; +    case 0x60: +        return "\u0104"; +    } + +    // leave untouched +    return std::string((char*) &value, 1); +} + + +std::string CharsetTools::ConvertTextToUTF8(const uint8_t *data, size_t len, int charset, std::string* charset_name) { +    // remove undesired chars +    std::vector<uint8_t> cleaned_data; +    for(size_t i = 0; i < len; i++) { +        switch(data[i]) { +        case 0x00:  // NULL +        case 0x0A:  // PLB +        case 0x0B:  // EoH +        case 0x1F:  // PWB +            continue; +        default: +            cleaned_data.push_back(data[i]); +        } +    } + +    // convert characters +    if(charset == 0b0000) {         // EBU Latin based +        if(charset_name) +            *charset_name = "EBU Latin based"; + +        std::string result; +        for(const uint8_t& c : cleaned_data) +            result += ConvertCharEBUToUTF8(c); +        return result; +    } + +    if(charset == 0b1111) {         // UTF-8 +        if(charset_name) +            *charset_name = "UTF-8"; + +        return std::string((char*) &cleaned_data[0], cleaned_data.size()); +    } + +    // ignore unsupported charset +    return ""; +} + + +size_t StringTools::UTF8CharsLen(const std::string &s, size_t chars) { +    size_t result; +    for(result = 0; result < s.size(); result++) { +        // if not a continuation byte, handle counter +        if((s[result] & 0xC0) != 0x80) { +            if(chars == 0) +                break; +            chars--; +        } +    } +    return result; +} + +size_t StringTools::UTF8Len(const std::string &s) { +    // ignore continuation bytes +    return std::count_if(s.cbegin(), s.cend(), [](const char c){return (c & 0xC0) != 0x80;}); +} + +std::string StringTools::UTF8Substr(const std::string &s, size_t pos, size_t count) { +    std::string result = s; +    result.erase(0, UTF8CharsLen(result, pos)); +    result.erase(UTF8CharsLen(result, count)); +    return result; +} diff --git a/src/CharsetTools.h b/src/CharsetTools.h new file mode 100644 index 0000000..f86692f --- /dev/null +++ b/src/CharsetTools.h @@ -0,0 +1,58 @@ +/* +   Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty +   the Queen in Right of Canada (Communications Research Center Canada) + +   Most parts of this file are taken from dablin, +   Copyright (C) 2015-2022 Stefan Pöschel + +   Copyright (C) 2023 +   Matthias P. Braendli, matthias.braendli@mpb.li + +    http://opendigitalradio.org + */ +/* +   This file is part of ODR-DabMod. + +   ODR-DabMod is free software: you can redistribute it and/or modify +   it under the terms of the GNU General Public License as +   published by the Free Software Foundation, either version 3 of the +   License, or (at your option) any later version. + +   ODR-DabMod is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU General Public License for more details. + +   You should have received a copy of the GNU General Public License +   along with ODR-DabMod.  If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once +#include <vector> +#include <stdexcept> +#include <string> +#include <ctime> +#include <cstdint> +#include <cstdlib> +#include <cstring> + +class CharsetTools { +    private: +        static const char* no_char; +        static const char* ebu_values_0x00_to_0x1F[]; +        static const char* ebu_values_0x7B_to_0xFF[]; +        static std::string ConvertCharEBUToUTF8(const uint8_t value); +    public: +        static std::string ConvertTextToUTF8(const uint8_t *data, size_t len, int charset, std::string* charset_name); +}; + +typedef std::vector<std::string> string_vector_t; + +// --- StringTools ----------------------------------------------------------------- +class StringTools { +private: +	static size_t UTF8CharsLen(const std::string &s, size_t chars); +public: +	static size_t UTF8Len(const std::string &s); +	static std::string UTF8Substr(const std::string &s, size_t pos, size_t count); +}; diff --git a/src/ConfigParser.cpp b/src/ConfigParser.cpp index ee7acc3..1219ae7 100644 --- a/src/ConfigParser.cpp +++ b/src/ConfigParser.cpp @@ -3,7 +3,7 @@     Her Majesty the Queen in Right of Canada (Communications Research     Center Canada) -   Copyright (C) 2018 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -37,6 +37,7 @@  #include "ConfigParser.h"  #include "Utils.h"  #include "Log.h" +#include "Events.h"  #include "DabModulator.h"  #include "output/SDR.h" @@ -113,14 +114,17 @@ static void parse_configfile(      }      mod_settings.inputTransport = pt.Get("input.transport", "file"); -    mod_settings.inputMaxFramesQueued = pt.GetInteger("input.max_frames_queued", -            ZMQ_INPUT_MAX_FRAME_QUEUE); -    mod_settings.edi_max_delay_ms = pt.GetReal("input.edi_max_delay", 0.0f); +    mod_settings.edi_max_delay_ms = pt.GetReal("input.edi_max_delay", 0.0);      mod_settings.inputName = pt.Get("input.source", "/dev/stdin");      // log parameters: +    const string events_endpoint = pt.Get("log.events_endpoint", ""); +    if (not events_endpoint.empty()) { +        events.bind(events_endpoint); +    } +      if (pt.GetInteger("log.syslog", 0) == 1) {          etiLog.register_backend(make_shared<LogToSyslog>());      } @@ -247,7 +251,7 @@ static void parse_configfile(              throw std::runtime_error("Configuration error");          }          else if (sdr_device_config.frequency == 0) { -            sdr_device_config.frequency = parseChannel(chan); +            sdr_device_config.frequency = parse_channel(chan);          }          else if (sdr_device_config.frequency != 0 && chan != "") {              std::cerr << "       UHD output: cannot define both frequency and channel.\n"; @@ -280,7 +284,8 @@ static void parse_configfile(          mod_settings.sdr_device_config = sdr_device_config;          mod_settings.useUHDOutput = true;      } -#endif +#endif // defined(HAVE_OUTPUT_UHD) +  #if defined(HAVE_SOAPYSDR)      else if (output_selected == "soapysdr") {          auto& outputsoapy_conf = mod_settings.sdr_device_config; @@ -300,7 +305,7 @@ static void parse_configfile(              throw std::runtime_error("Configuration error");          }          else if (outputsoapy_conf.frequency == 0) { -            outputsoapy_conf.frequency =  parseChannel(chan); +            outputsoapy_conf.frequency = parse_channel(chan);          }          else if (outputsoapy_conf.frequency != 0 && chan != "") {              std::cerr << "       soapy output: cannot define both frequency and channel.\n"; @@ -311,7 +316,34 @@ static void parse_configfile(          mod_settings.useSoapyOutput = true;      } -#endif +#endif // defined(HAVE_SOAPYSDR) + +#if defined(HAVE_DEXTER) +    else if (output_selected == "dexter") { +        auto& outputdexter_conf = mod_settings.sdr_device_config; +        outputdexter_conf.txgain = pt.GetReal("dexteroutput.txgain", 0.0); +        outputdexter_conf.lo_offset = pt.GetReal("dexteroutput.lo_offset", 0.0); +        outputdexter_conf.frequency = pt.GetReal("dexteroutput.frequency", 0); +        std::string chan = pt.Get("dexteroutput.channel", ""); +        outputdexter_conf.dabMode = mod_settings.dabMode; +        outputdexter_conf.maxGPSHoldoverTime = pt.GetInteger("dexteroutput.max_gps_holdover_time", 0); + +        if (outputdexter_conf.frequency == 0 && chan == "") { +            std::cerr << "       dexter output enabled, but neither frequency nor channel defined.\n"; +            throw std::runtime_error("Configuration error"); +        } +        else if (outputdexter_conf.frequency == 0) { +            outputdexter_conf.frequency = parse_channel(chan); +        } +        else if (outputdexter_conf.frequency != 0 && chan != "") { +            std::cerr << "       dexter output: cannot define both frequency and channel.\n"; +            throw std::runtime_error("Configuration error"); +        } + +        mod_settings.useDexterOutput = true; +    } +#endif // defined(HAVE_DEXTER) +  #if defined(HAVE_LIMESDR)      else if (output_selected == "limesdr") {          auto& outputlime_conf = mod_settings.sdr_device_config; @@ -330,7 +362,7 @@ static void parse_configfile(              throw std::runtime_error("Configuration error");          }          else if (outputlime_conf.frequency == 0) { -            outputlime_conf.frequency =  parseChannel(chan); +            outputlime_conf.frequency = parse_channel(chan);          }          else if (outputlime_conf.frequency != 0 && chan != "") {              std::cerr << "       Lime output: cannot define both frequency and channel.\n"; @@ -341,7 +373,7 @@ static void parse_configfile(          mod_settings.useLimeOutput = true;      } -#endif +#endif // defined(HAVE_LIMESDR)  #if defined(HAVE_BLADERF)      else if (output_selected == "bladerf") { @@ -359,7 +391,7 @@ static void parse_configfile(              throw std::runtime_error("Configuration error");          }          else if (outputbladerf_conf.frequency == 0) { -            outputbladerf_conf.frequency =  parseChannel(chan); +            outputbladerf_conf.frequency = parse_channel(chan);          }          else if (outputbladerf_conf.frequency != 0 && chan != "") {              std::cerr << "       BladeRF output: cannot define both frequency and channel.\n"; @@ -370,7 +402,7 @@ static void parse_configfile(          mod_settings.useBladeRFOutput = true;      } -#endif +#endif // defined(HAVE_BLADERF)  #if defined(HAVE_ZEROMQ)      else if (output_selected == "zmq") { @@ -385,7 +417,7 @@ static void parse_configfile(      } -#if defined(HAVE_OUTPUT_UHD) +#if defined(HAVE_OUTPUT_UHD) || defined(HAVE_DEXTER)      mod_settings.sdr_device_config.enableSync = (pt.GetInteger("delaymanagement.synchronous", 0) == 1);      mod_settings.sdr_device_config.muteNoTimestamps = (pt.GetInteger("delaymanagement.mutenotimestamps", 0) == 1);      if (mod_settings.sdr_device_config.enableSync) { @@ -406,7 +438,6 @@ static void parse_configfile(              throw std::runtime_error("Configuration error");          }      } -  #endif @@ -551,8 +582,7 @@ void parse_args(int argc, char **argv, mod_settings_t& mod_settings)              if (mod_settings.inputName.substr(0, 4) == "zmq+" &&                  mod_settings.inputName.find("://") != std::string::npos) { -                // if the name starts with zmq+XYZ://somewhere:port -                mod_settings.inputTransport = "zeromq"; +                throw std::runtime_error("Support for ZeroMQ input transport has been removed.");              }              else if (mod_settings.inputName.substr(0, 6) == "tcp://") {                  mod_settings.inputTransport = "tcp"; diff --git a/src/ConfigParser.h b/src/ConfigParser.h index 574caa2..d35432b 100644 --- a/src/ConfigParser.h +++ b/src/ConfigParser.h @@ -3,7 +3,7 @@     Her Majesty the Queen in Right of Canada (Communications Research     Center Canada) -   Copyright (C) 2017 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -40,8 +40,6 @@  #include "output/Lime.h"  #include "output/BladeRF.h" -#define ZMQ_INPUT_MAX_FRAME_QUEUE 500 -  struct mod_settings_t {      std::string outputName;      bool useZeroMQOutput = false; @@ -51,9 +49,9 @@ struct mod_settings_t {      bool fileOutputShowMetadata = false;      bool useUHDOutput = false;      bool useSoapyOutput = false; +    bool useDexterOutput = false;      bool useLimeOutput = false;      bool useBladeRFOutput = false; -    const std::string BladeRFOutputFormat = "s16"; // to transmit SC16 IQ      size_t outputRate = 2048000;      size_t clockRate = 0; @@ -69,7 +67,6 @@ struct mod_settings_t {      bool loop = false;      std::string inputName = "";      std::string inputTransport = "file"; -    unsigned inputMaxFramesQueued = ZMQ_INPUT_MAX_FRAME_QUEUE;      float edi_max_delay_ms = 0.0f;      tii_config_t tiiConfig; @@ -87,9 +84,7 @@ struct mod_settings_t {      // Settings for the OFDM windowing      size_t ofdmWindowOverlap = 0; -#if defined(HAVE_OUTPUT_UHD) || defined(HAVE_SOAPYSDR) || defined(HAVE_LIMESDR) || defined(HAVE_BLADERF)      Output::SDRDeviceConfig sdr_device_config; -#endif      bool showProcessTime = true;  }; diff --git a/src/DabMod.cpp b/src/DabMod.cpp index f97c05d..e0e8a5b 100644 --- a/src/DabMod.cpp +++ b/src/DabMod.cpp @@ -3,7 +3,7 @@     Her Majesty the Queen in Right of Canada (Communications Research     Center Canada) -   Copyright (C) 2019 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -25,6 +25,7 @@     along with ODR-DabMod.  If not, see <http://www.gnu.org/licenses/>.   */ +#include <fftw3.h>  #ifdef HAVE_CONFIG_H  #   include "config.h"  #endif @@ -46,6 +47,7 @@  #   include <netinet/in.h>  #endif +#include "Events.h"  #include "Utils.h"  #include "Log.h"  #include "DabModulator.h" @@ -56,6 +58,7 @@  #include "output/SDR.h"  #include "output/UHD.h"  #include "output/Soapy.h" +#include "output/Dexter.h"  #include "output/Lime.h"  #include "output/BladeRF.h"  #include "OutputZeroMQ.h" @@ -93,18 +96,147 @@ void signalHandler(int signalNb)      running = 0;  } -struct modulator_data -{ -    // For ETI -    std::shared_ptr<InputReader> inputReader; -    std::shared_ptr<EtiReader> etiReader; +class ModulatorData : public RemoteControllable { +    public: +        // For ETI +        std::shared_ptr<InputReader> inputReader; +        std::shared_ptr<EtiReader> etiReader; + +        // For EDI +        std::shared_ptr<EdiInput> ediInput; + +        // Common to both EDI and EDI +        uint64_t framecount = 0; +        Flowgraph *flowgraph = nullptr; + + +        // RC-related +        ModulatorData() : RemoteControllable("mainloop") { +            RC_ADD_PARAMETER(num_modulator_restarts, "(Read-only) Number of mod restarts"); +            RC_ADD_PARAMETER(most_recent_edi_decoded, "(Read-only) UNIX Timestamp of most recently decoded EDI frame"); +            RC_ADD_PARAMETER(edi_source, "(Read-only) URL of the EDI/TCP source"); +            RC_ADD_PARAMETER(running_since, "(Read-only) UNIX Timestamp of most recent modulator restart"); +            RC_ADD_PARAMETER(ensemble_label, "(Read-only) Label of the ensemble"); +            RC_ADD_PARAMETER(ensemble_eid, "(Read-only) Ensemble ID"); +            RC_ADD_PARAMETER(ensemble_services, "(Read-only, only JSON) Ensemble service information"); +            RC_ADD_PARAMETER(num_services, "(Read-only) Number of services in the ensemble"); +        } -    // For EDI -    std::shared_ptr<EdiInput> ediInput; +        virtual ~ModulatorData() {} + +        virtual void set_parameter(const std::string& parameter, const std::string& value) { +            throw ParameterError("Parameter " + parameter + " is read-only"); +        } -    // Common to both EDI and EDI -    uint64_t framecount = 0; -    Flowgraph *flowgraph = nullptr; +        virtual const std::string get_parameter(const std::string& parameter) const { +            stringstream ss; +            if (parameter == "num_modulator_restarts") { +                ss << num_modulator_restarts; +            } +            if (parameter == "running_since") { +                ss << running_since; +            } +            else if (parameter == "most_recent_edi_decoded") { +                ss << most_recent_edi_decoded; +            } +            else if (parameter == "ensemble_label") { +                if (ediInput) { +                    const auto ens = ediInput->ediReader.getEnsembleInfo(); +                    if (ens) { +                        ss << FICDecoder::ConvertLabelToUTF8(ens->label, nullptr); +                    } +                    else { +                        throw ParameterError("Not available yet"); +                    } +                } +                else { +                    throw ParameterError("Not available yet"); +                } +            } +            else if (parameter == "ensemble_eid") { +                if (ediInput) { +                    const auto ens = ediInput->ediReader.getEnsembleInfo(); +                    if (ens) { +                        ss << ens->eid; +                    } +                    else { +                        throw ParameterError("Not available yet"); +                    } +                } +                else { +                    throw ParameterError("Not available yet"); +                } +            } +            else if (parameter == "edi_source") { +                if (ediInput) { +                    ss << ediInput->ediTransport.getTcpUri(); +                } +                else { +                    throw ParameterError("Not available yet"); +                } +            } +            else if (parameter == "num_services") { +                if (ediInput) { +                    ss << ediInput->ediReader.getSubchannels().size(); +                } +                else { +                    throw ParameterError("Not available yet"); +                } +            } +            else if (parameter == "ensemble_services") { +                throw ParameterError("ensemble_services is only available through 'showjson'"); +            } +            else { +                ss << "Parameter '" << parameter << +                    "' is not exported by controllable " << get_rc_name(); +                throw ParameterError(ss.str()); +            } +            return ss.str(); +        } + +        virtual const json::map_t get_all_values() const +        { +            json::map_t map; +            map["num_modulator_restarts"].v = num_modulator_restarts; +            map["running_since"].v = running_since; +            map["most_recent_edi_decoded"].v = most_recent_edi_decoded; + +            if (ediInput) { +                map["edi_source"].v = ediInput->ediTransport.getTcpUri(); +                map["num_services"].v = ediInput->ediReader.getSubchannels().size(); + +                const auto ens = ediInput->ediReader.getEnsembleInfo(); +                if (ens) { +                    map["ensemble_label"].v = FICDecoder::ConvertLabelToUTF8(ens->label, nullptr); +                    map["ensemble_eid"].v = ens->eid; +                } +                else { +                    map["ensemble_label"].v = nullopt; +                    map["ensemble_eid"].v = nullopt; +                } + +                std::vector<json::value_t> services; + +                for (const auto& s : ediInput->ediReader.getServiceInfo()) { +                    auto service_map = make_shared<json::map_t>(); +                    (*service_map)["sad"].v = s.second.subchannel.start; +                    (*service_map)["sid"].v = s.second.sid; +                    (*service_map)["label"].v = FICDecoder::ConvertLabelToUTF8(s.second.label, nullptr); +                    (*service_map)["bitrate"].v = s.second.subchannel.bitrate; +                    json::value_t v; +                    v.v = service_map; +                    services.push_back(v); +                } + +                map["ensemble_services"].v = services; + +            } +            return map; +        } + +        size_t num_modulator_restarts = 0; +        time_t most_recent_edi_decoded = 0; +        time_t running_since = 0;  };  enum class run_modulator_state_t { @@ -114,86 +246,10 @@ enum class run_modulator_state_t {      reconfigure // Some sort of change of configuration we cannot handle happened  }; -static run_modulator_state_t run_modulator(modulator_data& m); +static run_modulator_state_t run_modulator(const mod_settings_t& mod_settings, ModulatorData& m); -static void printModSettings(const mod_settings_t& mod_settings) -{ -    stringstream ss; -    // Print settings -    ss << "Input\n"; -    ss << "  Type: " << mod_settings.inputTransport << "\n"; -    ss << "  Source: " << mod_settings.inputName << "\n"; - -    ss << "Output\n"; -    if (mod_settings.useFileOutput) { -        ss << "  Name: " << mod_settings.outputName << "\n"; -    } -#if defined(HAVE_OUTPUT_UHD) -    else if (mod_settings.useUHDOutput) { -        ss << " UHD\n" << -            "  Device: " << mod_settings.sdr_device_config.device << "\n" << -            "  Subdevice: " << -                mod_settings.sdr_device_config.subDevice << "\n" << -            "  master_clock_rate: " << -                mod_settings.sdr_device_config.masterClockRate << "\n" << -            "  refclk: " << -                mod_settings.sdr_device_config.refclk_src << "\n" << -            "  pps source: " << -                mod_settings.sdr_device_config.pps_src << "\n"; -    } -#endif -#if defined(HAVE_SOAPYSDR) -    else if (mod_settings.useSoapyOutput) { -        ss << " SoapySDR\n" -            "  Device: " << mod_settings.sdr_device_config.device << "\n" << -            "  master_clock_rate: " << -                mod_settings.sdr_device_config.masterClockRate << "\n"; -    } -#endif -#if defined(HAVE_LIMESDR) -    else if (mod_settings.useLimeOutput) { -        ss << " LimeSDR\n" -            "  Device: " << mod_settings.sdr_device_config.device << "\n" << -            "  master_clock_rate: " << -                mod_settings.sdr_device_config.masterClockRate << "\n"; -    } -#endif -#if defined(HAVE_BLADERF) -    else if (mod_settings.useBladeRFOutput) { -        ss << " BladeRF\n" -            "  Device: " << mod_settings.sdr_device_config.device << "\n" << -            "  refclk: " << mod_settings.sdr_device_config.refclk_src << "\n"; -    } -#endif -    else if (mod_settings.useZeroMQOutput) { -        ss << " ZeroMQ\n" << -            "  Listening on: " << mod_settings.outputName << "\n" << -            "  Socket type : " << mod_settings.zmqOutputSocketType << "\n"; -    } - -    ss << "  Sampling rate: "; -    if (mod_settings.outputRate > 1000) { -        if (mod_settings.outputRate > 1000000) { -            ss << std::fixed << std::setprecision(4) << -                mod_settings.outputRate / 1000000.0 << -                " MHz\n"; -        } -        else { -            ss << std::fixed << std::setprecision(4) << -                mod_settings.outputRate / 1000.0 << -                " kHz\n"; -        } -    } -    else { -        ss << std::fixed << std::setprecision(4) << -            mod_settings.outputRate << " Hz\n"; -    } -    fprintf(stderr, "%s", ss.str().c_str()); -} - -static shared_ptr<ModOutput> prepare_output( -        mod_settings_t& s) +static shared_ptr<ModOutput> prepare_output(mod_settings_t& s)  {      shared_ptr<ModOutput> output; @@ -249,6 +305,16 @@ static shared_ptr<ModOutput> prepare_output(          rcs.enrol((Output::SDR*)output.get());      }  #endif +#if defined(HAVE_DEXTER) +    else if (s.useDexterOutput) { +        /* We normalise specifically range [-32768; 32767] */ +        s.normalise = 32767.0f / normalise_factor; +        s.sdr_device_config.sampleRate = s.outputRate; +        auto dexterdevice = make_shared<Output::Dexter>(s.sdr_device_config); +        output = make_shared<Output::SDR>(s.sdr_device_config, dexterdevice); +        rcs.enrol((Output::SDR*)output.get()); +    } +#endif  #if defined(HAVE_LIMESDR)      else if (s.useLimeOutput) {          /* We normalise the same way as for the UHD output */ @@ -308,6 +374,8 @@ int launch_modulator(int argc, char* argv[])      mod_settings_t mod_settings;      parse_args(argc, argv, mod_settings); +    etiLog.register_backend(make_shared<LogToEventSender>()); +      etiLog.level(info) << "Configuration parsed. Starting up version " <<  #if defined(GITVERSION)              GITVERSION; @@ -319,6 +387,7 @@ int launch_modulator(int argc, char* argv[])               mod_settings.useUHDOutput or               mod_settings.useZeroMQOutput or               mod_settings.useSoapyOutput or +             mod_settings.useDexterOutput or               mod_settings.useLimeOutput or               mod_settings.useBladeRFOutput)) {          throw std::runtime_error("Configuration error: Output not specified"); @@ -326,19 +395,49 @@ int launch_modulator(int argc, char* argv[])      printModSettings(mod_settings); -    shared_ptr<FormatConverter> format_converter; +    ModulatorData m; +    rcs.enrol(&m); + +    { +        // This is mostly useful on ARM systems where FFTW planning takes some time. If we do it here +        // it will be done before the modulator starts up +        etiLog.level(debug) << "Running FFTW planning..."; +        constexpr size_t fft_size = 2048; // Transmission Mode I. If different, it'll recalculate on OfdmGenerator +                                          // initialisation +        auto *fft_in = (fftwf_complex*)fftwf_malloc(sizeof(fftwf_complex) * fft_size); +        auto *fft_out = (fftwf_complex*)fftwf_malloc(sizeof(fftwf_complex) * fft_size); +        if (fft_in == nullptr or fft_out == nullptr) { +            throw std::runtime_error("FFTW malloc failed"); +        } +        fftwf_set_timelimit(2); +        fftwf_plan plan = fftwf_plan_dft_1d(fft_size, fft_in, fft_out, FFTW_FORWARD, FFTW_MEASURE); +        fftwf_destroy_plan(plan); +        plan = fftwf_plan_dft_1d(fft_size, fft_in, fft_out, FFTW_BACKWARD, FFTW_MEASURE); +        fftwf_destroy_plan(plan); +        fftwf_free(fft_in); +        fftwf_free(fft_out); +        etiLog.level(debug) << "FFTW planning done."; +    } + +    std::string output_format;      if (mod_settings.useFileOutput and              (mod_settings.fileOutputFormat == "s8" or               mod_settings.fileOutputFormat == "u8" or               mod_settings.fileOutputFormat == "s16")) { -        format_converter = make_shared<FormatConverter>(mod_settings.fileOutputFormat); +        output_format = mod_settings.fileOutputFormat; +    } +    else if (mod_settings.useBladeRFOutput or mod_settings.useDexterOutput) { +        output_format = "s16";      } -    else if (mod_settings.useBladeRFOutput) { -        format_converter = make_shared<FormatConverter>(mod_settings.BladeRFOutputFormat); -    }       auto output = prepare_output(mod_settings); +    if (not output_format.empty()) { +        if (auto o = dynamic_pointer_cast<Output::SDR>(output)) { +            o->set_sample_size(FormatConverter::get_format_size(output_format)); +        } +    } +      // Set thread priority to realtime      if (int r = set_realtime_prio(1)) {          etiLog.level(error) << "Could not set priority for modulator:" << r; @@ -365,17 +464,6 @@ int launch_modulator(int argc, char* argv[])          inputReader = inputFileReader;      } -    else if (mod_settings.inputTransport == "zeromq") { -#if !defined(HAVE_ZEROMQ) -        throw std::runtime_error("Unable to open input: " -                "ZeroMQ input transport selected, but not compiled in!"); -#else -        auto inputZeroMQReader = make_shared<InputZeroMQReader>(); -        inputZeroMQReader->Open(mod_settings.inputName, mod_settings.inputMaxFramesQueued); -        rcs.enrol(inputZeroMQReader.get()); -        inputReader = inputZeroMQReader; -#endif -    }      else if (mod_settings.inputTransport == "tcp") {          auto inputTcpReader = make_shared<InputTcpReader>();          inputTcpReader->Open(mod_settings.inputName); @@ -386,40 +474,37 @@ int launch_modulator(int argc, char* argv[])                  "invalid input transport " + mod_settings.inputTransport + " selected!");      } +    m.ediInput = ediInput; +    m.inputReader = inputReader; +      bool run_again = true;      while (run_again) { +        m.running_since = get_clock_realtime_seconds(); +          Flowgraph flowgraph(mod_settings.showProcessTime); -        modulator_data m; -        m.ediInput = ediInput; -        m.inputReader = inputReader; +        m.framecount = 0;          m.flowgraph = &flowgraph;          shared_ptr<DabModulator> modulator;          if (inputReader) {              m.etiReader = make_shared<EtiReader>(mod_settings.tist_offset_s); -            modulator = make_shared<DabModulator>(*m.etiReader, mod_settings); +            modulator = make_shared<DabModulator>(*m.etiReader, mod_settings, output_format);          }          else if (ediInput) { -            modulator = make_shared<DabModulator>(ediInput->ediReader, mod_settings); +            modulator = make_shared<DabModulator>(ediInput->ediReader, mod_settings, output_format);          }          rcs.enrol(modulator.get()); -        if (format_converter) { -            flowgraph.connect(modulator, format_converter); -            flowgraph.connect(format_converter, output); -        } -        else { -            flowgraph.connect(modulator, output); -        } +        flowgraph.connect(modulator, output);          if (inputReader) {              etiLog.level(info) << inputReader->GetPrintableInfo();          } -        run_modulator_state_t st = run_modulator(m); +        run_modulator_state_t st = run_modulator(mod_settings, m);          etiLog.log(trace, "DABMOD,run_modulator() = %d", st);          switch (st) { @@ -440,17 +525,6 @@ int launch_modulator(int argc, char* argv[])                          run_again = true;                      }                  } -#if defined(HAVE_ZEROMQ) -                else if (auto in_zmq = dynamic_pointer_cast<InputZeroMQReader>(inputReader)) { -                    run_again = true; -                    // Create a new input reader -                    rcs.remove_controllable(in_zmq.get()); -                    auto inputZeroMQReader = make_shared<InputZeroMQReader>(); -                    inputZeroMQReader->Open(mod_settings.inputName, mod_settings.inputMaxFramesQueued); -                    rcs.enrol(inputZeroMQReader.get()); -                    inputReader = inputZeroMQReader; -                } -#endif                  else if (dynamic_pointer_cast<InputTcpReader>(inputReader)) {                      // Keep the same inputReader, as there is no input buffer overflow                      run_again = true; @@ -473,28 +547,21 @@ int launch_modulator(int argc, char* argv[])                  break;          } -        etiLog.level(info) << m.framecount << " DAB frames encoded"; -        etiLog.level(info) << ((float)m.framecount * 0.024f) << " seconds encoded"; +        etiLog.level(info) << m.framecount << " DAB frames, " << ((float)m.framecount * 0.024f) << " seconds encoded"; +        m.num_modulator_restarts++;      }      etiLog.level(info) << "Terminating";      return ret;  } -struct zmq_input_timeout : public std::exception -{ -    const char* what() const throw() -    { -        return "InputZMQ timeout"; -    } -}; - -static run_modulator_state_t run_modulator(modulator_data& m) +static run_modulator_state_t run_modulator(const mod_settings_t& mod_settings, ModulatorData& m)  {      auto ret = run_modulator_state_t::failure;      try {          int last_eti_fct = -1;          auto last_frame_received = chrono::steady_clock::now(); +        frame_timestamp ts;          Buffer data;          if (m.inputReader) {              data.setLength(6144); @@ -515,36 +582,9 @@ static run_modulator_state_t run_modulator(modulator_data& m)                          ret = run_modulator_state_t::normal_end;                          break;                      } -#if defined(HAVE_ZEROMQ) -                    else if (dynamic_pointer_cast<InputZeroMQReader>(m.inputReader)) { -                        /* An empty frame marks a timeout. We ignore it, but we are -                         * now able to handle SIGINT properly. -                         * -                         * Also, we reconnect zmq every 10 seconds to avoid some -                         * issues, discussed in -                         * https://stackoverflow.com/questions/26112992/zeromq-pub-sub-on-unreliable-connection -                         * -                         * > It is possible that the PUB socket sees the error -                         * > while the SUB socket does not. -                         * > -                         * > The ZMTP RFC has a proposal for heartbeating that would -                         * > solve this problem.  The current best solution is for -                         * > PUB sockets to send heartbeats (e.g. 1 per second) when -                         * > traffic is low, and for SUB sockets to disconnect / -                         * > reconnect if they stop getting these. -                         * -                         * We don't need a heartbeat, because our application is constant frame rate, -                         * the frames themselves can act as heartbeats. -                         */ - -                        const auto now = chrono::steady_clock::now(); -                        if (last_frame_received + chrono::seconds(10) < now) { -                            throw zmq_input_timeout(); -                        } -                    } -#endif // defined(HAVE_ZEROMQ)                      else if (dynamic_pointer_cast<InputTcpReader>(m.inputReader)) { -                        /* Same as for ZeroMQ */ +                        /* An empty frame marks a timeout. We ignore it, but we are +                         * now able to handle SIGINT properly. */                      }                      else {                          throw logic_error("Unhandled framesize==0!"); @@ -568,6 +608,7 @@ static run_modulator_state_t run_modulator(modulator_data& m)                  fct = m.etiReader->getFct();                  fp = m.etiReader->getFp(); +                ts = m.etiReader->getTimestamp();              }              else if (m.ediInput) {                  while (running and not m.ediInput->ediReader.isFrameReady()) { @@ -594,39 +635,53 @@ static run_modulator_state_t run_modulator(modulator_data& m)                      break;                  } +                m.most_recent_edi_decoded = get_clock_realtime_seconds();                  fct = m.ediInput->ediReader.getFct();                  fp = m.ediInput->ediReader.getFp(); +                ts = m.ediInput->ediReader.getTimestamp();              } -            const unsigned expected_fct = (last_eti_fct + 1) % 250; -            if (last_eti_fct == -1) { -                if (fp != 0) { -                    // Do not start the flowgraph before we get to FP 0 -                    // to ensure all blocks are properly aligned. -                    if (m.ediInput) { -                        m.ediInput->ediReader.clearFrame(); +            // timestamp is good if we run unsynchronised, or if margin is sufficient +            bool ts_good = not mod_settings.sdr_device_config.enableSync or +                (ts.timestamp_valid and ts.offset_to_system_time() > 0.2); + +            if (!ts_good) { +                etiLog.level(warn) << "Modulator skipping frame " << fct << +                    " TS " << (ts.timestamp_valid ? "valid" : "invalid") << +                    " offset " << (ts.timestamp_valid ? ts.offset_to_system_time() : 0); +            } +            else { +                bool modulate = true; +                if (last_eti_fct == -1) { +                    if (fp != 0) { +                        // Do not start the flowgraph before we get to FP 0 +                        // to ensure all blocks are properly aligned. +                        modulate = false; +                    } +                    else { +                        last_eti_fct = fct;                      } -                    continue;                  }                  else { -                    last_eti_fct = fct; +                    const unsigned expected_fct = (last_eti_fct + 1) % 250; +                    if (fct == expected_fct) { +                        last_eti_fct = fct; +                    } +                    else { +                        etiLog.level(warn) << "ETI FCT discontinuity, expected " << +                            expected_fct << " received " << fct; +                        if (m.ediInput) { +                            m.ediInput->ediReader.clearFrame(); +                        } +                        return run_modulator_state_t::again; +                    } +                } + +                if (modulate) {                      m.framecount++;                      m.flowgraph->run();                  }              } -            else if (fct == expected_fct) { -                last_eti_fct = fct; -                m.framecount++; -                m.flowgraph->run(); -            } -            else { -                etiLog.level(info) << "ETI FCT discontinuity, expected " << -                    expected_fct << " received " << fct; -                if (m.ediInput) { -                    m.ediInput->ediReader.clearFrame(); -                } -                return run_modulator_state_t::again; -            }              if (m.ediInput) {                  m.ediInput->ediReader.clearFrame(); @@ -639,16 +694,6 @@ static run_modulator_state_t run_modulator(modulator_data& m)              }          }      } -    catch (const zmq_input_timeout&) { -        // The ZeroMQ input timeout -        etiLog.level(warn) << "Timeout"; -        ret = run_modulator_state_t::again; -    } -    catch (const zmq_input_overflow& e) { -        // The ZeroMQ input has overflowed its buffer -        etiLog.level(warn) << e.what(); -        ret = run_modulator_state_t::again; -    }      catch (const FrameMultiplexerError& e) {          // The FrameMultiplexer saw an error or a change in the size of a          // subchannel. This can be due to a multiplex reconfiguration. diff --git a/src/DabModulator.cpp b/src/DabModulator.cpp index aa4f2a8..4a29132 100644 --- a/src/DabModulator.cpp +++ b/src/DabModulator.cpp @@ -3,7 +3,7 @@     Her Majesty the Queen in Right of Canada (Communications Research     Center Canada) -   Copyright (C) 2019 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -27,53 +27,54 @@  #include <string>  #include <memory> +#include <vector>  #include "DabModulator.h"  #include "PcDebug.h" -#if !defined(BUILD_FOR_EASYDABV3) -# include "QpskSymbolMapper.h" -# include "FrequencyInterleaver.h" -# include "PhaseReference.h" -# include "DifferentialModulator.h" -# include "NullSymbol.h" -# include "CicEqualizer.h" -# include "OfdmGenerator.h" -# include "GainControl.h" -# include "GuardIntervalInserter.h" -# include "Resampler.h" -# include "FIRFilter.h" -# include "MemlessPoly.h" -# include "TII.h" -#endif - -#include "FrameMultiplexer.h" -#include "PrbsGenerator.h"  #include "BlockPartitioner.h" -#include "SignalMultiplexer.h" +#include "CicEqualizer.h"  #include "ConvEncoder.h" +#include "DifferentialModulator.h" +#include "FIRFilter.h" +#include "FrameMultiplexer.h" +#include "FrequencyInterleaver.h" +#include "GainControl.h" +#include "GuardIntervalInserter.h" +#include "Log.h" +#include "MemlessPoly.h" +#include "NullSymbol.h" +#include "OfdmGenerator.h" +#include "PhaseReference.h" +#include "PrbsGenerator.h"  #include "PuncturingEncoder.h" +#include "QpskSymbolMapper.h" +#include "RemoteControl.h" +#include "Resampler.h" +#include "SignalMultiplexer.h" +#include "TII.h"  #include "TimeInterleaver.h"  #include "TimestampDecoder.h" -#include "RemoteControl.h" -#include "Log.h"  using namespace std;  DabModulator::DabModulator(EtiSource& etiSource, -                           mod_settings_t& settings) : +                           mod_settings_t& settings, +                           const std::string& format) :      ModInput(),      RemoteControllable("modulator"),      m_settings(settings), -    myEtiSource(etiSource), -    myFlowgraph() +    m_format(format), +    m_etiSource(etiSource), +    m_flowgraph()  {      PDEBUG("DabModulator::DabModulator() @ %p\n", this);      RC_ADD_PARAMETER(rate, "(Read-only) IQ output samplerate"); +    RC_ADD_PARAMETER(num_clipped_samples, "(Read-only) Number of samples clipped in last frame during format conversion");      if (m_settings.dabMode == 0) { -        setMode(2); +        setMode(1);      }      else {          setMode(m_settings.dabMode); @@ -85,36 +86,36 @@ void DabModulator::setMode(unsigned mode)  {      switch (mode) {      case 1: -        myNbSymbols = 76; -        myNbCarriers = 1536; -        mySpacing = 2048; -        myNullSize = 2656; -        mySymSize = 2552; -        myFicSizeOut = 288; +        m_nbSymbols = 76; +        m_nbCarriers = 1536; +        m_spacing = 2048; +        m_nullSize = 2656; +        m_symSize = 2552; +        m_ficSizeOut = 288;          break;      case 2: -        myNbSymbols = 76; -        myNbCarriers = 384; -        mySpacing = 512; -        myNullSize = 664; -        mySymSize = 638; -        myFicSizeOut = 288; +        m_nbSymbols = 76; +        m_nbCarriers = 384; +        m_spacing = 512; +        m_nullSize = 664; +        m_symSize = 638; +        m_ficSizeOut = 288;          break;      case 3: -        myNbSymbols = 153; -        myNbCarriers = 192; -        mySpacing = 256; -        myNullSize = 345; -        mySymSize = 319; -        myFicSizeOut = 384; +        m_nbSymbols = 153; +        m_nbCarriers = 192; +        m_spacing = 256; +        m_nullSize = 345; +        m_symSize = 319; +        m_ficSizeOut = 384;          break;      case 4: -        myNbSymbols = 76; -        myNbCarriers = 768; -        mySpacing = 1024; -        myNullSize = 1328; -        mySymSize = 1276; -        myFicSizeOut = 288; +        m_nbSymbols = 76; +        m_nbCarriers = 768; +        m_spacing = 1024; +        m_nullSize = 1328; +        m_symSize = 1276; +        m_ficSizeOut = 288;          break;      default:          throw std::runtime_error("DabModulator::setMode invalid mode size"); @@ -128,27 +129,27 @@ int DabModulator::process(Buffer* dataOut)      PDEBUG("DabModulator::process(dataOut: %p)\n", dataOut); -    if (not myFlowgraph) { +    if (not m_flowgraph) { +        etiLog.level(debug) << "Setting up DabModulator...";          const unsigned mode = m_settings.dabMode;          setMode(mode); -        myFlowgraph = make_shared<Flowgraph>(m_settings.showProcessTime); +        m_flowgraph = make_shared<Flowgraph>(m_settings.showProcessTime);          ////////////////////////////////////////////////////////////////          // CIF data initialisation          ////////////////////////////////////////////////////////////////          auto cifPrbs = make_shared<PrbsGenerator>(864 * 8, 0x110); -        auto cifMux = make_shared<FrameMultiplexer>(myEtiSource); +        auto cifMux = make_shared<FrameMultiplexer>(m_etiSource);          auto cifPart = make_shared<BlockPartitioner>(mode); -#if !defined(BUILD_FOR_EASYDABV3) -        auto cifMap = make_shared<QpskSymbolMapper>(myNbCarriers); +        auto cifMap = make_shared<QpskSymbolMapper>(m_nbCarriers);          auto cifRef = make_shared<PhaseReference>(mode);          auto cifFreq = make_shared<FrequencyInterleaver>(mode); -        auto cifDiff = make_shared<DifferentialModulator>(myNbCarriers); +        auto cifDiff = make_shared<DifferentialModulator>(m_nbCarriers); -        auto cifNull = make_shared<NullSymbol>(myNbCarriers); +        auto cifNull = make_shared<NullSymbol>(m_nbCarriers);          auto cifSig = make_shared<SignalMultiplexer>( -                (1 + myNbSymbols) * myNbCarriers * sizeof(complexf)); +                (1 + m_nbSymbols) * m_nbCarriers * sizeof(complexf));          // TODO this needs a review          bool useCicEq = false; @@ -169,8 +170,8 @@ int DabModulator::process(Buffer* dataOut)          shared_ptr<CicEqualizer> cifCicEq;          if (useCicEq) {              cifCicEq = make_shared<CicEqualizer>( -                myNbCarriers, -                (float)mySpacing * (float)m_settings.outputRate / 2048000.0f, +                m_nbCarriers, +                (float)m_spacing * (float)m_settings.outputRate / 2048000.0f,                  cic_ratio);          } @@ -188,9 +189,9 @@ int DabModulator::process(Buffer* dataOut)          }          auto cifOfdm = make_shared<OfdmGenerator>( -                (1 + myNbSymbols), -                myNbCarriers, -                mySpacing, +                (1 + m_nbSymbols), +                m_nbCarriers, +                m_spacing,                  m_settings.enableCfr,                  m_settings.cfrClip,                  m_settings.cfrErrorClip); @@ -198,7 +199,7 @@ int DabModulator::process(Buffer* dataOut)          rcs.enrol(cifOfdm.get());          auto cifGain = make_shared<GainControl>( -                mySpacing, +                m_spacing,                  m_settings.gainMode,                  m_settings.digitalgain,                  m_settings.normalise, @@ -207,7 +208,7 @@ int DabModulator::process(Buffer* dataOut)          rcs.enrol(cifGain.get());          auto cifGuard = make_shared<GuardIntervalInserter>( -                myNbSymbols, mySpacing, myNullSize, mySymSize, +                m_nbSymbols, m_spacing, m_nullSize, m_symSize,                  m_settings.ofdmWindowOverlap);          rcs.enrol(cifGuard.get()); @@ -229,18 +230,21 @@ int DabModulator::process(Buffer* dataOut)              cifRes = make_shared<Resampler>(                      2048000,                      m_settings.outputRate, -                    mySpacing); +                    m_spacing); +        } + +        if (not m_format.empty()) { +            m_formatConverter = make_shared<FormatConverter>(m_format);          } -#endif -        myOutput = make_shared<OutputMemory>(dataOut); +        m_output = make_shared<OutputMemory>(dataOut); -        myFlowgraph->connect(cifPrbs, cifMux); +        m_flowgraph->connect(cifPrbs, cifMux);          ////////////////////////////////////////////////////////////////          // Processing FIC          //////////////////////////////////////////////////////////////// -        shared_ptr<FicSource> fic(myEtiSource.getFic()); +        shared_ptr<FicSource> fic(m_etiSource.getFic());          ////////////////////////////////////////////////////////////////          // Data initialisation          //////////////////////////////////////////////////////////////// @@ -272,15 +276,15 @@ int DabModulator::process(Buffer* dataOut)          PDEBUG(" Adding tail\n");          ficPunc->append_tail_rule(PuncturingRule(3, 0xcccccc)); -        myFlowgraph->connect(fic, ficPrbs); -        myFlowgraph->connect(ficPrbs, ficConv); -        myFlowgraph->connect(ficConv, ficPunc); -        myFlowgraph->connect(ficPunc, cifPart); +        m_flowgraph->connect(fic, ficPrbs); +        m_flowgraph->connect(ficPrbs, ficConv); +        m_flowgraph->connect(ficConv, ficPunc); +        m_flowgraph->connect(ficPunc, cifPart);          ////////////////////////////////////////////////////////////////          // Configuring subchannels          //////////////////////////////////////////////////////////////// -        for (const auto& subchannel : myEtiSource.getSubchannels()) { +        for (const auto& subchannel : m_etiSource.getSubchannels()) {              ////////////////////////////////////////////////////////////              // Data initialisation @@ -332,59 +336,59 @@ int DabModulator::process(Buffer* dataOut)              // Configuring time interleaver              auto subchInterleaver = make_shared<TimeInterleaver>(subchSizeOut); -            myFlowgraph->connect(subchannel, subchPrbs); -            myFlowgraph->connect(subchPrbs, subchConv); -            myFlowgraph->connect(subchConv, subchPunc); -            myFlowgraph->connect(subchPunc, subchInterleaver); -            myFlowgraph->connect(subchInterleaver, cifMux); +            m_flowgraph->connect(subchannel, subchPrbs); +            m_flowgraph->connect(subchPrbs, subchConv); +            m_flowgraph->connect(subchConv, subchPunc); +            m_flowgraph->connect(subchPunc, subchInterleaver); +            m_flowgraph->connect(subchInterleaver, cifMux);          } -        myFlowgraph->connect(cifMux, cifPart); -#if defined(BUILD_FOR_EASYDABV3) -        myFlowgraph->connect(cifPart, myOutput); -#else -        myFlowgraph->connect(cifPart, cifMap); -        myFlowgraph->connect(cifMap, cifFreq); -        myFlowgraph->connect(cifRef, cifDiff); -        myFlowgraph->connect(cifFreq, cifDiff); -        myFlowgraph->connect(cifNull, cifSig); -        myFlowgraph->connect(cifDiff, cifSig); +        m_flowgraph->connect(cifMux, cifPart); +        m_flowgraph->connect(cifPart, cifMap); +        m_flowgraph->connect(cifMap, cifFreq); +        m_flowgraph->connect(cifRef, cifDiff); +        m_flowgraph->connect(cifFreq, cifDiff); +        m_flowgraph->connect(cifNull, cifSig); +        m_flowgraph->connect(cifDiff, cifSig);          if (tii) { -            myFlowgraph->connect(tiiRef, tii); -            myFlowgraph->connect(tii, cifSig); +            m_flowgraph->connect(tiiRef, tii); +            m_flowgraph->connect(tii, cifSig);          }          shared_ptr<ModPlugin> prev_plugin = static_pointer_cast<ModPlugin>(cifSig); -        const std::list<shared_ptr<ModPlugin> > plugins({ +        const std::vector<shared_ptr<ModPlugin> > plugins({                  static_pointer_cast<ModPlugin>(cifCicEq),                  static_pointer_cast<ModPlugin>(cifOfdm),                  static_pointer_cast<ModPlugin>(cifGain),                  static_pointer_cast<ModPlugin>(cifGuard), -                static_pointer_cast<ModPlugin>(cifFilter), // optional block -                static_pointer_cast<ModPlugin>(cifRes),    // optional block -                static_pointer_cast<ModPlugin>(cifPoly),   // optional block -                static_pointer_cast<ModPlugin>(myOutput), +                // optional blocks +                static_pointer_cast<ModPlugin>(cifFilter), +                static_pointer_cast<ModPlugin>(cifRes), +                static_pointer_cast<ModPlugin>(cifPoly), +                static_pointer_cast<ModPlugin>(m_formatConverter), +                // mandatory block +                static_pointer_cast<ModPlugin>(m_output),                  });          for (auto& p : plugins) {              if (p) { -                myFlowgraph->connect(prev_plugin, p); +                m_flowgraph->connect(prev_plugin, p);                  prev_plugin = p;              }          } -#endif +        etiLog.level(debug) << "DabModulator set up.";      }      ////////////////////////////////////////////////////////////////////      // Processing data      //////////////////////////////////////////////////////////////////// -    return myFlowgraph->run(); +    return m_flowgraph->run();  }  meta_vec_t DabModulator::process_metadata(const meta_vec_t& metadataIn)  { -    if (myOutput) { -        return myOutput->get_latest_metadata(); +    if (m_output) { +        return m_output->get_latest_metadata();      }      return {}; @@ -396,6 +400,9 @@ void DabModulator::set_parameter(const string& parameter, const string& value)      if (parameter == "rate") {          throw ParameterError("Parameter 'rate' is read-only");      } +    else if (parameter == "num_clipped_samples") { +        throw ParameterError("Parameter 'num_clipped_samples' is read-only"); +    }      else {          stringstream ss;          ss << "Parameter '" << parameter << @@ -410,6 +417,16 @@ const string DabModulator::get_parameter(const string& parameter) const      if (parameter == "rate") {          ss << m_settings.outputRate;      } +    else if (parameter == "num_clipped_samples") { +        if (m_formatConverter) { +            ss << m_formatConverter->get_num_clipped_samples(); +        } +        else { +            ss << "Parameter '" << parameter << +                "' is not available when no format conversion is done."; +            throw ParameterError(ss.str()); +        } +    }      else {          ss << "Parameter '" << parameter <<              "' is not exported by controllable " << get_rc_name(); @@ -417,3 +434,11 @@ const string DabModulator::get_parameter(const string& parameter) const      }      return ss.str();  } + +const json::map_t DabModulator::get_all_values() const +{ +    json::map_t map; +    map["rate"].v = m_settings.outputRate; +    map["num_clipped_samples"].v = m_formatConverter ? m_formatConverter->get_num_clipped_samples() : 0; +    return map; +} diff --git a/src/DabModulator.h b/src/DabModulator.h index 00d71f5..093a782 100644 --- a/src/DabModulator.h +++ b/src/DabModulator.h @@ -3,7 +3,7 @@     Her Majesty the Queen in Right of Canada (Communications Research     Center Canada) -   Copyright (C) 2019 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -39,6 +39,7 @@  #include "ConfigParser.h"  #include "EtiReader.h"  #include "Flowgraph.h" +#include "FormatConverter.h"  #include "GainControl.h"  #include "OutputMemory.h"  #include "RemoteControl.h" @@ -49,7 +50,10 @@  class DabModulator : public ModInput, public ModMetadata, public RemoteControllable  {  public: -    DabModulator(EtiSource& etiSource, mod_settings_t& settings); +    DabModulator(EtiSource& etiSource, mod_settings_t& settings, const std::string& format); +    // Allowed formats: s8, u8 and s16. Empty string means no conversion + +    virtual ~DabModulator() {}      int process(Buffer* dataOut) override;      const char* name() override { return "DabModulator"; } @@ -57,30 +61,30 @@ public:      virtual meta_vec_t process_metadata(const meta_vec_t& metadataIn) override;      /* Required to get the timestamp */ -    EtiSource* getEtiSource() { return &myEtiSource; } +    EtiSource* getEtiSource() { return &m_etiSource; }      /******* REMOTE CONTROL ********/ -    virtual void set_parameter(const std::string& parameter, -            const std::string& value) override; - -    virtual const std::string get_parameter( -            const std::string& parameter) const override; +    virtual void set_parameter(const std::string& parameter, const std::string& value) override; +    virtual const std::string get_parameter(const std::string& parameter) const override; +    virtual const json::map_t get_all_values() const override;  protected:      void setMode(unsigned mode);      mod_settings_t& m_settings; +    std::string m_format; -    EtiSource& myEtiSource; -    std::shared_ptr<Flowgraph> myFlowgraph; +    EtiSource& m_etiSource; +    std::shared_ptr<Flowgraph> m_flowgraph; -    size_t myNbSymbols; -    size_t myNbCarriers; -    size_t mySpacing; -    size_t myNullSize; -    size_t mySymSize; -    size_t myFicSizeOut; +    size_t m_nbSymbols; +    size_t m_nbCarriers; +    size_t m_spacing; +    size_t m_nullSize; +    size_t m_symSize; +    size_t m_ficSizeOut; -    std::shared_ptr<OutputMemory> myOutput; +    std::shared_ptr<FormatConverter> m_formatConverter; +    std::shared_ptr<OutputMemory> m_output;  }; diff --git a/src/EtiReader.cpp b/src/EtiReader.cpp index d1c7622..eda2f23 100644 --- a/src/EtiReader.cpp +++ b/src/EtiReader.cpp @@ -78,6 +78,11 @@ unsigned EtiReader::getFct()      return eti_fc.FCT;  } +frame_timestamp EtiReader::getTimestamp() +{ +    return myTimestampDecoder.getTimestamp(); +} +  const std::vector<std::shared_ptr<SubchannelSource> > EtiReader::getSubchannels() const  { @@ -223,7 +228,7 @@ int EtiReader::loadEtiData(const Buffer& dataIn)                      unsigned size = mySources[i]->framesize();                      PDEBUG("Writting %i bytes of subchannel data\n", size);                      Buffer subch(size, in); -                    mySources[i]->loadSubchannelData(move(subch)); +                    mySources[i]->loadSubchannelData(std::move(subch));                      input_size -= size;                      framesize -= size;                      in += size; @@ -278,28 +283,21 @@ int EtiReader::loadEtiData(const Buffer& dataIn)      return dataIn.getLength() - input_size;  } -bool EtiReader::sourceContainsTimestamp() -{ -    return (ntohl(eti_tist.TIST) & 0xFFFFFF) != 0xFFFFFF; -    /* See ETS 300 799, Annex C.2.2 */ -} -  uint32_t EtiReader::getPPSOffset()  { -    if (!sourceContainsTimestamp()) { -        //fprintf(stderr, "****** SOURCE NO TS\n"); +    const uint32_t timestamp = ntohl(eti_tist.TIST) & 0xFFFFFF; + +    /* See ETS 300 799, Annex C.2.2 */ +    if (timestamp == 0xFFFFFF) {          return 0.0;      } -    uint32_t timestamp = ntohl(eti_tist.TIST) & 0xFFFFFF; -    //fprintf(stderr, "****** TIST 0x%x\n", timestamp); -      return timestamp;  } -EdiReader::EdiReader( -        double& tist_offset_s) : -    m_timestamp_decoder(tist_offset_s) +EdiReader::EdiReader(double& tist_offset_s) : +    m_timestamp_decoder(tist_offset_s), +    m_fic_decoder(/*verbose*/ false)  {      rcs.enrol(&m_timestamp_decoder);  } @@ -329,6 +327,11 @@ unsigned EdiReader::getFct()      return m_fc.fct();  } +frame_timestamp EdiReader::getTimestamp() +{ +    return m_timestamp_decoder.getTimestamp(); +} +  const std::vector<std::shared_ptr<SubchannelSource> > EdiReader::getSubchannels() const  {      std::vector<std::shared_ptr<SubchannelSource> > sources; @@ -346,15 +349,6 @@ const std::vector<std::shared_ptr<SubchannelSource> > EdiReader::getSubchannels(      return sources;  } -bool EdiReader::sourceContainsTimestamp() -{ -    if (not (m_frameReady and m_fc_valid)) { -        throw std::runtime_error("Trying to get timestamp before it is ready"); -    } - -    return m_fc.tsta != 0xFFFFFF; -} -  bool EdiReader::isFrameReady()  {      return m_frameReady; @@ -417,7 +411,10 @@ void EdiReader::update_fic(std::vector<uint8_t>&& fic)      if (not m_proto_valid) {          throw std::logic_error("Cannot update FIC before protocol");      } -    m_fic = move(fic); + +    m_fic_decoder.Process(fic.data(), fic.size()); + +    m_fic = std::move(fic);  }  void EdiReader::update_edi_time( @@ -469,7 +466,7 @@ void EdiReader::add_subchannel(EdiDecoder::eti_stc_data&& stc)          throw std::invalid_argument(                  "EDI: MST data length inconsistent with FIC");      } -    source->loadSubchannelData(move(stc.mst)); +    source->loadSubchannelData(std::move(stc.mst));      if (m_sources.size() > 64) {          throw std::invalid_argument("Too many subchannels"); @@ -543,8 +540,8 @@ void EdiTransport::Open(const std::string& uri)  {      etiLog.level(info) << "Opening EDI :" << uri; -    const string proto = uri.substr(0, 3); -    if (proto == "udp") { +    const string proto = uri.substr(0, 6); +    if (proto == "udp://") {          if (m_proto == Proto::TCP) {              throw std::invalid_argument("Cannot specify both TCP and UDP urls");          } @@ -574,7 +571,7 @@ void EdiTransport::Open(const std::string& uri)          m_proto = Proto::UDP;          m_enabled = true;      } -    else if (proto == "tcp") { +    else if (proto == "tcp://") {          if (m_proto != Proto::Unspecified) {              throw std::invalid_argument("Cannot call Open several times with TCP");          } @@ -585,10 +582,11 @@ void EdiTransport::Open(const std::string& uri)          }          m_port = std::stoi(uri.substr(found_port+1)); -        const std::string hostname = uri.substr(6, found_port-6);// skip tcp:// +        const std::string hostname = uri.substr(6, found_port-6);          etiLog.level(info) << "EDI TCP connect to " << hostname << ":" << m_port; +        m_tcp_uri = uri;          m_tcpclient.connect(hostname, m_port);          m_proto = Proto::TCP;          m_enabled = true; @@ -615,7 +613,7 @@ bool EdiTransport::rxPacket()                          received_from = rp.received_from;                          EdiDecoder::Packet p; -                        p.buf = move(rp.packetdata); +                        p.buf = std::move(rp.packetdata);                          p.received_on_port = rp.port_received_on;                          m_decoder.push_packet(p);                      } @@ -652,7 +650,7 @@ bool EdiTransport::rxPacket()                  const int timeout_ms = 1000;                  try {                      ssize_t ret = m_tcpclient.recv(m_tcpbuffer.data(), m_tcpbuffer.size(), 0, timeout_ms); -                    if (ret == 0 or ret == -1) { +                    if (ret <= 0) {                          return false;                      }                      else if (ret > (ssize_t)m_tcpbuffer.size()) { diff --git a/src/EtiReader.h b/src/EtiReader.h index d97acf6..703e42a 100644 --- a/src/EtiReader.h +++ b/src/EtiReader.h @@ -34,6 +34,7 @@  #include "Eti.h"  #include "Log.h"  #include "FicSource.h" +#include "FigParser.h"  #include "Socket.h"  #include "SubchannelSource.h"  #include "TimestampDecoder.h" @@ -59,8 +60,8 @@ public:      /* Get the current Frame Count */      virtual unsigned getFct() = 0; -    /* Returns true if we have valid time stamps in the ETI*/ -    virtual bool sourceContainsTimestamp() = 0; +    /* Returns current Timestamp */ +    virtual frame_timestamp getTimestamp() = 0;      /* Return the FIC source to be used for modulation */      virtual std::shared_ptr<FicSource>& getFic(void); @@ -97,18 +98,17 @@ class EtiReader : public EtiSource  public:      EtiReader(double& tist_offset_s); -    virtual unsigned getMode(); -    virtual unsigned getFp(); -    virtual unsigned getFct(); +    virtual unsigned getMode() override; +    virtual unsigned getFp() override; +    virtual unsigned getFct() override; +    virtual frame_timestamp getTimestamp() override;      /* Read ETI data from dataIn. Returns the number of bytes       * read from the buffer.       */      int loadEtiData(const Buffer& dataIn); -    virtual bool sourceContainsTimestamp(); - -    virtual const std::vector<std::shared_ptr<SubchannelSource> > getSubchannels() const; +    virtual const std::vector<std::shared_ptr<SubchannelSource> > getSubchannels() const override;  private:      /* Transform the ETI TIST to a PPS offset in units of 1/16384000 s */ @@ -141,7 +141,7 @@ public:      virtual unsigned getMode() override;      virtual unsigned getFp() override;      virtual unsigned getFct() override; -    virtual bool sourceContainsTimestamp() override; +    virtual frame_timestamp getTimestamp() override;      virtual const std::vector<std::shared_ptr<SubchannelSource> > getSubchannels() const override;      virtual bool isFrameReady(void); @@ -175,6 +175,15 @@ public:      // Gets called by the EDI library to tell us that all data for a frame was given to us      virtual void assemble(EdiDecoder::ReceivedTagPacket&& tagpacket) override; + +    std::optional<FIC_ENSEMBLE> getEnsembleInfo() const { +        return m_fic_decoder.observer.ensemble; +    } + +    std::map<int /*SId*/, LISTED_SERVICE> getServiceInfo() const { +        return m_fic_decoder.observer.services; +    } +  private:      bool m_proto_valid = false;      bool m_frameReady = false; @@ -198,6 +207,7 @@ private:      std::map<uint8_t, std::shared_ptr<SubchannelSource> > m_sources;      TimestampDecoder m_timestamp_decoder; +    FICDecoder m_fic_decoder;  };  /* The EDI input does not use the inputs defined in InputReader.h, as they were @@ -211,6 +221,7 @@ class EdiTransport {          void Open(const std::string& uri);          bool isEnabled(void) const { return m_enabled; } +        std::string getTcpUri(void) const { return m_tcp_uri; }          /* Receive a packet and give it to the decoder. Returns           * true if a packet was received, false in case of socket @@ -219,6 +230,7 @@ class EdiTransport {          bool rxPacket(void);      private: +        std::string m_tcp_uri;          bool m_enabled;          int m_port;          std::string m_bindto; diff --git a/src/Events.cpp b/src/Events.cpp new file mode 100644 index 0000000..3171cda --- /dev/null +++ b/src/Events.cpp @@ -0,0 +1,97 @@ +/* +   Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012 +   Her Majesty the Queen in Right of Canada (Communications Research +   Center Canada) + +   Copyright (C) 2023 +   Matthias P. Braendli, matthias.braendli@mpb.li + +    http://www.opendigitalradio.org + */ +/* +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU General Public License for more details. + +   You should have received a copy of the GNU General Public License +   along with this program.  If not, see <https://www.gnu.org/licenses/>. + */ +#include <list> +#include <string> +#include <iostream> +#include <sstream> +#include <iomanip> +#include <string> +#include <algorithm> + +#include "Events.h" + +EventSender events; + +EventSender::EventSender() : +    m_zmq_context(1), +    m_socket(m_zmq_context, zmq::socket_type::pub) +{ +    int linger = 2000; +    m_socket.setsockopt(ZMQ_LINGER, &linger, sizeof(linger)); +} + +EventSender::~EventSender() +{ } + +void EventSender::bind(const std::string& bind_endpoint) +{ +    try { +        m_socket.bind(bind_endpoint); +        m_socket_valid = true; +    } +    catch (const zmq::error_t& err) { +        fprintf(stderr, "Cannot bind event socket: %s", err.what()); +    } +} + +void EventSender::send(const std::string& event_name, const json::map_t& detail) +{ +    if (not m_socket_valid) { +        return; +    } + +    zmq::message_t zmsg1(event_name.data(), event_name.size()); +    const auto detail_json = json::map_to_json(detail); +    zmq::message_t zmsg2(detail_json.data(), detail_json.size()); + +    try { +        m_socket.send(zmsg1, zmq::send_flags::sndmore); +        m_socket.send(zmsg2, zmq::send_flags::none); +    } +    catch (const zmq::error_t& err) { +        fprintf(stderr, "Cannot send event %s: %s", event_name.c_str(), err.what()); +    } +} + + +void LogToEventSender::log(log_level_t level, const std::string& message) +{ +    std::string event_name; +    if (level == log_level_t::warn) { event_name = "warn"; } +    else if (level == log_level_t::error) { event_name = "error"; } +    else if (level == log_level_t::alert) { event_name = "alert"; } +    else if (level == log_level_t::emerg) { event_name = "emerg"; } + +    if (not event_name.empty()) { +        json::map_t detail; +        detail["message"].v = message; +        events.send(event_name, detail); +    } +} + +std::string LogToEventSender::get_name() const +{ +    return "EventSender"; +} diff --git a/src/Events.h b/src/Events.h new file mode 100644 index 0000000..9f838e5 --- /dev/null +++ b/src/Events.h @@ -0,0 +1,77 @@ +/* +   Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012 +   Her Majesty the Queen in Right of Canada (Communications Research +   Center Canada) + +   Copyright (C) 2023 +   Matthias P. Braendli, matthias.braendli@mpb.li + +    http://www.opendigitalradio.org + +   This module adds remote-control capability to some of the dabmux/dabmod modules. + */ +/* +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU General Public License for more details. + +   You should have received a copy of the GNU General Public License +   along with this program.  If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#ifdef HAVE_CONFIG_H +#  include "config.h" +#endif + +#if defined(HAVE_ZEROMQ) +#  include "zmq.hpp" +#endif + +#include <list> +#include <unordered_map> +#include <variant> +#include <map> +#include <memory> +#include <string> +#include <stdexcept> + +#include "Log.h" +#include "Json.h" + +class EventSender { +    public: +        EventSender(); +        EventSender(const EventSender& other) = delete; +        const EventSender& operator=(const EventSender& other) = delete; +        EventSender(EventSender&& other) = delete; +        EventSender& operator=(EventSender&& other) = delete; +        ~EventSender(); + +        void bind(const std::string& bind_endpoint); + +        void send(const std::string& event_name, const json::map_t& detail); +    private: +        zmq::context_t m_zmq_context; +        zmq::socket_t m_socket; +        bool m_socket_valid = false; +}; + +class LogToEventSender: public LogBackend { +    public: +        virtual ~LogToEventSender() {}; +        virtual void log(log_level_t level, const std::string& message); +        virtual std::string get_name() const; +}; + +/* events is a singleton used in all parts of the program to output log messages. + * It is constructed in Events.cpp */ +extern EventSender events; + diff --git a/src/FIRFilter.cpp b/src/FIRFilter.cpp index 89cf0da..57e7127 100644 --- a/src/FIRFilter.cpp +++ b/src/FIRFilter.cpp @@ -2,7 +2,7 @@     Copyright (C) 2007, 2008, 2009, 2010, 2011 Her Majesty the Queen in     Right of Canada (Communications Research Center Canada) -   Copyright (C) 2017 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -347,3 +347,10 @@ const string FIRFilter::get_parameter(const string& parameter) const      return ss.str();  } +const json::map_t FIRFilter::get_all_values() const +{ +    json::map_t map; +    map["ntaps"].v = m_taps.size(); +    map["tapsfile"].v = m_taps_file; +    return map; +} diff --git a/src/FIRFilter.h b/src/FIRFilter.h index 8d2e707..a4effa1 100644 --- a/src/FIRFilter.h +++ b/src/FIRFilter.h @@ -2,7 +2,7 @@     Copyright (C) 2007, 2008, 2009, 2010, 2011 Her Majesty the Queen in     Right of Canada (Communications Research Center Canada) -   Copyright (C) 2017 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -59,11 +59,9 @@ public:      const char* name() override { return "FIRFilter"; }      /******* REMOTE CONTROL ********/ -    virtual void set_parameter(const std::string& parameter, -            const std::string& value) override; - -    virtual const std::string get_parameter( -            const std::string& parameter) const override; +    virtual void set_parameter(const std::string& parameter, const std::string& value) override; +    virtual const std::string get_parameter(const std::string& parameter) const override; +    virtual const json::map_t get_all_values() const override;  protected:      virtual int internal_process(Buffer* const dataIn, Buffer* dataOut) override; diff --git a/src/FicSource.cpp b/src/FicSource.cpp index 2b95085..d824058 100644 --- a/src/FicSource.cpp +++ b/src/FicSource.cpp @@ -2,7 +2,7 @@     Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty     the Queen in Right of Canada (Communications Research Center Canada) -   Copyright (C) 2018 +   Copyright (C) 2022     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -27,7 +27,6 @@  #include "FicSource.h"  #include "PcDebug.h"  #include "Log.h" -#include "TimestampDecoder.h"  #include <stdexcept>  #include <string> @@ -36,46 +35,45 @@  #include <string.h> -const std::vector<PuncturingRule>& FicSource::get_rules() -{ -    return d_puncturing_rules; -} - -  FicSource::FicSource(unsigned ficf, unsigned mid) :      ModInput()  {  //    PDEBUG("FicSource::FicSource(...)\n");  //    PDEBUG("  Start address: %i\n", d_start_address); -//    PDEBUG("  Framesize: %i\n", d_framesize); +//    PDEBUG("  Framesize: %i\n", m_framesize);  //    PDEBUG("  Protection: %i\n", d_protection);      if (ficf == 0) { -        d_framesize = 0; -        d_buffer.setLength(0); +        m_buffer.setLength(0);          return;      }      if (mid == 3) { -        d_framesize = 32 * 4; -        d_puncturing_rules.emplace_back(29 * 16, 0xeeeeeeee); -        d_puncturing_rules.emplace_back(3 * 16, 0xeeeeeeec); +        m_framesize = 32 * 4; +        m_puncturing_rules.emplace_back(29 * 16, 0xeeeeeeee); +        m_puncturing_rules.emplace_back(3 * 16, 0xeeeeeeec);      } else { -        d_framesize = 24 * 4; -        d_puncturing_rules.emplace_back(21 * 16, 0xeeeeeeee); -        d_puncturing_rules.emplace_back(3 * 16, 0xeeeeeeec); +        m_framesize = 24 * 4; +        m_puncturing_rules.emplace_back(21 * 16, 0xeeeeeeee); +        m_puncturing_rules.emplace_back(3 * 16, 0xeeeeeeec);      } -    d_buffer.setLength(d_framesize); +    m_buffer.setLength(m_framesize); +} + +size_t FicSource::getFramesize() const +{ +    return m_framesize;  } -size_t FicSource::getFramesize() +const std::vector<PuncturingRule>& FicSource::get_rules() const  { -    return d_framesize; +    return m_puncturing_rules;  } +  void FicSource::loadFicData(const Buffer& fic)  { -    d_buffer = fic; +    m_buffer = fic;  }  int FicSource::process(Buffer* outputData) @@ -83,34 +81,31 @@ int FicSource::process(Buffer* outputData)      PDEBUG("FicSource::process (outputData: %p, outputSize: %zu)\n",              outputData, outputData->getLength()); -    if (d_buffer.getLength() != d_framesize) { +    if (m_buffer.getLength() != m_framesize) {          throw std::runtime_error( -                "ERROR: FicSource::process.outputSize != d_framesize: " + -                std::to_string(d_buffer.getLength()) + " != " + -                std::to_string(d_framesize)); +                "ERROR: FicSource::process.outputSize != m_framesize: " + +                std::to_string(m_buffer.getLength()) + " != " + +                std::to_string(m_framesize));      } -    *outputData = d_buffer; +    *outputData = m_buffer;      return outputData->getLength();  } -void FicSource::loadTimestamp(const std::shared_ptr<struct frame_timestamp>& ts) +void FicSource::loadTimestamp(const frame_timestamp& ts)  { -    d_ts = ts; +    m_ts_valid = true; +    m_ts = ts;  } -  meta_vec_t FicSource::process_metadata(const meta_vec_t& metadataIn)  { -    if (not d_ts) { -        return {}; -    } - -    using namespace std;      meta_vec_t md_vec; -    flowgraph_metadata meta; -    meta.ts = d_ts; -    md_vec.push_back(meta); +    if (m_ts_valid) { +        flowgraph_metadata meta; +        meta.ts = m_ts; +        md_vec.push_back(meta); +    }      return md_vec;  } diff --git a/src/FicSource.h b/src/FicSource.h index 93c1a7f..01dba2d 100644 --- a/src/FicSource.h +++ b/src/FicSource.h @@ -2,7 +2,7 @@     Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty     the Queen in Right of Canada (Communications Research Center Canada) -   Copyright (C) 2016 +   Copyright (C) 2022     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -33,6 +33,7 @@  #include "PuncturingRule.h"  #include "Eti.h"  #include "ModPlugin.h" +#include "TimestampDecoder.h"  #include <vector>  #include <sys/types.h> @@ -41,21 +42,21 @@ class FicSource : public ModInput, public ModMetadata  public:      FicSource(unsigned ficf, unsigned mid); -    size_t getFramesize(); -    const std::vector<PuncturingRule>& get_rules(); +    size_t getFramesize() const; +    const std::vector<PuncturingRule>& get_rules() const;      void loadFicData(const Buffer& fic);      int process(Buffer* outputData) override;      const char* name() override { return "FicSource"; } -    void loadTimestamp(const std::shared_ptr<struct frame_timestamp>& ts); -    virtual meta_vec_t process_metadata( -            const meta_vec_t& metadataIn) override; +    void loadTimestamp(const frame_timestamp& ts); +    virtual meta_vec_t process_metadata(const meta_vec_t& metadataIn) override;  private: -    size_t d_framesize; -    Buffer d_buffer; -    std::shared_ptr<struct frame_timestamp> d_ts; -    std::vector<PuncturingRule> d_puncturing_rules; +    size_t m_framesize = 0; +    Buffer m_buffer; +    frame_timestamp m_ts; +    bool m_ts_valid = false; +    std::vector<PuncturingRule> m_puncturing_rules;  }; diff --git a/src/FigParser.cpp b/src/FigParser.cpp new file mode 100644 index 0000000..bda2f83 --- /dev/null +++ b/src/FigParser.cpp @@ -0,0 +1,1047 @@ +/* +   Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty +   the Queen in Right of Canada (Communications Research Center Canada) + +   Most parts of this file are taken from dablin, +   Copyright (C) 2015-2022 Stefan Pöschel + +   Copyright (C) 2023 +   Matthias P. Braendli, matthias.braendli@mpb.li + +    http://opendigitalradio.org + */ +/* +   This file is part of ODR-DabMod. + +   ODR-DabMod is free software: you can redistribute it and/or modify +   it under the terms of the GNU General Public License as +   published by the Free Software Foundation, either version 3 of the +   License, or (at your option) any later version. + +   ODR-DabMod is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU General Public License for more details. + +   You should have received a copy of the GNU General Public License +   along with ODR-DabMod.  If not, see <http://www.gnu.org/licenses/>. + */ + +#include "FigParser.h" +#include "PcDebug.h" +#include "Log.h" +#include "crc.h" +#include "CharsetTools.h" + +#include <stdexcept> +#include <string> +#include <cstdint> +#include <cstdlib> +#include <cstring> + + +template<class T> +static uint16_t read_16b(T buf) +{ +    uint16_t value = 0; +    value = (uint16_t)(buf[0]) << 8; +    value |= (uint16_t)(buf[1]); +    return value; +} + +static bool checkCRC(const uint8_t *buf, size_t size) +{ +    const uint16_t crc_from_packet = read_16b(buf + size - 2); +    uint16_t crc_calc = 0xffff; +    crc_calc = crc16(crc_calc, buf, size - 2); +    crc_calc ^= 0xffff; +    return crc_from_packet == crc_calc; +} + +void FICDecoderObserver::FICChangeEnsemble(const FIC_ENSEMBLE& e) +{ +    services.clear(); +    ensemble = e; +} +void FICDecoderObserver::FICChangeService(const LISTED_SERVICE& ls) +{ +    services[ls.sid] = ls; +} +void FICDecoderObserver::FICChangeUTCDateTime(const FIC_DAB_DT& dt) +{ +    utc_dt = dt; +} + +// --- FICDecoder ----------------------------------------------------------------- +FICDecoder::FICDecoder(bool verbose) : +            verbose(verbose), +            utc_dt_long(false) +{ } + + +void FICDecoder::Reset() { +    ensemble = FIC_ENSEMBLE(); +    services.clear(); +    subchannels.clear(); +    utc_dt = FIC_DAB_DT(); +} + +void FICDecoder::Process(const uint8_t *data, size_t len) { +    // check for integer FIB count +    if(len % 32) { +        etiLog.log(warn, "FICDecoder: Ignoring non-integer FIB count FIC data with %zu bytes\n", len); +        return; +    } + +    for(size_t i = 0; i < len; i += 32) +        ProcessFIB(data + i); +} + +void FICDecoder::ProcessFIB(const uint8_t *data) { +    if (not checkCRC(data, 32)) { +        observer.FICDiscardedFIB(); +        return; +    } + +    // iterate over all FIGs +    for(size_t offset = 0; offset < 30 && data[offset] != 0xFF;) { +        int type = data[offset] >> 5; +        size_t len = data[offset] & 0x1F; +        offset++; + +        switch(type) { +            case 0: +                ProcessFIG0(data + offset, len); +                break; +            case 1: +                ProcessFIG1(data + offset, len); +                break; +                //      default: +                //          etiLog.log(warn, "FICDecoder: received unsupported FIG %d with %zu bytes\n", type, len); +        } +        offset += len; +    } +} + + +void FICDecoder::ProcessFIG0(const uint8_t *data, size_t len) { +    if(len < 1) { +        etiLog.log(warn, "FICDecoder: received empty FIG 0\n"); +        return; +    } + +    // read/skip FIG 0 header +    FIG0_HEADER header(data[0]); +    data++; +    len--; + +    // ignore next config/other ensembles/data services +    if(header.cn || header.oe || header.pd) +        return; + + +    // handle extension +    switch(header.extension) { +        case 0: +            ProcessFIG0_0(data, len); +            break; +        case 1: +            ProcessFIG0_1(data, len); +            break; +        case 2: +            ProcessFIG0_2(data, len); +            break; +        case 5: +            ProcessFIG0_5(data, len); +            break; +        case 8: +            ProcessFIG0_8(data, len); +            break; +        case 9: +            ProcessFIG0_9(data, len); +            break; +        case 10: +            ProcessFIG0_10(data, len); +            break; +        case 13: +            ProcessFIG0_13(data, len); +            break; +        case 17: +            ProcessFIG0_17(data, len); +            break; +        case 18: +            ProcessFIG0_18(data, len); +            break; +        case 19: +            ProcessFIG0_19(data, len); +            break; +            //  default: +            //      etiLog.log(warn, "FICDecoder: received unsupported FIG 0/%d with %zu field bytes\n", header.extension, len); +    } +} + +void FICDecoder::ProcessFIG0_0(const uint8_t *data, size_t len) { +    // FIG 0/0 - Ensemble information +    // EId and alarm flag only + +    if(len < 4) +        return; + +    FIC_ENSEMBLE new_ensemble = ensemble; +    new_ensemble.eid = data[0] << 8 | data[1]; +    new_ensemble.al_flag = data[2] & 0x20; + +    if(ensemble != new_ensemble) { +        ensemble = new_ensemble; + +        if (verbose) +            etiLog.log(debug, "FICDecoder: EId 0x%04X: alarm flag: %s\n", +                ensemble.eid, ensemble.al_flag ? "true" : "false"); + +        UpdateEnsemble(); +    } +} + +void FICDecoder::ProcessFIG0_1(const uint8_t *data, size_t len) { +    // FIG 0/1 - Basic sub-channel organization + +    // iterate through all sub-channels +    for(size_t offset = 0; offset < len;) { +        int subchid = data[offset] >> 2; +        size_t start_address = (data[offset] & 0x03) << 8 | data[offset + 1]; +        offset += 2; + +        FIC_SUBCHANNEL sc; +        sc.start = start_address; + +        bool short_long_form = data[offset] & 0x80; +        if(short_long_form) { +            // long form +            int option = (data[offset] & 0x70) >> 4; +            int pl = (data[offset] & 0x0C) >> 2; +            size_t subch_size = (data[offset] & 0x03) << 8 | data[offset + 1]; + +            switch(option) { +                case 0b000: +                    sc.size = subch_size; +                    sc.pl = "EEP " + std::to_string(pl + 1) + "-A"; +                    sc.bitrate = subch_size / eep_a_size_factors[pl] *  8; +                    break; +                case 0b001: +                    sc.size = subch_size; +                    sc.pl = "EEP " + std::to_string(pl + 1) + "-B"; +                    sc.bitrate = subch_size / eep_b_size_factors[pl] * 32; +                    break; +            } +            offset += 2; +        } else { +            // short form + +            bool table_switch = data[offset] & 0x40; +            if(!table_switch) { +                int table_index = data[offset] & 0x3F; +                sc.size = uep_sizes[table_index]; +                sc.pl = "UEP " + std::to_string(uep_pls[table_index]); +                sc.bitrate = uep_bitrates[table_index]; +            } +            offset++; +        } + +        if(!sc.IsNone()) { +            FIC_SUBCHANNEL& current_sc = GetSubchannel(subchid); +            sc.language = current_sc.language;  // ignored for comparison +            if(current_sc != sc) { +                current_sc = sc; + +                if (verbose) +                    etiLog.log(debug, "FICDecoder: SubChId %2d: start %3zu CUs, size %3zu CUs, PL %-7s = %3d kBit/s\n", subchid, sc.start, sc.size, sc.pl.c_str(), sc.bitrate); + +                UpdateSubchannel(subchid); +            } +        } +    } +} + +void FICDecoder::ProcessFIG0_2(const uint8_t *data, size_t len) { +    // FIG 0/2 - Basic service and service component definition +    // programme services only + +    // iterate through all services +    for(size_t offset = 0; offset < len;) { +        uint16_t sid = data[offset] << 8 | data[offset + 1]; +        offset += 2; + +        size_t num_service_comps = data[offset++] & 0x0F; + +        // iterate through all service components +        for(size_t comp = 0; comp < num_service_comps; comp++) { +            int tmid = data[offset] >> 6; + +            switch(tmid) { +                case 0b00:  // MSC stream audio +                    int ascty = data[offset] & 0x3F; +                    int subchid = data[offset + 1] >> 2; +                    bool ps = data[offset + 1] & 0x02; +                    bool ca = data[offset + 1] & 0x01; + +                    if(!ca) { +                        switch(ascty) { +                            case 0:     // DAB +                            case 63:    // DAB+ +                                bool dab_plus = ascty == 63; + +                                AUDIO_SERVICE audio_service(subchid, dab_plus); + +                                FIC_SERVICE& service = GetService(sid); +                                AUDIO_SERVICE& current_audio_service = service.audio_comps[subchid]; +                                if(current_audio_service != audio_service || ps != (service.pri_comp_subchid == subchid)) { +                                    current_audio_service = audio_service; +                                    if(ps) +                                        service.pri_comp_subchid = subchid; + +                                    if (verbose) +                                        etiLog.log(debug, "FICDecoder: SId 0x%04X: audio service (SubChId %2d, %-4s, %s)\n", sid, subchid, dab_plus ? "DAB+" : "DAB", ps ? "primary" : "secondary"); + +                                    UpdateService(service); +                                } + +                                break; +                        } +                    } +            } + +            offset += 2; +        } +    } +} + +void FICDecoder::ProcessFIG0_5(const uint8_t *data, size_t len) { +    // FIG 0/5 - Service component language +    // programme services only + +    // iterate through all components +    for(size_t offset = 0; offset < len;) { +        bool ls_flag = data[offset] & 0x80; +        if(ls_flag) { +            // long form - skipped, as not relevant +            offset += 3; +        } else { +            // short form +            bool msc_fic_flag = data[offset] & 0x40; + +            // handle only MSC components +            if(!msc_fic_flag) { +                int subchid = data[offset] & 0x3F; +                int language = data[offset + 1]; + +                FIC_SUBCHANNEL& current_sc = GetSubchannel(subchid); +                if(current_sc.language != language) { +                    current_sc.language = language; + +                    if (verbose) +                        etiLog.log(debug, "FICDecoder: SubChId %2d: language '%s'\n", subchid, ConvertLanguageToString(language).c_str()); + +                    UpdateSubchannel(subchid); +                } +            } + +            offset += 2; +        } +    } +} + +void FICDecoder::ProcessFIG0_8(const uint8_t *data, size_t len) { +    // FIG 0/8 - Service component global definition +    // programme services only + +    // iterate through all service components +    for(size_t offset = 0; offset < len;) { +        uint16_t sid = data[offset] << 8 | data[offset + 1]; +        offset += 2; + +        bool ext_flag = data[offset] & 0x80; +        int scids = data[offset] & 0x0F; +        offset++; + +        bool ls_flag = data[offset] & 0x80; +        if(ls_flag) { +            // long form - skipped, as not relevant +            offset += 2; +        } else { +            // short form +            bool msc_fic_flag = data[offset] & 0x40; + +            // handle only MSC components +            if(!msc_fic_flag) { +                int subchid = data[offset] & 0x3F; + +                FIC_SERVICE& service = GetService(sid); +                bool new_comp = service.comp_defs.find(scids) == service.comp_defs.end(); +                int& current_subchid = service.comp_defs[scids]; +                if(new_comp || current_subchid != subchid) { +                    current_subchid = subchid; + +                    if (verbose) +                        etiLog.log(debug, "FICDecoder: SId 0x%04X, SCIdS %2d: MSC service component (SubChId %2d)\n", sid, scids, subchid); + +                    UpdateService(service); +                } +            } + +            offset++; +        } + +        // skip Rfa field, if needed +        if(ext_flag) +            offset++; +    } +} + +void FICDecoder::ProcessFIG0_9(const uint8_t *data, size_t len) { +    // FIG 0/9 - Time and country identifier - Country, LTO and International table +    // ensemble ECC/LTO and international table ID only + +    if(len < 3) +        return; + +    FIC_ENSEMBLE new_ensemble = ensemble; +    new_ensemble.lto = (data[0] & 0x20 ? -1 : 1) * (data[0] & 0x1F); +    new_ensemble.ecc = data[1]; +    new_ensemble.inter_table_id = data[2]; + +    if(ensemble != new_ensemble) { +        ensemble = new_ensemble; + +        if (verbose) +            etiLog.log(debug, "FICDecoder: ECC: 0x%02X, LTO: %s, international table ID: 0x%02X (%s)\n", +                ensemble.ecc, ConvertLTOToString(ensemble.lto).c_str(), ensemble.inter_table_id, ConvertInterTableIDToString(ensemble.inter_table_id).c_str()); + +        UpdateEnsemble(); + +        // update services that changes may affect +        for(const fic_services_t::value_type& service : services) { +            const FIC_SERVICE& s = service.second; +            if(s.pty_static != FIC_SERVICE::pty_none || s.pty_dynamic != FIC_SERVICE::pty_none) +                UpdateService(s); +        } +    } +} + +void FICDecoder::ProcessFIG0_10(const uint8_t *data, size_t len) { +    // FIG 0/10 - Date and time (d&t) + +    if(len < 4) +        return; + +    FIC_DAB_DT new_utc_dt; + +    // ignore short form, once long form available +    bool utc_flag = data[2] & 0x08; +    if(!utc_flag && utc_dt_long) +        return; + +    // retrieve date +    int mjd = (data[0] & 0x7F) << 10 | data[1] << 2 | data[2] >> 6; + +    int y0 = floor((mjd - 15078.2) / 365.25); +    int m0 = floor((mjd - 14956.1 - floor(y0 * 365.25)) / 30.6001); +    int d = mjd - 14956 - floor(y0 * 365.25) - floor(m0 * 30.6001); +    int k = (m0 == 14 || m0 == 15) ? 1 : 0; +    int y = y0 + k; +    int m = m0 - 1 - k * 12; + +    new_utc_dt.dt.tm_year = y;      // from 1900 +    new_utc_dt.dt.tm_mon = m - 1;   // 0-based +    new_utc_dt.dt.tm_mday = d; + +    // retrieve time +    new_utc_dt.dt.tm_hour = (data[2] & 0x07) << 2 | data[3] >> 6; +    new_utc_dt.dt.tm_min = data[3] & 0x3F; +    new_utc_dt.dt.tm_isdst = -1;    // ignore DST +    if(utc_flag) { +        // long form +        if(len < 6) +            return; +        new_utc_dt.dt.tm_sec = data[4] >> 2; +        new_utc_dt.ms = (data[4] & 0x03) << 8 | data[5]; +        utc_dt_long = true; +    } else { +        // short form +        new_utc_dt.dt.tm_sec = 0; +        new_utc_dt.ms = FIC_DAB_DT::ms_none; +    } + +    if(utc_dt != new_utc_dt) { +        // print only once (or once again on precision change) +        if(utc_dt.IsNone() || utc_dt.IsMsNone() != new_utc_dt.IsMsNone()) +            if (verbose) +                etiLog.log(debug, "FICDecoder: UTC date/time: %s\n", ConvertDateTimeToString(new_utc_dt, 0, true).c_str()); + +        utc_dt = new_utc_dt; + +        observer.FICChangeUTCDateTime(utc_dt); +    } +} + +void FICDecoder::ProcessFIG0_13(const uint8_t *data, size_t len) { +    // FIG 0/13 - User application information +    // programme services only + +    // iterate through all service components +    for(size_t offset = 0; offset < len;) { +        uint16_t sid = data[offset] << 8 | data[offset + 1]; +        offset += 2; + +        int scids = data[offset] >> 4; +        size_t num_scids_uas = data[offset] & 0x0F; +        offset++; + +        // iterate through all user applications +        for(size_t scids_ua = 0; scids_ua < num_scids_uas; scids_ua++) { +            int ua_type = data[offset] << 3 | data[offset + 1] >> 5; +            size_t ua_data_length = data[offset + 1] & 0x1F; +            offset += 2; + +            // handle only Slideshow +            if(ua_type == 0x002) { +                FIC_SERVICE& service = GetService(sid); +                if(service.comp_sls_uas.find(scids) == service.comp_sls_uas.end()) { +                    ua_data_t& sls_ua_data = service.comp_sls_uas[scids]; + +                    sls_ua_data.resize(ua_data_length); +                    if(ua_data_length) +                        memcpy(&sls_ua_data[0], data + offset, ua_data_length); + +                    if (verbose) +                        etiLog.log(debug, "FICDecoder: SId 0x%04X, SCIdS %2d: Slideshow (%zu bytes UA data)\n", sid, scids, ua_data_length); + +                    UpdateService(service); +                } +            } + +            offset += ua_data_length; +        } +    } +} + +void FICDecoder::ProcessFIG0_17(const uint8_t *data, size_t len) { +    // FIG 0/17 - Programme Type +    // programme type only + +    // iterate through all services +    for(size_t offset = 0; offset < len;) { +        uint16_t sid = data[offset] << 8 | data[offset + 1]; +        bool sd = data[offset + 2] & 0x80; +        bool l_flag = data[offset + 2] & 0x20; +        bool cc_flag = data[offset + 2] & 0x10; +        offset += 3; + +        // skip language, if present +        if(l_flag) +            offset++; + +        // programme type (international code) +        int pty = data[offset] & 0x1F; +        offset++; + +        // skip CC part, if present +        if(cc_flag) +            offset++; + +        FIC_SERVICE& service = GetService(sid); +        int& current_pty = sd ? service.pty_dynamic : service.pty_static; +        if(current_pty != pty) { +            // suppress message, if dynamic FIC messages disabled and dynamic PTY not initally be set +            bool show_msg = !(sd && current_pty != FIC_SERVICE::pty_none); + +            current_pty = pty; + +            if(verbose && show_msg) { +                // assuming international table ID 0x01 here! +                etiLog.log(debug, "FICDecoder: SId 0x%04X: programme type (%s): '%s'\n", +                        sid, sd ? "dynamic" : "static", ConvertPTYToString(pty, 0x01).c_str()); +            } + +            UpdateService(service); +        } +    } +} + +void FICDecoder::ProcessFIG0_18(const uint8_t *data, size_t len) { +    // FIG 0/18 - Announcement support + +    // iterate through all services +    for(size_t offset = 0; offset < len;) { +        uint16_t sid = data[offset] << 8 | data[offset + 1]; +        uint16_t asu_flags = data[offset + 2] << 8 | data[offset + 3]; +        size_t number_of_clusters = data[offset + 4] & 0x1F; +        offset += 5; + +        cids_t cids; +        for(size_t i = 0; i < number_of_clusters; i++) +            cids.emplace(data[offset++]); + +        FIC_SERVICE& service = GetService(sid); +        uint16_t& current_asu_flags = service.asu_flags; +        cids_t& current_cids = service.cids; +        if(current_asu_flags != asu_flags || current_cids != cids) { +            current_asu_flags = asu_flags; +            current_cids = cids; + +            std::string cids_str; +            char cid_string[5]; +            for(const cids_t::value_type& cid : cids) { +                if(!cids_str.empty()) +                    cids_str += "/"; +                snprintf(cid_string, sizeof(cid_string), "0x%02X", cid); +                cids_str += std::string(cid_string); +            } + +            if (verbose) +                etiLog.log(debug, "FICDecoder: SId 0x%04X: ASu flags 0x%04X, cluster(s) %s\n", +                    sid, asu_flags, cids_str.c_str()); + +            UpdateService(service); +        } +    } +} + +void FICDecoder::ProcessFIG0_19(const uint8_t *data, size_t len) { +    // FIG 0/19 - Announcement switching + +    // iterate through all announcement clusters +    for(size_t offset = 0; offset < len;) { +        uint8_t cid = data[offset]; +        uint16_t asw_flags = data[offset + 1] << 8 | data[offset + 2]; +        bool region_flag = data[offset + 3] & 0x40; +        int subchid = data[offset + 3] & 0x3F; +        offset += region_flag ? 5 : 4; + +        FIC_ASW_CLUSTER ac; +        ac.asw_flags = asw_flags; +        ac.subchid = subchid; + +        FIC_ASW_CLUSTER& current_ac = ensemble.asw_clusters[cid]; +        if(current_ac != ac) { +            current_ac = ac; + +            if (verbose) { +                etiLog.log(debug, "FICDecoder: ASw cluster 0x%02X: flags 0x%04X, SubChId %2d\n", +                        cid, asw_flags, subchid); +            } + +            UpdateEnsemble(); + +            // update services that changes may affect +            for(const fic_services_t::value_type& service : services) { +                const FIC_SERVICE& s = service.second; +                if(s.cids.find(cid) != s.cids.cend()) +                    UpdateService(s); +            } +        } +    } +} + +void FICDecoder::ProcessFIG1(const uint8_t *data, size_t len) { +    if(len < 1) { +        etiLog.log(warn, "FICDecoder: received empty FIG 1\n"); +        return; +    } + +    // read/skip FIG 1 header +    FIG1_HEADER header(data[0]); +    data++; +    len--; + +    // ignore other ensembles +    if(header.oe) +        return; + +    // check for (un)supported extension + set ID field len +    size_t len_id = -1; +    switch(header.extension) { +        case 0: // ensemble +        case 1: // programme service +            len_id = 2; +            break; +        case 4: // service component +                // programme services only (P/D = 0) +            if(data[0] & 0x80) +                return; +            len_id = 3; +            break; +        default: +            //      etiLog.log(debug, "FICDecoder: received unsupported FIG 1/%d with %zu field bytes\n", header.extension, len); +            return; +    } + +    // check length +    size_t len_calced = len_id + 16 + 2; +    if(len != len_calced) { +        etiLog.log(warn, "FICDecoder: received FIG 1/%d having %zu field bytes (expected: %zu)\n", header.extension, len, len_calced); +        return; +    } + +    // parse actual label data +    FIC_LABEL label; +    label.charset = header.charset; +    memcpy(label.label, data + len_id, 16); +    label.short_label_mask = data[len_id + 16] << 8 | data[len_id + 17]; + + +    // handle extension +    switch(header.extension) { +        case 0: {   // ensemble +                    uint16_t eid = data[0] << 8 | data[1]; +                    ProcessFIG1_0(eid, label); +                    break; } +        case 1: {   // programme service +                    uint16_t sid = data[0] << 8 | data[1]; +                    ProcessFIG1_1(sid, label); +                    break; } +        case 4: {   // service component +                    int scids = data[0] & 0x0F; +                    uint16_t sid = data[1] << 8 | data[2]; +                    ProcessFIG1_4(sid, scids, label); +                    break; } +    } +} + +void FICDecoder::ProcessFIG1_0(uint16_t eid, const FIC_LABEL& label) { +    if(ensemble.label != label) { +        ensemble.label = label; + +        std::string label_str = ConvertLabelToUTF8(label, nullptr); +        std::string short_label_str = DeriveShortLabelUTF8(label_str, label.short_label_mask); +        if (verbose) +            etiLog.log(debug, "FICDecoder: EId 0x%04X: ensemble label '" "\x1B[32m" "%s" "\x1B[0m" "' ('" "\x1B[32m" "%s" "\x1B[0m" "')\n", +                eid, label_str.c_str(), short_label_str.c_str()); + +        UpdateEnsemble(); +    } +} + +void FICDecoder::ProcessFIG1_1(uint16_t sid, const FIC_LABEL& label) { +    FIC_SERVICE& service = GetService(sid); +    if(service.label != label) { +        service.label = label; + +        if (verbose) { +            std::string label_str = ConvertLabelToUTF8(label, nullptr); +            std::string short_label_str = DeriveShortLabelUTF8(label_str, label.short_label_mask); +            etiLog.log(debug, "FICDecoder: SId 0x%04X: programme service label '" "\x1B[32m" "%s" "\x1B[0m" "' ('" "\x1B[32m" "%s" "\x1B[0m" "')\n", +                    sid, label_str.c_str(), short_label_str.c_str()); +        } + +        UpdateService(service); +    } +} + +void FICDecoder::ProcessFIG1_4(uint16_t sid, int scids, const FIC_LABEL& label) { +    // programme services only + +    FIC_SERVICE& service = GetService(sid); +    FIC_LABEL& comp_label = service.comp_labels[scids]; +    if(comp_label != label) { +        comp_label = label; + +        if (verbose) { +            std::string label_str = ConvertLabelToUTF8(label, nullptr); +            std::string short_label_str = DeriveShortLabelUTF8(label_str, label.short_label_mask); +            etiLog.log(debug, "FICDecoder: SId 0x%04X, SCIdS %2d: service component label '" "\x1B[32m" "%s" "\x1B[0m" "' ('" "\x1B[32m" "%s" "\x1B[0m" "')\n", +                    sid, scids, label_str.c_str(), short_label_str.c_str()); +        } + +        UpdateService(service); +    } +} + +FIC_SUBCHANNEL& FICDecoder::GetSubchannel(int subchid) { +    // created automatically, if not yet existing +    return subchannels[subchid]; +} + +void FICDecoder::UpdateSubchannel(int subchid) { +    // update services that consist of this sub-channel +    for(const fic_services_t::value_type& service : services) { +        const FIC_SERVICE& s = service.second; +        if(s.audio_comps.find(subchid) != s.audio_comps.end()) +            UpdateService(s); +    } +} + +FIC_SERVICE& FICDecoder::GetService(uint16_t sid) { +    FIC_SERVICE& result = services[sid];    // created, if not yet existing + +    // if new service, set SID +    if(result.IsNone()) +        result.sid = sid; +    return result; +} + +void FICDecoder::UpdateService(const FIC_SERVICE& service) { +    // abort update, if primary component or label not yet present +    if(service.HasNoPriCompSubchid() || service.label.IsNone()) +        return; + +    // secondary components (if both component and definition are present) +    bool multi_comps = false; +    for(const comp_defs_t::value_type& comp_def : service.comp_defs) { +        if(comp_def.second == service.pri_comp_subchid || service.audio_comps.find(comp_def.second) == service.audio_comps.end()) +            continue; +        UpdateListedService(service, comp_def.first, true); +        multi_comps = true; +    } + +    // primary component +    UpdateListedService(service, LISTED_SERVICE::scids_none, multi_comps); +} + +void FICDecoder::UpdateListedService(const FIC_SERVICE& service, int scids, bool multi_comps) { +    // assemble listed service +    LISTED_SERVICE ls; +    ls.sid = service.sid; +    ls.scids = scids; +    ls.label = service.label; +    ls.pty_static = service.pty_static; +    ls.pty_dynamic = service.pty_dynamic; +    ls.asu_flags = service.asu_flags; +    ls.cids = service.cids; +    ls.pri_comp_subchid = service.pri_comp_subchid; +    ls.multi_comps = multi_comps; + +    if(scids == LISTED_SERVICE::scids_none) {   // primary component +        ls.audio_service = service.audio_comps.at(service.pri_comp_subchid); +    } else {                                    // secondary component +        ls.audio_service = service.audio_comps.at(service.comp_defs.at(scids)); + +        // use component label, if available +        comp_labels_t::const_iterator cl_it = service.comp_labels.find(scids); +        if(cl_it != service.comp_labels.end()) +            ls.label = cl_it->second; +    } + +    // use sub-channel information, if available +    fic_subchannels_t::const_iterator sc_it = subchannels.find(ls.audio_service.subchid); +    if(sc_it != subchannels.end()) +        ls.subchannel = sc_it->second; + +    /* check (for) Slideshow; currently only supported in X-PAD +     * - derive the required SCIdS (if not yet known) +     * - derive app type from UA data (if present) +     */ +    int sls_scids = scids; +    if(sls_scids == LISTED_SERVICE::scids_none) { +        for(const comp_defs_t::value_type& comp_def : service.comp_defs) { +            if(comp_def.second == ls.audio_service.subchid) { +                sls_scids = comp_def.first; +                break; +            } +        } +    } +    if(sls_scids != LISTED_SERVICE::scids_none && service.comp_sls_uas.find(sls_scids) != service.comp_sls_uas.end()) +        ls.sls_app_type = GetSLSAppType(service.comp_sls_uas.at(sls_scids)); + +    // forward to observer +    observer.FICChangeService(ls); +} + +int FICDecoder::GetSLSAppType(const ua_data_t& ua_data) { +    // default values, if no UA data present +    bool ca_flag = false; +    int xpad_app_type = 12; +    bool dg_flag = false; +    int dscty = 60; // MOT + +    // if UA data present, parse X-PAD data +    if(ua_data.size() >= 2) { +        ca_flag = ua_data[0] & 0x80; +        xpad_app_type = ua_data[0] & 0x1F; +        dg_flag = ua_data[1] & 0x80; +        dscty = ua_data[1] & 0x3F; +    } + +    // if no CA is used, but DGs and MOT, enable Slideshow +    if(!ca_flag && !dg_flag && dscty == 60) +        return xpad_app_type; +    else +        return LISTED_SERVICE::sls_app_type_none; +} + +void FICDecoder::UpdateEnsemble() { +    // abort update, if EId or label not yet present +    if(ensemble.IsNone() || ensemble.label.IsNone()) +        return; + +    // forward to observer +    observer.FICChangeEnsemble(ensemble); +} + +std::string FICDecoder::ConvertLabelToUTF8(const FIC_LABEL& label, std::string* charset_name) { +    std::string result = CharsetTools::ConvertTextToUTF8(label.label, sizeof(label.label), label.charset, charset_name); + +    // discard trailing spaces +    size_t last_pos = result.find_last_not_of(' '); +    if(last_pos != std::string::npos) +        result.resize(last_pos + 1); + +    return result; +} + +const size_t FICDecoder::uep_sizes[] = { +    16,  21,  24,  29,  35,  24,  29,  35,  42,  52,  29,  35,  42,  52,  32,  42, +    48,  58,  70,  40,  52,  58,  70,  84,  48,  58,  70,  84, 104,  58,  70,  84, +    104,  64,  84,  96, 116, 140,  80, 104, 116, 140, 168,  96, 116, 140, 168, 208, +    116, 140, 168, 208, 232, 128, 168, 192, 232, 280, 160, 208, 280, 192, 280, 416 +}; +const int FICDecoder::uep_pls[] = { +    5, 4, 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 3, 2, 5, 4, +    3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 3, +    2, 5, 4, 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 3, 2, 1, +    5, 4, 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 2, 5, 3, 1 +}; +const int FICDecoder::uep_bitrates[] = { +    32,  32,  32,  32,  32,  48,  48,  48,  48,  48,  56,  56,  56,  56,  64,  64, +    64,  64,  64,  80,  80,  80,  80,  80,  96,  96,  96,  96,  96, 112, 112, 112, +    112, 128, 128, 128, 128, 128, 160, 160, 160, 160, 160, 192, 192, 192, 192, 192, +    224, 224, 224, 224, 224, 256, 256, 256, 256, 256, 320, 320, 320, 384, 384, 384 +}; +const int FICDecoder::eep_a_size_factors[] = {12,  8,  6,  4}; +const int FICDecoder::eep_b_size_factors[] = {27, 21, 18, 15}; + +const char* FICDecoder::languages_0x00_to_0x2B[] = { +    "unknown/not applicable", "Albanian", "Breton", "Catalan", "Croatian", "Welsh", "Czech", "Danish", +    "German", "English", "Spanish", "Esperanto", "Estonian", "Basque", "Faroese", "French", +    "Frisian", "Irish", "Gaelic", "Galician", "Icelandic", "Italian", "Sami", "Latin", +    "Latvian", "Luxembourgian", "Lithuanian", "Hungarian", "Maltese", "Dutch", "Norwegian", "Occitan", +    "Polish", "Portuguese", "Romanian", "Romansh", "Serbian", "Slovak", "Slovene", "Finnish", +    "Swedish", "Turkish", "Flemish", "Walloon" +}; +const char* FICDecoder::languages_0x7F_downto_0x45[] = { +    "Amharic", "Arabic", "Armenian", "Assamese", "Azerbaijani", "Bambora", "Belorussian", "Bengali", +    "Bulgarian", "Burmese", "Chinese", "Chuvash", "Dari", "Fulani", "Georgian", "Greek", +    "Gujurati", "Gurani", "Hausa", "Hebrew", "Hindi", "Indonesian", "Japanese", "Kannada", +    "Kazakh", "Khmer", "Korean", "Laotian", "Macedonian", "Malagasay", "Malaysian", "Moldavian", +    "Marathi", "Ndebele", "Nepali", "Oriya", "Papiamento", "Persian", "Punjabi", "Pushtu", +    "Quechua", "Russian", "Rusyn", "Serbo-Croat", "Shona", "Sinhalese", "Somali", "Sranan Tongo", +    "Swahili", "Tadzhik", "Tamil", "Tatar", "Telugu", "Thai", "Ukranian", "Urdu", +    "Uzbek", "Vietnamese", "Zulu" +}; + +const char* FICDecoder::ptys_rds_0x00_to_0x1D[] = { +    "No programme type", "News", "Current Affairs", "Information", +    "Sport", "Education", "Drama", "Culture", +    "Science", "Varied", "Pop Music", "Rock Music", +    "Easy Listening Music", "Light Classical", "Serious Classical", "Other Music", +    "Weather/meteorology", "Finance/Business", "Children's programmes", "Social Affairs", +    "Religion", "Phone In", "Travel", "Leisure", +    "Jazz Music", "Country Music", "National Music", "Oldies Music", +    "Folk Music", "Documentary" +}; +const char* FICDecoder::ptys_rbds_0x00_to_0x1D[] = { +    "No program type", "News", "Information", "Sports", +    "Talk", "Rock", "Classic Rock", "Adult Hits", +    "Soft Rock", "Top 40", "Country", "Oldies", +    "Soft", "Nostalgia", "Jazz", "Classical", +    "Rhythm and Blues", "Soft Rhythm and Blues", "Foreign Language", "Religious Music", +    "Religious Talk", "Personality", "Public", "College", +    "(rfu)", "(rfu)", "(rfu)", "(rfu)", +    "(rfu)", "Weather" +}; + +const char* FICDecoder::asu_types_0_to_10[] = { +    "Alarm", "Road Traffic flash", "Transport flash", "Warning/Service", +    "News flash", "Area weather flash", "Event announcement", "Special event", +    "Programme Information", "Sport report", "Financial report" +}; + +std::string FICDecoder::ConvertLanguageToString(const int value) { +    if(value >= 0x00 && value <= 0x2B) +        return languages_0x00_to_0x2B[value]; +    if(value == 0x40) +        return "background sound/clean feed"; +    if(value >= 0x45 && value <= 0x7F) +        return languages_0x7F_downto_0x45[0x7F - value]; +    return "unknown (" + std::to_string(value) + ")"; +} + +std::string FICDecoder::ConvertLTOToString(const int value) { +    // just to silence recent GCC's truncation warnings +    int lto_value = value % 0x3F; + +    char lto_string[7]; +    snprintf(lto_string, sizeof(lto_string), "%+03d:%02d", lto_value / 2, (lto_value % 2) ? 30 : 0); +    return lto_string; +} + +std::string FICDecoder::ConvertInterTableIDToString(const int value) { +    switch(value) { +        case 0x01: +            return "RDS PTY"; +        case 0x02: +            return "RBDS PTY"; +        default: +            return "unknown"; +    } +} + +std::string FICDecoder::ConvertDateTimeToString(FIC_DAB_DT utc_dt, const int lto, bool output_ms) { +    const char* weekdays[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; + +    // if desired, apply LTO +    if(lto) +        utc_dt.dt.tm_min += lto * 30; + +    // normalize time (apply LTO, set day of week) +    if(mktime(&utc_dt.dt) == (time_t) -1) +        throw std::runtime_error("FICDecoder: error while normalizing date/time"); + +    std::string result; +    char s[11]; + +    strftime(s, sizeof(s), "%F", &utc_dt.dt); +    result += std::string(s) + ", " + weekdays[utc_dt.dt.tm_wday] + " - "; + +    if(!utc_dt.IsMsNone()) { +        // long form +        strftime(s, sizeof(s), "%T", &utc_dt.dt); +        result += s; +        if(output_ms) { +            snprintf(s, sizeof(s), ".%03d", utc_dt.ms); +            result += s; +        } +    } else { +        // short form +        strftime(s, sizeof(s), "%R", &utc_dt.dt); +        result += s; +    } + +    return result; +} + +std::string FICDecoder::ConvertPTYToString(const int value, const int inter_table_id) { +    switch(inter_table_id) { +        case 0x01: +            return value <= 0x1D ? ptys_rds_0x00_to_0x1D[value]  : "(not used)"; +        case 0x02: +            return value <= 0x1D ? ptys_rbds_0x00_to_0x1D[value] : "(not used)"; +        default: +            return "(unknown)"; +    } +} + +std::string FICDecoder::ConvertASuTypeToString(const int value) { +    if(value >= 0 && value <= 10) +        return asu_types_0_to_10[value]; +    return "unknown (" + std::to_string(value) + ")"; +} + +std::string FICDecoder::DeriveShortLabelUTF8(const std::string& long_label, uint16_t short_label_mask) { +    std::string short_label; + +    for(size_t i = 0; i < long_label.length(); i++)     // consider discarded trailing spaces +        if(short_label_mask & (0x8000 >> i)) +            short_label += StringTools::UTF8Substr(long_label, i, 1); + +    return short_label; +} diff --git a/src/FigParser.h b/src/FigParser.h new file mode 100644 index 0000000..b241123 --- /dev/null +++ b/src/FigParser.h @@ -0,0 +1,385 @@ +/* +   Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty +   the Queen in Right of Canada (Communications Research Center Canada) + +   Most parts of this file are taken from dablin, +   Copyright (C) 2015-2022 Stefan Pöschel + +   Copyright (C) 2023 +   Matthias P. Braendli, matthias.braendli@mpb.li + +    http://opendigitalradio.org + */ +/* +   This file is part of ODR-DabMod. + +   ODR-DabMod is free software: you can redistribute it and/or modify +   it under the terms of the GNU General Public License as +   published by the Free Software Foundation, either version 3 of the +   License, or (at your option) any later version. + +   ODR-DabMod is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU General Public License for more details. + +   You should have received a copy of the GNU General Public License +   along with ODR-DabMod.  If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once +#include <map> +#include <set> +#include <vector> +#include <optional> +#include <stdexcept> +#include <string> +#include <cmath> +#include <ctime> +#include <cstdint> +#include <cstdlib> +#include <cstring> + +struct FIG0_HEADER { +    bool cn; +    bool oe; +    bool pd; +    int extension; + +    FIG0_HEADER(uint8_t data) : cn(data & 0x80), oe(data & 0x40), pd(data & 0x20), extension(data & 0x1F) {} +}; + +struct FIG1_HEADER { +    int charset; +    bool oe; +    int extension; + +    FIG1_HEADER(uint8_t data) : charset(data >> 4), oe(data & 0x08), extension(data & 0x07) {} +}; + +struct FIC_LABEL { +    int charset; +    uint8_t label[16]; +    uint16_t short_label_mask; + +    static const int charset_none = -1; +    bool IsNone() const {return charset == charset_none;} + +    FIC_LABEL() : charset(charset_none), short_label_mask(0x0000) { +        memset(label, 0x00, sizeof(label)); +    } + +    bool operator==(const FIC_LABEL & fic_label) const { +        return charset == fic_label.charset && !memcmp(label, fic_label.label, sizeof(label)) && short_label_mask == fic_label.short_label_mask; +    } +    bool operator!=(const FIC_LABEL & fic_label) const { +        return !(*this == fic_label); +    } +}; + +struct FIC_SUBCHANNEL { +    size_t start; +    size_t size; +    std::string pl; +    int bitrate; +    int language; + +    static const int language_none = -1; +    bool IsNone() const {return pl.empty() && language == language_none;} + +    FIC_SUBCHANNEL() : start(0), size(0), bitrate(-1), language(language_none) {} + +    bool operator==(const FIC_SUBCHANNEL & fic_subchannel) const { +        return +            start == fic_subchannel.start && +            size == fic_subchannel.size && +            pl == fic_subchannel.pl && +            bitrate == fic_subchannel.bitrate && +            language == fic_subchannel.language; +    } +    bool operator!=(const FIC_SUBCHANNEL & fic_subchannel) const { +        return !(*this == fic_subchannel); +    } +}; + +struct FIC_ASW_CLUSTER { +    uint16_t asw_flags; +    int subchid; + +    static const int asw_flags_none = 0x0000; + +    static const int subchid_none = -1; +    bool IsNone() const {return subchid == subchid_none;} + +    FIC_ASW_CLUSTER() : asw_flags(asw_flags_none), subchid(subchid_none) {} + +    bool operator==(const FIC_ASW_CLUSTER & fic_asw_cluster) const { +        return asw_flags == fic_asw_cluster.asw_flags && subchid == fic_asw_cluster.subchid; +    } +    bool operator!=(const FIC_ASW_CLUSTER & fic_asw_cluster) const { +        return !(*this == fic_asw_cluster); +    } +}; + +typedef std::map<uint8_t,FIC_ASW_CLUSTER> asw_clusters_t; + +struct FIC_DAB_DT { +    struct tm dt; +    int ms; + +    static const int none = -1; +    bool IsNone() const {return dt.tm_year == none;} + +    static const int ms_none = -1; +    bool IsMsNone() const {return ms == ms_none;} + +    FIC_DAB_DT() : ms(ms_none) { +        dt.tm_year = none; +    } + +    bool operator==(const FIC_DAB_DT & fic_dab_dt) const { +        return +            ms == fic_dab_dt.ms && +            dt.tm_sec == fic_dab_dt.dt.tm_sec && +            dt.tm_min == fic_dab_dt.dt.tm_min && +            dt.tm_hour == fic_dab_dt.dt.tm_hour && +            dt.tm_mday == fic_dab_dt.dt.tm_mday && +            dt.tm_mon == fic_dab_dt.dt.tm_mon && +            dt.tm_year == fic_dab_dt.dt.tm_year; +    } +    bool operator!=(const FIC_DAB_DT & fic_dab_dt) const { +        return !(*this == fic_dab_dt); +    } +}; + +struct FIC_ENSEMBLE { +    int eid; +    bool al_flag; +    FIC_LABEL label; +    int ecc; +    int lto; +    int inter_table_id; +    asw_clusters_t asw_clusters; + +    static const int eid_none = -1; +    bool IsNone() const {return eid == eid_none;} + +    static const int ecc_none = -1; +    static const int lto_none = -100; +    static const int inter_table_id_none = -1; + +    FIC_ENSEMBLE() : +        eid(eid_none), +        al_flag(false), +        ecc(ecc_none), +        lto(lto_none), +        inter_table_id(inter_table_id_none) +    {} + +    bool operator==(const FIC_ENSEMBLE & ensemble) const { +        return +            eid == ensemble.eid && +            al_flag == ensemble.al_flag && +            label == ensemble.label && +            ecc == ensemble.ecc && +            lto == ensemble.lto && +            inter_table_id == ensemble.inter_table_id && +            asw_clusters == ensemble.asw_clusters; +    } +    bool operator!=(const FIC_ENSEMBLE & ensemble) const { +        return !(*this == ensemble); +    } +}; + +struct AUDIO_SERVICE { +    int subchid; +    bool dab_plus; + +    static const int subchid_none = -1; +    bool IsNone() const {return subchid == subchid_none;} + +    AUDIO_SERVICE() : AUDIO_SERVICE(subchid_none, false) {} +    AUDIO_SERVICE(int subchid, bool dab_plus) : subchid(subchid), dab_plus(dab_plus) {} + +    bool operator==(const AUDIO_SERVICE & audio_service) const { +        return subchid == audio_service.subchid && dab_plus == audio_service.dab_plus; +    } +    bool operator!=(const AUDIO_SERVICE & audio_service) const { +        return !(*this == audio_service); +    } +}; + +typedef std::map<int,AUDIO_SERVICE> audio_comps_t; +typedef std::map<int,int> comp_defs_t; +typedef std::map<int,FIC_LABEL> comp_labels_t; +typedef std::vector<uint8_t> ua_data_t; +typedef std::map<int,ua_data_t> comp_sls_uas_t; +typedef std::set<uint8_t> cids_t; + +struct FIC_SERVICE { +    int sid; +    int pri_comp_subchid; +    FIC_LABEL label; +    int pty_static; +    int pty_dynamic; +    uint16_t asu_flags; +    cids_t cids; + +    // components +    audio_comps_t audio_comps;      // from FIG 0/2 : SubChId -> AUDIO_SERVICE +    comp_defs_t comp_defs;          // from FIG 0/8 : SCIdS -> SubChId +    comp_labels_t comp_labels;      // from FIG 1/4 : SCIdS -> FIC_LABEL +    comp_sls_uas_t comp_sls_uas;    // from FIG 0/13: SCIdS -> UA data + +    static const int sid_none = -1; +    bool IsNone() const {return sid == sid_none;} + +    static const int pri_comp_subchid_none = -1; +    bool HasNoPriCompSubchid() const {return pri_comp_subchid == pri_comp_subchid_none;} + +    static const int pty_none = -1; + +    static const int asu_flags_none = 0x0000; + +    FIC_SERVICE() : sid(sid_none), pri_comp_subchid(pri_comp_subchid_none), pty_static(pty_none), pty_dynamic(pty_none), asu_flags(asu_flags_none) {} +}; + +struct LISTED_SERVICE { +    int sid; +    int scids; +    FIC_SUBCHANNEL subchannel; +    AUDIO_SERVICE audio_service; +    FIC_LABEL label; +    int pty_static; +    int pty_dynamic; +    int sls_app_type; +    uint16_t asu_flags; +    cids_t cids; + +    int pri_comp_subchid;   // only used for sorting +    bool multi_comps; + +    static const int sid_none = -1; +    bool IsNone() const {return sid == sid_none;} + +    static const int scids_none = -1; +    bool IsPrimary() const {return scids == scids_none;} + +    static const int pty_none = -1; + +    static const int asu_flags_none = 0x0000; + +    static const int sls_app_type_none = -1; +    bool HasSLS() const {return sls_app_type != sls_app_type_none;} + +    LISTED_SERVICE() : +        sid(sid_none), +        scids(scids_none), +        pty_static(pty_none), +        pty_dynamic(pty_none), +        sls_app_type(sls_app_type_none), +        asu_flags(asu_flags_none), +        pri_comp_subchid(AUDIO_SERVICE::subchid_none), +        multi_comps(false) +    {} + +    bool operator<(const LISTED_SERVICE & service) const { +        if(pri_comp_subchid != service.pri_comp_subchid) +            return pri_comp_subchid < service.pri_comp_subchid; +        if(sid != service.sid) +            return sid < service.sid; +        return scids < service.scids; +    } +}; + +typedef std::map<uint16_t, FIC_SERVICE> fic_services_t; +typedef std::map<int, FIC_SUBCHANNEL> fic_subchannels_t; + +// --- FICDecoderObserver ----------------------------------------------------------------- +class FICDecoderObserver { +    public: +        virtual ~FICDecoderObserver() {} + +        std::optional<FIC_ENSEMBLE> ensemble; +        std::optional<FIC_DAB_DT> utc_dt; +        std::map<int /*SId*/, LISTED_SERVICE> services; + +        virtual void FICChangeEnsemble(const FIC_ENSEMBLE& ensemble); +        virtual void FICChangeService(const LISTED_SERVICE& service); +        virtual void FICChangeUTCDateTime(const FIC_DAB_DT& utc_dt); + +        virtual void FICDiscardedFIB() {} +}; + + +// --- FICDecoder ----------------------------------------------------------------- +class FICDecoder { +    private: +        bool verbose; + +        void ProcessFIB(const uint8_t *data); + +        void ProcessFIG0(const uint8_t *data, size_t len); +        void ProcessFIG0_0(const uint8_t *data, size_t len); +        void ProcessFIG0_1(const uint8_t *data, size_t len); +        void ProcessFIG0_2(const uint8_t *data, size_t len); +        void ProcessFIG0_5(const uint8_t *data, size_t len); +        void ProcessFIG0_8(const uint8_t *data, size_t len); +        void ProcessFIG0_9(const uint8_t *data, size_t len); +        void ProcessFIG0_10(const uint8_t *data, size_t len); +        void ProcessFIG0_13(const uint8_t *data, size_t len); +        void ProcessFIG0_17(const uint8_t *data, size_t len); +        void ProcessFIG0_18(const uint8_t *data, size_t len); +        void ProcessFIG0_19(const uint8_t *data, size_t len); + +        void ProcessFIG1(const uint8_t *data, size_t len); +        void ProcessFIG1_0(uint16_t eid, const FIC_LABEL& label); +        void ProcessFIG1_1(uint16_t sid, const FIC_LABEL& label); +        void ProcessFIG1_4(uint16_t sid, int scids, const FIC_LABEL& label); + +        FIC_SUBCHANNEL& GetSubchannel(int subchid); +        void UpdateSubchannel(int subchid); +        FIC_SERVICE& GetService(uint16_t sid); +        void UpdateService(const FIC_SERVICE& service); +        void UpdateListedService(const FIC_SERVICE& service, int scids, bool multi_comps); +        int GetSLSAppType(const ua_data_t& ua_data); + +        FIC_ENSEMBLE ensemble; +        void UpdateEnsemble(); + +        fic_services_t services; +        fic_subchannels_t subchannels;  // from FIG 0/1: SubChId -> FIC_SUBCHANNEL + +        FIC_DAB_DT utc_dt; +        bool utc_dt_long; + +        static const size_t uep_sizes[]; +        static const int uep_pls[]; +        static const int uep_bitrates[]; +        static const int eep_a_size_factors[]; +        static const int eep_b_size_factors[]; + +        static const char* languages_0x00_to_0x2B[]; +        static const char* languages_0x7F_downto_0x45[]; + +        static const char* ptys_rds_0x00_to_0x1D[]; +        static const char* ptys_rbds_0x00_to_0x1D[]; + +        static const char* asu_types_0_to_10[]; +    public: +        FICDecoder(bool verbose); +        void Process(const uint8_t *data, size_t len); +        void Reset(); + +        FICDecoderObserver observer; + +        static std::string ConvertLabelToUTF8(const FIC_LABEL& label, std::string* charset_name); +        static std::string ConvertLanguageToString(const int value); +        static std::string ConvertLTOToString(const int value); +        static std::string ConvertInterTableIDToString(const int value); +        static std::string ConvertDateTimeToString(FIC_DAB_DT utc_dt, const int lto, bool output_ms); +        static std::string ConvertPTYToString(const int value, const int inter_table_id); +        static std::string ConvertASuTypeToString(const int value); +        static std::string DeriveShortLabelUTF8(const std::string& long_label, uint16_t short_label_mask); +}; + diff --git a/src/FormatConverter.cpp b/src/FormatConverter.cpp index 0f86d42..fc4cc2f 100644 --- a/src/FormatConverter.cpp +++ b/src/FormatConverter.cpp @@ -2,7 +2,7 @@     Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty     the Queen in Right of Canada (Communications Research Center Canada) -   Copyright (C) 2017 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -34,10 +34,6 @@  #include <stdexcept>  #include <assert.h> -#ifdef __SSE__ -#  include <xmmintrin.h> -#endif -  FormatConverter::FormatConverter(const std::string& format) :      ModCodec(),      m_format(format) @@ -49,6 +45,8 @@ int FormatConverter::process(Buffer* const dataIn, Buffer* dataOut)      PDEBUG("FormatConverter::process(dataIn: %p, dataOut: %p)\n",              dataIn, dataOut); +    size_t num_clipped_samples = 0; +      size_t sizeIn = dataIn->getLength() / sizeof(float);      float* in = reinterpret_cast<float*>(dataIn->getData()); @@ -57,7 +55,17 @@ int FormatConverter::process(Buffer* const dataIn, Buffer* dataOut)          int16_t* out = reinterpret_cast<int16_t*>(dataOut->getData());          for (size_t i = 0; i < sizeIn; i++) { -            out[i] = in[i]; +            if (in[i] < INT16_MIN) { +                out[i] = INT16_MIN; +                num_clipped_samples++; +            } +            else if (in[i] > INT16_MAX) { +                out[i] = INT16_MAX; +                num_clipped_samples++; +            } +            else { +                out[i] = in[i]; +            }          }      }      else if (m_format == "u8") { @@ -65,17 +73,44 @@ int FormatConverter::process(Buffer* const dataIn, Buffer* dataOut)          uint8_t* out = reinterpret_cast<uint8_t*>(dataOut->getData());          for (size_t i = 0; i < sizeIn; i++) { -            out[i] = in[i] + 128; +            const auto samp = in[i] + 128.0f; +            if (samp < 0) { +                out[i] = 0; +                num_clipped_samples++; +            } +            else if (samp > INT8_MAX) { +                out[i] = INT8_MAX; +                num_clipped_samples++; +            } +            else { +                out[i] = samp; +            } +          }      } -    else { +    else if (m_format == "s8") {          dataOut->setLength(sizeIn * sizeof(int8_t));          int8_t* out = reinterpret_cast<int8_t*>(dataOut->getData());          for (size_t i = 0; i < sizeIn; i++) { -            out[i] = in[i]; +            if (in[i] < INT8_MIN) { +                out[i] = INT8_MIN; +                num_clipped_samples++; +            } +            else if (in[i] > INT8_MAX) { +                out[i] = INT8_MAX; +                num_clipped_samples++; +            } +            else { +                out[i] = in[i]; +            }          }      } +    else { +        throw std::runtime_error("FormatConverter: Invalid format " + m_format); +    } + +    m_num_clipped_samples.store(num_clipped_samples);      return dataOut->getLength();  } @@ -85,3 +120,25 @@ const char* FormatConverter::name()      return "FormatConverter";  } +size_t FormatConverter::get_num_clipped_samples() const +{ +    return m_num_clipped_samples.load(); +} + + +size_t FormatConverter::get_format_size(const std::string& format) +{ +    // Returns 2*sizeof(SAMPLE_TYPE) because we have I + Q +    if (format == "s16") { +        return 4; +    } +    else if (format == "u8") { +        return 2; +    } +    else if (format == "s8") { +        return 2; +    } +    else { +        throw std::runtime_error("FormatConverter: Invalid format " + format); +    } +} diff --git a/src/FormatConverter.h b/src/FormatConverter.h index cc8a606..05511c0 100644 --- a/src/FormatConverter.h +++ b/src/FormatConverter.h @@ -2,7 +2,7 @@     Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty     the Queen in Right of Canada (Communications Research Center Canada) -   Copyright (C) 2017 +   Copyright (C) 2022     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -34,19 +34,27 @@  #include "ModPlugin.h"  #include <complex> +#include <atomic>  #include <string>  #include <cstdint>  class FormatConverter : public ModCodec  {      public: +        static size_t get_format_size(const std::string& format); + +        // Allowed formats: s8, u8 and s16          FormatConverter(const std::string& format);          int process(Buffer* const dataIn, Buffer* dataOut);          const char* name(); +        size_t get_num_clipped_samples() const; +      private:          std::string m_format; + +        std::atomic<size_t> m_num_clipped_samples = 0;  }; diff --git a/src/GainControl.cpp b/src/GainControl.cpp index b781640..84cf065 100644 --- a/src/GainControl.cpp +++ b/src/GainControl.cpp @@ -3,7 +3,7 @@     Her Majesty the Queen in Right of Canada (Communications Research     Center Canada) -   Copyright (C) 2017 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -127,21 +127,25 @@ int GainControl::internal_process(Buffer* const dataIn, Buffer* dataOut)      if ((sizeIn % m_frameSize) != 0) {          PDEBUG("%zu != %zu\n", sizeIn, m_frameSize); -        throw std::runtime_error( -                "GainControl::process input size not valid!"); +        throw std::runtime_error("GainControl::process input size not valid!");      }      const auto constantGain4 = _mm_set1_ps(constantGain);      for (size_t i = 0; i < sizeIn; i += m_frameSize) { -        gain128.m = computeGain(in, m_frameSize); +        // Do not apply gain computation to the NULL symbol, which either +        // is blank or contains TII. Apply the gain calculation from the next +        // symbol on the NULL symbol to get consistent TII power. +        if (i > 0) { +            gain128.m = computeGain(in, m_frameSize); +        } +        else { +            gain128.m = computeGain(in + m_frameSize, m_frameSize); +        }          gain128.m = _mm_mul_ps(gain128.m, constantGain4);          PDEBUG("********** Gain: %10f **********\n", gain128.f[0]); -        //////////////////////////////////////////////////////////////////////// -        // Applying gain to output data -        ////////////////////////////////////////////////////////////////////////          for (size_t sample = 0; sample < m_frameSize; ++sample) {              out[sample] = _mm_mul_ps(in[sample], gain128.m);          } @@ -163,7 +167,12 @@ int GainControl::internal_process(Buffer* const dataIn, Buffer* dataOut)      }      for (size_t i = 0; i < sizeIn; i += m_frameSize) { -        gain = constantGain * computeGain(in, m_frameSize); +        // Do not apply gain computation to the NULL symbol, which either +        // is blank or contains TII. Apply the gain calculation from the next +        // symbol on the NULL symbol to get consistent TII power. +        gain = constantGain * (i > 0 ? +                computeGain(in, m_frameSize) : +                computeGain(in + m_frameSize, m_frameSize));          PDEBUG("********** Gain: %10f **********\n", gain); @@ -574,3 +583,21 @@ const string GainControl::get_parameter(const string& parameter) const      return ss.str();  } +const json::map_t GainControl::get_all_values() const +{ +    json::map_t map; +    map["digital"].v = m_digGain; +    switch (m_gainmode) { +        case GainMode::GAIN_FIX: +            map["mode"].v = "fix"; +            break; +        case GainMode::GAIN_MAX: +            map["mode"].v = "max"; +            break; +        case GainMode::GAIN_VAR: +            map["mode"].v = "var"; +            break; +    } +    map["var"].v = m_var_variance_rc; +    return map; +} diff --git a/src/GainControl.h b/src/GainControl.h index 4c9a2bc..04f6b58 100644 --- a/src/GainControl.h +++ b/src/GainControl.h @@ -3,7 +3,7 @@     Her Majesty the Queen in Right of Canada (Communications Research     Center Canada) -   Copyright (C) 2017 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -38,6 +38,7 @@  #include <complex>  #include <string>  #include <mutex> +  #ifdef __SSE__  #   include <xmmintrin.h>  #endif @@ -63,13 +64,9 @@ class GainControl : public PipelinedModCodec, public RemoteControllable          const char* name() override { return "GainControl"; }          /* Functions for the remote control */ -        /* Base function to set parameters. */ -        virtual void set_parameter(const std::string& parameter, -                const std::string& value) override; - -        /* Getting a parameter always returns a string. */ -        virtual const std::string get_parameter( -                const std::string& parameter) const override; +        virtual void set_parameter(const std::string& parameter, const std::string& value) override; +        virtual const std::string get_parameter(const std::string& parameter) const override; +        virtual const json::map_t get_all_values() const override;      protected:          virtual int internal_process( diff --git a/src/GuardIntervalInserter.cpp b/src/GuardIntervalInserter.cpp index 0cd5bd5..3c2db14 100644 --- a/src/GuardIntervalInserter.cpp +++ b/src/GuardIntervalInserter.cpp @@ -2,7 +2,7 @@     Copyright (C) 2005, 2206, 2007, 2008, 2009, 2010, 2011 Her Majesty     the Queen in Right of Canada (Communications Research Center Canada) -   Copyright (C) 2017 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -302,3 +302,10 @@ const std::string GuardIntervalInserter::get_parameter(const std::string& parame      }      return ss.str();  } + +const json::map_t GuardIntervalInserter::get_all_values() const +{ +    json::map_t map; +    map["windowlen"].v = d_windowOverlap; +    return map; +} diff --git a/src/GuardIntervalInserter.h b/src/GuardIntervalInserter.h index 02ba72c..f78ac91 100644 --- a/src/GuardIntervalInserter.h +++ b/src/GuardIntervalInserter.h @@ -2,7 +2,7 @@     Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty     the Queen in Right of Canada (Communications Research Center Canada) -   Copyright (C) 2017 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -52,15 +52,15 @@ class GuardIntervalInserter : public ModCodec, public RemoteControllable                  size_t symSize,                  size_t& windowOverlap); -        int process(Buffer* const dataIn, Buffer* dataOut); -        const char* name() { return "GuardIntervalInserter"; } +        virtual ~GuardIntervalInserter() {} -        /******* REMOTE CONTROL ********/ -        virtual void set_parameter(const std::string& parameter, -                const std::string& value); +        int process(Buffer* const dataIn, Buffer* dataOut) override; +        const char* name() override { return "GuardIntervalInserter"; } -        virtual const std::string get_parameter( -                const std::string& parameter) const; +        /******* REMOTE CONTROL ********/ +        virtual void set_parameter(const std::string& parameter, const std::string& value) override; +        virtual const std::string get_parameter(const std::string& parameter) const override; +        virtual const json::map_t get_all_values() const override;      protected:          void update_window(size_t new_window_overlap); diff --git a/src/InputFileReader.cpp b/src/InputFileReader.cpp index 5a9780b..a6b482e 100644 --- a/src/InputFileReader.cpp +++ b/src/InputFileReader.cpp @@ -6,8 +6,7 @@     Copyrigth (C) 2018     Matthias P. Braendli, matthias.braendli@mpb.li - -   Input module for reading the ETI data from file or pipe, or ZeroMQ. +   Input module for reading the ETI data from file or pipe.     Supported file formats: RAW, FRAMED, STREAMED     Supports re-sync to RAW ETI file diff --git a/src/InputReader.h b/src/InputReader.h index ab45d4f..2484948 100644 --- a/src/InputReader.h +++ b/src/InputReader.h @@ -38,11 +38,6 @@  #include <memory>  #include <thread>  #include <unistd.h> -#if defined(HAVE_ZEROMQ) -#  include "zmq.hpp" -#  include "ThreadsafeQueue.h" -#  include "RemoteControl.h" -#endif  #include "Log.h"  #include "Socket.h"  #define INVALID_SOCKET   -1 @@ -148,60 +143,4 @@ class InputTcpReader : public InputReader          std::string m_uri;  }; -struct zmq_input_overflow : public std::exception -{ -    const char* what () const throw () -    { -        return "InputZMQ buffer overflow"; -    } -}; - -#if defined(HAVE_ZEROMQ) -/* A ZeroMQ input. See www.zeromq.org for more info */ - -class InputZeroMQReader : public InputReader, public RemoteControllable -{ -    public: -        InputZeroMQReader(); -        InputZeroMQReader(const InputZeroMQReader& other) = delete; -        InputZeroMQReader& operator=(const InputZeroMQReader& other) = delete; -        ~InputZeroMQReader(); - -        int Open(const std::string& uri, size_t max_queued_frames); -        virtual int GetNextFrame(void* buffer) override; -        virtual std::string GetPrintableInfo() const override; - -        /* Base function to set parameters. */ -        virtual void set_parameter( -                const std::string& parameter, -                const std::string& value) override; - -        /* Getting a parameter always returns a string. */ -        virtual const std::string get_parameter( -                const std::string& parameter) const override; - -    private: -        std::atomic<bool> m_running = ATOMIC_VAR_INIT(false); -        std::string m_uri; -        size_t m_max_queued_frames = 0; - -        // Either must contain a full ETI frame, or one flag must be set -        struct message_t { -            std::vector<uint8_t> eti_frame; -            bool overflow = false; -            bool timeout = false; -            bool fault = false; -        }; -        ThreadsafeQueue<message_t> m_in_messages; - -        mutable std::mutex m_last_in_messages_size_mutex; -        size_t m_last_in_messages_size = 0; - -        void RecvProcess(void); - -        zmq::context_t m_zmqcontext; // is thread-safe -        std::thread m_recv_thread; -}; - -#endif diff --git a/src/InputTcpReader.cpp b/src/InputTcpReader.cpp index 21f8496..8ba4d74 100644 --- a/src/InputTcpReader.cpp +++ b/src/InputTcpReader.cpp @@ -79,6 +79,9 @@ int InputTcpReader::GetNextFrame(void* buffer)          etiLog.level(debug) << "TCP input auto reconnect";          std::this_thread::sleep_for(std::chrono::seconds(1));      } +    else if (ret == -2) { +        etiLog.level(debug) << "TCP input timeout"; +    }      return ret;  } diff --git a/src/InputZeroMQReader.cpp b/src/InputZeroMQReader.cpp deleted file mode 100644 index 40a07d4..0000000 --- a/src/InputZeroMQReader.cpp +++ /dev/null @@ -1,323 +0,0 @@ -/* -   Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012 -   Her Majesty the Queen in Right of Canada (Communications Research -   Center Canada) - -   Copyright (C) 2018 -   Matthias P. Braendli, matthias.braendli@mpb.li - -    http://opendigitalradio.org - */ -/* -   This file is part of ODR-DabMod. - -   ODR-DabMod is free software: you can redistribute it and/or modify -   it under the terms of the GNU General Public License as -   published by the Free Software Foundation, either version 3 of the -   License, or (at your option) any later version. - -   ODR-DabMod is distributed in the hope that it will be useful, -   but WITHOUT ANY WARRANTY; without even the implied warranty of -   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the -   GNU General Public License for more details. - -   You should have received a copy of the GNU General Public License -   along with ODR-DabMod.  If not, see <http://www.gnu.org/licenses/>. - */ - -#ifdef HAVE_CONFIG_H -#   include "config.h" -#endif - -#if defined(HAVE_ZEROMQ) - -#include <string> -#include <cstring> -#include <cstdio> -#include <stdint.h> -#include "zmq.hpp" -#include "InputReader.h" -#include "PcDebug.h" -#include "Utils.h" - -using namespace std; - -constexpr int ZMQ_TIMEOUT_MS = 100; - -#define NUM_FRAMES_PER_ZMQ_MESSAGE 4 -/* A concatenation of four ETI frames, - * whose maximal size is 6144. - * - * Four frames in one zmq message are sent, so that - * we do not risk breaking ETI vs. transmission frame - * phase. - * - * The header is followed by the four ETI frames. - */ -struct zmq_msg_header_t -{ -    uint32_t version; -    uint16_t buflen[NUM_FRAMES_PER_ZMQ_MESSAGE]; -}; - -#define ZMQ_DAB_MESSAGE_T_HEADERSIZE \ -    (sizeof(uint32_t) + NUM_FRAMES_PER_ZMQ_MESSAGE*sizeof(uint16_t)) - -InputZeroMQReader::InputZeroMQReader() : -    InputReader(), -    RemoteControllable("inputzmq") -{ -    RC_ADD_PARAMETER(buffer, "Size of input buffer [us] (read-only)"); -} - -InputZeroMQReader::~InputZeroMQReader() -{ -    m_running = false; -    // This avoids the ugly "context was terminated" error because it lets -    // poll do its thing first -    this_thread::sleep_for(chrono::milliseconds(2 * ZMQ_TIMEOUT_MS)); -    m_zmqcontext.close(); -    if (m_recv_thread.joinable()) { -        m_recv_thread.join(); -    } -} - -int InputZeroMQReader::Open(const string& uri, size_t max_queued_frames) -{ -    // The URL might start with zmq+tcp:// -    if (uri.substr(0, 4) == "zmq+") { -        m_uri = uri.substr(4); -    } -    else { -        m_uri = uri; -    } - -    m_max_queued_frames = max_queued_frames; - -    m_running = true; -    m_recv_thread = std::thread(&InputZeroMQReader::RecvProcess, this); - -    return 0; -} - -int InputZeroMQReader::GetNextFrame(void* buffer) -{ -    if (not m_running) { -        throw runtime_error("ZMQ input is not ready yet"); -    } - -    message_t incoming; - -    /* Do some prebuffering because reads will happen in bursts -     * (4 ETI frames in TM1) and we should make sure that -     * we can serve the data required for a full transmission frame. -     */ -    if (m_in_messages.size() < 4) { -        const size_t prebuffering = 10; -        etiLog.log(trace, "ZMQ,wait1"); -        m_in_messages.wait_and_pop(incoming, prebuffering); -    } -    else { -        etiLog.log(trace, "ZMQ,wait2"); -        m_in_messages.wait_and_pop(incoming); -    } -    etiLog.log(trace, "ZMQ,pop"); - -    constexpr size_t framesize = 6144; - -    if (incoming.timeout) { -        return 0; -    } -    else if (incoming.fault) { -        throw runtime_error("ZMQ input has terminated"); -    } -    else if (incoming.overflow) { -        throw zmq_input_overflow(); -    } -    else if (incoming.eti_frame.size() == framesize) { -        unique_lock<mutex> lock(m_last_in_messages_size_mutex); -        m_last_in_messages_size--; -        lock.unlock(); - -        memcpy(buffer, &incoming.eti_frame.front(), framesize); - -        return framesize; -    } -    else { -        throw logic_error("ZMQ ETI not 6144"); -    } -} - -std::string InputZeroMQReader::GetPrintableInfo() const -{ -    return "Input ZeroMQ: Receiving from " + m_uri; -} - -void InputZeroMQReader::RecvProcess() -{ -    set_thread_name("zmqinput"); - -    size_t queue_size = 0; - -    zmq::socket_t subscriber(m_zmqcontext, ZMQ_SUB); -    // zmq sockets are not thread safe. That's why -    // we create it here, and not at object creation. - -    bool success = true; - -    try { -        subscriber.connect(m_uri.c_str()); -    } -    catch (const zmq::error_t& err) { -        etiLog.level(error) << "Failed to connect ZeroMQ socket to '" << -            m_uri << "': '" << err.what() << "'"; -        success = false; -    } - -    if (success) try { -        // subscribe to all messages -        subscriber.setsockopt(ZMQ_SUBSCRIBE, NULL, 0); -    } -    catch (const zmq::error_t& err) { -        etiLog.level(error) << "Failed to subscribe ZeroMQ socket to messages: '" << -            err.what() << "'"; -        success = false; -    } - -    if (success) try { -        while (m_running) { -            zmq::message_t incoming; -            zmq::pollitem_t items[1]; -            items[0].socket = subscriber; -            items[0].events = ZMQ_POLLIN; -            const int num_events = zmq::poll(items, 1, ZMQ_TIMEOUT_MS); -            if (num_events == 0) { -                message_t msg; -                msg.timeout = true; -                m_in_messages.push(move(msg)); -                continue; -            } - -            subscriber.recv(incoming); - -            if (queue_size < m_max_queued_frames) { -                if (incoming.size() < ZMQ_DAB_MESSAGE_T_HEADERSIZE) { -                    throw runtime_error("ZeroMQ packet too small for header"); -                } -                else { -                    zmq_msg_header_t dab_msg; -                    memcpy(&dab_msg, incoming.data(), sizeof(zmq_msg_header_t)); - -                    if (dab_msg.version != 1) { -                        etiLog.level(error) << -                            "ZeroMQ wrong packet version " << -                            dab_msg.version; -                    } - -                    int offset = sizeof(dab_msg.version) + -                        NUM_FRAMES_PER_ZMQ_MESSAGE * sizeof(*dab_msg.buflen); - -                    for (int i = 0; i < NUM_FRAMES_PER_ZMQ_MESSAGE; i++) { -                        if (dab_msg.buflen[i] > 6144) { -                            stringstream ss; -                            ss << "ZeroMQ buffer " << i << -                                " has invalid buflen " << dab_msg.buflen[i]; -                            throw runtime_error(ss.str()); -                        } -                        else { -                            vector<uint8_t> buf(6144, 0x55); - -                            const int framesize = dab_msg.buflen[i]; - -                            if ((ssize_t)incoming.size() < offset + framesize) { -                                throw runtime_error("ZeroMQ packet too small"); -                            } - -                            memcpy(&buf.front(), -                                    ((uint8_t*)incoming.data()) + offset, -                                    framesize); - -                            offset += framesize; - -                            message_t msg; -                            msg.eti_frame = move(buf); -                            queue_size = m_in_messages.push(move(msg)); -                            etiLog.log(trace, "ZMQ,push %zu", queue_size); - -                            unique_lock<mutex> lock(m_last_in_messages_size_mutex); -                            m_last_in_messages_size++; -                        } -                    } -                } -            } -            else { -                message_t msg; -                msg.overflow = true; -                queue_size = m_in_messages.push(move(msg)); -                etiLog.level(warn) << "ZeroMQ buffer overfull !"; -                throw runtime_error("ZMQ input full"); -            } - -            if (queue_size < 5) { -                etiLog.level(warn) << "ZeroMQ buffer low: " << queue_size << " elements !"; -            } -        } -    } -    catch (const zmq::error_t& err) { -        etiLog.level(error) << "ZeroMQ error during receive: '" << err.what() << "'"; -    } -    catch (const std::exception& err) { -        etiLog.level(error) << "Exception during receive: '" << err.what() << "'"; -    } - -    m_running = false; - -    etiLog.level(info) << "ZeroMQ input worker terminated"; - -    subscriber.close(); - -    message_t msg; -    msg.fault = true; -    queue_size = m_in_messages.push(move(msg)); -} - -// ======================================= -// Remote Control -// ======================================= -void InputZeroMQReader::set_parameter(const string& parameter, const string& value) -{ -    stringstream ss(value); -    ss.exceptions ( stringstream::failbit | stringstream::badbit ); - -    if (parameter == "buffer") { -        throw ParameterError("Parameter " + parameter + " is read-only."); -    } -    else { -        stringstream ss_err; -        ss_err << "Parameter '" << parameter -            << "' is not exported by controllable " << get_rc_name(); -        throw ParameterError(ss_err.str()); -    } -} - -const string InputZeroMQReader::get_parameter(const string& parameter) const -{ -    stringstream ss; -    ss << std::fixed; -    if (parameter == "buffer") { -        // Do not use size of the queue, as it will contain empty -        // frames to signal timeouts -        unique_lock<mutex> lock(m_last_in_messages_size_mutex); -        const long time_in_buffer_us = 24000 * m_last_in_messages_size; -        ss << time_in_buffer_us; -    } -    else { -        ss << "Parameter '" << parameter << -            "' is not exported by controllable " << get_rc_name(); -        throw ParameterError(ss.str()); -    } -    return ss.str(); -} - -#endif - diff --git a/src/MemlessPoly.cpp b/src/MemlessPoly.cpp index 905ca67..184b5bd 100644 --- a/src/MemlessPoly.cpp +++ b/src/MemlessPoly.cpp @@ -2,7 +2,7 @@     Copyright (C) 2007, 2008, 2009, 2010, 2011 Her Majesty the Queen in     Right of Canada (Communications Research Center Canada) -   Copyright (C) 2018 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li     Andreas Steger, andreas.steger@digris.ch @@ -314,7 +314,7 @@ void MemlessPoly::worker_thread(MemlessPoly::worker_t *workerdata)      set_thread_name("MemlessPoly");      while (true) { -        worker_t::input_data_t in_data; +        worker_t::input_data_t in_data = {};          try {              workerdata->in_queue.wait_and_pop(in_data);          } @@ -386,7 +386,7 @@ int MemlessPoly::internal_process(Buffer* const dataIn, Buffer* dataOut)              // Wait for completion of the tasks              for (auto& worker : m_workers) { -                int ret; +                int ret = 0;                  worker.out_queue.wait_and_pop(ret);              }          } @@ -467,3 +467,11 @@ const string MemlessPoly::get_parameter(const string& parameter) const      return ss.str();  } +const json::map_t MemlessPoly::get_all_values() const +{ +    json::map_t map; +    map["ncoefs"].v = m_coefs_am.size(); +    map["coefs"].v = serialise_coefficients(); +    map["coeffile"].v = m_coefs_file; +    return map; +} diff --git a/src/MemlessPoly.h b/src/MemlessPoly.h index 4642596..91e6860 100644 --- a/src/MemlessPoly.h +++ b/src/MemlessPoly.h @@ -2,7 +2,7 @@     Copyright (C) 2007, 2008, 2009, 2010, 2011 Her Majesty the Queen in     Right of Canada (Communications Research Center Canada) -   Copyright (C) 2018 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -63,17 +63,15 @@ public:      MemlessPoly& operator=(const MemlessPoly& other) = delete;      virtual ~MemlessPoly(); -    virtual const char* name() { return "MemlessPoly"; } +    virtual const char* name() override { return "MemlessPoly"; }      /******* REMOTE CONTROL ********/ -    virtual void set_parameter(const std::string& parameter, -            const std::string& value); - -    virtual const std::string get_parameter( -            const std::string& parameter) const; +    virtual void set_parameter(const std::string& parameter, const std::string& value) override; +    virtual const std::string get_parameter(const std::string& parameter) const override; +    virtual const json::map_t get_all_values() const override;  private: -    int internal_process(Buffer* const dataIn, Buffer* dataOut); +    int internal_process(Buffer* const dataIn, Buffer* dataOut) override;      void load_coefficients(std::istream& coefData);      std::string serialise_coefficients() const; diff --git a/src/ModPlugin.h b/src/ModPlugin.h index 7f03618..470508f 100644 --- a/src/ModPlugin.h +++ b/src/ModPlugin.h @@ -32,6 +32,7 @@  #include "Buffer.h"  #include "ThreadsafeQueue.h" +#include "TimestampDecoder.h"  #include <cstddef>  #include <vector>  #include <memory> @@ -41,9 +42,8 @@  // All flowgraph elements derive from ModPlugin, or a variant of it.  // Some ModPlugins also support handling metadata. -struct frame_timestamp;  struct flowgraph_metadata { -    std::shared_ptr<struct frame_timestamp> ts; +    frame_timestamp ts;  };  using meta_vec_t = std::vector<flowgraph_metadata>; diff --git a/src/OfdmGenerator.cpp b/src/OfdmGenerator.cpp index 2e68df0..cb799d3 100644 --- a/src/OfdmGenerator.cpp +++ b/src/OfdmGenerator.cpp @@ -2,7 +2,7 @@     Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty     the Queen in Right of Canada (Communications Research Center Canada) -   Copyright (C) 2017 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -457,3 +457,10 @@ const std::string OfdmGenerator::get_parameter(const std::string& parameter) con      }      return ss.str();  } + +const json::map_t OfdmGenerator::get_all_values() const +{ +    json::map_t map; +    // TODO needs rework of the values +    return map; +} diff --git a/src/OfdmGenerator.h b/src/OfdmGenerator.h index 30fdff4..dc1ad46 100644 --- a/src/OfdmGenerator.h +++ b/src/OfdmGenerator.h @@ -2,7 +2,7 @@     Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty     the Queen in Right of Canada (Communications Research Center Canada) -   Copyright (C) 2017 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -59,14 +59,9 @@ class OfdmGenerator : public ModCodec, public RemoteControllable          const char* name() override { return "OfdmGenerator"; }          /* Functions for the remote control */ -        /* Base function to set parameters. */ -        virtual void set_parameter( -                const std::string& parameter, -                const std::string& value) override; - -        /* Getting a parameter always returns a string. */ -        virtual const std::string get_parameter( -                const std::string& parameter) const override; +        virtual void set_parameter(const std::string& parameter, const std::string& value) override; +        virtual const std::string get_parameter(const std::string& parameter) const override; +        virtual const json::map_t get_all_values() const override;      protected:          struct cfr_iter_stat_t { diff --git a/src/OutputFile.cpp b/src/OutputFile.cpp index acaebad..2ee838c 100644 --- a/src/OutputFile.cpp +++ b/src/OutputFile.cpp @@ -74,28 +74,23 @@ meta_vec_t OutputFile::process_metadata(const meta_vec_t& metadataIn)          frame_timestamp first_ts;          for (const auto& md : metadataIn) { -            if (md.ts) { -                // The following code assumes TM I, where we get called every 96ms. -                // Support for other transmission modes skipped because this is mostly -                // debugging code. +            // The following code assumes TM I, where we get called every 96ms. +            // Support for other transmission modes skipped because this is mostly +            // debugging code. -                if (md.ts->fp == 0 or md.ts->fp == 4) { -                    first_ts = *md.ts; -                } +            if (md.ts.fp == 0 or md.ts.fp == 4) { +                first_ts = md.ts; +            } -                ss << " FCT=" << md.ts->fct << -                    " FP=" << (int)md.ts->fp; -                if (md.ts->timestamp_valid) { -                    ss << " TS=" << md.ts->timestamp_sec << " + " << -                        std::fixed -                        << (double)md.ts->timestamp_pps / 163840000.0 << ";"; -                } -                else { -                    ss << " TS invalid;"; -                } +            ss << " FCT=" << md.ts.fct << +                " FP=" << (int)md.ts.fp; +            if (md.ts.timestamp_valid) { +                ss << " TS=" << md.ts.timestamp_sec << " + " << +                    std::fixed +                    << (double)md.ts.timestamp_pps / 163840000.0 << ";";              }              else { -                ss << " void, "; +                ss << " TS invalid;";              }          } diff --git a/src/TII.cpp b/src/TII.cpp index 904f3ff..2656cbf 100644 --- a/src/TII.cpp +++ b/src/TII.cpp @@ -2,7 +2,7 @@     Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty     the Queen in Right of Canada (Communications Research Center Canada) -   Copyright (C) 2018 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -385,3 +385,12 @@ const std::string TII::get_parameter(const std::string& parameter) const      return ss.str();  } +const json::map_t TII::get_all_values() const +{ +    json::map_t map; +    map["enable"].v = m_conf.enable; +    map["pattern"].v = m_conf.pattern; +    map["comb"].v = m_conf.comb; +    map["old_variant"].v = m_conf.old_variant; +    return map; +} @@ -2,7 +2,7 @@     Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Her Majesty     the Queen in Right of Canada (Communications Research Center Canada) -   Copyright (C) 2018 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -82,16 +82,15 @@ class TII : public ModCodec, public RemoteControllable  {      public:          TII(unsigned int dabmode, tii_config_t& tii_config); +        virtual ~TII() {} -        int process(Buffer* dataIn, Buffer* dataOut); -        const char* name(); +        int process(Buffer* dataIn, Buffer* dataOut) override; +        const char* name() override;          /******* REMOTE CONTROL ********/ -        virtual void set_parameter(const std::string& parameter, -                const std::string& value); - -        virtual const std::string get_parameter( -                const std::string& parameter) const; +        virtual void set_parameter(const std::string& parameter, const std::string& value) override; +        virtual const std::string get_parameter(const std::string& parameter) const override; +        virtual const json::map_t get_all_values() const override;      protected:          // Fill m_Acp with the correct carriers for the pattern/comb diff --git a/src/TimestampDecoder.cpp b/src/TimestampDecoder.cpp index 2133125..a7972c9 100644 --- a/src/TimestampDecoder.cpp +++ b/src/TimestampDecoder.cpp @@ -2,7 +2,7 @@     Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the     Queen in Right of Canada (Communications Research Center Canada) -   Copyright (C) 2018 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -36,6 +36,31 @@  //#define MDEBUG(fmt, args...) fprintf (LOG, "*****" fmt , ## args)  #define MDEBUG(fmt, args...) PDEBUG(fmt, ## args) +double frame_timestamp::offset_to_system_time() const +{ +    if (not timestamp_valid) { +        throw new std::runtime_error("Cannot calculate offset for invalid timestamp"); +    } + +    struct timespec t; +    if (clock_gettime(CLOCK_REALTIME, &t) != 0) { +        throw std::runtime_error(std::string("Failed to retrieve CLOCK_REALTIME") + strerror(errno)); +    } + +    return get_real_secs() - (double)t.tv_sec - (t.tv_nsec / 1000000000.0); +} + +std::string frame_timestamp::to_string() const +{ +    time_t s = timestamp_sec; +    std::stringstream ss; +    char timestr[100]; +    if (std::strftime(timestr, sizeof(timestr), "%Y-%m-%dZ%H:%M:%S", std::gmtime(&s))) { +        ss << timestr << " + " << ((double)timestamp_pps / 16384000.0); +    } +    return ss.str(); +} +  frame_timestamp& frame_timestamp::operator+=(const double& diff)  {      double offset_pps, offset_secs; @@ -75,20 +100,21 @@ TimestampDecoder::TimestampDecoder(double& offset_s) :          timestamp_offset << " offset";  } -std::shared_ptr<frame_timestamp> TimestampDecoder::getTimestamp() +frame_timestamp TimestampDecoder::getTimestamp()  { -    auto ts = std::make_shared<frame_timestamp>(); +    frame_timestamp ts; -    ts->timestamp_valid = full_timestamp_received; -    ts->timestamp_sec = time_secs; -    ts->timestamp_pps = time_pps; -    ts->fct = latestFCT; -    ts->fp = latestFP; +    ts.timestamp_valid = full_timestamp_received; +    ts.timestamp_sec = time_secs; +    ts.timestamp_pps = time_pps; +    ts.fct = latestFCT; +    ts.fp = latestFP; -    ts->offset_changed = offset_changed; +    ts.timestamp_offset = timestamp_offset; +    ts.offset_changed = offset_changed;      offset_changed = false; -    *ts += timestamp_offset; +    ts += timestamp_offset;      return ts;  } @@ -275,3 +301,22 @@ const std::string TimestampDecoder::get_parameter(      return ss.str();  } +const json::map_t TimestampDecoder::get_all_values() const +{ +    json::map_t map; +    map["offset"].v = timestamp_offset; +    if (full_timestamp_received) { +        map["timestamp"].v = time_secs + ((double)time_pps / 16384000.0); +    } +    else { +        map["timestamp"].v = std::nullopt; +    } + +    if (full_timestamp_received) { +        map["timestamp0"].v = time_secs_of_frame0 + ((double)time_pps_of_frame0 / 16384000.0); +    } +    else { +        map["timestamp0"].v = std::nullopt; +    } +    return map; +} diff --git a/src/TimestampDecoder.h b/src/TimestampDecoder.h index dda8644..25796ca 100644 --- a/src/TimestampDecoder.h +++ b/src/TimestampDecoder.h @@ -2,7 +2,7 @@     Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the     Queen in Right of Canada (Communications Research Center Canada) -   Copyright (C) 2022 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -39,9 +39,11 @@ struct frame_timestamp      int32_t fct;      uint8_t fp; // Frame Phase -    uint32_t timestamp_sec; +    uint32_t timestamp_sec; // seconds in unix epoch      uint32_t timestamp_pps; // In units of 1/16384000 s      bool timestamp_valid = false; + +    double timestamp_offset = 0.0; // copy of the configured modulator offset      bool offset_changed = false;      frame_timestamp& operator+=(const double& diff); @@ -56,6 +58,8 @@ struct frame_timestamp          return timestamp_pps / 16384000.0;      } +    double offset_to_system_time() const; +      double get_real_secs() const {          double t = timestamp_sec;          t += pps_offset(); @@ -74,6 +78,8 @@ struct frame_timestamp          timestamp_pps = lrint(subsecond * 16384000.0);      } +    std::string to_string() const; +      void print(const char* t) const {          etiLog.log(debug,                  "%s <frame_timestamp(%s, %d, %.9f, %d)>\n", @@ -92,8 +98,9 @@ class TimestampDecoder : public RemoteControllable           * frame transmission           */          TimestampDecoder(double& offset_s); +        virtual ~TimestampDecoder() {} -        std::shared_ptr<frame_timestamp> getTimestamp(void); +        frame_timestamp getTimestamp(void);          /* Update timestamp data from ETI */          void updateTimestampEti( @@ -112,16 +119,12 @@ class TimestampDecoder : public RemoteControllable          /*********** REMOTE CONTROL ***************/          /* Base function to set parameters. */ -        virtual void set_parameter(const std::string& parameter, -                const std::string& value); - -        /* Getting a parameter always returns a string. */ -        virtual const std::string get_parameter( -                const std::string& parameter) const; +        virtual void set_parameter(const std::string& parameter, const std::string& value) override; +        virtual const std::string get_parameter(const std::string& parameter) const override; +        virtual const json::map_t get_all_values() const override;          const char* name() { return "TS"; } -      protected:          /* Push a new MNSC field into the decoder */          void pushMNSCData(uint8_t framephase, uint16_t mnsc); diff --git a/src/Utils.cpp b/src/Utils.cpp index f39c4c9..788d125 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -3,7 +3,7 @@     Her Majesty the Queen in Right of Canada (Communications Research     Center Canada) -   Copyright (C) 2018 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -25,6 +25,8 @@     along with ODR-DabMod.  If not, see <http://www.gnu.org/licenses/>.   */ +#include <ctime> +#include <sstream>  #include "Utils.h"  #include "GainControl.h"  #if defined(HAVE_PRCTL) @@ -62,10 +64,6 @@ static void printHeader()          "SSE " <<  #endif          "\n"; - -#if defined(BUILD_FOR_EASYDABV3) -    std::cerr << " This is a build for the EasyDABv3 board" << std::endl; -#endif  }  void printUsage(const char* progName) @@ -77,13 +75,8 @@ void printUsage(const char* progName)      fprintf(out, "Usage with command line options:\n");      fprintf(out, "\t%s"              " input" -#if defined(BUILD_FOR_EASYDABV3) -            " -f filename -F format" -#else              " (-f filename -F format | -u uhddevice -F frequency)" -#endif              " [-o offset]" -#if !defined(BUILD_FOR_EASYDABV3)              "\n\t"              " [-G txgain]"              " [-T filter_taps_file]" @@ -93,14 +86,12 @@ void printUsage(const char* progName)              " [-g gainMode]"              " [-m dabMode]"              " [-r samplingRate]" -#endif              " [-l]"              " [-h]"              "\n", progName);      fprintf(out, "Where:\n");      fprintf(out, "input:         ETI input filename (default: stdin), or\n");      fprintf(out, "                  tcp://source:port for ETI-over-TCP input, or\n"); -    fprintf(out, "                  zmq+tcp://source:port for ZMQ input.\n");      fprintf(out, "                  udp://:port for EDI input.\n");      fprintf(out, "-f name:       Use file output with given filename. (use /dev/stdout for standard output)\n");      fprintf(out, "-F format:     Set the output format (see doc/example.ini for formats) for the file output.\n"); @@ -108,7 +99,6 @@ void printUsage(const char* progName)      fprintf(out, "                  Specifying this option has two implications: It enables synchronous transmission,\n"                   "                  requiring an external REFCLK and PPS signal and frames that do not contain a valid timestamp\n"                   "                  get muted.\n\n"); -#if !defined(BUILD_FOR_EASYDABV3)      fprintf(out, "-u device:     Use UHD output with given device string. (use "" for default device)\n");      fprintf(out, "-F frequency:  Set the transmit frequency when using UHD output. (mandatory option when using UHD)\n");      fprintf(out, "-G txgain:     Set the transmit gain for the UHD driver (default: 0)\n"); @@ -119,7 +109,6 @@ void printUsage(const char* progName)      fprintf(out, "-g gainmode:   Set computation gain mode: fix, max or var\n");      fprintf(out, "-m mode:       Set DAB mode: (0: auto, 1-4: force).\n");      fprintf(out, "-r rate:       Set output sampling rate (default: 2048000).\n\n"); -#endif      fprintf(out, "-l:            Loop file when reach end of file.\n");      fprintf(out, "-h:            Print this help.\n");  } @@ -132,7 +121,7 @@ void printVersion(void)              "    ODR-DabMod is copyright (C) Her Majesty the Queen in Right of Canada,\n"              "    2005 -- 2012 Communications Research Centre (CRC),\n"              "     and\n" -            "    Copyright (C) 2018 Matthias P. Braendli, matthias.braendli@mpb.li\n" +            "    Copyright (C) 2023 Matthias P. Braendli, matthias.braendli@mpb.li\n"              "\n"              "    http://opendigitalradio.org\n"              "\n" @@ -157,6 +146,87 @@ void printStartupInfo()      printHeader();  } +void printModSettings(const mod_settings_t& mod_settings) +{ +    std::stringstream ss; +    // Print settings +    ss << "Input\n"; +    ss << "  Type: " << mod_settings.inputTransport << "\n"; +    ss << "  Source: " << mod_settings.inputName << "\n"; + +    ss << "Output\n"; + +    if (mod_settings.useFileOutput) { +        ss << "  Name: " << mod_settings.outputName << "\n"; +    } +#if defined(HAVE_OUTPUT_UHD) +    else if (mod_settings.useUHDOutput) { +        ss << " UHD\n" << +            "  Device: " << mod_settings.sdr_device_config.device << "\n" << +            "  Subdevice: " << +                mod_settings.sdr_device_config.subDevice << "\n" << +            "  master_clock_rate: " << +                mod_settings.sdr_device_config.masterClockRate << "\n" << +            "  refclk: " << +                mod_settings.sdr_device_config.refclk_src << "\n" << +            "  pps source: " << +                mod_settings.sdr_device_config.pps_src << "\n"; +    } +#endif +#if defined(HAVE_SOAPYSDR) +    else if (mod_settings.useSoapyOutput) { +        ss << " SoapySDR\n" +            "  Device: " << mod_settings.sdr_device_config.device << "\n" << +            "  master_clock_rate: " << +                mod_settings.sdr_device_config.masterClockRate << "\n"; +    } +#endif +#if defined(HAVE_DEXTER) +    else if (mod_settings.useDexterOutput) { +        ss << " PrecisionWave DEXTER\n"; +    } +#endif +#if defined(HAVE_LIMESDR) +    else if (mod_settings.useLimeOutput) { +        ss << " LimeSDR\n" +            "  Device: " << mod_settings.sdr_device_config.device << "\n" << +            "  master_clock_rate: " << +                mod_settings.sdr_device_config.masterClockRate << "\n"; +    } +#endif +#if defined(HAVE_BLADERF) +    else if (mod_settings.useBladeRFOutput) { +        ss << " BladeRF\n" +            "  Device: " << mod_settings.sdr_device_config.device << "\n" << +            "  refclk: " << mod_settings.sdr_device_config.refclk_src << "\n"; +    } +#endif +    else if (mod_settings.useZeroMQOutput) { +        ss << " ZeroMQ\n" << +            "  Listening on: " << mod_settings.outputName << "\n" << +            "  Socket type : " << mod_settings.zmqOutputSocketType << "\n"; +    } + +    ss << "  Sampling rate: "; +    if (mod_settings.outputRate > 1000) { +        if (mod_settings.outputRate > 1000000) { +            ss << std::fixed << std::setprecision(4) << +                mod_settings.outputRate / 1000000.0 << +                " MHz\n"; +        } +        else { +            ss << std::fixed << std::setprecision(4) << +                mod_settings.outputRate / 1000.0 << +                " kHz\n"; +        } +    } +    else { +        ss << std::fixed << std::setprecision(4) << +            mod_settings.outputRate << " Hz\n"; +    } +    fprintf(stderr, "%s", ss.str().c_str()); +} +  int set_realtime_prio(int prio)  {      // Set thread priority to realtime @@ -174,7 +244,7 @@ void set_thread_name(const char *name)  #endif  } -double parseChannel(const std::string& chan) +double parse_channel(const std::string& chan)  {      double freq;      if      (chan == "5A") freq = 174928000; @@ -216,12 +286,59 @@ double parseChannel(const std::string& chan)      else if (chan == "13E") freq = 237488000;      else if (chan == "13F") freq = 239200000;      else { -        std::cerr << "       soapy output: channel " << chan << " does not exist in table\n"; -        throw std::out_of_range("soapy channel selection error"); +        std::cerr << "Channel " << chan << " does not exist in table\n"; +        throw std::out_of_range("channel out of range");      }      return freq;  } +std::optional<std::string> convert_frequency_to_channel(double frequency) +{ +    const int freq = round(frequency); +    std::string chan; +    if      (freq == 174928000) chan = "5A"; +    else if (freq == 176640000) chan = "5B"; +    else if (freq == 178352000) chan = "5C"; +    else if (freq == 180064000) chan = "5D"; +    else if (freq == 181936000) chan = "6A"; +    else if (freq == 183648000) chan = "6B"; +    else if (freq == 185360000) chan = "6C"; +    else if (freq == 187072000) chan = "6D"; +    else if (freq == 188928000) chan = "7A"; +    else if (freq == 190640000) chan = "7B"; +    else if (freq == 192352000) chan = "7C"; +    else if (freq == 194064000) chan = "7D"; +    else if (freq == 195936000) chan = "8A"; +    else if (freq == 197648000) chan = "8B"; +    else if (freq == 199360000) chan = "8C"; +    else if (freq == 201072000) chan = "8D"; +    else if (freq == 202928000) chan = "9A"; +    else if (freq == 204640000) chan = "9B"; +    else if (freq == 206352000) chan = "9C"; +    else if (freq == 208064000) chan = "9D"; +    else if (freq == 209936000) chan = "10A"; +    else if (freq == 211648000) chan = "10B"; +    else if (freq == 213360000) chan = "10C"; +    else if (freq == 215072000) chan = "10D"; +    else if (freq == 216928000) chan = "11A"; +    else if (freq == 218640000) chan = "11B"; +    else if (freq == 220352000) chan = "11C"; +    else if (freq == 222064000) chan = "11D"; +    else if (freq == 223936000) chan = "12A"; +    else if (freq == 225648000) chan = "12B"; +    else if (freq == 227360000) chan = "12C"; +    else if (freq == 229072000) chan = "12D"; +    else if (freq == 230784000) chan = "13A"; +    else if (freq == 232496000) chan = "13B"; +    else if (freq == 234208000) chan = "13C"; +    else if (freq == 235776000) chan = "13D"; +    else if (freq == 237488000) chan = "13E"; +    else if (freq == 239200000) chan = "13F"; +    else { return std::nullopt; } + +    return chan; +} +  std::chrono::milliseconds transmission_frame_duration(unsigned int dabmode)  {      using namespace std::chrono; @@ -235,3 +352,13 @@ std::chrono::milliseconds transmission_frame_duration(unsigned int dabmode)      }  } + +time_t get_clock_realtime_seconds() +{ +    struct timespec t; +    if (clock_gettime(CLOCK_REALTIME, &t) != 0) { +        throw std::runtime_error(std::string("Failed to retrieve CLOCK_REALTIME") + strerror(errno)); +    } + +    return t.tv_sec; +} diff --git a/src/Utils.h b/src/Utils.h index 9e88488..584a756 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -3,7 +3,7 @@     Her Majesty the Queen in Right of Canada (Communications Research     Center Canada) -   Copyright (C) 2018 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -31,12 +31,15 @@  #   include "config.h"  #endif -#include <stdlib.h> -#include <unistd.h> -#include <stdio.h> -#include <time.h> +#include <optional>  #include <string>  #include <chrono> +#include <cstdio> +#include <ctime> +#include <cstdlib> +#include <cstdint> +#include <unistd.h> +#include "ConfigParser.h"  void printUsage(const char* progName); @@ -44,15 +47,22 @@ void printVersion(void);  void printStartupInfo(void); +void printModSettings(const mod_settings_t& mod_settings); +  // Set SCHED_RR with priority prio (0=lowest)  int set_realtime_prio(int prio);  // Set the name of the thread  void set_thread_name(const char *name); -// Convert a channel like 10A to a frequency -double parseChannel(const std::string& chan); +// Convert a channel like 10A to a frequency in Hz +double parse_channel(const std::string& chan); + +// Convert a frequency in Hz to a channel. +std::optional<std::string> convert_frequency_to_channel(double frequency);  // dabMode is either 1, 2, 3, 4, corresponding to TM I, TM II, TM III and TM IV.  // throws a runtime_error if dabMode is not one of these values.  std::chrono::milliseconds transmission_frame_duration(unsigned int dabmode); + +time_t get_clock_realtime_seconds(); diff --git a/src/output/BladeRF.cpp b/src/output/BladeRF.cpp index a6ad0cc..c16b64d 100755 --- a/src/output/BladeRF.cpp +++ b/src/output/BladeRF.cpp @@ -239,13 +239,10 @@ double BladeRF::get_bandwidth(void) const      return (double)bw;  } -SDRDevice::RunStatistics BladeRF::get_run_statistics(void) const +SDRDevice::run_statistics_t BladeRF::get_run_statistics(void) const  { -    RunStatistics rs; -    rs.num_underruns = underflows; -    rs.num_overruns = overflows; -    rs.num_late_packets = late_packets; -    rs.num_frames_modulated = num_frames_modulated; +    run_statistics_t rs; +    rs["frames"] = num_frames_modulated;      return rs;  } @@ -269,14 +266,14 @@ double BladeRF::get_rxgain(void) const  size_t BladeRF::receive_frame(      complexf *buf,      size_t num_samples, -    struct frame_timestamp &ts, +    frame_timestamp &ts,      double timeout_secs)  {      // TODO      return 0;  } -bool BladeRF::is_clk_source_ok() const +bool BladeRF::is_clk_source_ok()  {      // TODO      return true; @@ -287,24 +284,23 @@ const char *BladeRF::device_name(void) const      return "BladeRF";  } -double BladeRF::get_temperature(void) const +std::optional<double> BladeRF::get_temperature(void) const  {      if (not m_device)          throw runtime_error("BladeRF device not set up");      float temp = 0.0; -      int status = bladerf_get_rfic_temperature(m_device, &temp); -    if (status < 0) -    { +    if (status >= 0) { +        return (double)temp; +    } +    else {          etiLog.level(error) << "Error getting BladeRF temperature: %s " << bladerf_strerror(status); +        return std::nullopt;      } - -    return (double)temp;  } - -void BladeRF::transmit_frame(const struct FrameData &frame) // SC16 frames +void BladeRF::transmit_frame(struct FrameData&& frame) // SC16 frames  {      const size_t num_samples = frame.buf.size() / (2*sizeof(int16_t)); diff --git a/src/output/BladeRF.h b/src/output/BladeRF.h index bc6db38..fa3419e 100755 --- a/src/output/BladeRF.h +++ b/src/output/BladeRF.h @@ -74,8 +74,8 @@ class BladeRF : public Output::SDRDevice         virtual double get_txgain(void) const override;         virtual void set_bandwidth(double bandwidth) override;         virtual double get_bandwidth(void) const override; -       virtual void transmit_frame(const struct FrameData& frame) override; -       virtual RunStatistics get_run_statistics(void) const override; +       virtual void transmit_frame(struct FrameData&& frame) override; +       virtual run_statistics_t get_run_statistics(void) const override;         virtual double get_real_secs(void) const override;         virtual void set_rxgain(double rxgain) override; @@ -83,14 +83,14 @@ class BladeRF : public Output::SDRDevice         virtual size_t receive_frame(                 complexf *buf,                 size_t num_samples, -               struct frame_timestamp& ts, +               frame_timestamp& ts,                 double timeout_secs) override;         // Return true if GPS and reference clock inputs are ok -       virtual bool is_clk_source_ok(void) const override; +       virtual bool is_clk_source_ok(void) override;         virtual const char* device_name(void) const override; -       virtual double get_temperature(void) const override; +       virtual std::optional<double> get_temperature(void) const override;     private: @@ -99,14 +99,9 @@ class BladeRF : public Output::SDRDevice         bladerf_channel m_channel = BLADERF_CHANNEL_TX(0); // channel TX0         //struct bladerf_stream* m_stream; /* used for asynchronous api */ -       size_t underflows = 0; -       size_t overflows = 0; -       size_t late_packets = 0;         size_t num_frames_modulated = 0; -       //size_t num_underflows_previous = 0; -       //size_t num_late_packets_previous = 0;  };  } // namespace Output -#endif // HAVE_BLADERF
\ No newline at end of file +#endif // HAVE_BLADERF diff --git a/src/output/Dexter.cpp b/src/output/Dexter.cpp new file mode 100644 index 0000000..26472e8 --- /dev/null +++ b/src/output/Dexter.cpp @@ -0,0 +1,691 @@ +/* +   Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the +   Queen in Right of Canada (Communications Research Center Canada) + +   Copyright (C) 2023 +   Matthias P. Braendli, matthias.braendli@mpb.li + +    http://opendigitalradio.org + +DESCRIPTION: +   It is an output driver using libiio targeting the PrecisionWave DEXTER board. +*/ + +/* +   This file is part of ODR-DabMod. + +   ODR-DabMod is free software: you can redistribute it and/or modify +   it under the terms of the GNU General Public License as +   published by the Free Software Foundation, either version 3 of the +   License, or (at your option) any later version. + +   ODR-DabMod is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU General Public License for more details. + +   You should have received a copy of the GNU General Public License +   along with ODR-DabMod.  If not, see <http://www.gnu.org/licenses/>. + */ + +#include "output/Dexter.h" + +#ifdef HAVE_DEXTER + +#include <chrono> +#include <limits> +#include <cstdio> +#include <iomanip> + +#include "Log.h" +#include "Utils.h" + +using namespace std; + +namespace Output { + +static constexpr uint64_t DSP_CLOCK = 2048000uLL * 80; + +static constexpr uint64_t IIO_TIMEOUT_MS = 1000; + +static constexpr size_t TRANSMISSION_FRAME_LEN_SAMPS = (2656 + 76 * 2552) * /* I+Q */ 2; +static constexpr size_t IIO_BUFFERS = 2; +static constexpr size_t IIO_BUFFER_LEN_SAMPS = TRANSMISSION_FRAME_LEN_SAMPS / IIO_BUFFERS; + +static string get_iio_error(int err) +{ +    char dst[256]; +    iio_strerror(-err, dst, sizeof(dst)); +    return string(dst); +} + +static void fill_time(struct timespec *t) +{ +    if (clock_gettime(CLOCK_REALTIME, t) != 0) { +        throw std::runtime_error(string("Failed to retrieve CLOCK_REALTIME") + strerror(errno)); +    } +} + +Dexter::Dexter(SDRDeviceConfig& config) : +    SDRDevice(), +    m_conf(config) +{ +    etiLog.level(info) << "Dexter:Creating the device"; + +    m_ctx = iio_create_local_context(); +    if (not m_ctx) { +        throw std::runtime_error("Dexter: Unable to create iio context"); +    } + +    int r; +    if ((r = iio_context_set_timeout(m_ctx, IIO_TIMEOUT_MS)) != 0) { +        etiLog.level(error) << "Failed to set IIO timeout " << get_iio_error(r); +    } + +    m_dexter_dsp_tx = iio_context_find_device(m_ctx, "dexter_dsp_tx"); +    if (not m_dexter_dsp_tx) { +        throw std::runtime_error("Dexter: Unable to find dexter_dsp_tx iio device"); +    } + +    m_ad9957 = iio_context_find_device(m_ctx, "ad9957"); +    if (not m_ad9957) { +        throw std::runtime_error("Dexter: Unable to find ad9957 iio device"); +    } + +    m_ad9957_tx0 = iio_context_find_device(m_ctx, "ad9957_tx0"); +    if (not m_ad9957_tx0) { +        throw std::runtime_error("Dexter: Unable to find ad9957_tx0 iio device"); +    } + +    // TODO make DC offset configurable and add to RC +    if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "dc0", 0)) != 0) { +        throw std::runtime_error("Failed to set dexter_dsp_tx.dc0 = false: " + get_iio_error(r)); +    } + +    if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "dc1", 0)) != 0) { +        throw std::runtime_error("Failed to set dexter_dsp_tx.dc1 = false: " + get_iio_error(r)); +    } + +    if (m_conf.sampleRate != 2048000) { +        throw std::runtime_error("Dexter: Only 2048000 samplerate supported"); +    } + +    tune(m_conf.lo_offset, m_conf.frequency); +    // TODO m_conf.frequency = m_dexter_dsp_tx->getFrequency(SOAPY_SDR_TX, 0); +    etiLog.level(info) << "Dexter:Actual frequency: " << +        std::fixed << std::setprecision(3) << +        m_conf.frequency / 1000.0 << " kHz."; + +    // skip: Set bandwidth + +    // skip: antenna + +    // The FIFO should not contain data, but setting gain=0 before setting start_clks to zero is an additional security +    if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "gain0", 0)) != 0) { +        throw std::runtime_error("Failed to set dexter_dsp_tx.gain0 = 0 : " + get_iio_error(r)); +    } + +    if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "stream0_flush_fifo_trigger", 1)) != 0) { +        throw std::runtime_error("Failed to set dexter_dsp_tx.stream0_flush_fifo_trigger = 1 : " + get_iio_error(r)); +    } + +    if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "stream0_start_clks", 0)) != 0) { +        throw std::runtime_error("Failed to set dexter_dsp_tx.stream0_start_clks = 0 : " + get_iio_error(r)); +    } + +    constexpr int CHANNEL_INDEX = 0; +    m_tx_channel = iio_device_get_channel(m_ad9957_tx0, CHANNEL_INDEX); +    if (m_tx_channel == nullptr) { +        throw std::runtime_error("Dexter: Cannot create IIO channel."); +    } + +    iio_channel_enable(m_tx_channel); + +    m_buffer = iio_device_create_buffer(m_ad9957_tx0, IIO_BUFFER_LEN_SAMPS, 0); +    if (not m_buffer) { +        throw std::runtime_error("Dexter: Cannot create IIO buffer."); +    } + +    // Flush the FPGA FIFO +    { +        constexpr size_t buflen_samps = TRANSMISSION_FRAME_LEN_SAMPS / IIO_BUFFERS; +        constexpr size_t buflen = buflen_samps * sizeof(int16_t); + +        memset(iio_buffer_start(m_buffer), 0, buflen); +        ssize_t pushed = iio_buffer_push(m_buffer); +        if (pushed < 0) { +            etiLog.level(error) << "Dexter: init push buffer " << get_iio_error(pushed); +        } +        this_thread::sleep_for(chrono::milliseconds(200)); +    } + +    if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "gain0", m_conf.txgain)) != 0) { +        etiLog.level(error) << "Failed to set dexter_dsp_tx.gain0 = " << m_conf.txgain << +            " : " << get_iio_error(r); +    } + +    m_running = true; +    m_underflow_read_thread = std::thread(&Dexter::underflow_read_process, this); +} + +void Dexter::channel_up() +{ +    int r; +    if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "gain0", m_conf.txgain)) != 0) { +        etiLog.level(error) << "Failed to set dexter_dsp_tx.gain0 = " << m_conf.txgain << +            " : " << get_iio_error(r); +    } + +    m_channel_is_up = true; +    etiLog.level(debug) << "DEXTER CHANNEL_UP"; +} + +void Dexter::channel_down() +{ +    int r; +    if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "gain0", 0)) != 0) { +        etiLog.level(error) << "Failed to set dexter_dsp_tx.gain0 = 0: " << get_iio_error(r); +    } + +    // Setting stream0_start_clocks to 0 will flush out the FIFO, but we need to wait a bit before +    // we "up" the channel again +    if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "stream0_start_clks", 0)) != 0) { +        etiLog.level(warn) << "Failed to set dexter_dsp_tx.stream0_start_clks = 0 : " << get_iio_error(r); +    } + +    long long underflows_old = 0; + +    if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "buffer_underflows0", &underflows_old)) != 0) { +        etiLog.level(warn) << "Failed to read dexter_dsp_tx.buffer_underflows0 : " << get_iio_error(r); +    } + +    long long underflows = underflows_old; + +    // Limiting to 10*96ms is just a safety to avoid running into an infinite loop +    for (size_t i = 0; underflows == underflows_old && i < 10; i++) { +        if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "buffer_underflows0", &underflows)) != 0) { +            etiLog.level(warn) << "Failed to read dexter_dsp_tx.buffer_underflows0 : " << get_iio_error(r); +        } + +        this_thread::sleep_for(chrono::milliseconds(96)); +    } + +    if (underflows == underflows_old) { +        etiLog.level(warn) << "DEXTER CHANNEL_DOWN, no underflow detected! " << underflows; +    } + +    m_channel_is_up = false; +    etiLog.level(debug) << "DEXTER CHANNEL_DOWN"; +} + +void Dexter::handle_hw_time() +{ +    /* +     * On startup, wait until `gpsdo_locked==1` and `pps_loss_of_signal==0`, +     * then do the clocks alignment and go to normal state. +     * +     * In normal state, if `pps_loss_of_signal==1`, go to holdover state. +     * +     * If we've been in holdover state for longer than the configured time, or +     * if `pps_loss_of_signal==0` stop the mod and restart. +     */ +    int r; + +    switch (m_clock_state) { +        case DexterClockState::Startup: +            { +                long long gpsdo_locked = 0; +                if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "gpsdo_locked", &gpsdo_locked)) != 0) { +                    etiLog.level(error) << "Failed to get dexter_dsp_tx.gpsdo_locked: " << get_iio_error(r); +                    throw std::runtime_error("Dexter: Cannot read IIO attribute"); +                } + +                long long pps_loss_of_signal = 0; +                if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "pps_loss_of_signal", &pps_loss_of_signal)) != 0) { +                    etiLog.level(error) << "Failed to get dexter_dsp_tx.pps_loss_of_signal: " << get_iio_error(r); +                    throw std::runtime_error("Dexter: Cannot read IIO attribute"); +                } + +                if (gpsdo_locked == 1 and pps_loss_of_signal == 0) { +                    /* Procedure: +                     * Wait 200ms after second change, fetch pps_clks attribute +                     * idem at the next second, and check that pps_clks incremented by DSP_CLOCK +                     * If ok, store the correspondence between current second change (measured in UTC clock time) +                     * and the counter value at pps rising edge. */ + +                    etiLog.level(info) << "Dexter: Waiting for second change..."; + +                    struct timespec time_at_startup; +                    fill_time(&time_at_startup); +                    time_at_startup.tv_nsec = 0; + +                    struct timespec time_now; +                    do { +                        fill_time(&time_now); +                        this_thread::sleep_for(chrono::milliseconds(1)); +                    } while (time_at_startup.tv_sec == time_now.tv_sec); +                    this_thread::sleep_for(chrono::milliseconds(200)); + +                    long long pps_clks = 0; +                    if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "pps_clks", &pps_clks)) != 0) { +                        etiLog.level(error) << "Failed to get dexter_dsp_tx.pps_clks: " << get_iio_error(r); +                        throw std::runtime_error("Dexter: Cannot read IIO attribute"); +                    } + +                    time_t tnow = time_now.tv_sec; +                    etiLog.level(info) << "Dexter: pps_clks " << pps_clks << " at UTC " << +                        put_time(std::gmtime(&tnow), "%Y-%m-%d %H:%M:%S"); + +                    time_at_startup.tv_sec = time_now.tv_sec; +                    do { +                        fill_time(&time_now); +                        this_thread::sleep_for(chrono::milliseconds(1)); +                    } while (time_at_startup.tv_sec == time_now.tv_sec); +                    this_thread::sleep_for(chrono::milliseconds(200)); + +                    long long pps_clks2 = 0; +                    if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "pps_clks", &pps_clks2)) != 0) { +                        etiLog.level(error) << "Failed to get dexter_dsp_tx.pps_clks: " << get_iio_error(r); +                        throw std::runtime_error("Dexter: Cannot read IIO attribute"); +                    } +                    tnow = time_now.tv_sec; +                    etiLog.level(info) << "Dexter: pps_clks increased by " << pps_clks2 - pps_clks << " at UTC " << +                        put_time(std::gmtime(&tnow), "%Y-%m-%d %H:%M:%S"); + +                    if ((uint64_t)pps_clks + DSP_CLOCK != (uint64_t)pps_clks2) { +                        throw std::runtime_error("Dexter: Wrong increase of pps_clks, expected " + to_string(DSP_CLOCK)); +                    } + +                    m_utc_seconds_at_startup = time_now.tv_sec; +                    m_clock_count_at_startup = pps_clks2; +                    m_holdover_since = chrono::steady_clock::time_point::min(); +                    m_holdover_since_t = 0; +                    m_clock_state = DexterClockState::Normal; +                    etiLog.level(debug) << "Dexter: switch clock state Startup -> Normal"; +                } +            } +            break; +        case DexterClockState::Normal: +            { +                long long pps_loss_of_signal = 0; +                if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "pps_loss_of_signal", &pps_loss_of_signal)) != 0) { +                    etiLog.level(error) << "Failed to get dexter_dsp_tx.pps_loss_of_signal: " << get_iio_error(r); +                    throw std::runtime_error("Dexter: Cannot read IIO attribute"); +                } + +                if (pps_loss_of_signal == 1) { +                    m_holdover_since = chrono::steady_clock::now(); +                    m_holdover_since_t = chrono::system_clock::to_time_t(chrono::system_clock::now()); +                    m_clock_state = DexterClockState::Holdover; +                    etiLog.level(debug) << "Dexter: switch clock state Normal -> Holdover"; +                } +            } +            break; +        case DexterClockState::Holdover: +            { +                using namespace chrono; + +                long long pps_loss_of_signal = 0; +                if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "pps_loss_of_signal", &pps_loss_of_signal)) != 0) { +                    etiLog.level(error) << "Failed to get dexter_dsp_tx.pps_loss_of_signal: " << get_iio_error(r); +                    throw std::runtime_error("Dexter: Cannot read IIO attribute"); +                } + +                const duration<double> d = steady_clock::now() - m_holdover_since; +                const auto max_holdover_duration = seconds(m_conf.maxGPSHoldoverTime); +                if (d > max_holdover_duration or pps_loss_of_signal == 0) { +                    m_clock_state = DexterClockState::Startup; +                    m_utc_seconds_at_startup = 0; +                    m_clock_count_at_startup = 0; +                    m_holdover_since = chrono::steady_clock::time_point::min(); +                    m_holdover_since_t = 0; +                    etiLog.level(debug) << "Dexter: switch clock state Holdover -> Startup"; +                } +            } +            break; +    } +} + +void Dexter::tune(double lo_offset, double frequency) +{ +    // lo_offset is applied to the DSP, and frequency is given to the ad9957 + +    long long freq = frequency; +    int r = 0; +    if ((r = iio_device_attr_write_longlong(m_ad9957, "center_frequency", freq)) != 0) { +        etiLog.level(warn) << "Failed to set ad9957.center_frequency = " << freq << " : " << get_iio_error(r); +    } + +    long long lo_offs = lo_offset; + +    if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "frequency0", lo_offs)) != 0) { +        etiLog.level(warn) << "Failed to set dexter_dsp_tx.frequency0 = " << lo_offs << " : " << get_iio_error(r); +    } +} + +double Dexter::get_tx_freq(void) const +{ +    long long lo_offset = 0; +    int r = 0; + +    if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "frequency0", &lo_offset)) != 0) { +        etiLog.level(warn) << "Failed to read dexter_dsp_tx.frequency0: " << get_iio_error(r); +        return 0; +    } + +    long long frequency = 0; +    if ((r = iio_device_attr_read_longlong(m_ad9957, "center_frequency", &frequency)) != 0) { +        etiLog.level(warn) << "Failed to read ad9957.center_frequency: " << get_iio_error(r); +        return 0; +    } + +    return frequency + lo_offset; +} + +void Dexter::set_txgain(double txgain) +{ +    int r = 0; +    if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "gain0", txgain)) != 0) { +        etiLog.level(warn) << "Failed to set dexter_dsp_tx.gain0 = " << txgain << ": " << get_iio_error(r); +    } + +    long long txgain_readback = 0; +    if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "gain0", &txgain_readback)) != 0) { +        etiLog.level(warn) << "Failed to read dexter_dsp_tx.gain0: " << get_iio_error(r); +    } +    else { +        m_conf.txgain = txgain_readback; +    } +} + +double Dexter::get_txgain(void) const +{ +    long long txgain_readback = 0; +    int r = 0; +    if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "gain0", &txgain_readback)) != 0) { +        etiLog.level(warn) << "Failed to read dexter_dsp_tx.gain0: " << get_iio_error(r); +    } +    return txgain_readback; +} + +void Dexter::set_bandwidth(double bandwidth) +{ +    return; +} + +double Dexter::get_bandwidth(void) const +{ +    return 0; +} + +SDRDevice::run_statistics_t Dexter::get_run_statistics(void) const +{ +    run_statistics_t rs; +    { +        std::unique_lock<std::mutex> lock(m_attr_thread_mutex); +        rs["underruns"].v = underflows; +    } +    rs["latepackets"].v = num_late; +    rs["frames"].v = num_frames_modulated; + +    rs["in_holdover_since"].v = 0; +    rs["remaining_holdover_s"].v = nullopt; +    switch (m_clock_state) { +        case DexterClockState::Startup: +            rs["clock_state"].v = "startup"; break; +        case DexterClockState::Normal: +            rs["clock_state"].v = "normal"; break; +        case DexterClockState::Holdover: +            rs["clock_state"].v = "holdover"; +            rs["in_holdover_since"].v = m_holdover_since_t; +            { +                using namespace std::chrono; +                const auto max_holdover_duration = seconds(m_conf.maxGPSHoldoverTime); +                const duration<double> remaining = max_holdover_duration - (steady_clock::now() - m_holdover_since); +                rs["remaining_holdover_s"].v = (ssize_t)duration_cast<seconds>(remaining).count(); +            } +            break; +    } + +    return rs; +} + +double Dexter::get_real_secs(void) const +{ +    long long clks = 0; +    int r = 0; +    if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "clks", &clks)) != 0) { +        etiLog.level(error) << "Failed to get dexter_dsp_tx.clks: " << get_iio_error(r); +        throw std::runtime_error("Dexter: Cannot read IIO attribute"); +    } + +    switch (m_clock_state) { +        case DexterClockState::Startup: +            return 0; +        case DexterClockState::Normal: +        case DexterClockState::Holdover: +            return (double)m_utc_seconds_at_startup + (double)(clks - m_clock_count_at_startup) / (double)DSP_CLOCK; +    } +    throw std::logic_error("Unhandled switch"); +} + +void Dexter::set_rxgain(double rxgain) +{ +    // TODO +} + +double Dexter::get_rxgain(void) const +{ +    // TODO +    return 0; +} + +size_t Dexter::receive_frame( +        complexf *buf, +        size_t num_samples, +        frame_timestamp& ts, +        double timeout_secs) +{ +    // TODO +    return 0; +} + + +bool Dexter::is_clk_source_ok() +{ +    if (m_conf.enableSync) { +        handle_hw_time(); +        return m_clock_state != DexterClockState::Startup; +    } +    else { +        return true; +    } +} + +const char* Dexter::device_name(void) const +{ +    return "Dexter"; +} + +std::optional<double> Dexter::get_temperature(void) const +{ +    std::ifstream in("/sys/bus/i2c/devices/1-002f/hwmon/hwmon0/temp1_input", std::ios::in | std::ios::binary); +    if (in) { +        double tbaseboard; +        in >> tbaseboard; +        return tbaseboard / 1000.0; +    } +    else { +        return {}; +    } +} + +void Dexter::transmit_frame(struct FrameData&& frame) +{ +    constexpr size_t frame_len_bytes = TRANSMISSION_FRAME_LEN_SAMPS * sizeof(int16_t); +    if (frame.buf.size() != frame_len_bytes) { +        etiLog.level(debug) << "Dexter::transmit_frame Expected " << +            frame_len_bytes << " got " << frame.buf.size(); +        throw std::runtime_error("Dexter: invalid buffer size"); +    } + +    const bool require_timestamped_tx = (m_conf.enableSync and frame.ts.timestamp_valid); + +    if (not m_channel_is_up) { +        if (require_timestamped_tx) { +            if (m_clock_state == DexterClockState::Startup) { +                return; // not ready +            } +            else { +                constexpr uint64_t TIMESTAMP_PPS_PER_DSP_CLOCKS = DSP_CLOCK / 16384000; +                // TIMESTAMP_PPS_PER_DSP_CLOCKS=10 because timestamp_pps is represented in 16.384 MHz clocks +                uint64_t frame_start_clocks = +                    // at second level +                    ((int64_t)frame.ts.timestamp_sec - (int64_t)m_utc_seconds_at_startup) * DSP_CLOCK + m_clock_count_at_startup + +                    // at subsecond level +                    (uint64_t)frame.ts.timestamp_pps * TIMESTAMP_PPS_PER_DSP_CLOCKS; + +                const double margin_s = frame.ts.offset_to_system_time(); + +                long long clks = 0; +                int r = 0; +                if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "clks", &clks)) != 0) { +                    etiLog.level(error) << "Failed to get dexter_dsp_tx.clks: " << get_iio_error(r); +                    throw std::runtime_error("Dexter: Cannot read IIO attribute"); +                } + +                const double margin_device_s = (double)(frame_start_clocks - clks) / DSP_CLOCK; + +                etiLog.level(debug) << "DEXTER FCT " << frame.ts.fct << " TS CLK " << +                    ((int64_t)frame.ts.timestamp_sec - (int64_t)m_utc_seconds_at_startup) * DSP_CLOCK << " + " << +                    m_clock_count_at_startup << " + " << +                    (uint64_t)frame.ts.timestamp_pps * TIMESTAMP_PPS_PER_DSP_CLOCKS << " = " << +                    frame_start_clocks << " DELTA " << margin_s << " " << margin_device_s; + +                // Ensure we hand the frame over to HW with a bit of margin +                if (margin_s < 0.2) { +                    etiLog.level(warn) << "Skip frame short margin " << margin_s; +                    num_late++; +                    return; +                } + +                if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "stream0_start_clks", frame_start_clocks)) != 0) { +                    etiLog.level(warn) << "Skip frame, failed to set dexter_dsp_tx.stream0_start_clks = " << frame_start_clocks << " : " << get_iio_error(r); +                    num_late++; +                    return; +                } +                m_require_timestamp_refresh = false; +            } +        } + +        channel_up(); +    } + +    if (m_require_timestamp_refresh) { +        etiLog.level(debug) << "DEXTER REQUIRE REFRESH"; +        channel_down(); +        m_require_timestamp_refresh = false; +    } + +    // DabMod::launch_modulator ensures we get int16_t IQ here +    //const size_t num_samples = frame.buf.size() / (2*sizeof(int16_t)); +    //const int16_t *buf = reinterpret_cast<const int16_t*>(frame.buf.data()); + +    if (m_channel_is_up) { +        for (size_t i = 0; i < IIO_BUFFERS; i++) { +            constexpr size_t buflen_samps = TRANSMISSION_FRAME_LEN_SAMPS / IIO_BUFFERS; +            constexpr size_t buflen = buflen_samps * sizeof(int16_t); + +            memcpy(iio_buffer_start(m_buffer), frame.buf.data() + (i * buflen), buflen); +            ssize_t pushed = iio_buffer_push(m_buffer); +            if (pushed < 0) { +                etiLog.level(error) << "Dexter: failed to push buffer " << get_iio_error(pushed) << +                    " after " << num_buffers_pushed << " bufs"; +                num_buffers_pushed = 0; +                channel_down(); +                break; +            } +            num_buffers_pushed++; +        } +        num_frames_modulated++; +    } + +    { +        std::unique_lock<std::mutex> lock(m_attr_thread_mutex); +        size_t u = underflows; +        lock.unlock(); + +        if (u != 0 and u != prev_underflows) { +            etiLog.level(warn) << "Dexter: underflow! " << prev_underflows << " -> " << u; +        } + +        prev_underflows = u; +    } +} + +void Dexter::underflow_read_process() +{ +    m_underflow_ctx = iio_create_local_context(); +    if (not m_underflow_ctx) { +        throw std::runtime_error("Dexter: Unable to create iio context for underflow"); +    } + +    auto dexter_dsp_tx = iio_context_find_device(m_ctx, "dexter_dsp_tx"); +    if (not dexter_dsp_tx) { +        throw std::runtime_error("Dexter: Unable to find dexter_dsp_tx iio device"); +    } + +    set_thread_name("dexter_underflow"); + +    while (m_running) { +        this_thread::sleep_for(chrono::seconds(1)); +        long long underflows_attr = 0; + +        int r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "buffer_underflows0", &underflows_attr); + +        if (r == 0) { +            size_t underflows_new = underflows_attr; + +            std::unique_lock<std::mutex> lock(m_attr_thread_mutex); +            if (underflows_new != underflows and underflows_attr != 0) { +                underflows = underflows_new; +            } +        } +    } +    m_running = false; +} + +Dexter::~Dexter() +{ +    m_running = false; +    if (m_underflow_read_thread.joinable()) { +        m_underflow_read_thread.join(); +    } + +    if (m_ctx) { +        if (m_dexter_dsp_tx) { +            iio_device_attr_write_longlong(m_dexter_dsp_tx, "gain0", 0); +        } + +        if (m_buffer) { +            iio_buffer_destroy(m_buffer); +            m_buffer = nullptr; +        } + +        if (m_tx_channel) { +            iio_channel_disable(m_tx_channel); +        } + +        iio_context_destroy(m_ctx); +        m_ctx = nullptr; +    } + +    if (m_underflow_ctx) { +        iio_context_destroy(m_underflow_ctx); +        m_underflow_ctx = nullptr; +    } +} + +} // namespace Output + +#endif // HAVE_DEXTER diff --git a/src/output/Dexter.h b/src/output/Dexter.h new file mode 100644 index 0000000..d4f425f --- /dev/null +++ b/src/output/Dexter.h @@ -0,0 +1,138 @@ +/* +   Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the +   Queen in Right of Canada (Communications Research Center Canada) + +   Copyright (C) 2023 +   Matthias P. Braendli, matthias.braendli@mpb.li + +    http://opendigitalradio.org + +DESCRIPTION: +   It is an output driver using libiio targeting the PrecisionWave DEXTER board. +*/ + +/* +   This file is part of ODR-DabMod. + +   ODR-DabMod is free software: you can redistribute it and/or modify +   it under the terms of the GNU General Public License as +   published by the Free Software Foundation, either version 3 of the +   License, or (at your option) any later version. + +   ODR-DabMod is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU General Public License for more details. + +   You should have received a copy of the GNU General Public License +   along with ODR-DabMod.  If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#ifdef HAVE_CONFIG_H +#   include <config.h> +#endif + +#ifdef HAVE_DEXTER +#include "iio.h" + +#include <string> +#include <memory> +#include <ctime> +#include <mutex> +#include <thread> +#include <variant> + +#include "output/SDR.h" +#include "ModPlugin.h" +#include "EtiReader.h" +#include "RemoteControl.h" + +namespace Output { + +enum class DexterClockState { +   Startup, +   Normal, +   Holdover +}; + +class Dexter : public Output::SDRDevice +{ +    public: +        Dexter(SDRDeviceConfig& config); +        Dexter(const Dexter& other) = delete; +        Dexter& operator=(const Dexter& other) = delete; +        virtual ~Dexter(); + +        virtual void tune(double lo_offset, double frequency) override; +        virtual double get_tx_freq(void) const override; +        virtual void set_txgain(double txgain) override; +        virtual double get_txgain() const override; +        virtual void set_bandwidth(double bandwidth) override; +        virtual double get_bandwidth() const override; +        virtual void transmit_frame(struct FrameData&& frame) override; +        virtual run_statistics_t get_run_statistics() const override; +        virtual double get_real_secs() const override; + +        virtual void set_rxgain(double rxgain) override; +        virtual double get_rxgain() const override; +        virtual size_t receive_frame( +                complexf *buf, +                size_t num_samples, +                frame_timestamp& ts, +                double timeout_secs) override; + +        // Return true if GPS and reference clock inputs are ok +        virtual bool is_clk_source_ok() override; +        virtual const char* device_name() const override; + +        virtual std::optional<double> get_temperature() const override; + +    private: +        void channel_up(); +        void channel_down(); +        void handle_hw_time(); + +        bool m_channel_is_up = false; + +        SDRDeviceConfig& m_conf; + +        struct iio_context* m_ctx = nullptr; +        struct iio_device* m_dexter_dsp_tx = nullptr; + +        struct iio_device* m_ad9957 = nullptr; +        struct iio_device* m_ad9957_tx0 = nullptr; +        struct iio_channel* m_tx_channel = nullptr; +        struct iio_buffer *m_buffer = nullptr; + +        /* Underflows are counted in a separate thread */ +        struct iio_context* m_underflow_ctx = nullptr; +        std::atomic<bool> m_running = ATOMIC_VAR_INIT(false); +        std::thread m_underflow_read_thread; +        void underflow_read_process(); +        mutable std::mutex m_attr_thread_mutex; +        size_t underflows = 0; + +        size_t prev_underflows = 0; +        size_t num_late = 0; +        size_t num_frames_modulated = 0; + +        size_t num_buffers_pushed = 0; + +        DexterClockState m_clock_state = DexterClockState::Startup; + +        // Only valid when m_clock_state is not Startup +        uint64_t m_utc_seconds_at_startup = 0; +        uint64_t m_clock_count_at_startup = 0; + +        // Only valid when m_clock_state Holdover +        std::chrono::steady_clock::time_point m_holdover_since = +            std::chrono::steady_clock::time_point::min(); +        std::time_t m_holdover_since_t = 0; +}; + +} // namespace Output + +#endif //HAVE_DEXTER + diff --git a/src/output/Feedback.cpp b/src/output/Feedback.cpp index 88d8319..d112b5a 100644 --- a/src/output/Feedback.cpp +++ b/src/output/Feedback.cpp @@ -84,7 +84,7 @@ DPDFeedbackServer::~DPDFeedbackServer()  void DPDFeedbackServer::set_tx_frame(          const std::vector<uint8_t> &buf, -        const struct frame_timestamp &buf_ts) +        const frame_timestamp &buf_ts)  {      if (not m_running) {          throw runtime_error("DPDFeedbackServer not running"); diff --git a/src/output/Feedback.h b/src/output/Feedback.h index aef86b0..b31347f 100644 --- a/src/output/Feedback.h +++ b/src/output/Feedback.h @@ -94,7 +94,7 @@ class DPDFeedbackServer {          ~DPDFeedbackServer();          void set_tx_frame(const std::vector<uint8_t> &buf, -                const struct frame_timestamp& ts); +                const frame_timestamp& ts);      private:          // Thread that reacts to burstRequests and receives from the SDR device diff --git a/src/output/Lime.cpp b/src/output/Lime.cpp index 593cddb..47045e7 100644 --- a/src/output/Lime.cpp +++ b/src/output/Lime.cpp @@ -318,13 +318,14 @@ double Lime::get_bandwidth(void) const      return bw;  } -SDRDevice::RunStatistics Lime::get_run_statistics(void) const +SDRDevice::run_statistics_t Lime::get_run_statistics(void) const  { -    RunStatistics rs; -    rs.num_underruns = underflows; -    rs.num_overruns = overflows; -    rs.num_late_packets = late_packets; -    rs.num_frames_modulated = num_frames_modulated; +    run_statistics_t rs; +    rs["underruns"] = underflows; +    rs["overruns"] = overflows; +    rs["dropped_packets"] = dropped_packets; +    rs["frames"] = num_frames_modulated; +    rs["fifo_fill"] = m_last_fifo_fill_percent * 100;      return rs;  } @@ -348,14 +349,14 @@ double Lime::get_rxgain(void) const  size_t Lime::receive_frame(      complexf *buf,      size_t num_samples, -    struct frame_timestamp &ts, +    frame_timestamp &ts,      double timeout_secs)  {      // TODO      return 0;  } -bool Lime::is_clk_source_ok() const +bool Lime::is_clk_source_ok()  {      // TODO      return true; @@ -366,25 +367,23 @@ const char *Lime::device_name(void) const      return "Lime";  } -double Lime::get_temperature(void) const +std::optional<double> Lime::get_temperature(void) const  {      if (not m_device)          throw runtime_error("Lime device not set up"); -    float_type temp = numeric_limits<float_type>::quiet_NaN(); -    if (LMS_GetChipTemperature(m_device, 0, &temp) < 0) -    { +    float_type temp = 0; +    if (LMS_GetChipTemperature(m_device, 0, &temp) >= 0) { +        return temp; +    } +    else {          etiLog.level(error) << "Error getting LimeSDR temperature: %s " << LMS_GetLastErrorMessage(); +        return std::nullopt;      } -    return temp;  } -float Lime::get_fifo_fill_percent(void) const -{ -    return m_last_fifo_fill_percent * 100; -} -void Lime::transmit_frame(const struct FrameData &frame) +void Lime::transmit_frame(struct FrameData&& frame)  {      if (not m_device)          throw runtime_error("Lime device not set up"); @@ -406,7 +405,7 @@ void Lime::transmit_frame(const struct FrameData &frame)      LMS_GetStreamStatus(&m_tx_stream, &LimeStatus);      overflows += LimeStatus.overrun;      underflows += LimeStatus.underrun; -    late_packets += LimeStatus.droppedPackets; +    dropped_packets += LimeStatus.droppedPackets;  #ifdef LIMEDEBUG      etiLog.level(info) << LimeStatus.fifoFilledCount << "/" << LimeStatus.fifoSize << ":" << numSamples << "Rate" << LimeStatus.linkRate / (2 * 2.0); diff --git a/src/output/Lime.h b/src/output/Lime.h index 72a018e..4510bf2 100644 --- a/src/output/Lime.h +++ b/src/output/Lime.h @@ -66,8 +66,8 @@ class Lime : public Output::SDRDevice      virtual double get_txgain(void) const override;      virtual void set_bandwidth(double bandwidth) override;      virtual double get_bandwidth(void) const override; -    virtual void transmit_frame(const struct FrameData &frame) override; -    virtual RunStatistics get_run_statistics(void) const override; +    virtual void transmit_frame(struct FrameData&& frame) override; +    virtual run_statistics_t get_run_statistics(void) const override;      virtual double get_real_secs(void) const override;      virtual void set_rxgain(double rxgain) override; @@ -75,15 +75,14 @@ class Lime : public Output::SDRDevice      virtual size_t receive_frame(          complexf *buf,          size_t num_samples, -        struct frame_timestamp &ts, +        frame_timestamp &ts,          double timeout_secs) override;      // Return true if GPS and reference clock inputs are ok -    virtual bool is_clk_source_ok(void) const override; +    virtual bool is_clk_source_ok(void) override;      virtual const char *device_name(void) const override; -    virtual double get_temperature(void) const override; -    virtual float get_fifo_fill_percent(void) const; +    virtual std::optional<double> get_temperature(void) const override;    private:      SDRDeviceConfig &m_conf; @@ -95,11 +94,10 @@ class Lime : public Output::SDRDevice      std::vector<complexf> interpolatebuf;      std::vector<short> m_i16samples;       std::atomic<float> m_last_fifo_fill_percent = ATOMIC_VAR_INIT(0); -          size_t underflows = 0;      size_t overflows = 0; -    size_t late_packets = 0; +    size_t dropped_packets = 0;      size_t num_frames_modulated = 0;  }; diff --git a/src/output/SDR.cpp b/src/output/SDR.cpp index b0c09b6..75a7ee3 100644 --- a/src/output/SDR.cpp +++ b/src/output/SDR.cpp @@ -2,7 +2,7 @@     Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the     Queen in Right of Canada (Communications Research Center Canada) -   Copyright (C) 2022 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -25,7 +25,9 @@   */  #include "output/SDR.h" +#include "output/UHD.h"  #include "output/Lime.h" +#include "output/Dexter.h"  #include "PcDebug.h"  #include "Log.h" @@ -46,17 +48,16 @@ using namespace std;  namespace Output { -// Maximum number of frames that can wait in frames -static constexpr size_t FRAMES_MAX_SIZE = 8; +// Maximum number of frames that can wait in frames. +// Keep it low when not using synchronised transmission, in order to reduce delay. +// When using synchronised transmission, use a 6s buffer to give us enough margin. +static constexpr size_t FRAMES_MAX_SIZE_UNSYNC = 8; +static constexpr size_t FRAMES_MAX_SIZE_SYNC = 250;  // If the timestamp is further in the future than  // 100 seconds, abort  static constexpr double TIMESTAMP_ABORT_FUTURE = 100; -// Add a delay to increase buffers when -// frames are too far in the future -static constexpr double TIMESTAMP_MARGIN_FUTURE = 0.5; -  SDR::SDR(SDRDeviceConfig& config, std::shared_ptr<SDRDevice> device) :      ModOutput(), ModMetadata(), RemoteControllable("sdr"),      m_config(config), @@ -77,20 +78,40 @@ SDR::SDR(SDRDeviceConfig& config, std::shared_ptr<SDRDevice> device) :      RC_ADD_PARAMETER(txgain, "TX gain");      RC_ADD_PARAMETER(rxgain, "RX gain for DPD feedback");      RC_ADD_PARAMETER(bandwidth, "Analog front-end bandwidth"); -    RC_ADD_PARAMETER(freq, "Transmission frequency"); +    RC_ADD_PARAMETER(freq, "Transmission frequency in Hz"); +    RC_ADD_PARAMETER(channel, "Transmission frequency as channel");      RC_ADD_PARAMETER(muting, "Mute the output by stopping the transmitter");      RC_ADD_PARAMETER(temp, "Temperature in degrees C of the device");      RC_ADD_PARAMETER(underruns, "Counter of number of underruns");      RC_ADD_PARAMETER(latepackets, "Counter of number of late packets");      RC_ADD_PARAMETER(frames, "Counter of number of frames modulated"); -    RC_ADD_PARAMETER(gpsdo_num_sv, "Number of Satellite Vehicles tracked by GPSDO"); -    RC_ADD_PARAMETER(gpsdo_holdover, "1 if the GPSDO is in holdover, 0 if it is using gnss"); +    RC_ADD_PARAMETER(synchronous, "1 if configured for synchronous transmission"); +    RC_ADD_PARAMETER(max_gps_holdover_time, "Max holdover duration in seconds"); + +#ifdef HAVE_OUTPUT_UHD +    if (std::dynamic_pointer_cast<UHD>(device)) { +        RC_ADD_PARAMETER(gpsdo_num_sv, "Number of Satellite Vehicles tracked by GPSDO"); +        RC_ADD_PARAMETER(gpsdo_holdover, "1 if the GPSDO is in holdover, 0 if it is using gnss"); +    } +#endif // HAVE_OUTPUT_UHD + +    RC_ADD_PARAMETER(queued_frames_ms, "Number of frames queued, represented in milliseconds");  #ifdef HAVE_LIMESDR      if (std::dynamic_pointer_cast<Lime>(device)) {          RC_ADD_PARAMETER(fifo_fill, "A value representing the Lime FIFO fullness [percent]");      }  #endif // HAVE_LIMESDR + +#ifdef HAVE_DEXTER +    if (std::dynamic_pointer_cast<Dexter>(device)) { +        RC_ADD_PARAMETER(in_holdover_since, "DEXTER timestamp when holdover began"); +        RC_ADD_PARAMETER(remaining_holdover_s, "DEXTER remaining number of seconds in holdover"); +        RC_ADD_PARAMETER(clock_state, "DEXTER clock state: startup/normal/holdover"); +    } +#endif // HAVE_DEXTER + +  }  SDR::~SDR() @@ -104,6 +125,11 @@ SDR::~SDR()      }  } +void SDR::set_sample_size(size_t size) +{ +    m_size = size; +} +  int SDR::process(Buffer *dataIn)  {      if (not m_running) { @@ -125,6 +151,7 @@ meta_vec_t SDR::process_metadata(const meta_vec_t& metadataIn)      if (m_device and m_running) {          FrameData frame;          frame.buf = std::move(m_frame); +        frame.sampleSize = m_size;          if (metadataIn.empty()) {              etiLog.level(info) << @@ -138,7 +165,7 @@ meta_vec_t SDR::process_metadata(const meta_vec_t& metadataIn)               * This behaviour is different to earlier versions of ODR-DabMod,               * which took the timestamp from the latest ETI frame.               */ -            frame.ts = *(metadataIn[0].ts); +            frame.ts = metadataIn[0].ts;              // TODO check device running @@ -157,9 +184,12 @@ meta_vec_t SDR::process_metadata(const meta_vec_t& metadataIn)                          m_config.sampleRate);              } -            size_t num_frames = m_queue.push_wait_if_full(frame, -                    FRAMES_MAX_SIZE); -            etiLog.log(trace, "SDR,push %zu", num_frames); + +            const auto max_size = m_config.enableSync ? FRAMES_MAX_SIZE_SYNC : FRAMES_MAX_SIZE_UNSYNC; +            auto r = m_queue.push_overflow(std::move(frame), max_size); +            etiLog.log(trace, "SDR,push %d %zu", r.overflowed, r.new_size); + +            num_queue_overflows += r.overflowed ? 1 : 0;          }      }      else { @@ -180,16 +210,13 @@ void SDR::process_thread_entry()      last_tx_time_initialised = false; -    size_t last_num_underflows = 0; -    size_t pop_prebuffering = FRAMES_MAX_SIZE; -      m_running.store(true);      try {          while (m_running.load()) {              struct FrameData frame;              etiLog.log(trace, "SDR,wait"); -            m_queue.wait_and_pop(frame, pop_prebuffering); +            m_queue.wait_and_pop(frame);              etiLog.log(trace, "SDR,pop");              if (m_running.load() == false) { @@ -197,20 +224,7 @@ void SDR::process_thread_entry()              }              if (m_device) { -                handle_frame(frame); - -                const auto rs = m_device->get_run_statistics(); - -                /* Ensure we fill frames after every underrun and -                 * at startup to reduce underrun likelihood. */ -                if (last_num_underflows < rs.num_underruns) { -                    pop_prebuffering = FRAMES_MAX_SIZE; -                } -                else { -                    pop_prebuffering = 1; -                } - -                last_num_underflows = rs.num_underruns; +                handle_frame(std::move(frame));              }          }      } @@ -236,46 +250,19 @@ const char* SDR::name()      return m_name.c_str();  } -void SDR::sleep_through_frame() -{ -    using namespace std::chrono; - -    const auto now = steady_clock::now(); -    if (not t_last_frame_initialised) { -        t_last_frame = now; -        t_last_frame_initialised = true; -    } - -    const auto delta = now - t_last_frame; -    const auto wait_time = transmission_frame_duration(m_config.dabMode); - -    if (wait_time > delta) { -        this_thread::sleep_for(wait_time - delta); -    } - -    t_last_frame += wait_time; -} - -void SDR::handle_frame(struct FrameData& frame) +void SDR::handle_frame(struct FrameData&& frame)  {      // Assumes m_device is valid -    constexpr double tx_timeout = 20.0; -      if (not m_device->is_clk_source_ok()) { -        sleep_through_frame();          return;      }      const auto& time_spec = frame.ts; -    if (m_config.enableSync and m_config.muteNoTimestamps and -            not time_spec.timestamp_valid) { -        sleep_through_frame(); -        etiLog.log(info, -                "OutputSDR: Muting sample %d : no timestamp\n", -                frame.ts.fct); +    if (m_config.enableSync and m_config.muteNoTimestamps and not time_spec.timestamp_valid) { +        etiLog.log(info, "OutputSDR: Muting sample %d : no timestamp\n", frame.ts.fct);          return;      } @@ -298,11 +285,12 @@ void SDR::handle_frame(struct FrameData& frame)          }          if (frame.ts.offset_changed) { +            etiLog.level(debug) << "TS offset changed";              m_device->require_timestamp_refresh();          }          if (last_tx_time_initialised) { -            const size_t sizeIn = frame.buf.size() / sizeof(complexf); +            const size_t sizeIn = frame.buf.size() / frame.sampleSize;              // Checking units for the increment calculation:              // samps  * ticks/s  / (samps/s) @@ -341,7 +329,7 @@ void SDR::handle_frame(struct FrameData& frame)          etiLog.log(trace, "SDR,tist %f", time_spec.get_real_secs()); -        if (time_spec.get_real_secs() + tx_timeout < device_time) { +        if (time_spec.get_real_secs() < device_time) {              etiLog.level(warn) <<                  "OutputSDR: Timestamp in the past at FCT=" << frame.ts.fct << " offset: " <<                  std::fixed << @@ -350,6 +338,7 @@ void SDR::handle_frame(struct FrameData& frame)                  " frame " << frame.ts.fct <<                  ", tx_second " << tx_second <<                  ", pps " << pps_offset; +            m_device->require_timestamp_refresh();              return;          } @@ -363,13 +352,12 @@ void SDR::handle_frame(struct FrameData& frame)      }      if (m_config.muting) { -        etiLog.log(info, -                "OutputSDR: Muting FCT=%d requested", -                frame.ts.fct); +        etiLog.log(info, "OutputSDR: Muting FCT=%d requested", frame.ts.fct); +        m_device->require_timestamp_refresh();          return;      } -    m_device->transmit_frame(frame); +    m_device->transmit_frame(std::move(frame));  }  // ======================================= @@ -397,21 +385,33 @@ void SDR::set_parameter(const string& parameter, const string& value)          m_device->tune(m_config.lo_offset, m_config.frequency);          m_config.frequency = m_device->get_tx_freq();      } +    else if (parameter == "channel") { +        try { +            const double frequency = parse_channel(value); + +            m_config.frequency = frequency; +            m_device->tune(m_config.lo_offset, m_config.frequency); +            m_config.frequency = m_device->get_tx_freq(); +        } +        catch (const std::out_of_range& e) { +            throw ParameterError("Cannot parse channel"); +        } +    }      else if (parameter == "muting") {          ss >> m_config.muting;      } -    else if (parameter == "underruns" or -             parameter == "latepackets" or -             parameter == "frames" or -             parameter == "gpsdo_num_sv" or -             parameter == "gpsdo_holdover" or -             parameter == "fifo_fill") { -        throw ParameterError("Parameter " + parameter + " is read-only."); +    else if (parameter == "synchronous") { +        uint32_t enableSync = 0; +        ss >> enableSync; +        m_config.enableSync = enableSync > 0; +    } +    else if (parameter == "max_gps_holdover_time") { +        ss >> m_config.maxGPSHoldoverTime;      }      else {          stringstream ss_err;          ss_err << "Parameter '" << parameter -            << "' is not exported by controllable " << get_rc_name(); +            << "' is read-only or not exported by controllable " << get_rc_name();          throw ParameterError(ss_err.str());      }  } @@ -432,6 +432,16 @@ const string SDR::get_parameter(const string& parameter) const      else if (parameter == "freq") {          ss << m_config.frequency;      } +    else if (parameter == "channel") { +        const auto maybe_freq = convert_frequency_to_channel(m_config.frequency); + +        if (maybe_freq.has_value()) { +            ss << *maybe_freq; +        } +        else { +            throw ParameterError("Frequency is outside list of channels"); +        } +    }      else if (parameter == "muting") {          ss << m_config.muting;      } @@ -439,55 +449,57 @@ const string SDR::get_parameter(const string& parameter) const          if (not m_device) {              throw ParameterError("OutputSDR has no device");          } -        const double temp = m_device->get_temperature(); -        if (std::isnan(temp)) { -            throw ParameterError("Temperature not available"); +        const std::optional<double> temp = m_device->get_temperature(); +        if (temp) { +            ss << *temp;          }          else { -            ss << temp; -        } -    } -    else if (parameter == "underruns" or -            parameter == "latepackets" or -            parameter == "frames" ) { -        if (not m_device) { -            throw ParameterError("OutputSDR has no device"); -        } -        const auto stat = m_device->get_run_statistics(); - -        if (parameter == "underruns") { -            ss << stat.num_underruns; -        } -        else if (parameter == "latepackets") { -            ss << stat.num_late_packets; -        } -        else if (parameter == "frames") { -            ss << stat.num_frames_modulated; +            throw ParameterError("Temperature not available");          }      } -    else if (parameter == "gpsdo_num_sv") { -        const auto stat = m_device->get_run_statistics(); -        ss << stat.gpsdo_num_sv; +    else if (parameter == "queued_frames_ms") { +        ss << m_queue.size() * +            chrono::duration_cast<chrono::milliseconds>(transmission_frame_duration(m_config.dabMode)) +            .count();      } -    else if (parameter == "gpsdo_holdover") { -        const auto stat = m_device->get_run_statistics(); -        ss << (stat.gpsdo_holdover ? 1 : 0); +    else if (parameter == "synchronous") { +        ss << m_config.enableSync;      } -#ifdef HAVE_LIMESDR -    else if (parameter == "fifo_fill") { -        const auto dev = std::dynamic_pointer_cast<Lime>(m_device); - -        if (dev) { -            ss << dev->get_fifo_fill_percent(); -        } -        else { -            ss << "Parameter '" << parameter << -                "' is not exported by controllable " << get_rc_name(); -            throw ParameterError(ss.str()); -        } +    else if (parameter == "max_gps_holdover_time") { +        ss << m_config.maxGPSHoldoverTime;      } -#endif // HAVE_LIMESDR      else { +        if (m_device) { +            const auto stat = m_device->get_run_statistics(); +            try { +                const auto& value = stat.at(parameter).v; +                if (std::holds_alternative<string>(value)) { +                    ss << std::get<string>(value); +                } +                else if (std::holds_alternative<double>(value)) { +                    ss << std::get<double>(value); +                } +                else if (std::holds_alternative<ssize_t>(value)) { +                    ss << std::get<ssize_t>(value); +                } +                else if (std::holds_alternative<size_t>(value)) { +                    ss << std::get<size_t>(value); +                } +                else if (std::holds_alternative<bool>(value)) { +                    ss << (std::get<bool>(value) ? 1 : 0); +                } +                else if (std::holds_alternative<std::nullopt_t>(value)) { +                    ss << ""; +                } +                else { +                    throw std::logic_error("variant alternative not handled"); +                } +                return ss.str(); +            } +            catch (const std::out_of_range&) { +            } +        } +          ss << "Parameter '" << parameter <<              "' is not exported by controllable " << get_rc_name();          throw ParameterError(ss.str()); @@ -495,4 +507,39 @@ const string SDR::get_parameter(const string& parameter) const      return ss.str();  } +const json::map_t SDR::get_all_values() const +{ +    json::map_t stat = m_device->get_run_statistics(); + +    stat["txgain"].v = m_config.txgain; +    stat["rxgain"].v = m_config.rxgain; +    stat["freq"].v = m_config.frequency; +    stat["muting"].v = m_config.muting; +    stat["temp"].v = std::nullopt; + +    const auto maybe_freq = convert_frequency_to_channel(m_config.frequency); + +    if (maybe_freq.has_value()) { +        stat["channel"].v = *maybe_freq; +    } +    else { +        stat["channel"].v = std::nullopt; +    } + +    if (m_device) { +        const std::optional<double> temp = m_device->get_temperature(); +        if (temp) { +            stat["temp"].v = *temp; +        } +    } +    stat["queued_frames_ms"].v = m_queue.size() * +            (size_t)chrono::duration_cast<chrono::milliseconds>(transmission_frame_duration(m_config.dabMode)) +            .count(); + +    stat["synchronous"].v = m_config.enableSync; +    stat["max_gps_holdover_time"].v = (size_t)m_config.maxGPSHoldoverTime; + +    return stat; +} +  } // namespace Output diff --git a/src/output/SDR.h b/src/output/SDR.h index 33477bf..960de0c 100644 --- a/src/output/SDR.h +++ b/src/output/SDR.h @@ -2,7 +2,7 @@     Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the     Queen in Right of Canada (Communications Research Center Canada) -   Copyright (C) 2022 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -51,6 +51,7 @@ class SDR : public ModOutput, public ModMetadata, public RemoteControllable {          SDR operator=(const SDR& other) = delete;          virtual ~SDR(); +        virtual void set_sample_size(size_t size);          virtual int process(Buffer *dataIn) override;          virtual meta_vec_t process_metadata(const meta_vec_t& metadataIn) override; @@ -66,15 +67,17 @@ class SDR : public ModOutput, public ModMetadata, public RemoteControllable {          virtual const std::string get_parameter(                  const std::string& parameter) const override; +        virtual const json::map_t get_all_values() const override; +      private:          void process_thread_entry(void); -        void handle_frame(struct FrameData &frame); -        void sleep_through_frame(void); +        void handle_frame(struct FrameData&& frame);          SDRDeviceConfig& m_config;          std::atomic<bool> m_running = ATOMIC_VAR_INIT(false);          std::thread m_device_thread; +        size_t m_size = sizeof(complexf);          std::vector<uint8_t> m_frame;          ThreadsafeQueue<FrameData> m_queue; @@ -86,9 +89,7 @@ class SDR : public ModOutput, public ModMetadata, public RemoteControllable {          bool     last_tx_time_initialised = false;          uint32_t last_tx_second = 0;          uint32_t last_tx_pps = 0; - -        bool     t_last_frame_initialised = false; -        std::chrono::steady_clock::time_point t_last_frame; +        size_t   num_queue_overflows = 0;  };  } diff --git a/src/output/SDRDevice.h b/src/output/SDRDevice.h index b599f5a..378829c 100644 --- a/src/output/SDRDevice.h +++ b/src/output/SDRDevice.h @@ -2,7 +2,7 @@     Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the     Queen in Right of Canada (Communications Research Center Canada) -   Copyright (C) 2022 +   Copyright (C) 2023     Matthias P. Braendli, matthias.braendli@mpb.li      http://opendigitalradio.org @@ -38,6 +38,9 @@ DESCRIPTION:  #include <string>  #include <vector>  #include <complex> +#include <variant> +#include <optional> +#include <unordered_map>  #include "TimestampDecoder.h" @@ -98,32 +101,25 @@ struct SDRDeviceConfig {  struct FrameData {      // Buffer holding frame data      std::vector<uint8_t> buf; +    size_t sampleSize = sizeof(complexf);      // A full timestamp contains a TIST according to standard      // and time information within MNSC with tx_second. -    struct frame_timestamp ts; +    frame_timestamp ts;  };  // All SDR Devices must implement the SDRDevice interface  class SDRDevice {      public: -        struct RunStatistics { -            size_t num_underruns = 0; -            size_t num_late_packets = 0; -            size_t num_overruns = 0; -            size_t num_frames_modulated = 0; - -            int gpsdo_num_sv = 0; -            bool gpsdo_holdover = false; -        }; +        using run_statistics_t = json::map_t;          virtual void tune(double lo_offset, double frequency) = 0;          virtual double get_tx_freq(void) const = 0;          virtual void set_txgain(double txgain) = 0;          virtual double get_txgain(void) const = 0; -        virtual void transmit_frame(const struct FrameData& frame) = 0; -        virtual RunStatistics get_run_statistics(void) const = 0; +        virtual void transmit_frame(struct FrameData&& frame) = 0; +        virtual run_statistics_t get_run_statistics(void) const = 0;          virtual double get_real_secs(void) const = 0;          virtual void set_rxgain(double rxgain) = 0;          virtual double get_rxgain(void) const = 0; @@ -132,14 +128,14 @@ class SDRDevice {          virtual size_t receive_frame(                  complexf *buf,                  size_t num_samples, -                struct frame_timestamp& ts, +                frame_timestamp& ts,                  double timeout_secs) = 0; -        // Returns device temperature in degrees C or NaN if not available -        virtual double get_temperature(void) const = 0; +        // Returns device temperature in degrees C +        virtual std::optional<double> get_temperature(void) const = 0;          // Return true if GPS and reference clock inputs are ok -        virtual bool is_clk_source_ok(void) const = 0; +        virtual bool is_clk_source_ok(void) = 0;          virtual const char* device_name(void) const = 0; diff --git a/src/output/Soapy.cpp b/src/output/Soapy.cpp index 684a9a4..7931860 100644 --- a/src/output/Soapy.cpp +++ b/src/output/Soapy.cpp @@ -180,13 +180,13 @@ double Soapy::get_bandwidth(void) const      return m_device->getBandwidth(SOAPY_SDR_TX, 0);  } -SDRDevice::RunStatistics Soapy::get_run_statistics(void) const +SDRDevice::run_statistics_t Soapy::get_run_statistics(void) const  { -    RunStatistics rs; -    rs.num_underruns = underflows; -    rs.num_overruns = overflows; -    rs.num_late_packets = late_packets; -    rs.num_frames_modulated = num_frames_modulated; +    run_statistics_t rs; +    rs["underruns"].v = underflows; +    rs["overruns"].v = overflows; +    rs["timeouts"].v = timeouts; +    rs["frames"].v = num_frames_modulated;      return rs;  } @@ -216,7 +216,7 @@ double Soapy::get_rxgain(void) const  size_t Soapy::receive_frame(          complexf *buf,          size_t num_samples, -        struct frame_timestamp& ts, +        frame_timestamp& ts,          double timeout_secs)  {      int flags = 0; @@ -254,7 +254,7 @@ size_t Soapy::receive_frame(  } -bool Soapy::is_clk_source_ok() const +bool Soapy::is_clk_source_ok()  {      // TODO      return true; @@ -265,14 +265,14 @@ const char* Soapy::device_name(void) const      return "Soapy";  } -double Soapy::get_temperature(void) const +std::optional<double> Soapy::get_temperature(void) const  {      // TODO Unimplemented      // LimeSDR exports 'lms7_temp' -    return std::numeric_limits<double>::quiet_NaN(); +    return std::nullopt;  } -void Soapy::transmit_frame(const struct FrameData& frame) +void Soapy::transmit_frame(struct FrameData&& frame)  {      if (not m_device) throw runtime_error("Soapy device not set up"); @@ -320,6 +320,7 @@ void Soapy::transmit_frame(const struct FrameData& frame)                  m_tx_stream, buffs, samps_to_send, flags, timeNs);          if (num_sent == SOAPY_SDR_TIMEOUT) { +            timeouts++;              continue;          }          else if (num_sent == SOAPY_SDR_OVERFLOW) { @@ -349,6 +350,7 @@ void Soapy::transmit_frame(const struct FrameData& frame)                          SoapySDR::errToStr(ret_deact));              }              m_tx_stream_active = false; +            m_require_timestamp_refresh = false;          }          if (eob_because_muting) { diff --git a/src/output/Soapy.h b/src/output/Soapy.h index 4ee53ca..4fce11a 100644 --- a/src/output/Soapy.h +++ b/src/output/Soapy.h @@ -65,8 +65,8 @@ class Soapy : public Output::SDRDevice          virtual double get_txgain(void) const override;          virtual void set_bandwidth(double bandwidth) override;          virtual double get_bandwidth(void) const override; -        virtual void transmit_frame(const struct FrameData& frame) override; -        virtual RunStatistics get_run_statistics(void) const override; +        virtual void transmit_frame(struct FrameData&& frame) override; +        virtual run_statistics_t get_run_statistics(void) const override;          virtual double get_real_secs(void) const override;          virtual void set_rxgain(double rxgain) override; @@ -74,14 +74,14 @@ class Soapy : public Output::SDRDevice          virtual size_t receive_frame(                  complexf *buf,                  size_t num_samples, -                struct frame_timestamp& ts, +                frame_timestamp& ts,                  double timeout_secs) override;          // Return true if GPS and reference clock inputs are ok -        virtual bool is_clk_source_ok(void) const override; +        virtual bool is_clk_source_ok(void) override;          virtual const char* device_name(void) const override; -        virtual double get_temperature(void) const override; +        virtual std::optional<double> get_temperature(void) const override;      private:          SDRDeviceConfig& m_conf; @@ -91,9 +91,9 @@ class Soapy : public Output::SDRDevice          SoapySDR::Stream *m_rx_stream = nullptr;          bool m_rx_stream_active = false; +        size_t timeouts = 0;          size_t underflows = 0;          size_t overflows = 0; -        size_t late_packets = 0;          size_t num_frames_modulated = 0;  }; diff --git a/src/output/UHD.cpp b/src/output/UHD.cpp index ac34ce4..094e021 100644 --- a/src/output/UHD.cpp +++ b/src/output/UHD.cpp @@ -315,7 +315,7 @@ double UHD::get_bandwidth(void) const      return m_usrp->get_tx_bandwidth();  } -void UHD::transmit_frame(const struct FrameData& frame) +void UHD::transmit_frame(struct FrameData&& frame)  {      const double tx_timeout = 20.0;      const size_t sizeIn = frame.buf.size() / sizeof(complexf); @@ -350,6 +350,7 @@ void UHD::transmit_frame(const struct FrameData& frame)                  frame.ts.timestamp_valid and                  m_require_timestamp_refresh and                  samps_to_send <= usrp_max_num_samps ); +        m_require_timestamp_refresh = false;          //send a single packet          size_t num_tx_samps = m_tx_stream->send( @@ -359,7 +360,7 @@ void UHD::transmit_frame(const struct FrameData& frame)          num_acc_samps += num_tx_samps; -        md_tx.time_spec += uhd::time_spec_t(0, num_tx_samps/m_conf.sampleRate); +        md_tx.time_spec += uhd::time_spec_t::from_ticks(num_tx_samps, (double)m_conf.sampleRate);          if (num_tx_samps == 0) {              etiLog.log(warn, @@ -376,18 +377,22 @@ void UHD::transmit_frame(const struct FrameData& frame)  } -SDRDevice::RunStatistics UHD::get_run_statistics(void) const +SDRDevice::run_statistics_t UHD::get_run_statistics(void) const  { -    RunStatistics rs; -    rs.num_underruns = num_underflows; -    rs.num_overruns = num_overflows; -    rs.num_late_packets = num_late_packets; -    rs.num_frames_modulated = num_frames_modulated; +    run_statistics_t rs; +    rs["underruns"].v = num_underflows; +    rs["overruns"].v = num_overflows; +    rs["late_packets"].v = num_late_packets; +    rs["frames"].v = num_frames_modulated;      if (m_device_time) {          const auto gpsdo_stat = m_device_time->get_gnss_stats(); -        rs.gpsdo_holdover = gpsdo_stat.holdover; -        rs.gpsdo_num_sv = gpsdo_stat.num_sv; +        rs["gpsdo_holdover"].v = gpsdo_stat.holdover; +        rs["gpsdo_num_sv"].v = gpsdo_stat.num_sv; +    } +    else { +        rs["gpsdo_holdover"].v = true; +        rs["gpsdo_num_sv"].v = 0;      }      return rs;  } @@ -411,7 +416,7 @@ double UHD::get_rxgain() const  size_t UHD::receive_frame(          complexf *buf,          size_t num_samples, -        struct frame_timestamp& ts, +        frame_timestamp& ts,          double timeout_secs)  {      uhd::stream_cmd_t cmd( @@ -434,7 +439,7 @@ size_t UHD::receive_frame(  }  // Return true if GPS and reference clock inputs are ok -bool UHD::is_clk_source_ok(void) const +bool UHD::is_clk_source_ok(void)  {      bool ok = true; @@ -471,13 +476,13 @@ const char* UHD::device_name(void) const      return "UHD";  } -double UHD::get_temperature(void) const +std::optional<double> UHD::get_temperature(void) const  {      try {          return std::round(m_usrp->get_tx_sensor("temp", 0).to_real());      }      catch (const uhd::lookup_error &e) { -        return std::numeric_limits<double>::quiet_NaN(); +        return std::nullopt;      }  } diff --git a/src/output/UHD.h b/src/output/UHD.h index 29867fb..9891c7a 100644 --- a/src/output/UHD.h +++ b/src/output/UHD.h @@ -55,14 +55,6 @@ DESCRIPTION:  #include <stdio.h>  #include <sys/types.h> -// If the timestamp is further in the future than -// 100 seconds, abort -#define TIMESTAMP_ABORT_FUTURE 100 - -// Add a delay to increase buffers when -// frames are too far in the future -#define TIMESTAMP_MARGIN_FUTURE 0.5 -  namespace Output {  class UHD : public Output::SDRDevice @@ -79,8 +71,8 @@ class UHD : public Output::SDRDevice          virtual double get_txgain(void) const override;          virtual void set_bandwidth(double bandwidth) override;          virtual double get_bandwidth(void) const override; -        virtual void transmit_frame(const struct FrameData& frame) override; -        virtual RunStatistics get_run_statistics(void) const override; +        virtual void transmit_frame(struct FrameData&& frame) override; +        virtual run_statistics_t get_run_statistics(void) const override;          virtual double get_real_secs(void) const override;          virtual void set_rxgain(double rxgain) override; @@ -88,14 +80,14 @@ class UHD : public Output::SDRDevice          virtual size_t receive_frame(                  complexf *buf,                  size_t num_samples, -                struct frame_timestamp& ts, +                frame_timestamp& ts,                  double timeout_secs) override;          // Return true if GPS and reference clock inputs are ok -        virtual bool is_clk_source_ok(void) const override; +        virtual bool is_clk_source_ok(void) override;          virtual const char* device_name(void) const override; -        virtual double get_temperature(void) const override; +        virtual std::optional<double> get_temperature(void) const override;      private:          SDRDeviceConfig& m_conf; diff --git a/src/output/USRPTime.cpp b/src/output/USRPTime.cpp index d1197ec..5a11851 100644 --- a/src/output/USRPTime.cpp +++ b/src/output/USRPTime.cpp @@ -46,11 +46,14 @@ USRPTime::USRPTime(      m_conf(conf),      time_last_check(timepoint_t::clock::now())  { -    if (m_conf.pps_src == "none") { +    if (m_conf.refclk_src == "internal" and m_conf.pps_src != "none") { +        etiLog.level(warn) << "OutputUHD: Unusal refclk and pps source settings. Setting time once, no monitoring."; +        set_usrp_time_from_pps(); +    } +    else if (m_conf.pps_src == "none") {          if (m_conf.enableSync) {              etiLog.level(warn) << -                "OutputUHD: WARNING:" -                " you are using synchronous transmission without PPS input!"; +                "OutputUHD: you are using synchronous transmission without PPS input!";          }          set_usrp_time_from_localtime(); | 
