aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--Makefile.am24
-rw-r--r--README.md2
-rw-r--r--configure.ac35
-rw-r--r--doc/easydabv3.ini37
-rw-r--r--doc/example.ini21
-rwxr-xr-xdoc/receive_events.py59
-rwxr-xr-xdoc/zmq-ctrl/json_remote_server.py131
-rwxr-xr-xdoc/zmq-ctrl/zmq_remote.py22
-rw-r--r--lib/Json.cpp122
-rw-r--r--lib/Json.h63
-rw-r--r--lib/RemoteControl.cpp57
-rw-r--r--lib/RemoteControl.h12
-rw-r--r--lib/Socket.cpp2
-rw-r--r--lib/ThreadsafeQueue.h54
-rw-r--r--src/Buffer.cpp1
-rw-r--r--src/Buffer.h1
-rw-r--r--src/CharsetTools.cpp143
-rw-r--r--src/CharsetTools.h58
-rw-r--r--src/ConfigParser.cpp62
-rw-r--r--src/ConfigParser.h9
-rw-r--r--src/DabMod.cpp447
-rw-r--r--src/DabModulator.cpp229
-rw-r--r--src/DabModulator.h38
-rw-r--r--src/EtiReader.cpp62
-rw-r--r--src/EtiReader.h30
-rw-r--r--src/Events.cpp97
-rw-r--r--src/Events.h77
-rw-r--r--src/FIRFilter.cpp9
-rw-r--r--src/FIRFilter.h10
-rw-r--r--src/FicSource.cpp69
-rw-r--r--src/FicSource.h21
-rw-r--r--src/FigParser.cpp1047
-rw-r--r--src/FigParser.h385
-rw-r--r--src/FormatConverter.cpp75
-rw-r--r--src/FormatConverter.h10
-rw-r--r--src/GainControl.cpp43
-rw-r--r--src/GainControl.h13
-rw-r--r--src/GuardIntervalInserter.cpp9
-rw-r--r--src/GuardIntervalInserter.h16
-rw-r--r--src/InputFileReader.cpp3
-rw-r--r--src/InputReader.h61
-rw-r--r--src/InputTcpReader.cpp3
-rw-r--r--src/InputZeroMQReader.cpp323
-rw-r--r--src/MemlessPoly.cpp14
-rw-r--r--src/MemlessPoly.h14
-rw-r--r--src/ModPlugin.h4
-rw-r--r--src/OfdmGenerator.cpp9
-rw-r--r--src/OfdmGenerator.h13
-rw-r--r--src/OutputFile.cpp31
-rw-r--r--src/TII.cpp11
-rw-r--r--src/TII.h15
-rw-r--r--src/TimestampDecoder.cpp65
-rw-r--r--src/TimestampDecoder.h23
-rw-r--r--src/Utils.cpp163
-rw-r--r--src/Utils.h24
-rwxr-xr-xsrc/output/BladeRF.cpp28
-rwxr-xr-xsrc/output/BladeRF.h17
-rw-r--r--src/output/Dexter.cpp691
-rw-r--r--src/output/Dexter.h138
-rw-r--r--src/output/Feedback.cpp2
-rw-r--r--src/output/Feedback.h2
-rw-r--r--src/output/Lime.cpp37
-rw-r--r--src/output/Lime.h14
-rw-r--r--src/output/SDR.cpp281
-rw-r--r--src/output/SDR.h13
-rw-r--r--src/output/SDRDevice.h30
-rw-r--r--src/output/Soapy.cpp24
-rw-r--r--src/output/Soapy.h12
-rw-r--r--src/output/UHD.cpp33
-rw-r--r--src/output/UHD.h18
-rw-r--r--src/output/USRPTime.cpp9
72 files changed, 4427 insertions, 1303 deletions
diff --git a/.gitignore b/.gitignore
index 3ac59f6..2a6b136 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/README.md b/README.md
index a23de3d..23e5c36 100644
--- a/README.md
+++ b/README.md
@@ -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;
+}
diff --git a/src/TII.h b/src/TII.h
index d8c785d..f6de70b 100644
--- a/src/TII.h
+++ b/src/TII.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) 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();