aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ChangeLog16
-rw-r--r--INSTALL.md64
-rw-r--r--README.md22
-rw-r--r--TODO.md25
-rw-r--r--configure.ac4
-rw-r--r--doc/STATS.md7
-rw-r--r--doc/advanced.mux18
-rw-r--r--doc/example.mux6
-rwxr-xr-xdoc/show_dabmux_stats.py22
-rw-r--r--gui/README.md32
-rw-r--r--gui/muxconfig.py171
-rw-r--r--gui/muxrc.py107
-rwxr-xr-xgui/odr-dabmux-gui.py182
-rw-r--r--gui/rcparam.json48
-rw-r--r--gui/static/intercooler-1.0.1.min.js2
-rw-r--r--gui/static/jquery-1.10.2.min.js6
-rw-r--r--gui/static/script.js4
-rw-r--r--gui/static/stats.js17
-rw-r--r--gui/static/style.css73
-rw-r--r--gui/views/configeditor.tpl29
-rw-r--r--gui/views/index.tpl129
-rw-r--r--gui/views/rcparam.tpl37
-rw-r--r--gui/views/services.tpl31
-rw-r--r--gui/views/stats.tpl21
-rw-r--r--lib/Json.cpp4
-rw-r--r--lib/Json.h4
-rw-r--r--lib/Socket.cpp58
-rw-r--r--lib/Socket.h35
-rw-r--r--lib/ThreadsafeQueue.h38
-rw-r--r--lib/edi/STIDecoder.cpp21
-rw-r--r--lib/edi/STIDecoder.hpp5
-rw-r--r--lib/edioutput/EDIConfig.h31
-rw-r--r--lib/edioutput/PFT.cpp12
-rw-r--r--lib/edioutput/PFT.h15
-rw-r--r--lib/edioutput/Transport.cpp268
-rw-r--r--lib/edioutput/Transport.h106
-rw-r--r--man/odr-dabmux.12
-rw-r--r--src/ConfigParser.cpp11
-rw-r--r--src/DabMultiplexer.cpp230
-rw-r--r--src/DabMultiplexer.h89
-rw-r--r--src/DabMux.cpp52
-rw-r--r--src/ManagementServer.cpp75
-rw-r--r--src/ManagementServer.h20
-rw-r--r--src/fig/FIG.h10
-rw-r--r--src/fig/FIG0_10.cpp11
-rw-r--r--src/fig/FIG0structs.h18
-rw-r--r--src/fig/FIGCarousel.cpp7
-rw-r--r--src/fig/FIGCarousel.h4
-rw-r--r--src/utils.cpp25
-rw-r--r--src/utils.h6
-rw-r--r--src/zmq2edi/EDISender.cpp390
-rw-r--r--src/zmq2edi/EDISender.h91
-rw-r--r--src/zmq2edi/zmq2edi.cpp419
53 files changed, 1728 insertions, 1402 deletions
diff --git a/ChangeLog b/ChangeLog
index c006e15..48683aa 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,6 +1,22 @@
This file contains information about the changes done to
ODR-DabMux in this repository
+2025-06-25: Matthias P. Braendli <matthias@mpb.li>
+ (v5.3.0):
+ Remove broken gui/ and point towards ODR-DabMux-GUI in README instead.
+ Improve logging about SSnn zero.
+ Rework FCT and TIST startup initialisation to guarantee TIST@FCT0 setting
+
+2025-05-19: Matthias P. Braendli <matthias@mpb.li>
+ (v5.2.0):
+ Rework FIG0/10 DAB time indication to match EDI time.
+ Make PFT per-output configurable.
+
+2025-03-18: Matthias P. Braendli <matthias@mpb.li>
+ (v5.1.0):
+ Fix startup value of DLFC and FCT.
+ Add statistics for EDI/TCP outputs.
+
2024-10-03: Matthias P. Braendli <matthias@mpb.li>
(v5.0.0):
Remove odr-zmq2edi.
diff --git a/INSTALL.md b/INSTALL.md
index 96fc2a4..7e15018 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -1,25 +1,26 @@
+# Installation
+
You have 3 ways to install odr-dabmux on your host:
-# Using your linux distribution packaging system
-`odr-dabmux` is available on the official repositories of several debian-based distributions, such as Debian
-(from Debian 12), Ubuntu (from 24.10), Opensuse and Arch.
+## Installing binary packages on some linux distributions
-If you are using Debian 12 (Bookworm), you will need to
-[add the backports repository](https://backports.debian.org/Instructions/)
+[![Packaging status](https://repology.org/badge/vertical-allrepos/odr-dabmux.svg)](https://repology.org/project/odr-dabmux/versions)
-**Notice**: this debian package does not include the Mux Web Management GUI
+## Using installation scripts
-# Using installation scripts
If your linux distribution is debian-based, you can install odr-dabmux
-as well as the other main components of the mmbTools set with the
+as well as the other main components of the mmbTools set with the
[Opendigitalradio dab-scripts](https://github.com/opendigitalradio/dab-scripts.git)
-# Compiling manually
+## Compiling manually
+
Unlike the 2 previous options, this one allows you to compile odr-dabmux with the features you really need.
-## Dependencies
-### Debian Bullseye-based OS:
-```
+### Dependencies
+
+#### Debian Bullseye-based OS
+
+```sh
# Required packages
## C++11 compiler
sudo apt-get install --yes build-essential automake libtool
@@ -35,7 +36,8 @@ sudo apt-get install --yes libboost-system-dev
sudo apt-get install --yes libcurl4-openssl-dev
```
-### Dependencies on other linux distributions
+#### Other linux distributions
+
For CentOS, in addition to the packages needed to install a compiler, install the packages:
boost-devel libcurl-devel zeromq-devel
@@ -47,49 +49,59 @@ the [radio RaBe repository](https://github.com/radiorabe/).
For openSUSE, mnhauke is maintaining packages, also built using
[OBS](https://build.opensuse.org/project/show/home:mnhauke:ODR-mmbTools).
-## Compilation
+### Compilation
+
The *master* branch in the repository always points to the
latest release. If you are looking for a new feature or bug-fix
that did not yet make its way into a release, you can clone the
*next* branch from the repository.
1. Clone this repository:
- ```
+
+ ```sh
# stable version:
git clone https://github.com/Opendigitalradio/ODR-DabMux.git
# or development version (at your own risk):
git clone https://github.com/Opendigitalradio/ODR-DabMux.git -b next
```
+
1. Configure the project
- ```
+
+ ```sh
cd ODR-DabMux
./bootstrap
./configure
```
+
1. Compile and install:
- ```
+
+ ```sh
make
sudo make install
```
Notes:
+
- It is advised to run the bootstrap and configure steps again every time you pull updates from the repository.
- The configure script can be launched with a variety of options. Run `./configure --help` to display a complete list
-# Develop on OSX and FreeBSD
-If you want to develop on OSX platform install the necessary build tools
-and dependencies with brew
+## Develop on OSX and FreeBSD
+
+If you want to develop on OSX platform install the necessary build tools and dependencies with brew
- brew install boost zeromq automake curl
+```sh
+brew install boost zeromq automake curl
+```
On FreeBSD, pkg installs all dependencies to /usr/local, but the build
tools will not search there by default. Set the following environment variables
before calling ./configure
- LDFLAGS="-L/usr/local/lib"
- CFLAGS="-I/usr/local/include"
- CXXFLAGS="-I/usr/local/include"
+```sh
+LDFLAGS="-L/usr/local/lib"
+CFLAGS="-I/usr/local/include"
+CXXFLAGS="-I/usr/local/include"
+```
-On both systems, RAW output is not available. Note that these systems
-are not tested regularly.
+On both systems, RAW output is not available. Note that these systems are not tested regularly.
diff --git a/README.md b/README.md
index 198adc3..90d6402 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,4 @@
-Overview
-========
+# Overview
ODR-DabMux is a *DAB (Digital Audio Broadcasting) multiplexer* compliant to
ETSI EN 300 401. It is the continuation of the work started by the
@@ -20,8 +19,6 @@ Features of ODR-DabMux:
- Includes a Telnet and ZMQ Remote Control for setting/getting parameters
- EDI input and output, both over UDP and TCP
- Support for FarSync TE1 and TE1e cards (G.703)
-- Something that will (with your help?) one day become a nice GUI for
- configuration, see `gui/README.md`
- Experimental STI-D(PI, X)/RTP input intended to be compatible
with compliant encoders.
- ZeroMQ and TCP ETI outputs that can be used with ODR-DabMod
@@ -46,18 +43,17 @@ Up to v4.5, this repository also contained
This was superseded by `digris-zmq-converter` in the
[digris-edi-zmq-bridge](https://github.com/digris/digris-edi-zmq-bridge) repository.
-Install
-=======
+## Install
-See `INSTALL.md` file for installation instructions.
+[Check the installation instructions.](INSTALL.md)
-Licence
-=======
+You may find [ODR-DabMux-GUI](https://github.com/Opendigitalradio/ODR-DabMux-GUI/) for configuring a DAB Ensemble.
+
+## Licence
See the files `LICENCE` and `COPYING`
-Contributions and Contact
-=========================
+## Contributions and Contact
Contributions to this tool are welcome, you can reach users and developers
through the
@@ -73,12 +69,10 @@ Matthias P. Braendli *matthias [at] mpb [dot] li*
Pascal Charest *pascal [dot] charest [at] crc [dot] ca*
-Acknowledgements
-================
+## Acknowledgements
David Lutton, Yoann Queret, Stefan Pöschel and Maik for bug-fix patches,
Wim Nelis for the Xymon monitoring scripts,
and many more for feedback and bug reports.
- [http://opendigitalradio.org/](http://opendigitalradio.org/)
-
diff --git a/TODO.md b/TODO.md
index 3ddc797..5e3e7e8 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,48 +1,43 @@
+# To do
+
This TODO file lists ideas and features for future developments. They are
more or less ordered according to their benefit, but that is subjective
to some degree.
Unless written, no activity has been started on the topics.
+## Explicit Service Linking
-Explicit Service Linking
-------------------------
It is impossible to activate/deactive linkage sets. Commit 5c3c6d7 added
some code to transmit a FIG0/6 CEI, but this was subsequently reverted
because it was not tested enough.
+## Inputs for packet data
-Inputs for packet data
-----------------------
It is currently unclear what input formats and sources work for packet data,
and which ones would make sense to add.
Also, there is no documentation on the possibilites of packet data.
+## Improvements for inputs
-Improvements for inputs
------------------------
Add statistics to UDP input, in a similar way that ZeroMQ offers statistics.
This would mean we have to move the packet buffer from the operating system
into our own buffer, so that we can actually get the statistics.
+## Fix DMB input
-Fix DMB input
--------------
The code that does interleaving and reed-solomon encoding for DMB is not used
-anymore, and is untested. The relevant parts are `src/dabInputDmb*` and
-`src/Dmb.cpp`
+anymore, and is untested. The relevant parts are `src/dabInputDmb*` and `src/Dmb.cpp`
+## Communicate Leap Seconds
-Communicate Leap Seconds
-------------------------
Actually, we're supposed to say in FIG0/10 when there is a UTC leap second
upcoming, but since that's not trivial to find out because the POSIX time
concept is totally unaware of that, this is not done. We need to know for EDI
TIST, and the ClockTAI class can get the information from the Internet, but it
is not used in FIG0/10.
+## Implement FIG0/20 Service List
-Implement FIG0/20 Service List
-------------------------------
-See ETSI TS 103 176
+See [ETSI TS 103 176](https://www.etsi.org/deliver/etsi_ts/103100_103199/103176/02.01.01_60/ts_103176v020101p.pdf)
diff --git a/configure.ac b/configure.ac
index 2d3231e..316e177 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1,7 +1,7 @@
# Copyright (C) 2008, 2009 Her Majesty the Queen in Right of Canada
# (Communications Research Center Canada)
#
-# Copyright (C) 2024 Matthias P. Braendli, http://opendigitalradio.org
+# Copyright (C) 2025 Matthias P. Braendli, http://opendigitalradio.org
# This file is part of ODR-DabMux.
#
@@ -19,7 +19,7 @@
# along with ODR-DabMux. If not, see <http://www.gnu.org/licenses/>.
AC_PREREQ([2.69])
-AC_INIT([ODR-DabMux],[5.0.0],[matthias.braendli@mpb.li])
+AC_INIT([ODR-DabMux],[5.3.0],[matthias.braendli@mpb.li])
AC_CONFIG_AUX_DIR([build-aux])
AC_CONFIG_MACRO_DIR([m4])
AC_CANONICAL_TARGET
diff --git a/doc/STATS.md b/doc/STATS.md
index 385d41e..435a92e 100644
--- a/doc/STATS.md
+++ b/doc/STATS.md
@@ -4,12 +4,13 @@ Stats available through Management Server
Interface
---------
-The management server makes statistics about the inputs available through a ZMQ request/reply socket.
+The management server makes statistics about the inputs and EDI/TCP outputs
+available through a ZMQ request/reply socket.
The `show_dabmux_stats.py` illustrates how to access this information.
-Meaning of values
------------------
+Meaning of values for inputs
+----------------------------
`max` and `min` indicate input buffer fullness in bytes.
diff --git a/doc/advanced.mux b/doc/advanced.mux
index 246f981..d2cc0fd 100644
--- a/doc/advanced.mux
+++ b/doc/advanced.mux
@@ -32,9 +32,12 @@ general {
tist false
; On startup, the timestamp is initialised to system time. If you want
- ; to add an offset, uncomment the following line and give a number
- ; in seconds.
- ; tist_offset 0
+ ; to add an offset, uncomment the following line and give a positive
+ ; number in seconds. Granularity: 24ms
+ ; tist_offset 0.480
+
+ ; Specify the TIST value for the frame with FCT==0, in milliseconds
+ ; tist_at_fct0 768
; The management server is a simple TCP server that can present
; statistics data (buffers, overruns, underruns, etc)
@@ -435,6 +438,10 @@ outputs {
destination "192.168.23.23"
port 12000
+ enable_pft true
+ fec 1
+ verbose true
+
; For compatibility: if port is not specified in the destination itself,
; it is taken from the parent 'destinations' block.
}
@@ -449,6 +456,8 @@ outputs {
; The multicast TTL has to be adapted according to your network
ttl 1
+ enable_pft true
+ fec 1
}
example_tcp {
; example for EDI TCP server. TCP is reliable, so it is counterproductive to
@@ -466,7 +475,8 @@ outputs {
}
}
- ; The settings below apply to all destinations
+ ; The settings below apply to all destinations, unless they are overridden
+ ; inside a destination
; Enable the PFT subsystem. If false, AFPackets are sent.
; PFT is not necessary when using TCP.
diff --git a/doc/example.mux b/doc/example.mux
index d53b789..34cd2ee 100644
--- a/doc/example.mux
+++ b/doc/example.mux
@@ -57,9 +57,9 @@ general {
tist false
; On startup, the timestamp is initialised to system time. If you want
- ; to add an offset, uncomment the following line and give a number
- ; in seconds.
- ; tist_offset 0
+ ; to add an offset, uncomment the following line and give a positive
+ ; number in seconds. Granularity: 24ms
+ ; tist_offset 0.480
; The URLs used to fetch the TAI bulletin can be overridden if needed.
; URLs are given as a pipe-separated list, and the default value is:
diff --git a/doc/show_dabmux_stats.py b/doc/show_dabmux_stats.py
index 7ea60f7..3b6d869 100755
--- a/doc/show_dabmux_stats.py
+++ b/doc/show_dabmux_stats.py
@@ -46,6 +46,7 @@ if len(sys.argv) == 1:
data = sock.recv().decode("utf-8")
values = json.loads(data)['values']
+ print("## INPUT STATS")
tmpl = "{ident:20}{maxfill:>8}{minfill:>8}{under:>8}{over:>8}{audioleft:>8}{audioright:>8}{peakleft:>8}{peakright:>8}{state:>16}{version:>48}{uptime:>8}{offset:>8}"
print(tmpl.format(
ident="id",
@@ -89,6 +90,27 @@ if len(sys.argv) == 1:
uptime=v['uptime'],
offset=v['last_tist_offset']))
+ sock.send(b"output_values")
+
+ poller = zmq.Poller()
+ poller.register(sock, zmq.POLLIN)
+
+ socks = dict(poller.poll(1000))
+ if socks:
+ if socks.get(sock) == zmq.POLLIN:
+ print()
+ print("## OUTPUT STATS")
+ data = sock.recv().decode("utf-8")
+ values = json.loads(data)['output_values']
+ for identifier in values:
+ if identifier.startswith("edi_tcp_"):
+ listen_port = identifier.rsplit("_", 1)[-1]
+ num_connections = values[identifier]["num_connections"]
+ print(f"EDI TCP on port {listen_port}: {num_connections} connections")
+ else:
+ print(f"Unknown output type: {identifier}")
+
+
elif len(sys.argv) == 2 and sys.argv[1] == "config":
sock = connect()
diff --git a/gui/README.md b/gui/README.md
deleted file mode 100644
index fc0311d..0000000
--- a/gui/README.md
+++ /dev/null
@@ -1,32 +0,0 @@
-The ODR-DabMux Web Management GUI
-=================================
-
-The whole world has repeatedly been asking for a graphical administration
-console for the ODR-mmbTools. I give in now, and start working on this
-web-based GUI.
-
-In the current state, it can display part of the configuration of a running
-ODR-DabMux in your browser. It doesn't seem like much, but you *will* be
-impressed.
-
-Usage
------
-
-Launch ODR-DabMux with your preferred multiplex, and enable the statistics and
-management server in the configuration file to port 12720, and the zeromq RC on
-tcp://lo:12722
-
-Start the gui/odr-dabmux-gui.py script on the same machine
-
-Connect to http://localhost:8000
-
-Admire the fabulously well-designed presentation of the configuration. In the
-remote control tab, you can interact with the ODR-DabMux RC to get an set
-parameters.
-
-Expect more features to come: Better design; integrated statistics, dynamically
-updated information, configuration upload and download, less ridiculous README,
-and much more. We can even start dreaming about live multiplex reconfiguration.
-
-2016-10-07 mpb
-
diff --git a/gui/muxconfig.py b/gui/muxconfig.py
deleted file mode 100644
index 35587f4..0000000
--- a/gui/muxconfig.py
+++ /dev/null
@@ -1,171 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2015
-# Matthias P. Braendli, matthias.braendli@mpb.li
-#
-# http://www.opendigitalradio.org
-#
-# This file is part of ODR-DabMux.
-#
-# ODR-DabMux 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-DabMux 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-DabMux. If not, see <http://www.gnu.org/licenses/>.
-import zmq
-import json
-
-class General(object):
- """Container object for general options"""
- def __init__(self, pt):
- ptree = pt['general']
- for fieldname in [
- "nbframes",
- "statsserverport",
- "writescca",
- "tist",
- "dabmode",
- "syslog"]:
- if fieldname in ptree:
- setattr(self, fieldname.replace("-", "_"), ptree[fieldname])
- else:
- setattr(self, fieldname.replace("-", "_"), "")
- self.telnetport = pt['remotecontrol']['telnetport']
-
-class Service(object):
- """Container object for a service"""
- def __init__(self, name, ptree):
- self.name = name
-
- for fieldname in [
- "id",
- "label",
- "shortlabel",
- "pty",
- "language" ]:
- if fieldname in ptree:
- setattr(self, fieldname.replace("-", "_"), ptree[fieldname])
- else:
- setattr(self, fieldname.replace("-", "_"), "")
-
-class Subchannel(object):
- """Container object for a subchannel"""
- def __init__(self, name, ptree):
- self.name = name
- for fieldname in [
- "type",
- "inputfile",
- "zmq-buffer",
- "zmq-prebuffering",
- "bitrate",
- "id",
- "protection",
- "encryption",
- "secret-key",
- "public-key",
- "encoder-key"]:
- if fieldname in ptree:
- setattr(self, fieldname.replace("-", "_"), ptree[fieldname])
- else:
- setattr(self, fieldname.replace("-", "_"), "")
-
-class Component(object):
- """Container object for a component"""
- def __init__(self, name, ptree):
- self.name = name
- for fieldname in ['label', 'shortlabel', 'service',
- 'subchannel', 'figtype']:
- if fieldname in ptree:
- setattr(self, fieldname.replace("-", "_"), ptree[fieldname])
- else:
- setattr(self, fieldname.replace("-", "_"), "")
-
-class ConfigurationHandler(object):
- """Load and present the configration from ODR-DabMux"""
-
- def __init__(self, mux_host, mux_port=12720):
- self._host = mux_host
- self._port = mux_port
-
- # local copy of the configuration
- self._server_version = None
- self._config = None
- self._statistics = None
-
- #self._ctx = zmq.Context()
- #self.sock = zmq.Socket(self._ctx, zmq.REQ)
- #self.sock.setsockopt(zmq.LINGER, 0)
- #self.sock.connect("tcp://{}:{}".format(self._host, self._port))
-
- def zRead(self, key):
- self._ctx = zmq.Context()
- self.sock = zmq.Socket(self._ctx, zmq.REQ)
- self.sock.setsockopt(zmq.LINGER, 0)
- self.sock.connect("tcp://{}:{}".format(self._host, self._port))
- self.sock.send(key)
-
- # use poll for timeouts:
- poller = zmq.Poller()
- poller.register(self.sock, zmq.POLLIN)
- if poller.poll(5*1000): # 5s timeout in milliseconds
- recv = self.sock.recv()
- self.sock.close()
- self._ctx.term()
- return recv
- else:
- raise IOError("Timeout processing ZMQ request")
-
- def load(self):
- """Load the configuration from the multiplexer and save it locally"""
- server_info = self.zRead(b'info')
- config_info = self.zRead(b'getptree')
-
- self._server_version = json.loads(server_info)['service']
- self._config = json.loads(config_info)
-
- def update_stats(self):
- """Load the statistics from the multiplexer and
- save them locally"""
- server_info = self.zRead(b'info')
- stats_info = self.zRead(b'values')
-
- self._statistics = json.loads(stats_info)['values']
-
- def get_full_configuration(self):
- return self._config
-
- def set_full_configuration(self, config_json):
- self.sock.send(b'setptree', flags=zmq.SNDMORE)
- self.sock.send(config_json)
- return self.sock.recv() == "OK"
-
- def get_mux_version(self):
- return self._server_version
-
- def get_services(self):
- srv_pt = self._config['services']
- return [Service(name, srv_pt[name]) for name in srv_pt]
-
- def get_subchannels(self):
- sub_pt = self._config['subchannels']
- return [Subchannel(name, sub_pt[name]) for name in sub_pt]
-
- def get_components(self):
- comp_pt = self._config['components']
- return [Component(name, comp_pt[name]) for name in comp_pt]
-
- def get_general_options(self):
- return General(self._config)
-
- def get_stats_dict(self):
- """Return a dictionary with all stats"""
- self.update_stats()
- return self._statistics
diff --git a/gui/muxrc.py b/gui/muxrc.py
deleted file mode 100644
index 7f28f54..0000000
--- a/gui/muxrc.py
+++ /dev/null
@@ -1,107 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2019
-# Matthias P. Braendli, matthias.braendli@mpb.li
-#
-# http://www.opendigitalradio.org
-#
-# This file is part of ODR-DabMux.
-#
-# ODR-DabMux 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-DabMux 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-DabMux. If not, see <http://www.gnu.org/licenses/>.
-import zmq
-import json
-
-class RCParameter(object):
- def __init__(self, param, value):
- self.param = param
- self.value = value
-
-class RCModule(object):
- """Container object for RC module"""
- def __init__(self, name):
- self.name = name
- self.parameters = []
-
-class MuxRemoteControl(object):
- """Interact with ODR-DabMux using the ZMQ RC"""
-
- def __init__(self, mux_host, mux_port=12722):
- self._host = mux_host
- self._port = mux_port
- self._ctx = zmq.Context()
-
- self.module_list = []
-
- def zRead(self, message_parts):
- sock = zmq.Socket(self._ctx, zmq.REQ)
- sock.setsockopt(zmq.LINGER, 0)
- sock.connect("tcp://{}:{}".format(self._host, self._port))
-
- for i, part in enumerate(message_parts):
- if i == len(message_parts) - 1:
- f = 0
- else:
- f = zmq.SNDMORE
-
- print("Send {} {}".format(i, part))
- sock.send(part.encode(), flags=f)
-
- print("Poll")
-
- # use poll for timeouts:
- poller = zmq.Poller()
- poller.register(sock, zmq.POLLIN)
- if poller.poll(5*1000): # 5s timeout in milliseconds
- recv = sock.recv_multipart()
- print("RX {}".format(recv))
- sock.close()
- return recv
- else:
- raise IOError("Timeout processing ZMQ request")
-
- def load(self):
- """Load the list of RC modules"""
- module_jsons = self.zRead(['list'])
-
- self.module_list = []
-
- for module_json in module_jsons:
- module = json.loads(module_json)
- name = module['name']
- mod = RCModule(name)
- module_params = self.zRead(['show', name])
- print("m_p", module_params)
-
- for param in module_params:
- p, v = param.split(b': ')
- mod.parameters.append(RCParameter(p, v))
-
- self.module_list.append(mod)
-
- def get_modules(self):
- return self.module_list
-
- def get_param_value(self, module, param):
- value = self.zRead(['get', module, param])
- if value[0] == b'fail':
- raise ValueError("Error getting param: {}".format(value[1]))
- else:
- return value[0]
-
- def set_param_value(self, module, param, value):
- ret = self.zRead(['set', module, param, value])
- if ret[0] == b'fail':
- raise ValueError("Error getting param: {}".format(ret[1]))
-
diff --git a/gui/odr-dabmux-gui.py b/gui/odr-dabmux-gui.py
deleted file mode 100755
index c469b35..0000000
--- a/gui/odr-dabmux-gui.py
+++ /dev/null
@@ -1,182 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2016
-# Matthias P. Braendli, matthias.braendli@mpb.li
-#
-# http://www.opendigitalradio.org
-#
-# This is a management server for ODR-DabMux, and it will become much
-# more interesting in the future.
-#
-# Run this script and connect your browser to
-# http://localhost:8000 to show the currently running
-#
-# This file is part of ODR-DabMux.
-#
-# ODR-DabMux 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-DabMux 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-DabMux. If not, see <http://www.gnu.org/licenses/>.
-
-import os
-import argparse
-import json
-import cherrypy
-import muxconfig
-import muxrc
-import jinja2
-
-class Root:
- def __init__(self, env, conf, rc):
- self.mconf = conf
- self.mrc = rc
- self.env = env
- self.mparam = ModuleParameter(env, rc)
-
- def _cp_dispatch(self, vpath):
- if len(vpath) == 3:
- vpath.pop(0) # /rc/
- cherrypy.request.params['module'] = vpath.pop(0) # /module name/
- cherrypy.request.params['param'] = vpath.pop(0) # /parameter name/
- return self.mparam
-
- return vpath
-
- @cherrypy.expose
- def config(self, config=None):
- if config == None:
- """Show the JSON ptree in a textbox for editing"""
- self.mconf.load()
- tmpl = self.env.get_template('configeditor.tpl')
- return tmpl.render(
- version = self.mconf.get_mux_version(),
- config = json.dumps(self.mconf.get_full_configuration(), indent=4),
- message = "")
- else:
- """Record the new configuration"""
- success = self.mconf.set_full_configuration(config)
- if success:
- successmessage = "Success"
- else:
- successmessage = "Failure"
- self.mconf.load()
- return template('configeditor',
- version = self.mconf.get_mux_version(),
- config = json.dumps(self.mconf.get_full_configuration(), indent=4),
- message = successmessage)
-
- @cherrypy.expose
- @cherrypy.tools.json_out()
- def config_json(self):
- """Return a application/json containing the full
- ptree of the mux"""
-
- self.mconf.load()
- return { 'version': self.mconf.get_mux_version(),
- 'config': self.mconf.get_full_configuration() }
-
- @cherrypy.expose
- def index(self):
- self.mconf.load()
- self.mrc.load()
- tmpl = self.env.get_template('index.tpl')
- return tmpl.render(
- version = self.mconf.get_mux_version(),
- g = self.mconf.get_general_options(),
- services = self.mconf.get_services(),
- subchannels = self.mconf.get_subchannels(),
- components = self.mconf.get_components(),
- rcmodules = self.mrc.get_modules())
-
- @cherrypy.expose
- def services(self):
- self.mconf.load()
- tmpl = self.env.get_template('services.tpl')
- return tmpl.render(
- version = self.mconf.get_mux_version(),
- services = self.mconf.get_services())
-
- @cherrypy.expose
- def stats(self):
- self.mconf.load()
- tmpl = self.env.get_template('stats.tpl')
- return tmpl.render(
- version = self.mconf.get_mux_version())
-
-
- @cherrypy.expose
- @cherrypy.tools.json_out()
- def stats_json(self):
- return self.mconf.get_stats_dict()
-
-class ModuleParameter:
- def __init__(self, env, rc):
- self.mrc = rc
- self.env = env
-
- @cherrypy.expose
- def index(self, module, param, newvalue=None):
- if newvalue != None:
- rc.set_param_value(module, param, newvalue)
- raise cherrypy.HTTPRedirect('/#rcmodules')
- else:
- self.mrc.load()
- value = self.mrc.get_param_value(module, param)
-
- if param in paramObj:
- paramList = paramObj[param]
- label = paramObj["labels"][param]
- else:
- paramList = []
- label = ""
-
- tmpl = self.env.get_template('rcparam.tpl')
- return tmpl.render(
- module = module,
- param = param,
- value = value,
- label = label,
- list = paramList)
-
-if __name__ == '__main__':
- # Get configuration file in argument
- parser = argparse.ArgumentParser(description='management server for ODR-DabMux')
- parser.add_argument('--host', default='127.0.0.1', help='socket host (default: 127.0.0.1)',required=False)
- parser.add_argument('--port', default='8000', help='socket port (default: 8000)',required=False)
- parser.add_argument('--mhost', default='127.0.0.1', help='mux host (default: 127.0.0.1)',required=False)
- parser.add_argument('--mport', default='12720', help='mux management server port (default: 12720)',required=False)
- parser.add_argument('--rcport', default='12722', help='mux zmq rc port (default: 12722)',required=False)
- cli_args = parser.parse_args()
-
- # Instanciate mux-configuration and mux-remote-control objects
- conf = muxconfig.ConfigurationHandler(cli_args.mhost, int(cli_args.mport))
- rc = muxrc.MuxRemoteControl(cli_args.mhost, int(cli_args.rcport))
-
- # Import selectable paramaters values
- paramFile = open("rcparam.json")
- paramStr = paramFile.read()
- paramObj = json.loads(paramStr)
-
- # Start cherrypy
- env = jinja2.Environment(loader=jinja2.FileSystemLoader('views'), trim_blocks=True)
- cherrypy.config.update({'server.socket_host': cli_args.host, 'server.socket_port': int(cli_args.port),})
- appconf = {
- '/': {
- 'tools.sessions.on': True,
- 'tools.staticdir.root': os.path.abspath(os.getcwd())
- },
- '/static': {
- 'tools.staticdir.on': True,
- 'tools.staticdir.dir': './static'
- }
- }
- cherrypy.quickstart(Root(env, conf, rc), '/', appconf)
diff --git a/gui/rcparam.json b/gui/rcparam.json
deleted file mode 100644
index f350060..0000000
--- a/gui/rcparam.json
+++ /dev/null
@@ -1,48 +0,0 @@
-{
- "labels": {
- "buffermanagement":"Buffer management",
- "ptysd":"Program-type mode",
- "pty":"Program type"
- },
- "buffermanagement": [
- {"value":"prebuffering", "desc":"prebuffering"},
- {"value":"timestamped", "desc":"timestamped"}
- ],
- "ptysd": [
- {"value":"dynamic", "desc":"dynamic"},
- {"value":"static", "desc":"static"}
- ],
- "pty": [
- {"value":"0", "desc":"None"},
- {"value":"1", "desc":"News"},
- {"value":"2", "desc":"Affairs"},
- {"value":"3", "desc":"Info"},
- {"value":"4", "desc":"Sport"},
- {"value":"5", "desc":"Educate"},
- {"value":"6", "desc":"Drama"},
- {"value":"7", "desc":"Arts"},
- {"value":"8", "desc":"Science"},
- {"value":"9", "desc":"Talk"},
- {"value":"10", "desc":"Pop"},
- {"value":"11", "desc":"Rock"},
- {"value":"12", "desc":"Easy"},
- {"value":"13", "desc":"Light classics"},
- {"value":"14", "desc":"Classics"},
- {"value":"15", "desc":"Other music"},
- {"value":"16", "desc":"Weather"},
- {"value":"17", "desc":"Finance"},
- {"value":"18", "desc":"Children"},
- {"value":"19", "desc":"Factual"},
- {"value":"20", "desc":"Religion"},
- {"value":"21", "desc":"Phone in"},
- {"value":"22", "desc":"Travel"},
- {"value":"23", "desc":"Leisure"},
- {"value":"24", "desc":"Jazz"},
- {"value":"25", "desc":"Country"},
- {"value":"26", "desc":"National music"},
- {"value":"27", "desc":"Oldies"},
- {"value":"28", "desc":"Folk"},
- {"value":"29", "desc":"Document"}
- ]
- }
- \ No newline at end of file
diff --git a/gui/static/intercooler-1.0.1.min.js b/gui/static/intercooler-1.0.1.min.js
deleted file mode 100644
index c89fdb9..0000000
--- a/gui/static/intercooler-1.0.1.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-/*! intercooler 1.0.1 2016-09-13 */
-!function(a,b){"function"==typeof define&&define.amd?define("intercooler",["jquery"],function(c){return a.Intercooler=b(c)}):"object"==typeof exports?module.exports=b(require("jquery")):a.Intercooler=b(jQuery)}(this,function($){var Intercooler=Intercooler||function(){"use strict";function remove(a){a.remove()}function showIndicator(a){a.closest(".ic-use-transition").length>0?(a.data("ic-use-transition",!0),a.removeClass("ic-use-transition")):a.show()}function hideIndicator(a){a.data("ic-use-transition")?(a.data("ic-use-transition",null),a.addClass("ic-use-transition")):a.hide()}function fixICAttributeName(a){return USE_DATA?"data-"+a:a}function getICAttribute(a,b){return a.attr(fixICAttributeName(b))}function setICAttribute(a,b,c){a.attr(fixICAttributeName(b),c)}function prepend(a,b){try{a.prepend(b)}catch(b){log(a,formatError(b),"ERROR")}if(getICAttribute(a,"ic-limit-children")){var c=parseInt(getICAttribute(a,"ic-limit-children"));a.children().length>c&&a.children().slice(c,a.children().length).remove()}}function append(a,b){try{a.append(b)}catch(b){log(a,formatError(b),"ERROR")}if(getICAttribute(a,"ic-limit-children")){var c=parseInt(getICAttribute(a,"ic-limit-children"));a.children().length>c&&a.children().slice(0,a.children().length-c).remove()}}function log(a,b,c){if(null==a&&(a=$("body")),a.trigger("log.ic",[b,c,a]),"ERROR"==c){window.console&&window.console.log("Intercooler Error : "+b);var d=closestAttrValue($("body"),"ic-post-errors-to");d&&$.post(d,{error:b})}}function uuid(){return _UUID++}function icSelectorFor(a){return getICAttributeSelector("ic-id='"+getIntercoolerId(a)+"'")}function parseInterval(a){return log(null,"POLL: Parsing interval string "+a,"DEBUG"),"null"==a||"false"==a||""==a?null:a.lastIndexOf("ms")==a.length-2?parseFloat(a.substr(0,a.length-2)):a.lastIndexOf("s")==a.length-1?1e3*parseFloat(a.substr(0,a.length-1)):1e3}function getICAttributeSelector(a){return"["+fixICAttributeName(a)+"]"}function initScrollHandler(){null==_scrollHandler&&(_scrollHandler=function(){$(getICAttributeSelector("ic-trigger-on='scrolled-into-view'")).each(function(){isScrolledIntoView(getTriggeredElement($(this)))&&1!=$(this).data("ic-scrolled-into-view-loaded")&&($(this).data("ic-scrolled-into-view-loaded",!0),fireICRequest($(this)))})},$(window).scroll(_scrollHandler))}function currentUrl(){return window.location.pathname+window.location.search+window.location.hash}function createDocument(a){var b=null;return/<(html|body)/i.test(a)?(b=document.documentElement.cloneNode(),b.innerHTML=a):(b=document.documentElement.cloneNode(!0),b.querySelector("body").innerHTML=a),$(b)}function getTarget(a){var b=$(a).closest(getICAttributeSelector("ic-target")),c=getICAttribute(b,"ic-target");return"this"==c?b:c&&0!=c.indexOf("this.")?0==c.indexOf("closest ")?a.closest(c.substr(8)):0==c.indexOf("find ")?a.find(c.substr(5)):$(c):a}function processHeaders(elt,xhr){elt.trigger("beforeHeaders.ic",[elt,xhr]),log(elt,"response headers: "+xhr.getAllResponseHeaders(),"DEBUG");var target=null;if(xhr.getResponseHeader("X-IC-Title")&&(document.title=xhr.getResponseHeader("X-IC-Title")),xhr.getResponseHeader("X-IC-Refresh")){var pathsToRefresh=xhr.getResponseHeader("X-IC-Refresh").split(",");log(elt,"X-IC-Refresh: refreshing "+pathsToRefresh,"DEBUG"),$.each(pathsToRefresh,function(a,b){refreshDependencies(b.replace(/ /g,""),elt)})}if(xhr.getResponseHeader("X-IC-Script")&&(log(elt,"X-IC-Script: evaling "+xhr.getResponseHeader("X-IC-Script"),"DEBUG"),eval(xhr.getResponseHeader("X-IC-Script"))),xhr.getResponseHeader("X-IC-Redirect")&&(log(elt,"X-IC-Redirect: redirecting to "+xhr.getResponseHeader("X-IC-Redirect"),"DEBUG"),window.location=xhr.getResponseHeader("X-IC-Redirect")),"true"==xhr.getResponseHeader("X-IC-CancelPolling")&&cancelPolling($(elt).closest(getICAttributeSelector("ic-poll"))),"true"==xhr.getResponseHeader("X-IC-ResumePolling")){var pollingElt=$(elt).closest(getICAttributeSelector("ic-poll"));setICAttribute(pollingElt,"ic-pause-polling",null),startPolling(pollingElt)}if(xhr.getResponseHeader("X-IC-SetPollInterval")){var pollingElt=$(elt).closest(getICAttributeSelector("ic-poll"));cancelPolling(pollingElt),setICAttribute(pollingElt,"ic-poll",xhr.getResponseHeader("X-IC-SetPollInterval")),startPolling(pollingElt)}xhr.getResponseHeader("X-IC-Open")&&(log(elt,"X-IC-Open: opening "+xhr.getResponseHeader("X-IC-Open"),"DEBUG"),window.open(xhr.getResponseHeader("X-IC-Open")));var triggerValue=xhr.getResponseHeader("X-IC-Trigger");if(triggerValue)if(log(elt,"X-IC-Trigger: found trigger "+triggerValue,"DEBUG"),target=getTarget(elt),xhr.getResponseHeader("X-IC-Trigger-Data")){var triggerArgs=$.parseJSON(xhr.getResponseHeader("X-IC-Trigger-Data"));target.trigger(triggerValue,triggerArgs)}else triggerValue.indexOf("{")>=0?$.each($.parseJSON(triggerValue),function(a,b){target.trigger(a,b)}):target.trigger(triggerValue,[]);var localVars=xhr.getResponseHeader("X-IC-Set-Local-Vars");return localVars&&$.each($.parseJSON(localVars),function(a,b){localStorage.setItem(a,b)}),xhr.getResponseHeader("X-IC-Remove")&&elt&&(target=getTarget(elt),log(elt,"X-IC-Remove header found.","DEBUG"),remove(target)),elt.trigger("afterHeaders.ic",[elt,xhr]),!0}function beforeRequest(a){a.addClass("disabled"),a.data("ic-request-in-flight",!0)}function requestCleanup(a,b){a.length>0&&hideIndicator(a),b.removeClass("disabled"),b.data("ic-request-in-flight",!1),b.data("ic-next-request")&&(b.data("ic-next-request")(),b.data("ic-next-request",null))}function replaceOrAddMethod(a,b){if("string"===$.type(a)){var c=/(&|^)_method=[^&]*/,d="&_method="+b;return c.test(a)?a.replace(c,d):a+d}return a.append("_method",b),a}function globalEval(a){return window.eval.call(window,a)}function closestAttrValue(a,b){var c=$(a).closest(getICAttributeSelector(b));return c.length>0?getICAttribute(c,b):null}function formatError(a){var b=a.toString()+"\n";try{b+=a.stack}catch(a){}return b}function handleRemoteRequest(a,b,c,d,e){beforeRequest(a),d=replaceOrAddMethod(d,b);var f=findIndicator(a);f.length>0&&showIndicator(f);var g,h=uuid(),i=new Date;g=USE_ACTUAL_HTTP_METHOD?b:"GET"==b?"GET":"POST";var j={type:g,url:c,data:d,dataType:"text",headers:{Accept:"text/html-partial, */*; q=0.9","X-IC-Request":!0,"X-HTTP-Method-Override":b},beforeSend:function(e,f){a.trigger("beforeSend.ic",[a,d,f,e,h]),log(a,"before AJAX request "+h+": "+b+" to "+c,"DEBUG");var g=closestAttrValue(a,"ic-on-beforeSend");g&&globalEval("(function (data, settings, xhr) {"+g+"})")(d,f,e)},success:function(b,c,d){a.trigger("success.ic",[a,b,c,d,h]),log(a,"AJAX request "+h+" was successful.","DEBUG");var g=closestAttrValue(a,"ic-on-success");if(!g||0!=globalEval("(function (data, textStatus, xhr) {"+g+"})")(b,c,d)){var i=new Date;try{if(processHeaders(a,d)){log(a,"Processed headers for request "+h+" in "+(new Date-i)+"ms","DEBUG");var j=new Date;if(d.getResponseHeader("X-IC-PushURL")||"true"==closestAttrValue(a,"ic-push-url"))try{requestCleanup(f,a);var k=d.getResponseHeader("X-IC-PushURL")||closestAttrValue(a,"ic-src");if(!_history)throw"History support not enabled";_history.snapshotForHistory(k)}catch(b){log(a,"Error during history snapshot for "+h+": "+formatError(b),"ERROR")}e(b,c,a,d),log(a,"Process content for request "+h+" in "+(new Date-j)+"ms","DEBUG")}a.trigger("after.success.ic",[a,b,c,d,h])}catch(b){log(a,"Error processing successful request "+h+" : "+formatError(b),"ERROR")}}},error:function(b,d,e){a.trigger("error.ic",[a,d,e,b]);var f=closestAttrValue(a,"ic-on-error");f&&globalEval("(function (status, str, xhr) {"+f+"})")(d,e,b),log(a,"AJAX request "+h+" to "+c+" experienced an error: "+e,"ERROR")},complete:function(b,c){log(a,"AJAX request "+h+" completed in "+(new Date-i)+"ms","DEBUG"),requestCleanup(f,a);try{$.contains(document,a[0])?$(a).trigger("complete.ic",[a,d,c,b,h]):$("body").trigger("complete.ic",[a,d,c,b,h])}catch(b){log(a,"Error during complete.ic event for "+h+" : "+formatError(b),"ERROR")}var e=closestAttrValue(a,"ic-on-complete");e&&globalEval("(function (xhr, status) {"+e+"})")(b,c)}};"string"!=$.type(d)&&(j.dataType=null,j.processData=!1,j.contentType=!1),$(document).trigger("beforeAjaxSend.ic",j),$.ajax(j)}function findIndicator(a){var b=null;if(getICAttribute($(a),"ic-indicator"))b=$(getICAttribute($(a),"ic-indicator")).first();else if(b=$(a).find(".ic-indicator").first(),0==b.length){var c=closestAttrValue(a,"ic-indicator");c?b=$(c).first():$(a).next().is(".ic-indicator")&&(b=$(a).next())}return b}function processIncludes(a,b){if(0==$.trim(b).indexOf("{")){var c=$.parseJSON(b);$.each(c,function(b,c){a=appendData(a,b,c)})}else $(b).each(function(){var b=$(this).serializeArray();$.each(b,function(b,c){a=appendData(a,c.name,c.value)})});return a}function processLocalVars(a,b){return $(b.split(",")).each(function(){var b=$.trim(this),c=localStorage.getItem(b);c&&(a=appendData(a,b,c))}),a}function appendData(a,b,c){return"string"===$.type(a)?a+"&"+b+"="+encodeURIComponent(c):(a.append(b,c),a)}function getParametersForElement(a,b,c){var d=getTarget(b),e=null;b.is("form")&&"multipart/form-data"==b.attr("enctype")?(e=new FormData(b[0]),e=appendData(e,"ic-request",!0)):(e="ic-request=true",e+="GET"!=a&&b.closest("form").length>0?"&"+b.closest("form").serialize():"&"+b.serialize());var f=closestAttrValue(b,"ic-prompt");if(f){var g=prompt(f);if(!g)return null;var h=closestAttrValue(b,"ic-prompt-name")||"ic-prompt-value";e=appendData(e,h,g)}b.attr("id")&&(e=appendData(e,"ic-element-id",b.attr("id"))),b.attr("name")&&(e=appendData(e,"ic-element-name",b.attr("name"))),getICAttribute(d,"ic-id")&&(e=appendData(e,"ic-id",getICAttribute(d,"ic-id"))),d.attr("id")&&(e=appendData(e,"ic-target-id",d.attr("id"))),c&&c.attr("id")&&(e=appendData(e,"ic-trigger-id",c.attr("id"))),c&&c.attr("name")&&(e=appendData(e,"ic-trigger-name",c.attr("name")));var i=closestAttrValue(b,"ic-include");i&&(e=processIncludes(e,i));var j=closestAttrValue(b,"ic-local-vars");return j&&(e=processLocalVars(e,j)),$(getICAttributeSelector("ic-global-include")).each(function(){e=processIncludes(e,getICAttribute($(this),"ic-global-include"))}),e=appendData(e,"ic-current-url",currentUrl()),log(b,"request parameters "+e,"DEBUG"),e}function maybeSetIntercoolerInfo(a){var b=getTarget(a);getIntercoolerId(b),1!=a.data("elementAdded.ic")&&(a.data("elementAdded.ic",!0),a.trigger("elementAdded.ic"))}function getIntercoolerId(a){return getICAttribute(a,"ic-id")||setICAttribute(a,"ic-id",uuid()),getICAttribute(a,"ic-id")}function processNodes(a){a.length>1?a.each(function(){processNodes($(this))}):(processMacros(a),processSources(a),processPolling(a),processTriggerOn(a),processRemoveAfter(a),processAddClasses(a),processRemoveClasses(a))}function fireReadyStuff(a){a.trigger("nodesProcessed.ic"),$.each(_readyHandlers,function(b,c){try{c(a)}catch(b){log(a,formatError(b),"ERROR")}})}function autoFocus(a){a.find("[autofocus]:last").focus()}function processMacros(a){$.each(_MACROS,function(b,c){0==$(a).closest(".ic-ignore").length&&($(a).is("["+c+"]")&&processMacro(c,$(a)),$(a).find("["+c+"]").each(function(){0==$(this).closest(".ic-ignore").length&&processMacro(c,$(this))}))})}function processSources(a){0==$(a).closest(".ic-ignore").length&&($(a).is(getICAttributeSelector("ic-src"))&&maybeSetIntercoolerInfo($(a)),$(a).find(getICAttributeSelector("ic-src")).each(function(){0==$(this).closest(".ic-ignore").length&&maybeSetIntercoolerInfo($(this))}))}function processPolling(a){0==$(a).closest(".ic-ignore").length&&($(a).is(getICAttributeSelector("ic-poll"))&&(maybeSetIntercoolerInfo($(a)),startPolling(a)),$(a).find(getICAttributeSelector("ic-poll")).each(function(){0==$(this).closest(".ic-ignore").length&&(maybeSetIntercoolerInfo($(this)),startPolling($(this)))}))}function processTriggerOn(a){0==$(a).closest(".ic-ignore").length&&(handleTriggerOn(a),$(a).find(getICAttributeSelector("ic-trigger-on")).each(function(){0==$(this).closest(".ic-ignore").length&&handleTriggerOn($(this))}))}function processRemoveAfter(a){0==$(a).closest(".ic-ignore").length&&(handleRemoveAfter(a),$(a).find(getICAttributeSelector("ic-remove-after")).each(function(){0==$(this).closest(".ic-ignore").length&&handleRemoveAfter($(this))}))}function processAddClasses(a){0==$(a).closest(".ic-ignore").length&&(handleAddClasses(a),$(a).find(getICAttributeSelector("ic-add-class")).each(function(){0==$(this).closest(".ic-ignore").length&&handleAddClasses($(this))}))}function processRemoveClasses(a){0==$(a).closest(".ic-ignore").length&&(handleRemoveClasses(a),$(a).find(getICAttributeSelector("ic-remove-class")).each(function(){0==$(this).closest(".ic-ignore").length&&handleRemoveClasses($(this))}))}function startPolling(a){if(null==a.data("ic-poll-interval-id")&&"true"!=getICAttribute($(a),"ic-pause-polling")){var b=parseInterval(getICAttribute(a,"ic-poll"));if(null!=b){var c=icSelectorFor(a),d=parseInt(getICAttribute(a,"ic-poll-repeats"))||-1,e=0;log(a,"POLL: Starting poll for element "+c,"DEBUG");var f=setInterval(function(){var b=$(c);a.trigger("onPoll.ic",b),0==b.length||e==d||a.data("ic-poll-interval-id")!=f?(log(a,"POLL: Clearing poll for element "+c,"DEBUG"),clearTimeout(f)):fireICRequest(b),e++},b);a.data("ic-poll-interval-id",f)}}}function cancelPolling(a){null!=a.data("ic-poll-interval-id")&&(clearTimeout(a.data("ic-poll-interval-id")),a.data("ic-poll-interval-id",null))}function refreshDependencies(a,b){log(b,"refreshing dependencies for path "+a,"DEBUG"),$(getICAttributeSelector("ic-src")).each(function(){var c=!1;"GET"==verbFor($(this))&&"ignore"!=getICAttribute($(this),"ic-deps")&&"undefined"==typeof getICAttribute($(this),"ic-poll")&&(isDependent(a,getICAttribute($(this),"ic-src"))?null!=b&&$(b)[0]==$(this)[0]||(fireICRequest($(this)),c=!0):(isDependent(a,getICAttribute($(this),"ic-deps"))||"*"==getICAttribute($(this),"ic-deps"))&&(null!=b&&$(b)[0]==$(this)[0]||(fireICRequest($(this)),c=!0))),c&&log($(this),"depends on path "+a+", refreshing...","DEBUG")})}function isDependent(a,b){return!!_isDependentFunction(a,b)}function verbFor(a){return getICAttribute(a,"ic-verb")?getICAttribute(a,"ic-verb").toUpperCase():"GET"}function eventFor(a,b){return"default"==a?$(b).is("button")?"click":$(b).is("form")?"submit":$(b).is(":input")?"change":"click":a}function preventDefault(a,b){return a.is("form")||a.is(":submit")&&1==a.closest("form").length||a.is("a")&&a.is("[href]")&&0!=a.attr("href").indexOf("#")}function handleRemoveAfter(a){if(getICAttribute($(a),"ic-remove-after")){var b=parseInterval(getICAttribute($(a),"ic-remove-after"));setTimeout(function(){remove(a)},b)}}function parseAndApplyClass(a,b,c){var d="",e=50;if(a.indexOf(":")>0){var f=a.split(":");d=f[0],e=parseInterval(f[1])}else d=a;setTimeout(function(){b[c](d)},e)}function handleAddClasses(a){if(getICAttribute($(a),"ic-add-class"))for(var b=getICAttribute($(a),"ic-add-class").split(","),c=b.length,d=0;d<c;d++)parseAndApplyClass($.trim(b[d]),a,"addClass")}function handleRemoveClasses(a){if(getICAttribute($(a),"ic-remove-class"))for(var b=getICAttribute($(a),"ic-remove-class").split(","),c=b.length,d=0;d<c;d++)parseAndApplyClass($.trim(b[d]),a,"removeClass")}function getTriggeredElement(elt){var triggerFrom=getICAttribute(elt,"ic-trigger-from");return triggerFrom?$($.inArray(triggerFrom,["document","window"])>=0?eval(triggerFrom):triggerFrom):elt}function handleTriggerOn(a){if(getICAttribute($(a),"ic-trigger-on"))if("load"==getICAttribute($(a),"ic-trigger-on"))fireICRequest(a);else if("scrolled-into-view"==getICAttribute($(a),"ic-trigger-on"))initScrollHandler(),setTimeout(function(){$(window).trigger("scroll")},100);else{var b=getICAttribute($(a),"ic-trigger-on").split(" ");$(getTriggeredElement(a)).on(eventFor(b[0],$(a)),function(c){var d=closestAttrValue(a,"ic-on-beforeTrigger");if(d&&0==globalEval("(function (evt, elt) {"+d+"})")(c,$(a)))return log($(a),"ic-trigger cancelled by ic-on-beforeTrigger","DEBUG"),!1;if("changed"==b[1]){var e=$(a).val(),f=$(a).data("ic-previous-val");$(a).data("ic-previous-val",e),e!=f&&fireICRequest($(a))}else fireICRequest($(a));return!preventDefault(a,c)||(c.preventDefault(),!1)})}}function macroIs(a,b){return a==fixICAttributeName(b)}function processMacro(a,b){macroIs(a,"ic-post-to")&&(setIfAbsent(b,"ic-src",getICAttribute(b,"ic-post-to")),setIfAbsent(b,"ic-verb","POST"),setIfAbsent(b,"ic-trigger-on","default"),setIfAbsent(b,"ic-deps","ignore")),macroIs(a,"ic-put-to")&&(setIfAbsent(b,"ic-src",getICAttribute(b,"ic-put-to")),setIfAbsent(b,"ic-verb","PUT"),setIfAbsent(b,"ic-trigger-on","default"),setIfAbsent(b,"ic-deps","ignore")),macroIs(a,"ic-patch-to")&&(setIfAbsent(b,"ic-src",getICAttribute(b,"ic-patch-to")),setIfAbsent(b,"ic-verb","PATCH"),setIfAbsent(b,"ic-trigger-on","default"),setIfAbsent(b,"ic-deps","ignore")),macroIs(a,"ic-get-from")&&(setIfAbsent(b,"ic-src",getICAttribute(b,"ic-get-from")),setIfAbsent(b,"ic-trigger-on","default"),setIfAbsent(b,"ic-deps","ignore")),macroIs(a,"ic-delete-from")&&(setIfAbsent(b,"ic-src",getICAttribute(b,"ic-delete-from")),setIfAbsent(b,"ic-verb","DELETE"),setIfAbsent(b,"ic-trigger-on","default"),setIfAbsent(b,"ic-deps","ignore")),macroIs(a,"ic-action")&&setIfAbsent(b,"ic-trigger-on","default");var c=null,d=null;if(macroIs(a,"ic-style-src")){c=getICAttribute(b,"ic-style-src").split(":");var e=c[0];d=c[1],setIfAbsent(b,"ic-src",d),setIfAbsent(b,"ic-target","this.style."+e)}if(macroIs(a,"ic-attr-src")){c=getICAttribute(b,"ic-attr-src").split(":");var f=c[0];d=c[1],setIfAbsent(b,"ic-src",d),setIfAbsent(b,"ic-target","this."+f)}macroIs(a,"ic-prepend-from")&&setIfAbsent(b,"ic-src",getICAttribute(b,"ic-prepend-from")),macroIs(a,"ic-append-from")&&setIfAbsent(b,"ic-src",getICAttribute(b,"ic-append-from"))}function setIfAbsent(a,b,c){null==getICAttribute(a,b)&&setICAttribute(a,b,c)}function isScrolledIntoView(a){var b=$(window).scrollTop(),c=b+$(window).height(),d=$(a).offset().top,e=d+$(a).height();return e>=b&&d<=c&&e<=c&&d>=b}function maybeScrollToTarget(a,b){if("false"!=closestAttrValue(a,"ic-scroll-to-target")&&("true"==closestAttrValue(a,"ic-scroll-to-target")||"true"==closestAttrValue(b,"ic-scroll-to-target"))){var c=-50;closestAttrValue(a,"ic-scroll-offset")?c=parseInt(closestAttrValue(a,"ic-scroll-offset")):closestAttrValue(b,"ic-scroll-offset")&&(c=parseInt(closestAttrValue(b,"ic-scroll-offset")));var d=b.offset().top,e=$(window).scrollTop(),f=e+window.innerHeight;(d<e||d>f)&&(c+=d,$("html,body").animate({scrollTop:c},400))}}function getTransitionDuration(a,b){var c=closestAttrValue(a,"ic-transition-duration");if(c)return parseInterval(c);if(c=closestAttrValue(b,"ic-transition-duration"))return parseInterval(c);var d=0,e=$(b).css("transition-duration");e&&(d+=parseInterval(e));var f=$(b).css("transition-delay");return f&&(d+=parseInterval(f)),d}function processICResponse(a,b,c){if(a&&""!=a&&" "!=a){log(b,"response content: \n"+a,"DEBUG");var d=getTarget(b),e=maybeFilter(a,closestAttrValue(b,"ic-select-from-response")),f=function(){if("true"==closestAttrValue(b,"ic-replace-target")){try{d.replaceWith(e)}catch(a){log(b,formatError(a),"ERROR")}processNodes(e),fireReadyStuff($(d)),autoFocus($(d))}else{if(b.is(getICAttributeSelector("ic-prepend-from")))prepend(d,e),processNodes(e),fireReadyStuff($(d)),autoFocus($(d));else if(b.is(getICAttributeSelector("ic-append-from")))append(d,e),processNodes(e),fireReadyStuff($(d)),autoFocus($(d));else{try{d.empty().append(e)}catch(a){log(b,formatError(a),"ERROR")}$(d).children().each(function(){processNodes($(this))}),fireReadyStuff($(d)),autoFocus($(d))}1!=c&&maybeScrollToTarget(b,d)}};if(0==d.length)return void log(b,"Invalid target for element: "+getICAttribute($(b).closest(getICAttributeSelector("ic-target")),"ic-target"),"ERROR");var g=getTransitionDuration(b,d);d.addClass("ic-transitioning"),setTimeout(function(){try{f()}catch(a){log(b,"Error during content swaop : "+formatError(a),"ERROR")}setTimeout(function(){try{d.removeClass("ic-transitioning"),_history&&_history.updateHistory(),d.trigger("complete_transition.ic",[d])}catch(a){log(b,"Error during transition complete : "+formatError(a),"ERROR")}},20)},g)}else log(b,"Empty response, nothing to do here.","DEBUG")}function maybeFilter(a,b){var c=$.parseHTML(a,null,!0),d=$(c);return b?d.filter(b).add(d.find(b)).contents():d}function getStyleTarget(a){var b=closestAttrValue(a,"ic-target");return b&&0==b.indexOf("this.style.")?b.substr(11):null}function getAttrTarget(a){var b=closestAttrValue(a,"ic-target");return b&&0==b.indexOf("this.")?b.substr(5):null}function fireICRequest(a,b){var c=a;a.is(getICAttributeSelector("ic-src"))||void 0!=getICAttribute(a,"ic-action")||(a=a.closest(getICAttributeSelector("ic-src")));var d=closestAttrValue(a,"ic-confirm");if((!d||confirm(d))&&a.length>0){var e=uuid();a.data("ic-event-id",e);var f=function(){if(1==a.data("ic-request-in-flight"))return void a.data("ic-next-request",f);if(a.data("ic-event-id")==e){var d=getStyleTarget(a),g=d?null:getAttrTarget(a),h=verbFor(a),i=getICAttribute(a,"ic-src");if(i){var j=b||function(b){d?a.css(d,b):g?a.attr(g,b):(processICResponse(b,a),"GET"!=h&&refreshDependencies(getICAttribute(a,"ic-src"),a))},k=getParametersForElement(h,a,c);k&&handleRemoteRequest(a,h,i,k,j)}var l=getICAttribute(a,"ic-action");l&&invokeLocalAction(a,l)}},g=closestAttrValue(a,"ic-trigger-delay");g?setTimeout(f,parseInterval(g)):f()}}function invokeLocalAction(a,b){var c=getTarget(a),d=b.split(";"),e=[],f=0;$.each(d,function(a,b){var d=$.trim(b),g=d,h=[];d.indexOf(":")>0&&(g=d.substr(0,d.indexOf(":")),h=computeArgs(d.substr(d.indexOf(":")+1,d.length))),""==g||("delay"==g?(null==f&&(f=0),f+=parseInterval(h[0]+"")):(null==f&&(f=420),e.push([f,makeApplyAction(c,g,h)]),f=null))}),f=0,$.each(e,function(a,b){f+=b[0],setTimeout(b[1],f)})}function computeArgs(args){try{return eval("["+args+"]")}catch(a){return[$.trim(args)]}}function makeApplyAction(a,b,c){return function(){var d=a[b]||window[b];d?d.apply(a,c):log(a,"Action "+b+" was not found","ERROR")}}function newIntercoolerHistory(a,b,c,d){function e(a){return null==a||a.slotLimit!=c||a.historyVersion!=d||null==a.lruList}function f(){for(var b=[],e=0;e<a.length;e++)0==a.key(e).indexOf(s)&&b.push(a.key(e));for(var f=0;f<b.length;f++)a.removeItem(b[f]);a.removeItem(r),t={slotLimit:c,historyVersion:d,lruList:[]}}function g(b){var c=t.lruList,d=c.indexOf(b),e=n($("body"));if(d>=0)log(e,"URL found in LRU list, moving to end","INFO"),c.splice(d,1),c.push(b);else if(log(e,"URL not found in LRU list, adding","INFO"),c.push(b),c.length>t.slotLimit){var f=c.shift();log(e,"History overflow, removing local history for "+f,"INFO"),a.removeItem(s+f)}return a.setItem(r,JSON.stringify(t)),c}function h(b){var d=JSON.stringify(b);try{a.setItem(b.id,d)}catch(e){try{f(),a.setItem(b.id,d)}catch(a){log(n($("body")),"Unable to save intercooler history with entire history cleared, is something else eating local storage? History Limit:"+c,"ERROR")}}}function i(a,b,c){var d={url:c,id:s+c,content:a,yOffset:b,timestamp:(new Date).getTime()};return g(c),h(d),d}function j(a){if(null==a.onpopstate||1!=a.onpopstate["ic-on-pop-state-handler"]){var b=a.onpopstate;a.onpopstate=function(a){n($("body")).trigger("handle.onpopstate.ic"),m(a)||b&&b(a),n($("body")).trigger("pageLoad.ic")},a.onpopstate["ic-on-pop-state-handler"]=!0}}function k(){u&&(l(u.newUrl,currentUrl(),u.oldHtml,u.yOffset),u=null)}function l(a,c,d,e){var f=i(d,e,c);b.replaceState({"ic-id":f.id},"","");var g=n($("body")),h=i(g.html(),window.pageYOffset,a);b.pushState({"ic-id":h.id},"",a),g.trigger("pushUrl.ic",[g,h])}function m(b){var c=b.state;if(c&&c["ic-id"]){var d=JSON.parse(a.getItem(c["ic-id"]));if(d)return processICResponse(d.content,n($("body")),!0),d.yOffset&&window.scrollTo(0,d.yOffset),!0;$.get(currentUrl(),{"ic-restore-history":!0},function(a,b){var c=createDocument(a),d=n(c).html();processICResponse(d,n($("body")),!0)})}return!1}function n(a){var b=a.find(getICAttributeSelector("ic-history-elt"));return b.length>0?b:a}function o(a){var b=n($("body"));b.trigger("beforeHistorySnapshot.ic",[b]),u={newUrl:a,oldHtml:b.html(),yOffset:window.pageYOffset}}function p(){var b="",c=[];for(var d in a)c.push(d);c.sort();var e=0;for(var f in c){var g=2*a[c[f]].length;e+=g,b+=c[f]+"="+(g/1024/1024).toFixed(2)+" MB\n"}return b+"\nTOTAL LOCAL STORAGE: "+(e/1024/1024).toFixed(2)+" MB"}function q(){return t}var r="ic-history-support",s="ic-hist-elt-",t=JSON.parse(a.getItem(r)),u=null;return e(t)&&(log(n($("body")),"Intercooler History configuration changed, clearing history","INFO"),f()),null==t&&(t={slotLimit:c,historyVersion:d,lruList:[]}),{clearHistory:f,updateHistory:k,addPopStateHandler:j,snapshotForHistory:o,_internal:{addPopStateHandler:j,supportData:q,dumpLocalStorage:p,updateLRUList:g}}}function getSlotLimit(){return 20}function refresh(a){return"string"==typeof a||a instanceof String?refreshDependencies(a):fireICRequest(a),Intercooler}function init(){var a=$("body");processNodes(a),fireReadyStuff(a),_history&&_history.addPopStateHandler(window),location.search&&location.search.indexOf("ic-launch-debugger=true")>=0&&Intercooler.debug()}var USE_DATA="true"==$('meta[name="intercoolerjs:use-data-prefix"]').attr("content"),USE_ACTUAL_HTTP_METHOD="true"==$('meta[name="intercoolerjs:use-actual-http-method"]').attr("content"),_MACROS=$.map(["ic-get-from","ic-post-to","ic-put-to","ic-patch-to","ic-delete-from","ic-style-src","ic-attr-src","ic-prepend-from","ic-append-from","ic-action"],function(a){return fixICAttributeName(a)}),_scrollHandler=null,_UUID=1,_readyHandlers=[],_isDependentFunction=function(a,b){if(!a||!b)return!1;var c=a.split(/[\?#]/,1)[0].split("/").filter(function(a){return""!=a}),d=b.split(/[\?#]/,1)[0].split("/").filter(function(a){return""!=a});return""!=c&&""!=d&&(d.slice(0,c.length).join("/")==c.join("/")||c.slice(0,d.length).join("/")==d.join("/"))},_history=null;try{_history=newIntercoolerHistory(localStorage,window.history,getSlotLimit(),.1)}catch(a){log($("body"),"Could not initialize history","WARN")}return $.ajaxTransport("text",function(a,b){if("#"==b.url[0]){var c=fixICAttributeName("ic-local-"),d=$(b.url),e=[],f=200,g="OK";d.each(function(a,b){$.each(b.attributes,function(a,b){if(b.name.substr(0,c.length)==c){var d=b.name.substring(c.length);if("status"==d){var h=b.value.match(/(\d+)\s?(.*)/);null!=h?(f=h[1],g=h[2]):(f="500",g="Attribute Error")}else e.push(d+": "+b.value)}})});var h=d.length>0?d.html():"";return{send:function(a,b){b(f,g,{html:h},e.join("\n"))},abort:function(){}}}return null}),$(function(){init()}),{refresh:refresh,history:_history,triggerRequest:fireICRequest,processNodes:processNodes,closestAttrValue:closestAttrValue,verbFor:verbFor,isDependent:isDependent,getTarget:getTarget,processHeaders:processHeaders,setIsDependentFunction:function(a){_isDependentFunction=a},ready:function(a){_readyHandlers.push(a)},debug:function(){var a=closestAttrValue("body","ic-debugger-url")||"https://intercoolerreleases-leaddynocom.netdna-ssl.com/intercooler-debugger.js";$.getScript(a).fail(function(a,b,c){log($("body"),formatError(c),"ERROR")})},_internal:{init:init,replaceOrAddMethod:replaceOrAddMethod}}}();return Intercooler}); \ No newline at end of file
diff --git a/gui/static/jquery-1.10.2.min.js b/gui/static/jquery-1.10.2.min.js
deleted file mode 100644
index da41706..0000000
--- a/gui/static/jquery-1.10.2.min.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/*! jQuery v1.10.2 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license
-//@ sourceMappingURL=jquery-1.10.2.min.map
-*/
-(function(e,t){var n,r,i=typeof t,o=e.location,a=e.document,s=a.documentElement,l=e.jQuery,u=e.$,c={},p=[],f="1.10.2",d=p.concat,h=p.push,g=p.slice,m=p.indexOf,y=c.toString,v=c.hasOwnProperty,b=f.trim,x=function(e,t){return new x.fn.init(e,t,r)},w=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,T=/\S+/g,C=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,N=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,k=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,E=/^[\],:{}\s]*$/,S=/(?:^|:|,)(?:\s*\[)+/g,A=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,j=/"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g,D=/^-ms-/,L=/-([\da-z])/gi,H=function(e,t){return t.toUpperCase()},q=function(e){(a.addEventListener||"load"===e.type||"complete"===a.readyState)&&(_(),x.ready())},_=function(){a.addEventListener?(a.removeEventListener("DOMContentLoaded",q,!1),e.removeEventListener("load",q,!1)):(a.detachEvent("onreadystatechange",q),e.detachEvent("onload",q))};x.fn=x.prototype={jquery:f,constructor:x,init:function(e,n,r){var i,o;if(!e)return this;if("string"==typeof e){if(i="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:N.exec(e),!i||!i[1]&&n)return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e);if(i[1]){if(n=n instanceof x?n[0]:n,x.merge(this,x.parseHTML(i[1],n&&n.nodeType?n.ownerDocument||n:a,!0)),k.test(i[1])&&x.isPlainObject(n))for(i in n)x.isFunction(this[i])?this[i](n[i]):this.attr(i,n[i]);return this}if(o=a.getElementById(i[2]),o&&o.parentNode){if(o.id!==i[2])return r.find(e);this.length=1,this[0]=o}return this.context=a,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return g.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(g.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,n,r,i,o,a,s=arguments[0]||{},l=1,u=arguments.length,c=!1;for("boolean"==typeof s&&(c=s,s=arguments[1]||{},l=2),"object"==typeof s||x.isFunction(s)||(s={}),u===l&&(s=this,--l);u>l;l++)if(null!=(o=arguments[l]))for(i in o)e=s[i],r=o[i],s!==r&&(c&&r&&(x.isPlainObject(r)||(n=x.isArray(r)))?(n?(n=!1,a=e&&x.isArray(e)?e:[]):a=e&&x.isPlainObject(e)?e:{},s[i]=x.extend(c,a,r)):r!==t&&(s[i]=r));return s},x.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=l),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){if(e===!0?!--x.readyWait:!x.isReady){if(!a.body)return setTimeout(x.ready);x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(a,[x]),x.fn.trigger&&x(a).trigger("ready").off("ready"))}},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray||function(e){return"array"===x.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?c[y.call(e)]||"object":typeof e},isPlainObject:function(e){var n;if(!e||"object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!v.call(e,"constructor")&&!v.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(r){return!1}if(x.support.ownLast)for(n in e)return v.call(e,n);for(n in e);return n===t||v.call(e,n)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||a;var r=k.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:function(n){return e.JSON&&e.JSON.parse?e.JSON.parse(n):null===n?n:"string"==typeof n&&(n=x.trim(n),n&&E.test(n.replace(A,"@").replace(j,"]").replace(S,"")))?Function("return "+n)():(x.error("Invalid JSON: "+n),t)},parseXML:function(n){var r,i;if(!n||"string"!=typeof n)return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(o){r=t}return r&&r.documentElement&&!r.getElementsByTagName("parsererror").length||x.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&x.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(D,"ms-").replace(L,H)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,a=M(e);if(n){if(a){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(a){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:b&&!b.call("\ufeff\u00a0")?function(e){return null==e?"":b.call(e)}:function(e){return null==e?"":(e+"").replace(C,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(M(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){var r;if(t){if(m)return m.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,n){var r=n.length,i=e.length,o=0;if("number"==typeof r)for(;r>o;o++)e[i++]=n[o];else while(n[o]!==t)e[i++]=n[o++];return e.length=i,e},grep:function(e,t,n){var r,i=[],o=0,a=e.length;for(n=!!n;a>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,a=M(e),s=[];if(a)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(s[s.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(s[s.length]=r);return d.apply([],s)},guid:1,proxy:function(e,n){var r,i,o;return"string"==typeof n&&(o=e[n],n=e,e=o),x.isFunction(e)?(r=g.call(arguments,2),i=function(){return e.apply(n||this,r.concat(g.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):t},access:function(e,n,r,i,o,a,s){var l=0,u=e.length,c=null==r;if("object"===x.type(r)){o=!0;for(l in r)x.access(e,n,l,r[l],!0,a,s)}else if(i!==t&&(o=!0,x.isFunction(i)||(s=!0),c&&(s?(n.call(e,i),n=null):(c=n,n=function(e,t,n){return c.call(x(e),n)})),n))for(;u>l;l++)n(e[l],r,s?i:i.call(e[l],l,n(e[l],r)));return o?e:c?n.call(e):u?n(e[0],r):a},now:function(){return(new Date).getTime()},swap:function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i}}),x.ready.promise=function(t){if(!n)if(n=x.Deferred(),"complete"===a.readyState)setTimeout(x.ready);else if(a.addEventListener)a.addEventListener("DOMContentLoaded",q,!1),e.addEventListener("load",q,!1);else{a.attachEvent("onreadystatechange",q),e.attachEvent("onload",q);var r=!1;try{r=null==e.frameElement&&a.documentElement}catch(i){}r&&r.doScroll&&function o(){if(!x.isReady){try{r.doScroll("left")}catch(e){return setTimeout(o,50)}_(),x.ready()}}()}return n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){c["[object "+t+"]"]=t.toLowerCase()});function M(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}r=x(a),function(e,t){var n,r,i,o,a,s,l,u,c,p,f,d,h,g,m,y,v,b="sizzle"+-new Date,w=e.document,T=0,C=0,N=st(),k=st(),E=st(),S=!1,A=function(e,t){return e===t?(S=!0,0):0},j=typeof t,D=1<<31,L={}.hasOwnProperty,H=[],q=H.pop,_=H.push,M=H.push,O=H.slice,F=H.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},B="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",P="[\\x20\\t\\r\\n\\f]",R="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",W=R.replace("w","w#"),$="\\["+P+"*("+R+")"+P+"*(?:([*^$|!~]?=)"+P+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+W+")|)|)"+P+"*\\]",I=":("+R+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+$.replace(3,8)+")*)|.*)\\)|)",z=RegExp("^"+P+"+|((?:^|[^\\\\])(?:\\\\.)*)"+P+"+$","g"),X=RegExp("^"+P+"*,"+P+"*"),U=RegExp("^"+P+"*([>+~]|"+P+")"+P+"*"),V=RegExp(P+"*[+~]"),Y=RegExp("="+P+"*([^\\]'\"]*)"+P+"*\\]","g"),J=RegExp(I),G=RegExp("^"+W+"$"),Q={ID:RegExp("^#("+R+")"),CLASS:RegExp("^\\.("+R+")"),TAG:RegExp("^("+R.replace("w","w*")+")"),ATTR:RegExp("^"+$),PSEUDO:RegExp("^"+I),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+P+"*(even|odd|(([+-]|)(\\d*)n|)"+P+"*(?:([+-]|)"+P+"*(\\d+)|))"+P+"*\\)|)","i"),bool:RegExp("^(?:"+B+")$","i"),needsContext:RegExp("^"+P+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+P+"*((?:-\\d)?\\d*)"+P+"*\\)|)(?=[^-]|$)","i")},K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,et=/^(?:input|select|textarea|button)$/i,tt=/^h\d$/i,nt=/'|\\/g,rt=RegExp("\\\\([\\da-f]{1,6}"+P+"?|("+P+")|.)","ig"),it=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(55296|r>>10,56320|1023&r)};try{M.apply(H=O.call(w.childNodes),w.childNodes),H[w.childNodes.length].nodeType}catch(ot){M={apply:H.length?function(e,t){_.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function at(e,t,n,i){var o,a,s,l,u,c,d,m,y,x;if((t?t.ownerDocument||t:w)!==f&&p(t),t=t||f,n=n||[],!e||"string"!=typeof e)return n;if(1!==(l=t.nodeType)&&9!==l)return[];if(h&&!i){if(o=Z.exec(e))if(s=o[1]){if(9===l){if(a=t.getElementById(s),!a||!a.parentNode)return n;if(a.id===s)return n.push(a),n}else if(t.ownerDocument&&(a=t.ownerDocument.getElementById(s))&&v(t,a)&&a.id===s)return n.push(a),n}else{if(o[2])return M.apply(n,t.getElementsByTagName(e)),n;if((s=o[3])&&r.getElementsByClassName&&t.getElementsByClassName)return M.apply(n,t.getElementsByClassName(s)),n}if(r.qsa&&(!g||!g.test(e))){if(m=d=b,y=t,x=9===l&&e,1===l&&"object"!==t.nodeName.toLowerCase()){c=mt(e),(d=t.getAttribute("id"))?m=d.replace(nt,"\\$&"):t.setAttribute("id",m),m="[id='"+m+"'] ",u=c.length;while(u--)c[u]=m+yt(c[u]);y=V.test(e)&&t.parentNode||t,x=c.join(",")}if(x)try{return M.apply(n,y.querySelectorAll(x)),n}catch(T){}finally{d||t.removeAttribute("id")}}}return kt(e.replace(z,"$1"),t,n,i)}function st(){var e=[];function t(n,r){return e.push(n+=" ")>o.cacheLength&&delete t[e.shift()],t[n]=r}return t}function lt(e){return e[b]=!0,e}function ut(e){var t=f.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function ct(e,t){var n=e.split("|"),r=e.length;while(r--)o.attrHandle[n[r]]=t}function pt(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function ft(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function dt(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function ht(e){return lt(function(t){return t=+t,lt(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}s=at.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},r=at.support={},p=at.setDocument=function(e){var n=e?e.ownerDocument||e:w,i=n.defaultView;return n!==f&&9===n.nodeType&&n.documentElement?(f=n,d=n.documentElement,h=!s(n),i&&i.attachEvent&&i!==i.top&&i.attachEvent("onbeforeunload",function(){p()}),r.attributes=ut(function(e){return e.className="i",!e.getAttribute("className")}),r.getElementsByTagName=ut(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),r.getElementsByClassName=ut(function(e){return e.innerHTML="<div class='a'></div><div class='a i'></div>",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),r.getById=ut(function(e){return d.appendChild(e).id=b,!n.getElementsByName||!n.getElementsByName(b).length}),r.getById?(o.find.ID=function(e,t){if(typeof t.getElementById!==j&&h){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){return e.getAttribute("id")===t}}):(delete o.find.ID,o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),o.find.TAG=r.getElementsByTagName?function(e,n){return typeof n.getElementsByTagName!==j?n.getElementsByTagName(e):t}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},o.find.CLASS=r.getElementsByClassName&&function(e,n){return typeof n.getElementsByClassName!==j&&h?n.getElementsByClassName(e):t},m=[],g=[],(r.qsa=K.test(n.querySelectorAll))&&(ut(function(e){e.innerHTML="<select><option selected=''></option></select>",e.querySelectorAll("[selected]").length||g.push("\\["+P+"*(?:value|"+B+")"),e.querySelectorAll(":checked").length||g.push(":checked")}),ut(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&g.push("[*^$]="+P+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||g.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),g.push(",.*:")})),(r.matchesSelector=K.test(y=d.webkitMatchesSelector||d.mozMatchesSelector||d.oMatchesSelector||d.msMatchesSelector))&&ut(function(e){r.disconnectedMatch=y.call(e,"div"),y.call(e,"[s!='']:x"),m.push("!=",I)}),g=g.length&&RegExp(g.join("|")),m=m.length&&RegExp(m.join("|")),v=K.test(d.contains)||d.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},A=d.compareDocumentPosition?function(e,t){if(e===t)return S=!0,0;var i=t.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(t);return i?1&i||!r.sortDetached&&t.compareDocumentPosition(e)===i?e===n||v(w,e)?-1:t===n||v(w,t)?1:c?F.call(c,e)-F.call(c,t):0:4&i?-1:1:e.compareDocumentPosition?-1:1}:function(e,t){var r,i=0,o=e.parentNode,a=t.parentNode,s=[e],l=[t];if(e===t)return S=!0,0;if(!o||!a)return e===n?-1:t===n?1:o?-1:a?1:c?F.call(c,e)-F.call(c,t):0;if(o===a)return pt(e,t);r=e;while(r=r.parentNode)s.unshift(r);r=t;while(r=r.parentNode)l.unshift(r);while(s[i]===l[i])i++;return i?pt(s[i],l[i]):s[i]===w?-1:l[i]===w?1:0},n):f},at.matches=function(e,t){return at(e,null,null,t)},at.matchesSelector=function(e,t){if((e.ownerDocument||e)!==f&&p(e),t=t.replace(Y,"='$1']"),!(!r.matchesSelector||!h||m&&m.test(t)||g&&g.test(t)))try{var n=y.call(e,t);if(n||r.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(i){}return at(t,f,null,[e]).length>0},at.contains=function(e,t){return(e.ownerDocument||e)!==f&&p(e),v(e,t)},at.attr=function(e,n){(e.ownerDocument||e)!==f&&p(e);var i=o.attrHandle[n.toLowerCase()],a=i&&L.call(o.attrHandle,n.toLowerCase())?i(e,n,!h):t;return a===t?r.attributes||!h?e.getAttribute(n):(a=e.getAttributeNode(n))&&a.specified?a.value:null:a},at.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},at.uniqueSort=function(e){var t,n=[],i=0,o=0;if(S=!r.detectDuplicates,c=!r.sortStable&&e.slice(0),e.sort(A),S){while(t=e[o++])t===e[o]&&(i=n.push(o));while(i--)e.splice(n[i],1)}return e},a=at.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=a(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=a(t);return n},o=at.selectors={cacheLength:50,createPseudo:lt,match:Q,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(rt,it),e[3]=(e[4]||e[5]||"").replace(rt,it),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||at.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&at.error(e[0]),e},PSEUDO:function(e){var n,r=!e[5]&&e[2];return Q.CHILD.test(e[0])?null:(e[3]&&e[4]!==t?e[2]=e[4]:r&&J.test(r)&&(n=mt(r,!0))&&(n=r.indexOf(")",r.length-n)-r.length)&&(e[0]=e[0].slice(0,n),e[2]=r.slice(0,n)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(rt,it).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=N[e+" "];return t||(t=RegExp("(^|"+P+")"+e+"("+P+"|$)"))&&N(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=at.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,l){var u,c,p,f,d,h,g=o!==a?"nextSibling":"previousSibling",m=t.parentNode,y=s&&t.nodeName.toLowerCase(),v=!l&&!s;if(m){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?m.firstChild:m.lastChild],a&&v){c=m[b]||(m[b]={}),u=c[e]||[],d=u[0]===T&&u[1],f=u[0]===T&&u[2],p=d&&m.childNodes[d];while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[T,d,f];break}}else if(v&&(u=(t[b]||(t[b]={}))[e])&&u[0]===T)f=u[1];else while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(v&&((p[b]||(p[b]={}))[e]=[T,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=o.pseudos[e]||o.setFilters[e.toLowerCase()]||at.error("unsupported pseudo: "+e);return r[b]?r(t):r.length>1?(n=[e,e,"",t],o.setFilters.hasOwnProperty(e.toLowerCase())?lt(function(e,n){var i,o=r(e,t),a=o.length;while(a--)i=F.call(e,o[a]),e[i]=!(n[i]=o[a])}):function(e){return r(e,0,n)}):r}},pseudos:{not:lt(function(e){var t=[],n=[],r=l(e.replace(z,"$1"));return r[b]?lt(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:lt(function(e){return function(t){return at(e,t).length>0}}),contains:lt(function(e){return function(t){return(t.textContent||t.innerText||a(t)).indexOf(e)>-1}}),lang:lt(function(e){return G.test(e||"")||at.error("unsupported lang: "+e),e=e.replace(rt,it).toLowerCase(),function(t){var n;do if(n=h?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===d},focus:function(e){return e===f.activeElement&&(!f.hasFocus||f.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!o.pseudos.empty(e)},header:function(e){return tt.test(e.nodeName)},input:function(e){return et.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:ht(function(){return[0]}),last:ht(function(e,t){return[t-1]}),eq:ht(function(e,t,n){return[0>n?n+t:n]}),even:ht(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:ht(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:ht(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:ht(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}},o.pseudos.nth=o.pseudos.eq;for(n in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})o.pseudos[n]=ft(n);for(n in{submit:!0,reset:!0})o.pseudos[n]=dt(n);function gt(){}gt.prototype=o.filters=o.pseudos,o.setFilters=new gt;function mt(e,t){var n,r,i,a,s,l,u,c=k[e+" "];if(c)return t?0:c.slice(0);s=e,l=[],u=o.preFilter;while(s){(!n||(r=X.exec(s)))&&(r&&(s=s.slice(r[0].length)||s),l.push(i=[])),n=!1,(r=U.exec(s))&&(n=r.shift(),i.push({value:n,type:r[0].replace(z," ")}),s=s.slice(n.length));for(a in o.filter)!(r=Q[a].exec(s))||u[a]&&!(r=u[a](r))||(n=r.shift(),i.push({value:n,type:a,matches:r}),s=s.slice(n.length));if(!n)break}return t?s.length:s?at.error(e):k(e,l).slice(0)}function yt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function vt(e,t,n){var r=t.dir,o=n&&"parentNode"===r,a=C++;return t.first?function(t,n,i){while(t=t[r])if(1===t.nodeType||o)return e(t,n,i)}:function(t,n,s){var l,u,c,p=T+" "+a;if(s){while(t=t[r])if((1===t.nodeType||o)&&e(t,n,s))return!0}else while(t=t[r])if(1===t.nodeType||o)if(c=t[b]||(t[b]={}),(u=c[r])&&u[0]===p){if((l=u[1])===!0||l===i)return l===!0}else if(u=c[r]=[p],u[1]=e(t,n,s)||i,u[1]===!0)return!0}}function bt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function xt(e,t,n,r,i){var o,a=[],s=0,l=e.length,u=null!=t;for(;l>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),u&&t.push(s));return a}function wt(e,t,n,r,i,o){return r&&!r[b]&&(r=wt(r)),i&&!i[b]&&(i=wt(i,o)),lt(function(o,a,s,l){var u,c,p,f=[],d=[],h=a.length,g=o||Nt(t||"*",s.nodeType?[s]:s,[]),m=!e||!o&&t?g:xt(g,f,e,s,l),y=n?i||(o?e:h||r)?[]:a:m;if(n&&n(m,y,s,l),r){u=xt(y,d),r(u,[],s,l),c=u.length;while(c--)(p=u[c])&&(y[d[c]]=!(m[d[c]]=p))}if(o){if(i||e){if(i){u=[],c=y.length;while(c--)(p=y[c])&&u.push(m[c]=p);i(null,y=[],u,l)}c=y.length;while(c--)(p=y[c])&&(u=i?F.call(o,p):f[c])>-1&&(o[u]=!(a[u]=p))}}else y=xt(y===a?y.splice(h,y.length):y),i?i(null,a,y,l):M.apply(a,y)})}function Tt(e){var t,n,r,i=e.length,a=o.relative[e[0].type],s=a||o.relative[" "],l=a?1:0,c=vt(function(e){return e===t},s,!0),p=vt(function(e){return F.call(t,e)>-1},s,!0),f=[function(e,n,r){return!a&&(r||n!==u)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;i>l;l++)if(n=o.relative[e[l].type])f=[vt(bt(f),n)];else{if(n=o.filter[e[l].type].apply(null,e[l].matches),n[b]){for(r=++l;i>r;r++)if(o.relative[e[r].type])break;return wt(l>1&&bt(f),l>1&&yt(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(z,"$1"),n,r>l&&Tt(e.slice(l,r)),i>r&&Tt(e=e.slice(r)),i>r&&yt(e))}f.push(n)}return bt(f)}function Ct(e,t){var n=0,r=t.length>0,a=e.length>0,s=function(s,l,c,p,d){var h,g,m,y=[],v=0,b="0",x=s&&[],w=null!=d,C=u,N=s||a&&o.find.TAG("*",d&&l.parentNode||l),k=T+=null==C?1:Math.random()||.1;for(w&&(u=l!==f&&l,i=n);null!=(h=N[b]);b++){if(a&&h){g=0;while(m=e[g++])if(m(h,l,c)){p.push(h);break}w&&(T=k,i=++n)}r&&((h=!m&&h)&&v--,s&&x.push(h))}if(v+=b,r&&b!==v){g=0;while(m=t[g++])m(x,y,l,c);if(s){if(v>0)while(b--)x[b]||y[b]||(y[b]=q.call(p));y=xt(y)}M.apply(p,y),w&&!s&&y.length>0&&v+t.length>1&&at.uniqueSort(p)}return w&&(T=k,u=C),x};return r?lt(s):s}l=at.compile=function(e,t){var n,r=[],i=[],o=E[e+" "];if(!o){t||(t=mt(e)),n=t.length;while(n--)o=Tt(t[n]),o[b]?r.push(o):i.push(o);o=E(e,Ct(i,r))}return o};function Nt(e,t,n){var r=0,i=t.length;for(;i>r;r++)at(e,t[r],n);return n}function kt(e,t,n,i){var a,s,u,c,p,f=mt(e);if(!i&&1===f.length){if(s=f[0]=f[0].slice(0),s.length>2&&"ID"===(u=s[0]).type&&r.getById&&9===t.nodeType&&h&&o.relative[s[1].type]){if(t=(o.find.ID(u.matches[0].replace(rt,it),t)||[])[0],!t)return n;e=e.slice(s.shift().value.length)}a=Q.needsContext.test(e)?0:s.length;while(a--){if(u=s[a],o.relative[c=u.type])break;if((p=o.find[c])&&(i=p(u.matches[0].replace(rt,it),V.test(s[0].type)&&t.parentNode||t))){if(s.splice(a,1),e=i.length&&yt(s),!e)return M.apply(n,i),n;break}}}return l(e,f)(i,t,!h,n,V.test(e)),n}r.sortStable=b.split("").sort(A).join("")===b,r.detectDuplicates=S,p(),r.sortDetached=ut(function(e){return 1&e.compareDocumentPosition(f.createElement("div"))}),ut(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||ct("type|href|height|width",function(e,n,r){return r?t:e.getAttribute(n,"type"===n.toLowerCase()?1:2)}),r.attributes&&ut(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||ct("value",function(e,n,r){return r||"input"!==e.nodeName.toLowerCase()?t:e.defaultValue}),ut(function(e){return null==e.getAttribute("disabled")})||ct(B,function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&i.specified?i.value:e[n]===!0?n.toLowerCase():null}),x.find=at,x.expr=at.selectors,x.expr[":"]=x.expr.pseudos,x.unique=at.uniqueSort,x.text=at.getText,x.isXMLDoc=at.isXML,x.contains=at.contains}(e);var O={};function F(e){var t=O[e]={};return x.each(e.match(T)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?O[e]||F(e):x.extend({},e);var n,r,i,o,a,s,l=[],u=!e.once&&[],c=function(t){for(r=e.memory&&t,i=!0,a=s||0,s=0,o=l.length,n=!0;l&&o>a;a++)if(l[a].apply(t[0],t[1])===!1&&e.stopOnFalse){r=!1;break}n=!1,l&&(u?u.length&&c(u.shift()):r?l=[]:p.disable())},p={add:function(){if(l){var t=l.length;(function i(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&p.has(n)||l.push(n):n&&n.length&&"string"!==r&&i(n)})})(arguments),n?o=l.length:r&&(s=t,c(r))}return this},remove:function(){return l&&x.each(arguments,function(e,t){var r;while((r=x.inArray(t,l,r))>-1)l.splice(r,1),n&&(o>=r&&o--,a>=r&&a--)}),this},has:function(e){return e?x.inArray(e,l)>-1:!(!l||!l.length)},empty:function(){return l=[],o=0,this},disable:function(){return l=u=r=t,this},disabled:function(){return!l},lock:function(){return u=t,r||p.disable(),this},locked:function(){return!u},fireWith:function(e,t){return!l||i&&!u||(t=t||[],t=[e,t.slice?t.slice():t],n?u.push(t):c(t)),this},fire:function(){return p.fireWith(this,arguments),this},fired:function(){return!!i}};return p},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var a=o[0],s=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=s&&s.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[a+"With"](this===r?n.promise():this,s?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=g.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),a=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?g.call(arguments):r,n===s?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},s,l,u;if(r>1)for(s=Array(r),l=Array(r),u=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(a(t,u,n)).fail(o.reject).progress(a(t,l,s)):--i;return i||o.resolveWith(u,n),o.promise()}}),x.support=function(t){var n,r,o,s,l,u,c,p,f,d=a.createElement("div");if(d.setAttribute("className","t"),d.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",n=d.getElementsByTagName("*")||[],r=d.getElementsByTagName("a")[0],!r||!r.style||!n.length)return t;s=a.createElement("select"),u=s.appendChild(a.createElement("option")),o=d.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t.getSetAttribute="t"!==d.className,t.leadingWhitespace=3===d.firstChild.nodeType,t.tbody=!d.getElementsByTagName("tbody").length,t.htmlSerialize=!!d.getElementsByTagName("link").length,t.style=/top/.test(r.getAttribute("style")),t.hrefNormalized="/a"===r.getAttribute("href"),t.opacity=/^0.5/.test(r.style.opacity),t.cssFloat=!!r.style.cssFloat,t.checkOn=!!o.value,t.optSelected=u.selected,t.enctype=!!a.createElement("form").enctype,t.html5Clone="<:nav></:nav>"!==a.createElement("nav").cloneNode(!0).outerHTML,t.inlineBlockNeedsLayout=!1,t.shrinkWrapBlocks=!1,t.pixelPosition=!1,t.deleteExpando=!0,t.noCloneEvent=!0,t.reliableMarginRight=!0,t.boxSizingReliable=!0,o.checked=!0,t.noCloneChecked=o.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!u.disabled;try{delete d.test}catch(h){t.deleteExpando=!1}o=a.createElement("input"),o.setAttribute("value",""),t.input=""===o.getAttribute("value"),o.value="t",o.setAttribute("type","radio"),t.radioValue="t"===o.value,o.setAttribute("checked","t"),o.setAttribute("name","t"),l=a.createDocumentFragment(),l.appendChild(o),t.appendChecked=o.checked,t.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,d.attachEvent&&(d.attachEvent("onclick",function(){t.noCloneEvent=!1}),d.cloneNode(!0).click());for(f in{submit:!0,change:!0,focusin:!0})d.setAttribute(c="on"+f,"t"),t[f+"Bubbles"]=c in e||d.attributes[c].expando===!1;d.style.backgroundClip="content-box",d.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===d.style.backgroundClip;for(f in x(t))break;return t.ownLast="0"!==f,x(function(){var n,r,o,s="padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",l=a.getElementsByTagName("body")[0];l&&(n=a.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",l.appendChild(n).appendChild(d),d.innerHTML="<table><tr><td></td><td>t</td></tr></table>",o=d.getElementsByTagName("td"),o[0].style.cssText="padding:0;margin:0;border:0;display:none",p=0===o[0].offsetHeight,o[0].style.display="",o[1].style.display="none",t.reliableHiddenOffsets=p&&0===o[0].offsetHeight,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",x.swap(l,null!=l.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===d.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(d,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(d,null)||{width:"4px"}).width,r=d.appendChild(a.createElement("div")),r.style.cssText=d.style.cssText=s,r.style.marginRight=r.style.width="0",d.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),typeof d.style.zoom!==i&&(d.innerHTML="",d.style.cssText=s+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=3===d.offsetWidth,d.style.display="block",d.innerHTML="<div></div>",d.firstChild.style.width="5px",t.shrinkWrapBlocks=3!==d.offsetWidth,t.inlineBlockNeedsLayout&&(l.style.zoom=1)),l.removeChild(n),n=d=o=r=null)}),n=s=l=u=r=o=null,t
-}({});var B=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;function R(e,n,r,i){if(x.acceptData(e)){var o,a,s=x.expando,l=e.nodeType,u=l?x.cache:e,c=l?e[s]:e[s]&&s;if(c&&u[c]&&(i||u[c].data)||r!==t||"string"!=typeof n)return c||(c=l?e[s]=p.pop()||x.guid++:s),u[c]||(u[c]=l?{}:{toJSON:x.noop}),("object"==typeof n||"function"==typeof n)&&(i?u[c]=x.extend(u[c],n):u[c].data=x.extend(u[c].data,n)),a=u[c],i||(a.data||(a.data={}),a=a.data),r!==t&&(a[x.camelCase(n)]=r),"string"==typeof n?(o=a[n],null==o&&(o=a[x.camelCase(n)])):o=a,o}}function W(e,t,n){if(x.acceptData(e)){var r,i,o=e.nodeType,a=o?x.cache:e,s=o?e[x.expando]:x.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){x.isArray(t)?t=t.concat(x.map(t,x.camelCase)):t in r?t=[t]:(t=x.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;while(i--)delete r[t[i]];if(n?!I(r):!x.isEmptyObject(r))return}(n||(delete a[s].data,I(a[s])))&&(o?x.cleanData([e],!0):x.support.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}x.extend({cache:{},noData:{applet:!0,embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?x.cache[e[x.expando]]:e[x.expando],!!e&&!I(e)},data:function(e,t,n){return R(e,t,n)},removeData:function(e,t){return W(e,t)},_data:function(e,t,n){return R(e,t,n,!0)},_removeData:function(e,t){return W(e,t,!0)},acceptData:function(e){if(e.nodeType&&1!==e.nodeType&&9!==e.nodeType)return!1;var t=e.nodeName&&x.noData[e.nodeName.toLowerCase()];return!t||t!==!0&&e.getAttribute("classid")===t}}),x.fn.extend({data:function(e,n){var r,i,o=null,a=0,s=this[0];if(e===t){if(this.length&&(o=x.data(s),1===s.nodeType&&!x._data(s,"parsedAttrs"))){for(r=s.attributes;r.length>a;a++)i=r[a].name,0===i.indexOf("data-")&&(i=x.camelCase(i.slice(5)),$(s,i,o[i]));x._data(s,"parsedAttrs",!0)}return o}return"object"==typeof e?this.each(function(){x.data(this,e)}):arguments.length>1?this.each(function(){x.data(this,e,n)}):s?$(s,e,x.data(s,e)):null},removeData:function(e){return this.each(function(){x.removeData(this,e)})}});function $(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(P,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:B.test(r)?x.parseJSON(r):r}catch(o){}x.data(e,n,r)}else r=t}return r}function I(e){var t;for(t in e)if(("data"!==t||!x.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}x.extend({queue:function(e,n,r){var i;return e?(n=(n||"fx")+"queue",i=x._data(e,n),r&&(!i||x.isArray(r)?i=x._data(e,n,x.makeArray(r)):i.push(r)),i||[]):t},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),a=function(){x.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return x._data(e,n)||x._data(e,n,{empty:x.Callbacks("once memory").add(function(){x._removeData(e,t+"queue"),x._removeData(e,n)})})}}),x.fn.extend({queue:function(e,n){var r=2;return"string"!=typeof e&&(n=e,e="fx",r--),r>arguments.length?x.queue(this[0],e):n===t?this:this.each(function(){var t=x.queue(this,e,n);x._queueHooks(this,e),"fx"===e&&"inprogress"!==t[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,n){var r,i=1,o=x.Deferred(),a=this,s=this.length,l=function(){--i||o.resolveWith(a,[a])};"string"!=typeof e&&(n=e,e=t),e=e||"fx";while(s--)r=x._data(a[s],e+"queueHooks"),r&&r.empty&&(i++,r.empty.add(l));return l(),o.promise(n)}});var z,X,U=/[\t\r\n\f]/g,V=/\r/g,Y=/^(?:input|select|textarea|button|object)$/i,J=/^(?:a|area)$/i,G=/^(?:checked|selected)$/i,Q=x.support.getSetAttribute,K=x.support.input;x.fn.extend({attr:function(e,t){return x.access(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return e=x.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,o,a=0,s=this.length,l="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,a=0,s=this.length,l=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var t,r=0,o=x(this),a=e.match(T)||[];while(t=a[r++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else(n===i||"boolean"===n)&&(this.className&&x._data(this,"__className__",this.className),this.className=this.className||e===!1?"":x._data(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(U," ").indexOf(t)>=0)return!0;return!1},val:function(e){var n,r,i,o=this[0];{if(arguments.length)return i=x.isFunction(e),this.each(function(n){var o;1===this.nodeType&&(o=i?e.call(this,n,x(this).val()):e,null==o?o="":"number"==typeof o?o+="":x.isArray(o)&&(o=x.map(o,function(e){return null==e?"":e+""})),r=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],r&&"set"in r&&r.set(this,o,"value")!==t||(this.value=o))});if(o)return r=x.valHooks[o.type]||x.valHooks[o.nodeName.toLowerCase()],r&&"get"in r&&(n=r.get(o,"value"))!==t?n:(n=o.value,"string"==typeof n?n.replace(V,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=x.find.attr(e,"value");return null!=t?t:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,l=0>i?s:o?i:0;for(;s>l;l++)if(n=r[l],!(!n.selected&&l!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;a.push(t)}return a},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),a=i.length;while(a--)r=i[a],(r.selected=x.inArray(x(r).val(),o)>=0)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,n,r){var o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===i?x.prop(e,n,r):(1===s&&x.isXMLDoc(e)||(n=n.toLowerCase(),o=x.attrHooks[n]||(x.expr.match.bool.test(n)?X:z)),r===t?o&&"get"in o&&null!==(a=o.get(e,n))?a:(a=x.find.attr(e,n),null==a?t:a):null!==r?o&&"set"in o&&(a=o.set(e,r,n))!==t?a:(e.setAttribute(n,r+""),r):(x.removeAttr(e,n),t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(T);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.bool.test(n)?K&&Q||!G.test(n)?e[r]=!1:e[x.camelCase("default-"+n)]=e[r]=!1:x.attr(e,n,""),e.removeAttribute(Q?n:r)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,n,r){var i,o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return a=1!==s||!x.isXMLDoc(e),a&&(n=x.propFix[n]||n,o=x.propHooks[n]),r!==t?o&&"set"in o&&(i=o.set(e,r,n))!==t?i:e[n]=r:o&&"get"in o&&null!==(i=o.get(e,n))?i:e[n]},propHooks:{tabIndex:{get:function(e){var t=x.find.attr(e,"tabindex");return t?parseInt(t,10):Y.test(e.nodeName)||J.test(e.nodeName)&&e.href?0:-1}}}}),X={set:function(e,t,n){return t===!1?x.removeAttr(e,n):K&&Q||!G.test(n)?e.setAttribute(!Q&&x.propFix[n]||n,n):e[x.camelCase("default-"+n)]=e[n]=!0,n}},x.each(x.expr.match.bool.source.match(/\w+/g),function(e,n){var r=x.expr.attrHandle[n]||x.find.attr;x.expr.attrHandle[n]=K&&Q||!G.test(n)?function(e,n,i){var o=x.expr.attrHandle[n],a=i?t:(x.expr.attrHandle[n]=t)!=r(e,n,i)?n.toLowerCase():null;return x.expr.attrHandle[n]=o,a}:function(e,n,r){return r?t:e[x.camelCase("default-"+n)]?n.toLowerCase():null}}),K&&Q||(x.attrHooks.value={set:function(e,n,r){return x.nodeName(e,"input")?(e.defaultValue=n,t):z&&z.set(e,n,r)}}),Q||(z={set:function(e,n,r){var i=e.getAttributeNode(r);return i||e.setAttributeNode(i=e.ownerDocument.createAttribute(r)),i.value=n+="","value"===r||n===e.getAttribute(r)?n:t}},x.expr.attrHandle.id=x.expr.attrHandle.name=x.expr.attrHandle.coords=function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&""!==i.value?i.value:null},x.valHooks.button={get:function(e,n){var r=e.getAttributeNode(n);return r&&r.specified?r.value:t},set:z.set},x.attrHooks.contenteditable={set:function(e,t,n){z.set(e,""===t?!1:t,n)}},x.each(["width","height"],function(e,n){x.attrHooks[n]={set:function(e,r){return""===r?(e.setAttribute(n,"auto"),r):t}}})),x.support.hrefNormalized||x.each(["href","src"],function(e,t){x.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),x.support.style||(x.attrHooks.style={get:function(e){return e.style.cssText||t},set:function(e,t){return e.style.cssText=t+""}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.support.enctype||(x.propFix.enctype="encoding"),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,n){return x.isArray(n)?e.checked=x.inArray(x(e).val(),n)>=0:t}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Z=/^(?:input|select|textarea)$/i,et=/^key/,tt=/^(?:mouse|contextmenu)|click/,nt=/^(?:focusinfocus|focusoutblur)$/,rt=/^([^.]*)(?:\.(.+)|)$/;function it(){return!0}function ot(){return!1}function at(){try{return a.activeElement}catch(e){}}x.event={global:{},add:function(e,n,r,o,a){var s,l,u,c,p,f,d,h,g,m,y,v=x._data(e);if(v){r.handler&&(c=r,r=c.handler,a=c.selector),r.guid||(r.guid=x.guid++),(l=v.events)||(l=v.events={}),(f=v.handle)||(f=v.handle=function(e){return typeof x===i||e&&x.event.triggered===e.type?t:x.event.dispatch.apply(f.elem,arguments)},f.elem=e),n=(n||"").match(T)||[""],u=n.length;while(u--)s=rt.exec(n[u])||[],g=y=s[1],m=(s[2]||"").split(".").sort(),g&&(p=x.event.special[g]||{},g=(a?p.delegateType:p.bindType)||g,p=x.event.special[g]||{},d=x.extend({type:g,origType:y,data:o,handler:r,guid:r.guid,selector:a,needsContext:a&&x.expr.match.needsContext.test(a),namespace:m.join(".")},c),(h=l[g])||(h=l[g]=[],h.delegateCount=0,p.setup&&p.setup.call(e,o,m,f)!==!1||(e.addEventListener?e.addEventListener(g,f,!1):e.attachEvent&&e.attachEvent("on"+g,f))),p.add&&(p.add.call(e,d),d.handler.guid||(d.handler.guid=r.guid)),a?h.splice(h.delegateCount++,0,d):h.push(d),x.event.global[g]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,l,u,c,p,f,d,h,g,m=x.hasData(e)&&x._data(e);if(m&&(c=m.events)){t=(t||"").match(T)||[""],u=t.length;while(u--)if(s=rt.exec(t[u])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){p=x.event.special[d]||{},d=(r?p.delegateType:p.bindType)||d,f=c[d]||[],s=s[2]&&RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),l=o=f.length;while(o--)a=f[o],!i&&g!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,p.remove&&p.remove.call(e,a));l&&!f.length&&(p.teardown&&p.teardown.call(e,h,m.handle)!==!1||x.removeEvent(e,d,m.handle),delete c[d])}else for(d in c)x.event.remove(e,d+t[u],n,r,!0);x.isEmptyObject(c)&&(delete m.handle,x._removeData(e,"events"))}},trigger:function(n,r,i,o){var s,l,u,c,p,f,d,h=[i||a],g=v.call(n,"type")?n.type:n,m=v.call(n,"namespace")?n.namespace.split("."):[];if(u=f=i=i||a,3!==i.nodeType&&8!==i.nodeType&&!nt.test(g+x.event.triggered)&&(g.indexOf(".")>=0&&(m=g.split("."),g=m.shift(),m.sort()),l=0>g.indexOf(":")&&"on"+g,n=n[x.expando]?n:new x.Event(g,"object"==typeof n&&n),n.isTrigger=o?2:3,n.namespace=m.join("."),n.namespace_re=n.namespace?RegExp("(^|\\.)"+m.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,n.result=t,n.target||(n.target=i),r=null==r?[n]:x.makeArray(r,[n]),p=x.event.special[g]||{},o||!p.trigger||p.trigger.apply(i,r)!==!1)){if(!o&&!p.noBubble&&!x.isWindow(i)){for(c=p.delegateType||g,nt.test(c+g)||(u=u.parentNode);u;u=u.parentNode)h.push(u),f=u;f===(i.ownerDocument||a)&&h.push(f.defaultView||f.parentWindow||e)}d=0;while((u=h[d++])&&!n.isPropagationStopped())n.type=d>1?c:p.bindType||g,s=(x._data(u,"events")||{})[n.type]&&x._data(u,"handle"),s&&s.apply(u,r),s=l&&u[l],s&&x.acceptData(u)&&s.apply&&s.apply(u,r)===!1&&n.preventDefault();if(n.type=g,!o&&!n.isDefaultPrevented()&&(!p._default||p._default.apply(h.pop(),r)===!1)&&x.acceptData(i)&&l&&i[g]&&!x.isWindow(i)){f=i[l],f&&(i[l]=null),x.event.triggered=g;try{i[g]()}catch(y){}x.event.triggered=t,f&&(i[l]=f)}return n.result}},dispatch:function(e){e=x.event.fix(e);var n,r,i,o,a,s=[],l=g.call(arguments),u=(x._data(this,"events")||{})[e.type]||[],c=x.event.special[e.type]||{};if(l[0]=e,e.delegateTarget=this,!c.preDispatch||c.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this,e,u),n=0;while((o=s[n++])&&!e.isPropagationStopped()){e.currentTarget=o.elem,a=0;while((i=o.handlers[a++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(i.namespace))&&(e.handleObj=i,e.data=i.data,r=((x.event.special[i.origType]||{}).handle||i.handler).apply(o.elem,l),r!==t&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,e),e.result}},handlers:function(e,n){var r,i,o,a,s=[],l=n.delegateCount,u=e.target;if(l&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!=this;u=u.parentNode||this)if(1===u.nodeType&&(u.disabled!==!0||"click"!==e.type)){for(o=[],a=0;l>a;a++)i=n[a],r=i.selector+" ",o[r]===t&&(o[r]=i.needsContext?x(r,this).index(u)>=0:x.find(r,this,null,[u]).length),o[r]&&o.push(i);o.length&&s.push({elem:u,handlers:o})}return n.length>l&&s.push({elem:this,handlers:n.slice(l)}),s},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,o=e,s=this.fixHooks[i];s||(this.fixHooks[i]=s=tt.test(i)?this.mouseHooks:et.test(i)?this.keyHooks:{}),r=s.props?this.props.concat(s.props):this.props,e=new x.Event(o),t=r.length;while(t--)n=r[t],e[n]=o[n];return e.target||(e.target=o.srcElement||a),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,s.filter?s.filter(e,o):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,n){var r,i,o,s=n.button,l=n.fromElement;return null==e.pageX&&null!=n.clientX&&(i=e.target.ownerDocument||a,o=i.documentElement,r=i.body,e.pageX=n.clientX+(o&&o.scrollLeft||r&&r.scrollLeft||0)-(o&&o.clientLeft||r&&r.clientLeft||0),e.pageY=n.clientY+(o&&o.scrollTop||r&&r.scrollTop||0)-(o&&o.clientTop||r&&r.clientTop||0)),!e.relatedTarget&&l&&(e.relatedTarget=l===e.target?n.toElement:l),e.which||s===t||(e.which=1&s?1:2&s?3:4&s?2:0),e}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==at()&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===at()&&this.blur?(this.blur(),!1):t},delegateType:"focusout"},click:{trigger:function(){return x.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):t},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==t&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=a.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===i&&(e[r]=null),e.detachEvent(r,n))},x.Event=function(e,n){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.returnValue===!1||e.getPreventDefault&&e.getPreventDefault()?it:ot):this.type=e,n&&x.extend(this,n),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,t):new x.Event(e,n)},x.Event.prototype={isDefaultPrevented:ot,isPropagationStopped:ot,isImmediatePropagationStopped:ot,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=it,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=it,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=it,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.submitBubbles||(x.event.special.submit={setup:function(){return x.nodeName(this,"form")?!1:(x.event.add(this,"click._submit keypress._submit",function(e){var n=e.target,r=x.nodeName(n,"input")||x.nodeName(n,"button")?n.form:t;r&&!x._data(r,"submitBubbles")&&(x.event.add(r,"submit._submit",function(e){e._submit_bubble=!0}),x._data(r,"submitBubbles",!0))}),t)},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&x.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return x.nodeName(this,"form")?!1:(x.event.remove(this,"._submit"),t)}}),x.support.changeBubbles||(x.event.special.change={setup:function(){return Z.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(x.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),x.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),x.event.simulate("change",this,e,!0)})),!1):(x.event.add(this,"beforeactivate._change",function(e){var t=e.target;Z.test(t.nodeName)&&!x._data(t,"changeBubbles")&&(x.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||x.event.simulate("change",this.parentNode,e,!0)}),x._data(t,"changeBubbles",!0))}),t)},handle:function(e){var n=e.target;return this!==n||e.isSimulated||e.isTrigger||"radio"!==n.type&&"checkbox"!==n.type?e.handleObj.handler.apply(this,arguments):t},teardown:function(){return x.event.remove(this,"._change"),!Z.test(this.nodeName)}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0===n++&&a.addEventListener(e,r,!0)},teardown:function(){0===--n&&a.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,n,r,i,o){var a,s;if("object"==typeof e){"string"!=typeof n&&(r=r||n,n=t);for(a in e)this.on(a,n,r,e[a],o);return this}if(null==r&&null==i?(i=n,r=n=t):null==i&&("string"==typeof n?(i=r,r=t):(i=r,r=n,n=t)),i===!1)i=ot;else if(!i)return this;return 1===o&&(s=i,i=function(e){return x().off(e),s.apply(this,arguments)},i.guid=s.guid||(s.guid=x.guid++)),this.each(function(){x.event.add(this,e,i,r,n)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,n,r){var i,o;if(e&&e.preventDefault&&e.handleObj)return i=e.handleObj,x(e.delegateTarget).off(i.namespace?i.origType+"."+i.namespace:i.origType,i.selector,i.handler),this;if("object"==typeof e){for(o in e)this.off(o,n,e[o]);return this}return(n===!1||"function"==typeof n)&&(r=n,n=t),r===!1&&(r=ot),this.each(function(){x.event.remove(this,e,r,n)})},trigger:function(e,t){return this.each(function(){x.event.trigger(e,t,this)})},triggerHandler:function(e,n){var r=this[0];return r?x.event.trigger(e,n,r,!0):t}});var st=/^.[^:#\[\.,]*$/,lt=/^(?:parents|prev(?:Until|All))/,ut=x.expr.match.needsContext,ct={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(x(e).filter(function(){for(t=0;i>t;t++)if(x.contains(r[t],this))return!0}));for(t=0;i>t;t++)x.find(e,r[t],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},has:function(e){var t,n=x(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(x.contains(this,n[t]))return!0})},not:function(e){return this.pushStack(ft(this,e||[],!0))},filter:function(e){return this.pushStack(ft(this,e||[],!1))},is:function(e){return!!ft(this,"string"==typeof e&&ut.test(e)?x(e):e||[],!1).length},closest:function(e,t){var n,r=0,i=this.length,o=[],a=ut.test(e)||"string"!=typeof e?x(e,t||this.context):0;for(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(a?a.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?x.inArray(this[0],x(e)):x.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function pt(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return pt(e,"nextSibling")},prev:function(e){return pt(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling")},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return x.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(ct[e]||(i=x.unique(i)),lt.test(e)&&(i=i.reverse())),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,n,r){var i=[],o=e[n];while(o&&9!==o.nodeType&&(r===t||1!==o.nodeType||!x(o).is(r)))1===o.nodeType&&i.push(o),o=o[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function ft(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(st.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return x.inArray(e,t)>=0!==n})}function dt(e){var t=ht.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}var ht="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",gt=/ jQuery\d+="(?:null|\d+)"/g,mt=RegExp("<(?:"+ht+")[\\s/>]","i"),yt=/^\s+/,vt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bt=/<([\w:]+)/,xt=/<tbody/i,wt=/<|&#?\w+;/,Tt=/<(?:script|style|link)/i,Ct=/^(?:checkbox|radio)$/i,Nt=/checked\s*(?:[^=]|=\s*.checked.)/i,kt=/^$|\/(?:java|ecma)script/i,Et=/^true\/(.*)/,St=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,At={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:x.support.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]},jt=dt(a),Dt=jt.appendChild(a.createElement("div"));At.optgroup=At.option,At.tbody=At.tfoot=At.colgroup=At.caption=At.thead,At.th=At.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===t?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||a).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(Ft(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&_t(Ft(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++){1===e.nodeType&&x.cleanData(Ft(e,!1));while(e.firstChild)e.removeChild(e.firstChild);e.options&&x.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return 1===n.nodeType?n.innerHTML.replace(gt,""):t;if(!("string"!=typeof e||Tt.test(e)||!x.support.htmlSerialize&&mt.test(e)||!x.support.leadingWhitespace&&yt.test(e)||At[(bt.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(vt,"<$1></$2>");try{for(;i>r;r++)n=this[r]||{},1===n.nodeType&&(x.cleanData(Ft(n,!1)),n.innerHTML=e);n=0}catch(o){}}n&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(r&&r.parentNode!==i&&(r=this.nextSibling),x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=d.apply([],e);var r,i,o,a,s,l,u=0,c=this.length,p=this,f=c-1,h=e[0],g=x.isFunction(h);if(g||!(1>=c||"string"!=typeof h||x.support.checkClone)&&Nt.test(h))return this.each(function(r){var i=p.eq(r);g&&(e[0]=h.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(l=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),r=l.firstChild,1===l.childNodes.length&&(l=r),r)){for(a=x.map(Ft(l,"script"),Ht),o=a.length;c>u;u++)i=l,u!==f&&(i=x.clone(i,!0,!0),o&&x.merge(a,Ft(i,"script"))),t.call(this[u],i,u);if(o)for(s=a[a.length-1].ownerDocument,x.map(a,qt),u=0;o>u;u++)i=a[u],kt.test(i.type||"")&&!x._data(i,"globalEval")&&x.contains(s,i)&&(i.src?x._evalUrl(i.src):x.globalEval((i.text||i.textContent||i.innerHTML||"").replace(St,"")));l=r=null}return this}});function Lt(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function Ht(e){return e.type=(null!==x.find.attr(e,"type"))+"/"+e.type,e}function qt(e){var t=Et.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function _t(e,t){var n,r=0;for(;null!=(n=e[r]);r++)x._data(n,"globalEval",!t||x._data(t[r],"globalEval"))}function Mt(e,t){if(1===t.nodeType&&x.hasData(e)){var n,r,i,o=x._data(e),a=x._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)x.event.add(t,n,s[n][r])}a.data&&(a.data=x.extend({},a.data))}}function Ot(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!x.support.noCloneEvent&&t[x.expando]){i=x._data(t);for(r in i.events)x.removeEvent(t,r,i.handle);t.removeAttribute(x.expando)}"script"===n&&t.text!==e.text?(Ht(t).text=e.text,qt(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),x.support.html5Clone&&e.innerHTML&&!x.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Ct.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){x.fn[e]=function(e){var n,r=0,i=[],o=x(e),a=o.length-1;for(;a>=r;r++)n=r===a?this:this.clone(!0),x(o[r])[t](n),h.apply(i,n.get());return this.pushStack(i)}});function Ft(e,n){var r,o,a=0,s=typeof e.getElementsByTagName!==i?e.getElementsByTagName(n||"*"):typeof e.querySelectorAll!==i?e.querySelectorAll(n||"*"):t;if(!s)for(s=[],r=e.childNodes||e;null!=(o=r[a]);a++)!n||x.nodeName(o,n)?s.push(o):x.merge(s,Ft(o,n));return n===t||n&&x.nodeName(e,n)?x.merge([e],s):s}function Bt(e){Ct.test(e.type)&&(e.defaultChecked=e.checked)}x.extend({clone:function(e,t,n){var r,i,o,a,s,l=x.contains(e.ownerDocument,e);if(x.support.html5Clone||x.isXMLDoc(e)||!mt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Dt.innerHTML=e.outerHTML,Dt.removeChild(o=Dt.firstChild)),!(x.support.noCloneEvent&&x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(r=Ft(o),s=Ft(e),a=0;null!=(i=s[a]);++a)r[a]&&Ot(i,r[a]);if(t)if(n)for(s=s||Ft(e),r=r||Ft(o),a=0;null!=(i=s[a]);a++)Mt(i,r[a]);else Mt(e,o);return r=Ft(o,"script"),r.length>0&&_t(r,!l&&Ft(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){var i,o,a,s,l,u,c,p=e.length,f=dt(t),d=[],h=0;for(;p>h;h++)if(o=e[h],o||0===o)if("object"===x.type(o))x.merge(d,o.nodeType?[o]:o);else if(wt.test(o)){s=s||f.appendChild(t.createElement("div")),l=(bt.exec(o)||["",""])[1].toLowerCase(),c=At[l]||At._default,s.innerHTML=c[1]+o.replace(vt,"<$1></$2>")+c[2],i=c[0];while(i--)s=s.lastChild;if(!x.support.leadingWhitespace&&yt.test(o)&&d.push(t.createTextNode(yt.exec(o)[0])),!x.support.tbody){o="table"!==l||xt.test(o)?"<table>"!==c[1]||xt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;while(i--)x.nodeName(u=o.childNodes[i],"tbody")&&!u.childNodes.length&&o.removeChild(u)}x.merge(d,s.childNodes),s.textContent="";while(s.firstChild)s.removeChild(s.firstChild);s=f.lastChild}else d.push(t.createTextNode(o));s&&f.removeChild(s),x.support.appendChecked||x.grep(Ft(d,"input"),Bt),h=0;while(o=d[h++])if((!r||-1===x.inArray(o,r))&&(a=x.contains(o.ownerDocument,o),s=Ft(f.appendChild(o),"script"),a&&_t(s),n)){i=0;while(o=s[i++])kt.test(o.type||"")&&n.push(o)}return s=null,f},cleanData:function(e,t){var n,r,o,a,s=0,l=x.expando,u=x.cache,c=x.support.deleteExpando,f=x.event.special;for(;null!=(n=e[s]);s++)if((t||x.acceptData(n))&&(o=n[l],a=o&&u[o])){if(a.events)for(r in a.events)f[r]?x.event.remove(n,r):x.removeEvent(n,r,a.handle);
-u[o]&&(delete u[o],c?delete n[l]:typeof n.removeAttribute!==i?n.removeAttribute(l):n[l]=null,p.push(o))}},_evalUrl:function(e){return x.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})}}),x.fn.extend({wrapAll:function(e){if(x.isFunction(e))return this.each(function(t){x(this).wrapAll(e.call(this,t))});if(this[0]){var t=x(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&1===e.firstChild.nodeType)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return x.isFunction(e)?this.each(function(t){x(this).wrapInner(e.call(this,t))}):this.each(function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=x.isFunction(e);return this.each(function(n){x(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){x.nodeName(this,"body")||x(this).replaceWith(this.childNodes)}).end()}});var Pt,Rt,Wt,$t=/alpha\([^)]*\)/i,It=/opacity\s*=\s*([^)]*)/,zt=/^(top|right|bottom|left)$/,Xt=/^(none|table(?!-c[ea]).+)/,Ut=/^margin/,Vt=RegExp("^("+w+")(.*)$","i"),Yt=RegExp("^("+w+")(?!px)[a-z%]+$","i"),Jt=RegExp("^([+-])=("+w+")","i"),Gt={BODY:"block"},Qt={position:"absolute",visibility:"hidden",display:"block"},Kt={letterSpacing:0,fontWeight:400},Zt=["Top","Right","Bottom","Left"],en=["Webkit","O","Moz","ms"];function tn(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=en.length;while(i--)if(t=en[i]+n,t in e)return t;return r}function nn(e,t){return e=t||e,"none"===x.css(e,"display")||!x.contains(e.ownerDocument,e)}function rn(e,t){var n,r,i,o=[],a=0,s=e.length;for(;s>a;a++)r=e[a],r.style&&(o[a]=x._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&nn(r)&&(o[a]=x._data(r,"olddisplay",ln(r.nodeName)))):o[a]||(i=nn(r),(n&&"none"!==n||!i)&&x._data(r,"olddisplay",i?n:x.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}x.fn.extend({css:function(e,n){return x.access(this,function(e,n,r){var i,o,a={},s=0;if(x.isArray(n)){for(o=Rt(e),i=n.length;i>s;s++)a[n[s]]=x.css(e,n[s],!1,o);return a}return r!==t?x.style(e,n,r):x.css(e,n)},e,n,arguments.length>1)},show:function(){return rn(this,!0)},hide:function(){return rn(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){nn(this)?x(this).show():x(this).hide()})}}),x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Wt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":x.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var o,a,s,l=x.camelCase(n),u=e.style;if(n=x.cssProps[l]||(x.cssProps[l]=tn(u,l)),s=x.cssHooks[n]||x.cssHooks[l],r===t)return s&&"get"in s&&(o=s.get(e,!1,i))!==t?o:u[n];if(a=typeof r,"string"===a&&(o=Jt.exec(r))&&(r=(o[1]+1)*o[2]+parseFloat(x.css(e,n)),a="number"),!(null==r||"number"===a&&isNaN(r)||("number"!==a||x.cssNumber[l]||(r+="px"),x.support.clearCloneStyle||""!==r||0!==n.indexOf("background")||(u[n]="inherit"),s&&"set"in s&&(r=s.set(e,r,i))===t)))try{u[n]=r}catch(c){}}},css:function(e,n,r,i){var o,a,s,l=x.camelCase(n);return n=x.cssProps[l]||(x.cssProps[l]=tn(e.style,l)),s=x.cssHooks[n]||x.cssHooks[l],s&&"get"in s&&(a=s.get(e,!0,r)),a===t&&(a=Wt(e,n,i)),"normal"===a&&n in Kt&&(a=Kt[n]),""===r||r?(o=parseFloat(a),r===!0||x.isNumeric(o)?o||0:a):a}}),e.getComputedStyle?(Rt=function(t){return e.getComputedStyle(t,null)},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s.getPropertyValue(n)||s[n]:t,u=e.style;return s&&(""!==l||x.contains(e.ownerDocument,e)||(l=x.style(e,n)),Yt.test(l)&&Ut.test(n)&&(i=u.width,o=u.minWidth,a=u.maxWidth,u.minWidth=u.maxWidth=u.width=l,l=s.width,u.width=i,u.minWidth=o,u.maxWidth=a)),l}):a.documentElement.currentStyle&&(Rt=function(e){return e.currentStyle},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s[n]:t,u=e.style;return null==l&&u&&u[n]&&(l=u[n]),Yt.test(l)&&!zt.test(n)&&(i=u.left,o=e.runtimeStyle,a=o&&o.left,a&&(o.left=e.currentStyle.left),u.left="fontSize"===n?"1em":l,l=u.pixelLeft+"px",u.left=i,a&&(o.left=a)),""===l?"auto":l});function on(e,t,n){var r=Vt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function an(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;for(;4>o;o+=2)"margin"===n&&(a+=x.css(e,n+Zt[o],!0,i)),r?("content"===n&&(a-=x.css(e,"padding"+Zt[o],!0,i)),"margin"!==n&&(a-=x.css(e,"border"+Zt[o]+"Width",!0,i))):(a+=x.css(e,"padding"+Zt[o],!0,i),"padding"!==n&&(a+=x.css(e,"border"+Zt[o]+"Width",!0,i)));return a}function sn(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=Rt(e),a=x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=Wt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Yt.test(i))return i;r=a&&(x.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+an(e,t,n||(a?"border":"content"),r,o)+"px"}function ln(e){var t=a,n=Gt[e];return n||(n=un(e,t),"none"!==n&&n||(Pt=(Pt||x("<iframe frameborder='0' width='0' height='0'/>").css("cssText","display:block !important")).appendTo(t.documentElement),t=(Pt[0].contentWindow||Pt[0].contentDocument).document,t.write("<!doctype html><html><body>"),t.close(),n=un(e,t),Pt.detach()),Gt[e]=n),n}function un(e,t){var n=x(t.createElement(e)).appendTo(t.body),r=x.css(n[0],"display");return n.remove(),r}x.each(["height","width"],function(e,n){x.cssHooks[n]={get:function(e,r,i){return r?0===e.offsetWidth&&Xt.test(x.css(e,"display"))?x.swap(e,Qt,function(){return sn(e,n,i)}):sn(e,n,i):t},set:function(e,t,r){var i=r&&Rt(e);return on(e,t,r?an(e,n,r,x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,i),i):0)}}}),x.support.opacity||(x.cssHooks.opacity={get:function(e,t){return It.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=x.isNumeric(t)?"alpha(opacity="+100*t+")":"",o=r&&r.filter||n.filter||"";n.zoom=1,(t>=1||""===t)&&""===x.trim(o.replace($t,""))&&n.removeAttribute&&(n.removeAttribute("filter"),""===t||r&&!r.filter)||(n.filter=$t.test(o)?o.replace($t,i):o+" "+i)}}),x(function(){x.support.reliableMarginRight||(x.cssHooks.marginRight={get:function(e,n){return n?x.swap(e,{display:"inline-block"},Wt,[e,"marginRight"]):t}}),!x.support.pixelPosition&&x.fn.position&&x.each(["top","left"],function(e,n){x.cssHooks[n]={get:function(e,r){return r?(r=Wt(e,n),Yt.test(r)?x(e).position()[n]+"px":r):t}}})}),x.expr&&x.expr.filters&&(x.expr.filters.hidden=function(e){return 0>=e.offsetWidth&&0>=e.offsetHeight||!x.support.reliableHiddenOffsets&&"none"===(e.style&&e.style.display||x.css(e,"display"))},x.expr.filters.visible=function(e){return!x.expr.filters.hidden(e)}),x.each({margin:"",padding:"",border:"Width"},function(e,t){x.cssHooks[e+t]={expand:function(n){var r=0,i={},o="string"==typeof n?n.split(" "):[n];for(;4>r;r++)i[e+Zt[r]+t]=o[r]||o[r-2]||o[0];return i}},Ut.test(e)||(x.cssHooks[e+t].set=on)});var cn=/%20/g,pn=/\[\]$/,fn=/\r?\n/g,dn=/^(?:submit|button|image|reset|file)$/i,hn=/^(?:input|select|textarea|keygen)/i;x.fn.extend({serialize:function(){return x.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=x.prop(this,"elements");return e?x.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!x(this).is(":disabled")&&hn.test(this.nodeName)&&!dn.test(e)&&(this.checked||!Ct.test(e))}).map(function(e,t){var n=x(this).val();return null==n?null:x.isArray(n)?x.map(n,function(e){return{name:t.name,value:e.replace(fn,"\r\n")}}):{name:t.name,value:n.replace(fn,"\r\n")}}).get()}}),x.param=function(e,n){var r,i=[],o=function(e,t){t=x.isFunction(t)?t():null==t?"":t,i[i.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(n===t&&(n=x.ajaxSettings&&x.ajaxSettings.traditional),x.isArray(e)||e.jquery&&!x.isPlainObject(e))x.each(e,function(){o(this.name,this.value)});else for(r in e)gn(r,e[r],n,o);return i.join("&").replace(cn,"+")};function gn(e,t,n,r){var i;if(x.isArray(t))x.each(t,function(t,i){n||pn.test(e)?r(e,i):gn(e+"["+("object"==typeof i?t:"")+"]",i,n,r)});else if(n||"object"!==x.type(t))r(e,t);else for(i in t)gn(e+"["+i+"]",t[i],n,r)}x.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){x.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),x.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}});var mn,yn,vn=x.now(),bn=/\?/,xn=/#.*$/,wn=/([?&])_=[^&]*/,Tn=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Cn=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Nn=/^(?:GET|HEAD)$/,kn=/^\/\//,En=/^([\w.+-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,Sn=x.fn.load,An={},jn={},Dn="*/".concat("*");try{yn=o.href}catch(Ln){yn=a.createElement("a"),yn.href="",yn=yn.href}mn=En.exec(yn.toLowerCase())||[];function Hn(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(T)||[];if(x.isFunction(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function qn(e,n,r,i){var o={},a=e===jn;function s(l){var u;return o[l]=!0,x.each(e[l]||[],function(e,l){var c=l(n,r,i);return"string"!=typeof c||a||o[c]?a?!(u=c):t:(n.dataTypes.unshift(c),s(c),!1)}),u}return s(n.dataTypes[0])||!o["*"]&&s("*")}function _n(e,n){var r,i,o=x.ajaxSettings.flatOptions||{};for(i in n)n[i]!==t&&((o[i]?e:r||(r={}))[i]=n[i]);return r&&x.extend(!0,e,r),e}x.fn.load=function(e,n,r){if("string"!=typeof e&&Sn)return Sn.apply(this,arguments);var i,o,a,s=this,l=e.indexOf(" ");return l>=0&&(i=e.slice(l,e.length),e=e.slice(0,l)),x.isFunction(n)?(r=n,n=t):n&&"object"==typeof n&&(a="POST"),s.length>0&&x.ajax({url:e,type:a,dataType:"html",data:n}).done(function(e){o=arguments,s.html(i?x("<div>").append(x.parseHTML(e)).find(i):e)}).complete(r&&function(e,t){s.each(r,o||[e.responseText,t,e])}),this},x.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){x.fn[t]=function(e){return this.on(t,e)}}),x.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:yn,type:"GET",isLocal:Cn.test(mn[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Dn,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":x.parseJSON,"text xml":x.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?_n(_n(e,x.ajaxSettings),t):_n(x.ajaxSettings,e)},ajaxPrefilter:Hn(An),ajaxTransport:Hn(jn),ajax:function(e,n){"object"==typeof e&&(n=e,e=t),n=n||{};var r,i,o,a,s,l,u,c,p=x.ajaxSetup({},n),f=p.context||p,d=p.context&&(f.nodeType||f.jquery)?x(f):x.event,h=x.Deferred(),g=x.Callbacks("once memory"),m=p.statusCode||{},y={},v={},b=0,w="canceled",C={readyState:0,getResponseHeader:function(e){var t;if(2===b){if(!c){c={};while(t=Tn.exec(a))c[t[1].toLowerCase()]=t[2]}t=c[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===b?a:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return b||(e=v[n]=v[n]||e,y[e]=t),this},overrideMimeType:function(e){return b||(p.mimeType=e),this},statusCode:function(e){var t;if(e)if(2>b)for(t in e)m[t]=[m[t],e[t]];else C.always(e[C.status]);return this},abort:function(e){var t=e||w;return u&&u.abort(t),k(0,t),this}};if(h.promise(C).complete=g.add,C.success=C.done,C.error=C.fail,p.url=((e||p.url||yn)+"").replace(xn,"").replace(kn,mn[1]+"//"),p.type=n.method||n.type||p.method||p.type,p.dataTypes=x.trim(p.dataType||"*").toLowerCase().match(T)||[""],null==p.crossDomain&&(r=En.exec(p.url.toLowerCase()),p.crossDomain=!(!r||r[1]===mn[1]&&r[2]===mn[2]&&(r[3]||("http:"===r[1]?"80":"443"))===(mn[3]||("http:"===mn[1]?"80":"443")))),p.data&&p.processData&&"string"!=typeof p.data&&(p.data=x.param(p.data,p.traditional)),qn(An,p,n,C),2===b)return C;l=p.global,l&&0===x.active++&&x.event.trigger("ajaxStart"),p.type=p.type.toUpperCase(),p.hasContent=!Nn.test(p.type),o=p.url,p.hasContent||(p.data&&(o=p.url+=(bn.test(o)?"&":"?")+p.data,delete p.data),p.cache===!1&&(p.url=wn.test(o)?o.replace(wn,"$1_="+vn++):o+(bn.test(o)?"&":"?")+"_="+vn++)),p.ifModified&&(x.lastModified[o]&&C.setRequestHeader("If-Modified-Since",x.lastModified[o]),x.etag[o]&&C.setRequestHeader("If-None-Match",x.etag[o])),(p.data&&p.hasContent&&p.contentType!==!1||n.contentType)&&C.setRequestHeader("Content-Type",p.contentType),C.setRequestHeader("Accept",p.dataTypes[0]&&p.accepts[p.dataTypes[0]]?p.accepts[p.dataTypes[0]]+("*"!==p.dataTypes[0]?", "+Dn+"; q=0.01":""):p.accepts["*"]);for(i in p.headers)C.setRequestHeader(i,p.headers[i]);if(p.beforeSend&&(p.beforeSend.call(f,C,p)===!1||2===b))return C.abort();w="abort";for(i in{success:1,error:1,complete:1})C[i](p[i]);if(u=qn(jn,p,n,C)){C.readyState=1,l&&d.trigger("ajaxSend",[C,p]),p.async&&p.timeout>0&&(s=setTimeout(function(){C.abort("timeout")},p.timeout));try{b=1,u.send(y,k)}catch(N){if(!(2>b))throw N;k(-1,N)}}else k(-1,"No Transport");function k(e,n,r,i){var c,y,v,w,T,N=n;2!==b&&(b=2,s&&clearTimeout(s),u=t,a=i||"",C.readyState=e>0?4:0,c=e>=200&&300>e||304===e,r&&(w=Mn(p,C,r)),w=On(p,w,C,c),c?(p.ifModified&&(T=C.getResponseHeader("Last-Modified"),T&&(x.lastModified[o]=T),T=C.getResponseHeader("etag"),T&&(x.etag[o]=T)),204===e||"HEAD"===p.type?N="nocontent":304===e?N="notmodified":(N=w.state,y=w.data,v=w.error,c=!v)):(v=N,(e||!N)&&(N="error",0>e&&(e=0))),C.status=e,C.statusText=(n||N)+"",c?h.resolveWith(f,[y,N,C]):h.rejectWith(f,[C,N,v]),C.statusCode(m),m=t,l&&d.trigger(c?"ajaxSuccess":"ajaxError",[C,p,c?y:v]),g.fireWith(f,[C,N]),l&&(d.trigger("ajaxComplete",[C,p]),--x.active||x.event.trigger("ajaxStop")))}return C},getJSON:function(e,t,n){return x.get(e,t,n,"json")},getScript:function(e,n){return x.get(e,t,n,"script")}}),x.each(["get","post"],function(e,n){x[n]=function(e,r,i,o){return x.isFunction(r)&&(o=o||i,i=r,r=t),x.ajax({url:e,type:n,dataType:o,data:r,success:i})}});function Mn(e,n,r){var i,o,a,s,l=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),o===t&&(o=e.mimeType||n.getResponseHeader("Content-Type"));if(o)for(s in l)if(l[s]&&l[s].test(o)){u.unshift(s);break}if(u[0]in r)a=u[0];else{for(s in r){if(!u[0]||e.converters[s+" "+u[0]]){a=s;break}i||(i=s)}a=a||i}return a?(a!==u[0]&&u.unshift(a),r[a]):t}function On(e,t,n,r){var i,o,a,s,l,u={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)u[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!l&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),l=o,o=c.shift())if("*"===o)o=l;else if("*"!==l&&l!==o){if(a=u[l+" "+o]||u["* "+o],!a)for(i in u)if(s=i.split(" "),s[1]===o&&(a=u[l+" "+s[0]]||u["* "+s[0]])){a===!0?a=u[i]:u[i]!==!0&&(o=s[0],c.unshift(s[1]));break}if(a!==!0)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(p){return{state:"parsererror",error:a?p:"No conversion from "+l+" to "+o}}}return{state:"success",data:t}}x.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(e){return x.globalEval(e),e}}}),x.ajaxPrefilter("script",function(e){e.cache===t&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),x.ajaxTransport("script",function(e){if(e.crossDomain){var n,r=a.head||x("head")[0]||a.documentElement;return{send:function(t,i){n=a.createElement("script"),n.async=!0,e.scriptCharset&&(n.charset=e.scriptCharset),n.src=e.url,n.onload=n.onreadystatechange=function(e,t){(t||!n.readyState||/loaded|complete/.test(n.readyState))&&(n.onload=n.onreadystatechange=null,n.parentNode&&n.parentNode.removeChild(n),n=null,t||i(200,"success"))},r.insertBefore(n,r.firstChild)},abort:function(){n&&n.onload(t,!0)}}}});var Fn=[],Bn=/(=)\?(?=&|$)|\?\?/;x.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Fn.pop()||x.expando+"_"+vn++;return this[e]=!0,e}}),x.ajaxPrefilter("json jsonp",function(n,r,i){var o,a,s,l=n.jsonp!==!1&&(Bn.test(n.url)?"url":"string"==typeof n.data&&!(n.contentType||"").indexOf("application/x-www-form-urlencoded")&&Bn.test(n.data)&&"data");return l||"jsonp"===n.dataTypes[0]?(o=n.jsonpCallback=x.isFunction(n.jsonpCallback)?n.jsonpCallback():n.jsonpCallback,l?n[l]=n[l].replace(Bn,"$1"+o):n.jsonp!==!1&&(n.url+=(bn.test(n.url)?"&":"?")+n.jsonp+"="+o),n.converters["script json"]=function(){return s||x.error(o+" was not called"),s[0]},n.dataTypes[0]="json",a=e[o],e[o]=function(){s=arguments},i.always(function(){e[o]=a,n[o]&&(n.jsonpCallback=r.jsonpCallback,Fn.push(o)),s&&x.isFunction(a)&&a(s[0]),s=a=t}),"script"):t});var Pn,Rn,Wn=0,$n=e.ActiveXObject&&function(){var e;for(e in Pn)Pn[e](t,!0)};function In(){try{return new e.XMLHttpRequest}catch(t){}}function zn(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(t){}}x.ajaxSettings.xhr=e.ActiveXObject?function(){return!this.isLocal&&In()||zn()}:In,Rn=x.ajaxSettings.xhr(),x.support.cors=!!Rn&&"withCredentials"in Rn,Rn=x.support.ajax=!!Rn,Rn&&x.ajaxTransport(function(n){if(!n.crossDomain||x.support.cors){var r;return{send:function(i,o){var a,s,l=n.xhr();if(n.username?l.open(n.type,n.url,n.async,n.username,n.password):l.open(n.type,n.url,n.async),n.xhrFields)for(s in n.xhrFields)l[s]=n.xhrFields[s];n.mimeType&&l.overrideMimeType&&l.overrideMimeType(n.mimeType),n.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");try{for(s in i)l.setRequestHeader(s,i[s])}catch(u){}l.send(n.hasContent&&n.data||null),r=function(e,i){var s,u,c,p;try{if(r&&(i||4===l.readyState))if(r=t,a&&(l.onreadystatechange=x.noop,$n&&delete Pn[a]),i)4!==l.readyState&&l.abort();else{p={},s=l.status,u=l.getAllResponseHeaders(),"string"==typeof l.responseText&&(p.text=l.responseText);try{c=l.statusText}catch(f){c=""}s||!n.isLocal||n.crossDomain?1223===s&&(s=204):s=p.text?200:404}}catch(d){i||o(-1,d)}p&&o(s,c,p,u)},n.async?4===l.readyState?setTimeout(r):(a=++Wn,$n&&(Pn||(Pn={},x(e).unload($n)),Pn[a]=r),l.onreadystatechange=r):r()},abort:function(){r&&r(t,!0)}}}});var Xn,Un,Vn=/^(?:toggle|show|hide)$/,Yn=RegExp("^(?:([+-])=|)("+w+")([a-z%]*)$","i"),Jn=/queueHooks$/,Gn=[nr],Qn={"*":[function(e,t){var n=this.createTween(e,t),r=n.cur(),i=Yn.exec(t),o=i&&i[3]||(x.cssNumber[e]?"":"px"),a=(x.cssNumber[e]||"px"!==o&&+r)&&Yn.exec(x.css(n.elem,e)),s=1,l=20;if(a&&a[3]!==o){o=o||a[3],i=i||[],a=+r||1;do s=s||".5",a/=s,x.style(n.elem,e,a+o);while(s!==(s=n.cur()/r)&&1!==s&&--l)}return i&&(a=n.start=+a||+r||0,n.unit=o,n.end=i[1]?a+(i[1]+1)*i[2]:+i[2]),n}]};function Kn(){return setTimeout(function(){Xn=t}),Xn=x.now()}function Zn(e,t,n){var r,i=(Qn[t]||[]).concat(Qn["*"]),o=0,a=i.length;for(;a>o;o++)if(r=i[o].call(n,t,e))return r}function er(e,t,n){var r,i,o=0,a=Gn.length,s=x.Deferred().always(function(){delete l.elem}),l=function(){if(i)return!1;var t=Xn||Kn(),n=Math.max(0,u.startTime+u.duration-t),r=n/u.duration||0,o=1-r,a=0,l=u.tweens.length;for(;l>a;a++)u.tweens[a].run(o);return s.notifyWith(e,[u,o,n]),1>o&&l?n:(s.resolveWith(e,[u]),!1)},u=s.promise({elem:e,props:x.extend({},t),opts:x.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:Xn||Kn(),duration:n.duration,tweens:[],createTween:function(t,n){var r=x.Tween(e,u.opts,t,n,u.opts.specialEasing[t]||u.opts.easing);return u.tweens.push(r),r},stop:function(t){var n=0,r=t?u.tweens.length:0;if(i)return this;for(i=!0;r>n;n++)u.tweens[n].run(1);return t?s.resolveWith(e,[u,t]):s.rejectWith(e,[u,t]),this}}),c=u.props;for(tr(c,u.opts.specialEasing);a>o;o++)if(r=Gn[o].call(u,e,c,u.opts))return r;return x.map(c,Zn,u),x.isFunction(u.opts.start)&&u.opts.start.call(e,u),x.fx.timer(x.extend(l,{elem:e,anim:u,queue:u.opts.queue})),u.progress(u.opts.progress).done(u.opts.done,u.opts.complete).fail(u.opts.fail).always(u.opts.always)}function tr(e,t){var n,r,i,o,a;for(n in e)if(r=x.camelCase(n),i=t[r],o=e[n],x.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),a=x.cssHooks[r],a&&"expand"in a){o=a.expand(o),delete e[r];for(n in o)n in e||(e[n]=o[n],t[n]=i)}else t[r]=i}x.Animation=x.extend(er,{tweener:function(e,t){x.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");var n,r=0,i=e.length;for(;i>r;r++)n=e[r],Qn[n]=Qn[n]||[],Qn[n].unshift(t)},prefilter:function(e,t){t?Gn.unshift(e):Gn.push(e)}});function nr(e,t,n){var r,i,o,a,s,l,u=this,c={},p=e.style,f=e.nodeType&&nn(e),d=x._data(e,"fxshow");n.queue||(s=x._queueHooks(e,"fx"),null==s.unqueued&&(s.unqueued=0,l=s.empty.fire,s.empty.fire=function(){s.unqueued||l()}),s.unqueued++,u.always(function(){u.always(function(){s.unqueued--,x.queue(e,"fx").length||s.empty.fire()})})),1===e.nodeType&&("height"in t||"width"in t)&&(n.overflow=[p.overflow,p.overflowX,p.overflowY],"inline"===x.css(e,"display")&&"none"===x.css(e,"float")&&(x.support.inlineBlockNeedsLayout&&"inline"!==ln(e.nodeName)?p.zoom=1:p.display="inline-block")),n.overflow&&(p.overflow="hidden",x.support.shrinkWrapBlocks||u.always(function(){p.overflow=n.overflow[0],p.overflowX=n.overflow[1],p.overflowY=n.overflow[2]}));for(r in t)if(i=t[r],Vn.exec(i)){if(delete t[r],o=o||"toggle"===i,i===(f?"hide":"show"))continue;c[r]=d&&d[r]||x.style(e,r)}if(!x.isEmptyObject(c)){d?"hidden"in d&&(f=d.hidden):d=x._data(e,"fxshow",{}),o&&(d.hidden=!f),f?x(e).show():u.done(function(){x(e).hide()}),u.done(function(){var t;x._removeData(e,"fxshow");for(t in c)x.style(e,t,c[t])});for(r in c)a=Zn(f?d[r]:0,r,u),r in d||(d[r]=a.start,f&&(a.end=a.start,a.start="width"===r||"height"===r?1:0))}}function rr(e,t,n,r,i){return new rr.prototype.init(e,t,n,r,i)}x.Tween=rr,rr.prototype={constructor:rr,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(x.cssNumber[n]?"":"px")},cur:function(){var e=rr.propHooks[this.prop];return e&&e.get?e.get(this):rr.propHooks._default.get(this)},run:function(e){var t,n=rr.propHooks[this.prop];return this.pos=t=this.options.duration?x.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):rr.propHooks._default.set(this),this}},rr.prototype.init.prototype=rr.prototype,rr.propHooks={_default:{get:function(e){var t;return null==e.elem[e.prop]||e.elem.style&&null!=e.elem.style[e.prop]?(t=x.css(e.elem,e.prop,""),t&&"auto"!==t?t:0):e.elem[e.prop]},set:function(e){x.fx.step[e.prop]?x.fx.step[e.prop](e):e.elem.style&&(null!=e.elem.style[x.cssProps[e.prop]]||x.cssHooks[e.prop])?x.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},rr.propHooks.scrollTop=rr.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},x.each(["toggle","show","hide"],function(e,t){var n=x.fn[t];x.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(ir(t,!0),e,r,i)}}),x.fn.extend({fadeTo:function(e,t,n,r){return this.filter(nn).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=x.isEmptyObject(e),o=x.speed(t,n,r),a=function(){var t=er(this,x.extend({},e),o);(i||x._data(this,"finish"))&&t.stop(!0)};return a.finish=a,i||o.queue===!1?this.each(a):this.queue(o.queue,a)},stop:function(e,n,r){var i=function(e){var t=e.stop;delete e.stop,t(r)};return"string"!=typeof e&&(r=n,n=e,e=t),n&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,n=null!=e&&e+"queueHooks",o=x.timers,a=x._data(this);if(n)a[n]&&a[n].stop&&i(a[n]);else for(n in a)a[n]&&a[n].stop&&Jn.test(n)&&i(a[n]);for(n=o.length;n--;)o[n].elem!==this||null!=e&&o[n].queue!==e||(o[n].anim.stop(r),t=!1,o.splice(n,1));(t||!r)&&x.dequeue(this,e)})},finish:function(e){return e!==!1&&(e=e||"fx"),this.each(function(){var t,n=x._data(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=x.timers,a=r?r.length:0;for(n.finish=!0,x.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;a>t;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}});function ir(e,t){var n,r={height:e},i=0;for(t=t?1:0;4>i;i+=2-t)n=Zt[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}x.each({slideDown:ir("show"),slideUp:ir("hide"),slideToggle:ir("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){x.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),x.speed=function(e,t,n){var r=e&&"object"==typeof e?x.extend({},e):{complete:n||!n&&t||x.isFunction(e)&&e,duration:e,easing:n&&t||t&&!x.isFunction(t)&&t};return r.duration=x.fx.off?0:"number"==typeof r.duration?r.duration:r.duration in x.fx.speeds?x.fx.speeds[r.duration]:x.fx.speeds._default,(null==r.queue||r.queue===!0)&&(r.queue="fx"),r.old=r.complete,r.complete=function(){x.isFunction(r.old)&&r.old.call(this),r.queue&&x.dequeue(this,r.queue)},r},x.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2}},x.timers=[],x.fx=rr.prototype.init,x.fx.tick=function(){var e,n=x.timers,r=0;for(Xn=x.now();n.length>r;r++)e=n[r],e()||n[r]!==e||n.splice(r--,1);n.length||x.fx.stop(),Xn=t},x.fx.timer=function(e){e()&&x.timers.push(e)&&x.fx.start()},x.fx.interval=13,x.fx.start=function(){Un||(Un=setInterval(x.fx.tick,x.fx.interval))},x.fx.stop=function(){clearInterval(Un),Un=null},x.fx.speeds={slow:600,fast:200,_default:400},x.fx.step={},x.expr&&x.expr.filters&&(x.expr.filters.animated=function(e){return x.grep(x.timers,function(t){return e===t.elem}).length}),x.fn.offset=function(e){if(arguments.length)return e===t?this:this.each(function(t){x.offset.setOffset(this,e,t)});var n,r,o={top:0,left:0},a=this[0],s=a&&a.ownerDocument;if(s)return n=s.documentElement,x.contains(n,a)?(typeof a.getBoundingClientRect!==i&&(o=a.getBoundingClientRect()),r=or(s),{top:o.top+(r.pageYOffset||n.scrollTop)-(n.clientTop||0),left:o.left+(r.pageXOffset||n.scrollLeft)-(n.clientLeft||0)}):o},x.offset={setOffset:function(e,t,n){var r=x.css(e,"position");"static"===r&&(e.style.position="relative");var i=x(e),o=i.offset(),a=x.css(e,"top"),s=x.css(e,"left"),l=("absolute"===r||"fixed"===r)&&x.inArray("auto",[a,s])>-1,u={},c={},p,f;l?(c=i.position(),p=c.top,f=c.left):(p=parseFloat(a)||0,f=parseFloat(s)||0),x.isFunction(t)&&(t=t.call(e,n,o)),null!=t.top&&(u.top=t.top-o.top+p),null!=t.left&&(u.left=t.left-o.left+f),"using"in t?t.using.call(e,u):i.css(u)}},x.fn.extend({position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===x.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),x.nodeName(e[0],"html")||(n=e.offset()),n.top+=x.css(e[0],"borderTopWidth",!0),n.left+=x.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-x.css(r,"marginTop",!0),left:t.left-n.left-x.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent||s;while(e&&!x.nodeName(e,"html")&&"static"===x.css(e,"position"))e=e.offsetParent;return e||s})}}),x.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,n){var r=/Y/.test(n);x.fn[e]=function(i){return x.access(this,function(e,i,o){var a=or(e);return o===t?a?n in a?a[n]:a.document.documentElement[i]:e[i]:(a?a.scrollTo(r?x(a).scrollLeft():o,r?o:x(a).scrollTop()):e[i]=o,t)},e,i,arguments.length,null)}});function or(e){return x.isWindow(e)?e:9===e.nodeType?e.defaultView||e.parentWindow:!1}x.each({Height:"height",Width:"width"},function(e,n){x.each({padding:"inner"+e,content:n,"":"outer"+e},function(r,i){x.fn[i]=function(i,o){var a=arguments.length&&(r||"boolean"!=typeof i),s=r||(i===!0||o===!0?"margin":"border");return x.access(this,function(n,r,i){var o;return x.isWindow(n)?n.document.documentElement["client"+e]:9===n.nodeType?(o=n.documentElement,Math.max(n.body["scroll"+e],o["scroll"+e],n.body["offset"+e],o["offset"+e],o["client"+e])):i===t?x.css(n,r,s):x.style(n,r,i,s)},n,a?i:t,a,null)}})}),x.fn.size=function(){return this.length},x.fn.andSelf=x.fn.addBack,"object"==typeof module&&module&&"object"==typeof module.exports?module.exports=x:(e.jQuery=e.$=x,"function"==typeof define&&define.amd&&define("jquery",[],function(){return x}))})(window);
diff --git a/gui/static/script.js b/gui/static/script.js
deleted file mode 100644
index a585f9f..0000000
--- a/gui/static/script.js
+++ /dev/null
@@ -1,4 +0,0 @@
-$(document).ready(function() {
-
-});
-
diff --git a/gui/static/stats.js b/gui/static/stats.js
deleted file mode 100644
index 7b07099..0000000
--- a/gui/static/stats.js
+++ /dev/null
@@ -1,17 +0,0 @@
-var updatefunc = function(event) {
- $('#statdata p').remove();
- $.getJSON('/stats.json', function(result) {
- $.each(result, function(name) {
- // TODO: use a hidden template inside the DOM instead
- // of building the HTML here
- $("<p></p>")
- .append(result[name]['inputstat']['num_underruns'])
- .appendTo('#statdata');
- });
- });
-}
-
-// Handle clicks on the to change visiblity of panes
-setInterval(updatefunc, 1000);
-
-
diff --git a/gui/static/style.css b/gui/static/style.css
deleted file mode 100644
index 43176f8..0000000
--- a/gui/static/style.css
+++ /dev/null
@@ -1,73 +0,0 @@
-body {
- font-family: "Lucida Sans Unicode","Lucida Grande",Sans-Serif;
- color: #3E3E3E;
- font-size: 12px;
-}
-
-p {
- padding: 5px;
-}
-
-div.cadre{
- border: 1px solid #999;
- padding: 0 10px;
- margin: 5px;
-}
-
-#info{
- width: 600px;
- border: 1px solid #999;
- padding: 0 10px;
-}
-#info p {
- width: inherit;
- background-color: inherit;
-}
-#info-nav{
- margin: 0;
- padding: 3px 0;
- width: 100%;
- list-style: none;
-}
-#info-nav li{
- display: inline;
- background: #ccc;
- border: 1px solid #888;
- border-bottom: 0;
- margin-right:2px;
- padding: 3px;
-}
-#info-nav li a:hover{
- color:#d15600;
-}
-#info-nav li.current{
- background: #fff;
- padding-bottom: 4px;
-}
-
-#celebs {
- clear: both;
-}
-
-table {
- border-collapse:collapse;
- font-size:12px;
- margin:0 20px 20px 20px;
- border-top:2px solid #015287;
- width:480px;
-}
-
-th {
- border-bottom: 2px solid #015287;
- color: #D15600;
- font-size: 14px;
- font-weight: normal;
- text-align: left;
- padding: 3px 8px;
-}
-
-td {
- padding: 6px;
-}
-
-
diff --git a/gui/views/configeditor.tpl b/gui/views/configeditor.tpl
deleted file mode 100644
index d302498..0000000
--- a/gui/views/configeditor.tpl
+++ /dev/null
@@ -1,29 +0,0 @@
-<!DOCTYPE html>
-<html>
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <title>ODR-DabMux Configuration Editor</title>
- <link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
- </head>
-<body class="w3-container">
- <h1>Configuration for {{version}}</h1>
-
- <p><a href="/config">Reload</a></p>
-
- {% if message %}
- <p>{{message}}</p>
- {% endif %}
-
- <form action="/config" method="post">
- <p>
- <textarea name="config" cols="60" rows="30">{{config}}</textarea>
- </p>
-
- <p>
- <input type="submit" value="Update ODR-DabMux configuration"></input>
- </p>
- </form>
-
- </body>
-</html> \ No newline at end of file
diff --git a/gui/views/index.tpl b/gui/views/index.tpl
deleted file mode 100644
index ce60533..0000000
--- a/gui/views/index.tpl
+++ /dev/null
@@ -1,129 +0,0 @@
-<!DOCTYPE html>
-<html>
- <head>
- <title>ODR-DabMux Configuration</title>
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
- </head>
- <body class="w3-container">
- <div class="w3-top w3-bar w3-blue-grey">
- <a href="#general" class="w3-bar-item w3-button">General Options</a>
- <a href="#servicelist" class="w3-bar-item w3-button">Services</a>
- <a href="#subchannels" class="w3-bar-item w3-button">Subchannels</a>
- <a href="#components" class="w3-bar-item w3-button">Components</a>
- <a href="#rcmodules" class="w3-bar-item w3-button">RC Modules</a>
- </div>
- <div id="general" class="w3-responsive w3-card-4">
- <br /><br />
- <table class="w3-table w3-striped w3-bordered">
- <tr class="w3-blue-grey">
- <th>General multiplex options</th>
- <th></th>
- </tr>
- <tr>
- <td>Number of frames to encode</td>
- <td>{{g.nbframes}}</td>
- </tr>
- <tr>
- <td>Statistics server port</td>
- <td>{{g.statsserverport}}</td>
- </tr>
- <tr>
- <td>Write SCCA field</td>
- <td>{{g.writescca}}</td>
- </tr>
- <tr>
- <td>Write TIST timestamp</td>
- <td>{{g.tist}}</td>
- </tr>
- <tr>
- <td>DAB mode</td>
- <td>{{g.dabmode}}</td>
- </tr>
- <tr>
- <td>Log to syslog</td>
- <td>{{g.syslog}}</td>
- </tr>
- </table>
- </div>
- <div id="servicelist" class="w3-responsive w3-card-4">
- <br /><br />
- <table class="w3-table w3-striped w3-bordered">
- <tr class="w3-blue-grey">
- <th>Service</th>
- <th>Id</th>
- <th>Label</th>
- <th>Short label</th>
- </tr>
- {% for s in services %}
- <tr>
- <td>{{s.name}}</td>
- <td>{{s.id}}</td>
- <td>{{s.label}}</td>
- <td>{{s.shortlabel}}</td>
- </tr>
- {% endfor %}
- </table>
- </div>
- <div id="subchannels" class="w3-responsive w3-card-4">
- <br /><br />
- <table class="w3-table w3-striped w3-bordered">
- <tr class="w3-blue-grey">
- <th>Sub channel</th>
- <th>Type</th>
- <th>Input file</th>
- <th>Bit rate (Kbps)</th>
- </tr>
- {% for s in subchannels %}
- <tr>
- <td>{{s.name}}</td>
- <td>{{s.type}}</td>
- <td>{{s.inputfile}}</td>
- <td>{{s.bitrate}}</td>
- </tr>
- {% endfor %}
- </table>
- </div>
- <div id="components" class="w3-responsive w3-card-4">
- <br /><br />
- <table class="w3-table w3-striped w3-bordered">
- <tr class="w3-blue-grey">
- <th>Component</th>
- <th>Label</th>
- <th>Short label</th>
- <th>Service</th>
- <th>Sub-channel</th>
- <th>Fig type</th>
- </tr>
- {% for s in components %}
- <tr>
- <td>{{s.name}}</td>
- <td>{{s.label}}</td>
- <td>{{s.shortlabel}}</td>
- <td>{{s.service}}</td>
- <td>{{s.subchannel}}</td>
- <td>{{s.figtype}}</td>
- </tr>
- {% endfor %}
- </table>
- </div>
- <div id="rcmodules" class="w3-responsive w3-card-4">
- <br /><br />
- <ul class="w3-ul">
- <li class="w3-blue-grey"><b>RC Modules</b></li>
- {% for m in rcmodules %}
- <li class="w3-light-grey"><b>{{m.name}}</b>
- <ul class="w3-ul">
- {% for p in m.parameters %}
- <li class="w3-white"><a href="/rc/{{m.name}}/{{p.param.decode()}}" class="w3-hover-blue-grey">{{p.param.decode()}}</a> : {{p.value.decode()}}</li>
- {% endfor %}
- </ul>
- </li>
- {% endfor %}
- </ul>
- </div>
- </div>
-
- </body>
-</html> \ No newline at end of file
diff --git a/gui/views/rcparam.tpl b/gui/views/rcparam.tpl
deleted file mode 100644
index edac5b7..0000000
--- a/gui/views/rcparam.tpl
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<html>
- <head>
- <title>ODR-DabMux Configuration</title>
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
- </head>
-
- <body class="w3-container">
- <h1 class="w3-blue-grey">Remote-Control: module {{module}}</h1>
- <div class="w3-card-4">
- <form class="w3-container" method="post">
- <p />
- {% if not list %}
- <label>{{param}}:</label>
- <input name="newvalue" type="text" value="{{value.decode()}}" autofocus>
- {% else %}
- <label>{{label}}:</label>
- <select id="newvalue" name="newvalue">
- {% for l in list %}
- {% if (l["value"] == value.decode()) %}
- <option selected value={{l["value"]}}>{{l["desc"]}}</option>
- {% else %}
- <option value={{l["value"]}}>{{l["desc"]}}</option>
- {% endif %}
- {% endfor %}
- </select>
- {% endif %}
- <p />
- <button class="w3-button w3-blue-grey" type="submit">Update</button>
- <p />
- </form>
- </div>
- </body>
-
-</html> \ No newline at end of file
diff --git a/gui/views/services.tpl b/gui/views/services.tpl
deleted file mode 100644
index 42438a8..0000000
--- a/gui/views/services.tpl
+++ /dev/null
@@ -1,31 +0,0 @@
-<!DOCTYPE html>
-<html>
- <head>
- <title>ODR-DabMux Services</title>
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
- </head>
- <body class="w3-container">
- <h1>Services for {{version}}</h1>
- <table class="w3-table w3-striped w3-bordered">
- <tr class="w3-blue-grey">
- <th>Service</th>
- <th>Id</th>
- <th>Label</th>
- <th>Short label</th>
- <th>Program type</th>
- <th>Language</th>
- </tr>
- {% for s in services %}
- <tr>
- <td>{{s.name}}</td>
- <td>{{s.id}}</td>
- <td>{{s.label}}</td>
- <td>{{s.shortlabel}}</td>
- <td>{{s.pty}}</td>
- <td>{{s.language}}</td>
- </tr>
- {% endfor %}
- </table>
- </body>
-</html> \ No newline at end of file
diff --git a/gui/views/stats.tpl b/gui/views/stats.tpl
deleted file mode 100644
index 4bb089f..0000000
--- a/gui/views/stats.tpl
+++ /dev/null
@@ -1,21 +0,0 @@
-<!DOCTYPE html>
-<html>
- <head>
- <title>ODR-DabMux Statistics</title>
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
- <script type="text/javascript" src="static/jquery-1.10.2.min.js"></script>
- <script type="text/javascript" src="static/stats.js"></script>
- </head>
- <body class="w3-container">
- <h1>Subchannel stats for {{version}}</h1>
-
- <a id="update">Update</a>
-
- <div id="subchannels">
- <p>Subchannels</p>
- <div id="statdata"></div>
- </div>
- </body>
-</html> \ No newline at end of file
diff --git a/lib/Json.cpp b/lib/Json.cpp
index 361a149..ee33671 100644
--- a/lib/Json.cpp
+++ b/lib/Json.cpp
@@ -3,7 +3,7 @@
Her Majesty the Queen in Right of Canada (Communications Research
Center Canada)
- Copyright (C) 2023
+ Copyright (C) 2025
Matthias P. Braendli, matthias.braendli@mpb.li
http://www.opendigitalradio.org
@@ -27,7 +27,7 @@
#include <sstream>
#include <iomanip>
#include <string>
-#include <algorithm>
+#include <stdexcept>
#include "Json.h"
diff --git a/lib/Json.h b/lib/Json.h
index b082f92..0168583 100644
--- a/lib/Json.h
+++ b/lib/Json.h
@@ -3,7 +3,7 @@
Her Majesty the Queen in Right of Canada (Communications Research
Center Canada)
- Copyright (C) 2023
+ Copyright (C) 2025
Matthias P. Braendli, matthias.braendli@mpb.li
http://www.opendigitalradio.org
@@ -34,10 +34,10 @@
#include <vector>
#include <memory>
#include <optional>
-#include <stdexcept>
#include <string>
#include <unordered_map>
#include <variant>
+#include <cstdint>
namespace json {
diff --git a/lib/Socket.cpp b/lib/Socket.cpp
index 2df1559..5c920d7 100644
--- a/lib/Socket.cpp
+++ b/lib/Socket.cpp
@@ -24,6 +24,7 @@
#include "Socket.h"
+#include <numeric>
#include <stdexcept>
#include <cstdio>
#include <cstring>
@@ -478,7 +479,7 @@ TCPSocket::~TCPSocket()
TCPSocket::TCPSocket(TCPSocket&& other) :
m_sock(other.m_sock),
- m_remote_address(move(other.m_remote_address))
+ m_remote_address(std::move(other.m_remote_address))
{
if (other.m_sock != -1) {
other.m_sock = -1;
@@ -967,12 +968,22 @@ ssize_t TCPClient::recv(void *buffer, size_t length, int flags, int timeout_ms)
reconnect();
}
+ m_last_received_packet_ts = chrono::steady_clock::now();
+
return ret;
}
catch (const TCPSocket::Interrupted&) {
return -1;
}
catch (const TCPSocket::Timeout&) {
+ const auto timeout = chrono::milliseconds(timeout_ms * 5);
+ if (m_last_received_packet_ts.has_value() and
+ chrono::steady_clock::now() - *m_last_received_packet_ts > timeout)
+ {
+ // This is to catch half-closed TCP connections
+ reconnect();
+ }
+
return 0;
}
@@ -983,6 +994,7 @@ void TCPClient::reconnect()
{
TCPSocket newsock;
m_sock = std::move(newsock);
+ m_last_received_packet_ts = nullopt;
m_sock.connect(m_hostname, m_port, true);
}
@@ -990,7 +1002,7 @@ TCPConnection::TCPConnection(TCPSocket&& sock) :
queue(),
m_running(true),
m_sender_thread(),
- m_sock(move(sock))
+ m_sock(std::move(sock))
{
#if MISSING_OWN_ADDR
auto own_addr = m_sock.getOwnAddress();
@@ -1052,6 +1064,17 @@ void TCPConnection::process()
#endif
}
+TCPConnection::stats_t TCPConnection::get_stats() const
+{
+ TCPConnection::stats_t s;
+ const vector<size_t> buffer_sizes = queue.map<size_t>(
+ [](const vector<uint8_t>& vec) { return vec.size(); }
+ );
+
+ s.buffer_fullness = std::accumulate(buffer_sizes.cbegin(), buffer_sizes.cend(), 0);
+ s.remote_address = m_sock.get_remote_address();
+ return s;
+}
TCPDataDispatcher::TCPDataDispatcher(size_t max_queue_size, size_t buffers_to_preroll) :
m_max_queue_size(max_queue_size),
@@ -1109,7 +1132,7 @@ void TCPDataDispatcher::process()
auto sock = m_listener_socket.accept(timeout_ms);
if (sock.valid()) {
auto lock = unique_lock<mutex>(m_mutex);
- m_connections.emplace(m_connections.begin(), move(sock));
+ m_connections.emplace(m_connections.begin(), std::move(sock));
if (m_buffers_to_preroll > 0) {
for (const auto& buf : m_preroll_queue) {
@@ -1125,6 +1148,16 @@ void TCPDataDispatcher::process()
}
}
+
+std::vector<TCPConnection::stats_t> TCPDataDispatcher::get_stats() const
+{
+ std::vector<TCPConnection::stats_t> s;
+ for (const auto& conn : m_connections) {
+ s.push_back(conn.get_stats());
+ }
+ return s;
+}
+
TCPReceiveServer::TCPReceiveServer(size_t blocksize) :
m_blocksize(blocksize)
{
@@ -1181,7 +1214,7 @@ void TCPReceiveServer::process()
}
else {
buf.resize(r);
- m_queue.push(make_shared<TCPReceiveMessageData>(move(buf)));
+ m_queue.push(make_shared<TCPReceiveMessageData>(std::move(buf)));
}
}
catch (const TCPSocket::Interrupted&) {
@@ -1222,7 +1255,7 @@ TCPSendClient::~TCPSendClient()
}
}
-void TCPSendClient::sendall(const std::vector<uint8_t>& buffer)
+TCPSendClient::ErrorStats TCPSendClient::sendall(const std::vector<uint8_t>& buffer)
{
if (not m_running) {
throw runtime_error(m_exception_data);
@@ -1234,6 +1267,17 @@ void TCPSendClient::sendall(const std::vector<uint8_t>& buffer)
vector<uint8_t> discard;
m_queue.try_pop(discard);
}
+
+ TCPSendClient::ErrorStats es;
+ es.num_reconnects = m_num_reconnects.load();
+
+ es.has_seen_new_errors = es.num_reconnects != m_num_reconnects_prev;
+ m_num_reconnects_prev = es.num_reconnects;
+
+ auto lock = unique_lock<mutex>(m_error_mutex);
+ es.last_error = m_last_error;
+
+ return es;
}
void TCPSendClient::process()
@@ -1255,12 +1299,16 @@ void TCPSendClient::process()
}
else {
try {
+ m_num_reconnects.fetch_add(1, std::memory_order_seq_cst);
m_sock.connect(m_hostname, m_port);
m_is_connected = true;
}
catch (const runtime_error& e) {
m_is_connected = false;
this_thread::sleep_for(chrono::seconds(1));
+
+ auto lock = unique_lock<mutex>(m_error_mutex);
+ m_last_error = e.what();
}
}
}
diff --git a/lib/Socket.h b/lib/Socket.h
index 1320a64..29b618a 100644
--- a/lib/Socket.h
+++ b/lib/Socket.h
@@ -31,9 +31,11 @@
#include "ThreadsafeQueue.h"
#include <cstdlib>
#include <atomic>
-#include <string>
+#include <chrono>
#include <list>
#include <memory>
+#include <optional>
+#include <string>
#include <thread>
#include <vector>
@@ -211,6 +213,8 @@ class TCPSocket {
SOCKET get_sockfd() const { return m_sock; }
+ InetAddress get_remote_address() const { return m_remote_address; }
+
private:
explicit TCPSocket(int sockfd);
explicit TCPSocket(int sockfd, InetAddress remote_address);
@@ -236,6 +240,8 @@ class TCPClient {
TCPSocket m_sock;
std::string m_hostname;
int m_port;
+
+ std::optional<std::chrono::steady_clock::time_point> m_last_received_packet_ts;
};
/* Helper class for TCPDataDispatcher, contains a queue of pending data and
@@ -250,6 +256,12 @@ class TCPConnection
ThreadsafeQueue<std::vector<uint8_t> > queue;
+ struct stats_t {
+ size_t buffer_fullness = 0;
+ InetAddress remote_address;
+ };
+ stats_t get_stats() const;
+
private:
std::atomic<bool> m_running;
std::thread m_sender_thread;
@@ -272,6 +284,8 @@ class TCPDataDispatcher
void start(int port, const std::string& address);
void write(const std::vector<uint8_t>& data);
+ std::vector<TCPConnection::stats_t> get_stats() const;
+
private:
void process();
@@ -329,10 +343,18 @@ class TCPSendClient {
public:
TCPSendClient(const std::string& hostname, int port);
~TCPSendClient();
+ TCPSendClient(const TCPSendClient&) = delete;
+ TCPSendClient& operator=(const TCPSendClient&) = delete;
- /* Throws a runtime_error on error
- */
- void sendall(const std::vector<uint8_t>& buffer);
+
+ struct ErrorStats {
+ std::string last_error = "";
+ size_t num_reconnects = 0;
+ bool has_seen_new_errors = false;
+ };
+
+ /* Throws a runtime_error when the process thread isn't running */
+ ErrorStats sendall(const std::vector<uint8_t>& buffer);
private:
void process();
@@ -349,6 +371,11 @@ class TCPSendClient {
std::string m_exception_data;
std::thread m_sender_thread;
TCPSocket m_listener_socket;
+
+ std::atomic<size_t> m_num_reconnects = ATOMIC_VAR_INIT(0);
+ size_t m_num_reconnects_prev = 0;
+ std::mutex m_error_mutex;
+ std::string m_last_error = "";
};
}
diff --git a/lib/ThreadsafeQueue.h b/lib/ThreadsafeQueue.h
index 8b385d6..13bc19e 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) 2023
+ Copyright (C) 2025
Matthias P. Braendli, matthias.braendli@mpb.li
An implementation for a threadsafe queue, depends on C++11
@@ -28,6 +28,7 @@
#pragma once
+#include <functional>
#include <mutex>
#include <condition_variable>
#include <queue>
@@ -63,10 +64,10 @@ public:
std::unique_lock<std::mutex> lock(the_mutex);
size_t queue_size_before = the_queue.size();
if (max_size == 0) {
- the_queue.push(val);
+ the_queue.push_back(val);
}
else if (queue_size_before < max_size) {
- the_queue.push(val);
+ the_queue.push_back(val);
}
size_t queue_size = the_queue.size();
lock.unlock();
@@ -80,10 +81,10 @@ public:
std::unique_lock<std::mutex> lock(the_mutex);
size_t queue_size_before = the_queue.size();
if (max_size == 0) {
- the_queue.emplace(std::move(val));
+ the_queue.emplace_back(std::move(val));
}
else if (queue_size_before < max_size) {
- the_queue.emplace(std::move(val));
+ the_queue.emplace_back(std::move(val));
}
size_t queue_size = the_queue.size();
lock.unlock();
@@ -110,9 +111,9 @@ public:
bool overflow = false;
while (the_queue.size() >= max_size) {
overflow = true;
- the_queue.pop();
+ the_queue.pop_front();
}
- the_queue.push(val);
+ the_queue.push_back(val);
const size_t queue_size = the_queue.size();
lock.unlock();
@@ -129,9 +130,9 @@ public:
bool overflow = false;
while (the_queue.size() >= max_size) {
overflow = true;
- the_queue.pop();
+ the_queue.pop_front();
}
- the_queue.emplace(std::move(val));
+ the_queue.emplace_back(std::move(val));
const size_t queue_size = the_queue.size();
lock.unlock();
@@ -152,7 +153,7 @@ public:
while (the_queue.size() >= threshold) {
the_tx_notification.wait(lock);
}
- the_queue.push(val);
+ the_queue.push_back(val);
size_t queue_size = the_queue.size();
lock.unlock();
@@ -198,7 +199,7 @@ public:
}
popped_value = the_queue.front();
- the_queue.pop();
+ the_queue.pop_front();
lock.unlock();
the_tx_notification.notify_one();
@@ -220,15 +221,26 @@ public:
}
else {
std::swap(popped_value, the_queue.front());
- the_queue.pop();
+ the_queue.pop_front();
lock.unlock();
the_tx_notification.notify_one();
}
}
+ template<typename R>
+ std::vector<R> map(std::function<R(const T&)> func) const
+ {
+ std::vector<R> result;
+ std::unique_lock<std::mutex> lock(the_mutex);
+ for (const T& elem : the_queue) {
+ result.push_back(func(elem));
+ }
+ return result;
+ }
+
private:
- std::queue<T> the_queue;
+ std::deque<T> the_queue;
mutable std::mutex the_mutex;
std::condition_variable the_rx_notification;
std::condition_variable the_tx_notification;
diff --git a/lib/edi/STIDecoder.cpp b/lib/edi/STIDecoder.cpp
index 2de828b..0499c53 100644
--- a/lib/edi/STIDecoder.cpp
+++ b/lib/edi/STIDecoder.cpp
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2020
+ Copyright (C) 2025
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -20,7 +20,6 @@
*/
#include "STIDecoder.hpp"
#include "buffer_unpack.hpp"
-#include "crc.h"
#include "Log.h"
#include <cstdio>
#include <cassert>
@@ -180,7 +179,13 @@ bool STIDecoder::decode_ssn(const std::vector<uint8_t>& value, const tag_name_t&
n |= (uint16_t)(name[3]);
if (n == 0) {
- etiLog.level(warn) << "EDI: Stream index SSnn tag is zero";
+ if (not m_ssnn_zero_warning_printed) {
+ etiLog.level(warn) << "EDI: Stream index SSnn tag is zero";
+ }
+ m_ssnn_zero_warning_printed = true;
+ }
+ else {
+ m_ssnn_zero_warning_printed = false;
}
if (m_filter_stream and m_filtered_stream_index != n) {
@@ -197,14 +202,20 @@ bool STIDecoder::decode_ssn(const std::vector<uint8_t>& value, const tag_name_t&
sti.stid = istc & 0xFFF;
if (sti.rfa != 0) {
- etiLog.level(warn) << "EDI: rfa field in SSnn tag non-null";
+ if (not m_rfa_nonnull_warning_printed) {
+ etiLog.level(warn) << "EDI: rfa field in SSnn tag non-null";
+ }
+ m_rfa_nonnull_warning_printed = true;
+ }
+ else {
+ m_rfa_nonnull_warning_printed = false;
}
copy( value.cbegin() + 3,
value.cend(),
back_inserter(sti.istd));
- m_data_collector.add_payload(move(sti));
+ m_data_collector.add_payload(std::move(sti));
return true;
}
diff --git a/lib/edi/STIDecoder.hpp b/lib/edi/STIDecoder.hpp
index 5e71ce7..81fbd82 100644
--- a/lib/edi/STIDecoder.hpp
+++ b/lib/edi/STIDecoder.hpp
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2020
+ Copyright (C) 2025
Matthias P. Braendli, matthias.braendli@mpb.li
http://opendigitalradio.org
@@ -139,6 +139,9 @@ class STIDecoder {
bool m_filter_stream = false;
uint16_t m_filtered_stream_index = 1;
+
+ bool m_ssnn_zero_warning_printed = false;
+ bool m_rfa_nonnull_warning_printed = false;
};
}
diff --git a/lib/edioutput/EDIConfig.h b/lib/edioutput/EDIConfig.h
index 1997210..7016e87 100644
--- a/lib/edioutput/EDIConfig.h
+++ b/lib/edioutput/EDIConfig.h
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2019
+ Copyright (C) 2025
Matthias P. Braendli, matthias.braendli@mpb.li
http://www.opendigitalradio.org
@@ -36,17 +36,31 @@ namespace edi {
/** Configuration for EDI output */
+struct pft_settings_t {
+ // protection and fragmentation settings
+ bool verbose = false;
+ bool enable_pft = false;
+ unsigned chunk_len = 207; // RSk, data length of each chunk
+ unsigned fec = 0; // number of fragments that can be recovered
+ double fragment_spreading_factor = 0.95;
+ // Spread transmission of fragments in time. 1.0 = 100% means spreading over the whole duration of a frame (24ms)
+ // Above 100% means that the fragments are spread over several 24ms periods, interleaving the AF packets.
+};
+
struct destination_t {
virtual ~destination_t() {};
+
+ pft_settings_t pft_settings = {};
};
+
// Can represent both unicast and multicast destinations
struct udp_destination_t : public destination_t {
std::string dest_addr;
- unsigned int dest_port = 0;
+ uint16_t dest_port = 0;
std::string source_addr;
- unsigned int source_port = 0;
- unsigned int ttl = 10;
+ uint16_t source_port = 0;
+ uint8_t ttl = 10;
};
// TCP server that can accept multiple connections
@@ -66,16 +80,9 @@ struct tcp_client_t : public destination_t {
};
struct configuration_t {
- unsigned chunk_len = 207; // RSk, data length of each chunk
- unsigned fec = 0; // number of fragments that can be recovered
- bool dump = false; // dump a file with the EDI packets
- bool verbose = false;
- bool enable_pft = false; // Enable protection and fragmentation
+ bool verbose = false;
unsigned int tagpacket_alignment = 0;
std::vector<std::shared_ptr<destination_t> > destinations;
- double fragment_spreading_factor = 0.95;
- // Spread transmission of fragments in time. 1.0 = 100% means spreading over the whole duration of a frame (24ms)
- // Above 100% means that the fragments are spread over several 24ms periods, interleaving the AF packets.
bool enabled() const { return destinations.size() > 0; }
diff --git a/lib/edioutput/PFT.cpp b/lib/edioutput/PFT.cpp
index 7e0e8e9..f65fd67 100644
--- a/lib/edioutput/PFT.cpp
+++ b/lib/edioutput/PFT.cpp
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2021
+ Copyright (C) 2025
Matthias P. Braendli, matthias.braendli@mpb.li
http://www.opendigitalradio.org
@@ -31,7 +31,6 @@
*/
#include <vector>
-#include <list>
#include <cstdio>
#include <cstring>
#include <cstdint>
@@ -41,6 +40,7 @@
#include "PFT.h"
#include "crc.h"
#include "ReedSolomon.h"
+#include "Log.h"
namespace edi {
@@ -51,11 +51,10 @@ using namespace std;
PFT::PFT() { }
-PFT::PFT(const configuration_t &conf) :
+PFT::PFT(const pft_settings_t& conf) :
+ m_enabled(conf.enable_pft),
m_k(conf.chunk_len),
m_m(conf.fec),
- m_pseq(0),
- m_num_chunks(0),
m_verbose(conf.verbose)
{
if (m_k > 207) {
@@ -324,5 +323,4 @@ void PFT::OverridePSeq(uint16_t pseq)
m_pseq = pseq;
}
-}
-
+} // namespace edi
diff --git a/lib/edioutput/PFT.h b/lib/edioutput/PFT.h
index 42569a0..52e9f46 100644
--- a/lib/edioutput/PFT.h
+++ b/lib/edioutput/PFT.h
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2021
+ Copyright (C) 2025
Matthias P. Braendli, matthias.braendli@mpb.li
http://www.opendigitalradio.org
@@ -33,12 +33,8 @@
#pragma once
#include <vector>
-#include <list>
-#include <stdexcept>
#include <cstdint>
#include "AFPacket.h"
-#include "Log.h"
-#include "ReedSolomon.h"
#include "EDIConfig.h"
namespace edi {
@@ -52,21 +48,24 @@ class PFT
static constexpr int PARITYBYTES = 48;
PFT();
- PFT(const configuration_t& conf);
+ PFT(const pft_settings_t& conf);
+
+ bool is_enabled() const { return m_enabled and m_k > 0; }
// return a list of PFT fragments with the correct
// PFT headers
- std::vector< PFTFragment > Assemble(AFPacket af_packet);
+ std::vector<PFTFragment> Assemble(AFPacket af_packet);
// Apply Reed-Solomon FEC to the AF Packet
RSBlock Protect(AFPacket af_packet);
// Cut a RSBlock into several fragments that can be transmitted
- std::vector< std::vector<uint8_t> > ProtectAndFragment(AFPacket af_packet);
+ std::vector<std::vector<uint8_t>> ProtectAndFragment(AFPacket af_packet);
void OverridePSeq(uint16_t pseq);
private:
+ bool m_enabled = false;
unsigned int m_k = 207; // length of RS data word
unsigned int m_m = 3; // number of fragments that can be recovered if lost
uint16_t m_pseq = 0;
diff --git a/lib/edioutput/Transport.cpp b/lib/edioutput/Transport.cpp
index 8ebb9fc..e9559b5 100644
--- a/lib/edioutput/Transport.cpp
+++ b/lib/edioutput/Transport.cpp
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2022
+ Copyright (C) 2025
Matthias P. Braendli, matthias.braendli@mpb.li
http://www.opendigitalradio.org
@@ -25,7 +25,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Transport.h"
-#include <iterator>
+#include "Log.h"
#include <cmath>
#include <thread>
@@ -57,13 +57,18 @@ void configuration_t::print() const
else {
throw logic_error("EDI destination not implemented");
}
+ etiLog.level(info) << " PFT=" << edi_dest->pft_settings.enable_pft;
+ if (edi_dest->pft_settings.enable_pft) {
+ etiLog.level(info) << " FEC=" << edi_dest->pft_settings.fec;
+ etiLog.level(info) << " Chunk Len=" << edi_dest->pft_settings.chunk_len;
+ etiLog.level(info) << " Fragment spreading factor=" << edi_dest->pft_settings.fragment_spreading_factor;
+ }
}
}
Sender::Sender(const configuration_t& conf) :
- m_conf(conf),
- edi_pft(m_conf)
+ m_conf(conf)
{
if (m_conf.verbose) {
etiLog.level(info) << "Setup EDI Output";
@@ -71,37 +76,39 @@ Sender::Sender(const configuration_t& conf) :
for (const auto& edi_dest : m_conf.destinations) {
if (const auto udp_dest = dynamic_pointer_cast<edi::udp_destination_t>(edi_dest)) {
- auto udp_socket = std::make_shared<Socket::UDPSocket>(udp_dest->source_port);
+ Socket::UDPSocket udp_socket(udp_dest->source_port);
if (not udp_dest->source_addr.empty()) {
- udp_socket->setMulticastSource(udp_dest->source_addr.c_str());
- udp_socket->setMulticastTTL(udp_dest->ttl);
+ udp_socket.setMulticastSource(udp_dest->source_addr.c_str());
+ udp_socket.setMulticastTTL(udp_dest->ttl);
}
- udp_sockets.emplace(udp_dest.get(), udp_socket);
+ auto sender = make_shared<udp_sender_t>(
+ udp_dest->dest_addr,
+ udp_dest->dest_port,
+ std::move(udp_socket));
+ m_pft_spreaders.emplace_back(
+ make_shared<PFTSpreader>(udp_dest->pft_settings, sender));
}
else if (auto tcp_dest = dynamic_pointer_cast<edi::tcp_server_t>(edi_dest)) {
- auto dispatcher = make_shared<Socket::TCPDataDispatcher>(
- tcp_dest->max_frames_queued, tcp_dest->tcp_server_preroll_buffers);
-
- dispatcher->start(tcp_dest->listen_port, "0.0.0.0");
- tcp_dispatchers.emplace(tcp_dest.get(), dispatcher);
+ auto sender = make_shared<tcp_dispatcher_t>(
+ tcp_dest->listen_port,
+ tcp_dest->max_frames_queued,
+ tcp_dest->tcp_server_preroll_buffers);
+ m_pft_spreaders.emplace_back(
+ make_shared<PFTSpreader>(tcp_dest->pft_settings, sender));
}
else if (auto tcp_dest = dynamic_pointer_cast<edi::tcp_client_t>(edi_dest)) {
- auto tcp_send_client = make_shared<Socket::TCPSendClient>(tcp_dest->dest_addr, tcp_dest->dest_port);
- tcp_senders.emplace(tcp_dest.get(), tcp_send_client);
+ auto sender = make_shared<tcp_send_client_t>(tcp_dest->dest_addr, tcp_dest->dest_port);
+ m_pft_spreaders.emplace_back(
+ make_shared<PFTSpreader>(tcp_dest->pft_settings, sender));
}
else {
throw logic_error("EDI destination not implemented");
}
}
- if (m_conf.dump) {
- edi_debug_file.open("./edi.debug");
- }
-
- if (m_conf.enable_pft) {
- unique_lock<mutex> lock(m_mutex);
+ {
m_running = true;
m_thread = thread(&Sender::run, this);
}
@@ -111,10 +118,52 @@ Sender::Sender(const configuration_t& conf) :
}
}
+void Sender::write(const TagPacket& tagpacket)
+{
+ // Assemble into one AF Packet
+ edi::AFPacket af_packet = edi_af_packetiser.Assemble(tagpacket);
+
+ write(af_packet);
+}
+
+void Sender::write(const AFPacket& af_packet)
+{
+ for (auto& sender : m_pft_spreaders) {
+ sender->send_af_packet(af_packet);
+ }
+}
+
+void Sender::override_af_sequence(uint16_t seq)
+{
+ edi_af_packetiser.OverrideSeq(seq);
+}
+
+void Sender::override_pft_sequence(uint16_t pseq)
+{
+ for (auto& spreader : m_pft_spreaders) {
+ spreader->edi_pft.OverridePSeq(pseq);
+ }
+}
+
+std::vector<Sender::stats_t> Sender::get_tcp_server_stats() const
+{
+ std::vector<Sender::stats_t> stats;
+
+ for (auto& spreader : m_pft_spreaders) {
+ if (auto sender = std::dynamic_pointer_cast<tcp_dispatcher_t>(spreader->sender)) {
+ Sender::stats_t s;
+ s.listen_port = sender->listen_port;
+ s.stats = sender->sock.get_stats();
+ stats.push_back(s);
+ }
+ }
+
+ return stats;
+}
+
Sender::~Sender()
{
{
- unique_lock<mutex> lock(m_mutex);
m_running = false;
}
@@ -123,36 +172,89 @@ Sender::~Sender()
}
}
-void Sender::write(const TagPacket& tagpacket)
+void Sender::run()
{
- // Assemble into one AF Packet
- edi::AFPacket af_packet = edi_afPacketiser.Assemble(tagpacket);
+ while (m_running) {
+ const auto now = chrono::steady_clock::now();
+ for (auto& spreader : m_pft_spreaders) {
+ spreader->tick(now);
+ }
- write(af_packet);
+ this_thread::sleep_for(chrono::microseconds(500));
+ }
}
-void Sender::write(const AFPacket& af_packet)
+
+void Sender::udp_sender_t::send_packet(const std::vector<uint8_t> &frame)
+{
+ Socket::InetAddress addr;
+ addr.resolveUdpDestination(dest_addr, dest_port);
+ sock.send(frame, addr);
+}
+
+void Sender::tcp_dispatcher_t::send_packet(const std::vector<uint8_t> &frame)
{
- if (m_conf.enable_pft) {
+ sock.write(frame);
+}
+
+void Sender::tcp_send_client_t::send_packet(const std::vector<uint8_t> &frame)
+{
+ sock.sendall(frame);
+}
+
+Sender::udp_sender_t::udp_sender_t(std::string dest_addr,
+ uint16_t dest_port,
+ Socket::UDPSocket&& sock) :
+ dest_addr(dest_addr),
+ dest_port(dest_port),
+ sock(std::move(sock))
+{
+}
+
+Sender::tcp_dispatcher_t::tcp_dispatcher_t(uint16_t listen_port,
+ size_t max_frames_queued,
+ size_t tcp_server_preroll_buffers) :
+ listen_port(listen_port),
+ sock(max_frames_queued, tcp_server_preroll_buffers)
+{
+ sock.start(listen_port, "0.0.0.0");
+}
+
+Sender::tcp_send_client_t::tcp_send_client_t(const std::string &dest_addr,
+ uint16_t dest_port) :
+ sock(dest_addr, dest_port)
+{
+}
+
+Sender::PFTSpreader::PFTSpreader(const pft_settings_t& conf, sender_sp sender) :
+ sender(sender),
+ edi_pft(conf)
+{
+}
+
+void Sender::PFTSpreader::send_af_packet(const AFPacket& af_packet)
+{
+ using namespace std::chrono;
+ if (edi_pft.is_enabled()) {
// Apply PFT layer to AF Packet (Reed Solomon FEC and Fragmentation)
vector<edi::PFTFragment> edi_fragments = edi_pft.Assemble(af_packet);
- if (m_conf.verbose and m_last_num_pft_fragments != edi_fragments.size()) {
+ if (settings.verbose and last_num_pft_fragments != edi_fragments.size()) {
etiLog.log(debug, "EDI Output: Number of PFT fragments %zu\n",
edi_fragments.size());
- m_last_num_pft_fragments = edi_fragments.size();
+ last_num_pft_fragments = edi_fragments.size();
}
/* Spread out the transmission of all fragments over part of the 24ms AF packet duration
* to reduce the risk of losing a burst of fragments because of congestion. */
- using namespace std::chrono;
auto inter_fragment_wait_time = microseconds(1);
if (edi_fragments.size() > 1) {
- if (m_conf.fragment_spreading_factor > 0) {
+ if (settings.fragment_spreading_factor > 0) {
inter_fragment_wait_time =
- microseconds(
- llrint(m_conf.fragment_spreading_factor * 24000.0 / edi_fragments.size())
- );
+ microseconds(llrint(
+ settings.fragment_spreading_factor * 24000.0 /
+ edi_fragments.size()
+ ));
}
}
@@ -162,99 +264,35 @@ void Sender::write(const AFPacket& af_packet)
auto tp = now;
unique_lock<mutex> lock(m_mutex);
for (auto& edi_frag : edi_fragments) {
- m_pending_frames[tp] = move(edi_frag);
+ m_pending_frames[tp] = std::move(edi_frag);
tp += inter_fragment_wait_time;
}
}
-
- // Transmission done in run() function
}
else /* PFT disabled */ {
- // Send over ethernet
- if (m_conf.dump) {
- ostream_iterator<uint8_t> debug_iterator(edi_debug_file);
- copy(af_packet.begin(), af_packet.end(), debug_iterator);
- }
-
- for (auto& dest : m_conf.destinations) {
- if (const auto& udp_dest = dynamic_pointer_cast<edi::udp_destination_t>(dest)) {
- Socket::InetAddress addr;
- addr.resolveUdpDestination(udp_dest->dest_addr, udp_dest->dest_port);
-
- if (af_packet.size() > 1400 and not m_udp_fragmentation_warning_printed) {
- fprintf(stderr, "EDI Output: AF packet larger than 1400,"
- " consider using PFT to avoid UP fragmentation.\n");
- m_udp_fragmentation_warning_printed = true;
- }
-
- udp_sockets.at(udp_dest.get())->send(af_packet, addr);
- }
- else if (auto tcp_dest = dynamic_pointer_cast<edi::tcp_server_t>(dest)) {
- tcp_dispatchers.at(tcp_dest.get())->write(af_packet);
- }
- else if (auto tcp_dest = dynamic_pointer_cast<edi::tcp_client_t>(dest)) {
- tcp_senders.at(tcp_dest.get())->sendall(af_packet);
- }
- else {
- throw logic_error("EDI destination not implemented");
- }
- }
+ const auto now = steady_clock::now();
+ unique_lock<mutex> lock(m_mutex);
+ m_pending_frames[now] = std::move(af_packet);
}
-}
-void Sender::override_af_sequence(uint16_t seq)
-{
- edi_afPacketiser.OverrideSeq(seq);
+ // Actual transmission done in tick() function
}
-void Sender::override_pft_sequence(uint16_t pseq)
+void Sender::PFTSpreader::tick(const std::chrono::steady_clock::time_point& now)
{
- edi_pft.OverridePSeq(pseq);
-}
+ unique_lock<mutex> lock(m_mutex);
-void Sender::run()
-{
- while (m_running) {
- unique_lock<mutex> lock(m_mutex);
- const auto now = chrono::steady_clock::now();
+ for (auto it = m_pending_frames.begin(); it != m_pending_frames.end(); ) {
+ const auto& edi_frag = it->second;
- // Send over ethernet
- for (auto it = m_pending_frames.begin(); it != m_pending_frames.end(); ) {
- const auto& edi_frag = it->second;
-
- if (it->first <= now) {
- if (m_conf.dump) {
- ostream_iterator<uint8_t> debug_iterator(edi_debug_file);
- copy(edi_frag.begin(), edi_frag.end(), debug_iterator);
- }
-
- for (auto& dest : m_conf.destinations) {
- if (const auto& udp_dest = dynamic_pointer_cast<edi::udp_destination_t>(dest)) {
- Socket::InetAddress addr;
- addr.resolveUdpDestination(udp_dest->dest_addr, udp_dest->dest_port);
-
- udp_sockets.at(udp_dest.get())->send(edi_frag, addr);
- }
- else if (auto tcp_dest = dynamic_pointer_cast<edi::tcp_server_t>(dest)) {
- tcp_dispatchers.at(tcp_dest.get())->write(edi_frag);
- }
- else if (auto tcp_dest = dynamic_pointer_cast<edi::tcp_client_t>(dest)) {
- tcp_senders.at(tcp_dest.get())->sendall(edi_frag);
- }
- else {
- throw logic_error("EDI destination not implemented");
- }
- }
- it = m_pending_frames.erase(it);
- }
- else {
- ++it;
- }
+ if (it->first <= now) {
+ sender->send_packet(edi_frag);
+ it = m_pending_frames.erase(it);
+ }
+ else {
+ ++it;
}
-
- lock.unlock();
- this_thread::sleep_for(chrono::microseconds(500));
}
}
-}
+} // namespace edi
diff --git a/lib/edioutput/Transport.h b/lib/edioutput/Transport.h
index 6a3f229..b8a9008 100644
--- a/lib/edioutput/Transport.h
+++ b/lib/edioutput/Transport.h
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2022
+ Copyright (C) 2025
Matthias P. Braendli, matthias.braendli@mpb.li
http://www.opendigitalradio.org
@@ -31,25 +31,23 @@
#include "AFPacket.h"
#include "PFT.h"
#include "Socket.h"
-#include <vector>
#include <chrono>
#include <map>
-#include <unordered_map>
-#include <stdexcept>
-#include <fstream>
#include <cstdint>
#include <thread>
#include <mutex>
+#include <vector>
namespace edi {
-/** STI sender for EDI output */
-
+/** ETI/STI sender for EDI output */
class Sender {
public:
Sender(const configuration_t& conf);
Sender(const Sender&) = delete;
- Sender operator=(const Sender&) = delete;
+ Sender& operator=(const Sender&) = delete;
+ Sender(Sender&&) = delete;
+ Sender& operator=(Sender&&) = delete;
~Sender();
// Assemble the tagpacket into an AF packet, and if needed,
@@ -66,33 +64,85 @@ class Sender {
void override_af_sequence(uint16_t seq);
void override_pft_sequence(uint16_t pseq);
- private:
- void run();
-
- bool m_udp_fragmentation_warning_printed = false;
+ struct stats_t {
+ uint16_t listen_port;
+ std::vector<Socket::TCPConnection::stats_t> stats;
+ };
+ std::vector<stats_t> get_tcp_server_stats() const;
+ private:
configuration_t m_conf;
- std::ofstream edi_debug_file;
// The TagPacket will then be placed into an AFPacket
- edi::AFPacketiser edi_afPacketiser;
+ edi::AFPacketiser edi_af_packetiser;
- // The AF Packet will be protected with reed-solomon and split in fragments
- edi::PFT edi_pft;
+ // PFT spreading requires sending UDP packets at specific time,
+ // independently of time when write() gets called
+ bool m_running = false;
+ std::thread m_thread;
+ virtual void run();
- std::unordered_map<udp_destination_t*, std::shared_ptr<Socket::UDPSocket>> udp_sockets;
- std::unordered_map<tcp_server_t*, std::shared_ptr<Socket::TCPDataDispatcher>> tcp_dispatchers;
- std::unordered_map<tcp_client_t*, std::shared_ptr<Socket::TCPSendClient>> tcp_senders;
- // PFT spreading requires sending UDP packets at specific time, independently of
- // time when write() gets called
- std::thread m_thread;
- std::mutex m_mutex;
- bool m_running = false;
- std::map<std::chrono::steady_clock::time_point, edi::PFTFragment> m_pending_frames;
- size_t m_last_num_pft_fragments = 0;
-};
-}
+ struct i_sender {
+ virtual void send_packet(const std::vector<uint8_t> &frame) = 0;
+ virtual ~i_sender() { }
+ };
+
+ struct udp_sender_t : public i_sender {
+ udp_sender_t(
+ std::string dest_addr,
+ uint16_t dest_port,
+ Socket::UDPSocket&& sock);
+
+ std::string dest_addr;
+ uint16_t dest_port;
+ Socket::UDPSocket sock;
+
+ virtual void send_packet(const std::vector<uint8_t> &frame) override;
+ };
+
+ struct tcp_dispatcher_t : public i_sender {
+ tcp_dispatcher_t(
+ uint16_t listen_port,
+ size_t max_frames_queued,
+ size_t tcp_server_preroll_buffers);
+
+ uint16_t listen_port;
+ Socket::TCPDataDispatcher sock;
+ virtual void send_packet(const std::vector<uint8_t> &frame) override;
+ };
+
+ struct tcp_send_client_t : public i_sender {
+ tcp_send_client_t(
+ const std::string& dest_addr,
+ uint16_t dest_port);
+
+ Socket::TCPSendClient sock;
+ virtual void send_packet(const std::vector<uint8_t> &frame) override;
+ };
+
+ class PFTSpreader {
+ public:
+ using sender_sp = std::shared_ptr<i_sender>;
+ PFTSpreader(const pft_settings_t &conf, sender_sp sender);
+ sender_sp sender;
+ edi::PFT edi_pft;
+
+ void send_af_packet(const AFPacket &af_packet);
+ void tick(const std::chrono::steady_clock::time_point& now);
+
+ private:
+ // send_af_packet() and tick() are called from different threads, both
+ // are accessing m_pending_frames
+ std::mutex m_mutex;
+ std::map<std::chrono::steady_clock::time_point, edi::PFTFragment> m_pending_frames;
+ pft_settings_t settings;
+ size_t last_num_pft_fragments = 0;
+ };
+
+ std::vector<std::shared_ptr<PFTSpreader>> m_pft_spreaders;
+};
+}
diff --git a/man/odr-dabmux.1 b/man/odr-dabmux.1
index 03b8df1..95f3181 100644
--- a/man/odr-dabmux.1
+++ b/man/odr-dabmux.1
@@ -1,4 +1,4 @@
-.TH ODR-DABMUX "1" "October 2024" "odr-dabmux 5.0.0" "User Commands"
+.TH ODR-DABMUX "1" "June 2025" "odr-dabmux 5.3.0" "User Commands"
.SH NAME
\fBodr\-dabmux\fR \- A software DAB multiplexer
.SH SYNOPSIS
diff --git a/src/ConfigParser.cpp b/src/ConfigParser.cpp
index 74e627b..7d166b6 100644
--- a/src/ConfigParser.cpp
+++ b/src/ConfigParser.cpp
@@ -36,16 +36,13 @@
# include "config.h"
#endif
-#include "dabOutput/dabOutput.h"
#include "utils.h"
-#include "DabMux.h"
-#include "ManagementServer.h"
#include "input/Edi.h"
#include "input/Prbs.h"
#include "input/Zmq.h"
#include "input/File.h"
#include "input/Udp.h"
-#include "Eti.h"
+#include "fig/FIG0structs.h"
#include <boost/property_tree/ptree.hpp>
#include <boost/algorithm/string/classification.hpp>
#include <boost/algorithm/string/split.hpp>
@@ -63,6 +60,12 @@ using namespace std;
using boost::property_tree::ptree;
using boost::property_tree::ptree_error;
+constexpr uint16_t DEFAULT_DATA_BITRATE = 384;
+constexpr uint16_t DEFAULT_PACKET_BITRATE = 32;
+
+constexpr uint32_t DEFAULT_SERVICE_ID = 50;
+
+
static void setup_subchannel_from_ptree(shared_ptr<DabSubchannel>& subchan,
const ptree &pt,
std::shared_ptr<dabEnsemble> ensemble,
diff --git a/src/DabMultiplexer.cpp b/src/DabMultiplexer.cpp
index bd1c909..52f053a 100644
--- a/src/DabMultiplexer.cpp
+++ b/src/DabMultiplexer.cpp
@@ -3,7 +3,7 @@
2011, 2012 Her Majesty the Queen in Right of Canada (Communications
Research Center Canada)
- Copyright (C) 2024
+ Copyright (C) 2025
Matthias P. Braendli, matthias.braendli@mpb.li
*/
/*
@@ -23,11 +23,14 @@
along with ODR-DabMux. If not, see <http://www.gnu.org/licenses/>.
*/
+#include <cmath>
#include <set>
#include <memory>
#include "DabMultiplexer.h"
#include "ConfigParser.h"
-#include "fig/FIG.h"
+#include "ManagementServer.h"
+#include "crc.h"
+#include "utils.h"
using namespace std;
@@ -44,16 +47,101 @@ static vector<string> split_pipe_separated_string(const std::string& s)
return components;
}
-DabMultiplexer::DabMultiplexer(
- boost::property_tree::ptree pt) :
+uint64_t MuxTime::init(uint32_t tist_at_fct0_ms, double tist_offset)
+{
+ // Things we must guarantee, up to granularity of 24ms:
+ // Difference between current time and EDI time = tist_offset
+ // TIST of frame 0 = tist_at_fct0_ms
+ // In order to achieve the second, we calculate the initial
+ // counter value so that FCT0 corresponds to the desired TIST.
+ //
+ // Changing the tist_offset at runtime will throw off the TIST@FCT0 value
+ m_tist_offset_ms = std::lround(tist_offset * 1000);
+
+ using Sec = chrono::seconds;
+ const auto now = chrono::system_clock::now() +
+ chrono::milliseconds(std::lround(tist_offset * 1000.0));
+
+ const auto offset = now - chrono::time_point_cast<Sec>(now);
+ if (offset >= chrono::seconds(1)) {
+ throw std::logic_error("Invalid startup offset calculation for TIST! " +
+ to_string(chrono::duration_cast<chrono::milliseconds>(offset).count()) +
+ " ms");
+ }
+ const time_t t_now = chrono::system_clock::to_time_t(chrono::time_point_cast<Sec>(now));
+ const auto offset_ms = chrono::duration_cast<chrono::milliseconds>(offset).count();
+
+ m_edi_time = t_now;
+ m_pps_offset_ms = std::lround(offset_ms / 24.0) * 24;
+
+ const auto counter_offset = tist_at_fct0_ms / 24;
+ const auto offset_as_count = m_pps_offset_ms / 24;
+
+ etiLog.level(debug) << "Init " << counter_offset << " " << offset_as_count;
+
+ return (250 - counter_offset + offset_as_count) % 250;
+}
+
+constexpr int TIMESTAMP_LEVEL_2_SHIFT = 14;
+
+void MuxTime::increment_timestamp()
+{
+ m_pps_offset_ms += 24;
+ if (m_pps_offset_ms >= 1000) {
+ m_pps_offset_ms -= 1000;
+ m_edi_time += 1;
+
+ // Also update MNSC time for next time FP==0
+ mnsc_increment_time = true;
+ }
+}
+
+void MuxTime::set_tist_offset(double new_tist_offset)
+{
+ const int32_t new_tist_offset_ms = std::lround(new_tist_offset * 1000.0);
+ int32_t delta = m_tist_offset_ms - new_tist_offset_ms;
+ if (delta > 0) {
+ while (delta > 0) {
+ increment_timestamp();
+ delta -= 24;
+ }
+ }
+ else if (delta < 0) {
+ while (delta < 0) {
+ m_edi_time -= 1;
+ delta += 1000;
+ }
+ // compensate the we subtracted too much
+ while (delta > 0) {
+ increment_timestamp();
+ delta -= 24;
+ }
+ }
+ m_tist_offset_ms = new_tist_offset_ms;
+}
+
+std::pair<uint32_t, std::time_t> MuxTime::get_tist_seconds()
+{
+ auto timestamp = m_pps_offset_ms * 16384;
+ return {timestamp % 0xfa0000, m_edi_time};
+}
+
+std::pair<uint32_t, std::time_t> MuxTime::get_milliseconds_seconds()
+{
+ auto tist_seconds = get_tist_seconds();
+ return {tist_seconds.first >> TIMESTAMP_LEVEL_2_SHIFT, tist_seconds.second};
+}
+
+
+DabMultiplexer::DabMultiplexer(boost::property_tree::ptree pt) :
RemoteControllable("mux"),
m_pt(pt),
+ m_time(),
ensemble(std::make_shared<dabEnsemble>()),
m_clock_tai(split_pipe_separated_string(pt.get("general.tai_clock_bulletins", ""))),
- fig_carousel(ensemble)
+ fig_carousel(ensemble, [&]() { return m_time.get_milliseconds_seconds(); })
{
RC_ADD_PARAMETER(frames, "Show number of frames generated [read-only]");
- RC_ADD_PARAMETER(tist_offset, "Timestamp offset in integral number of seconds");
rcs.enrol(&m_clock_tai);
}
@@ -99,59 +187,20 @@ void DabMultiplexer::prepare(bool require_tai_clock)
throw MuxInitException();
}
- /* At startup, derive edi_time, TIST and CIF count such that there is
- * a consistency across mux restarts. Ensure edi_time and TIST represent
- * current time.
- *
- * FCT and DLFC are directly derived from m_currentFrame.
- * Every 6s, FCT overflows. DLFC overflows at 5000 every 120s.
- *
- * Keep a granularity of 24ms, which corresponds to the duration of an ETI
- * frame, to get nicer timestamps.
- */
- using Sec = chrono::seconds;
- const auto now = chrono::system_clock::now();
- const time_t t_now = chrono::system_clock::to_time_t(chrono::time_point_cast<Sec>(now));
-
- m_edi_time = t_now - (t_now % 6);
- m_currentFrame = 0;
- time_t edi_time_at_cif0 = t_now - (t_now % 120);
- while (edi_time_at_cif0 < m_edi_time) {
- edi_time_at_cif0 += 6;
- m_currentFrame += 250;
- }
-
- if (edi_time_at_cif0 != m_edi_time) {
- throw std::logic_error("Invalid startup offset calculation for CIF!");
- }
-
- const auto offset = now - chrono::time_point_cast<Sec>(now);
- if (offset >= chrono::seconds(1)) {
- throw std::logic_error("Invalid startup offset calculation for TIST! " +
- to_string(chrono::duration_cast<chrono::milliseconds>(offset).count()) +
- " ms");
- }
-
- int64_t offset_ms = chrono::duration_cast<chrono::milliseconds>(offset).count();
- offset_ms += 1000 * (t_now - m_edi_time);
-
- m_timestamp = 0;
- while (offset_ms >= 24) {
- increment_timestamp();
- m_currentFrame++;
- offset_ms -= 24;
- }
-
- mnsc_increment_time = false;
+ const uint32_t tist_at_fct0_ms = m_pt.get<double>("general.tist_at_fct0", 0);
+ currentFrame = m_time.init(tist_at_fct0_ms, m_pt.get<double>("general.tist_offset", 0.0));
+ m_time.mnsc_increment_time = false;
bool tist_enabled = m_pt.get("general.tist", false);
- m_tist_offset = m_pt.get<int>("general.tist_offset", 0);
- mnsc_time = m_edi_time + m_tist_offset;
+ auto tist_edi_time = m_time.get_tist_seconds();
+ const auto timestamp = tist_edi_time.first;
+ const auto edi_time = tist_edi_time.second;
+ m_time.mnsc_time = edi_time;
etiLog.log(info, "Startup CIF Count %i with timestamp: %d + %f",
- m_currentFrame, m_edi_time,
- (m_timestamp & 0xFFFFFF) / 16384000.0);
+ currentFrame, edi_time,
+ (timestamp & 0xFFFFFF) / 16384000.0);
// Try to load offset once
@@ -391,17 +440,6 @@ void DabMultiplexer::prepare_data_inputs()
}
}
-void DabMultiplexer::increment_timestamp()
-{
- m_timestamp += 24 << 14; // Shift 24ms by 14 to Timestamp level 2
- if (m_timestamp > 0xf9FFff) {
- m_timestamp -= 0xfa0000; // Substract 16384000, corresponding to one second
- m_edi_time += 1;
-
- // Also update MNSC time for next time FP==0
- mnsc_increment_time = true;
- }
-}
/* Each call creates one ETI frame */
void DabMultiplexer::mux_frame(std::vector<std::shared_ptr<DabOutput> >& outputs)
@@ -432,9 +470,14 @@ void DabMultiplexer::mux_frame(std::vector<std::shared_ptr<DabOutput> >& outputs
etiLog.level(error) << "Could not get UTC-TAI offset for EDI timestamp";
}
}
- update_dab_time();
- const auto edi_time = m_edi_time + m_tist_offset;
+ auto tist_edi_time = m_time.get_tist_seconds();
+ const auto timestamp = tist_edi_time.first;
+ const auto edi_time = tist_edi_time.second;
+ /*
+ etiLog.level(debug) << "Frame " << currentFrame << " " << edi_time <<
+ " + " << (timestamp >> TIMESTAMP_LEVEL_2_SHIFT);
+ */
// Initialise the ETI frame
memset(etiFrame, 0, 6144);
@@ -450,7 +493,7 @@ void DabMultiplexer::mux_frame(std::vector<std::shared_ptr<DabOutput> >& outputs
//****** Field FSYNC *****//
// See ETS 300 799, 6.2.1.2
- if ((m_currentFrame & 1) == 0) {
+ if ((currentFrame & 1) == 0) {
etiSync->FSYNC = ETI_FSYNC1;
}
else {
@@ -468,9 +511,8 @@ void DabMultiplexer::mux_frame(std::vector<std::shared_ptr<DabOutput> >& outputs
eti_FC *fc = (eti_FC *) &etiFrame[4];
//****** FCT ******//
- // Incremente for each frame, overflows at 249
- fc->FCT = m_currentFrame % 250;
- edi_tagDETI.dlfc = m_currentFrame % 5000;
+ fc->FCT = currentFrame % 250;
+ edi_tagDETI.dlfc = currentFrame % 5000;
//****** FICF ******//
// Fast Information Channel Flag, 1 bit, =1 if FIC present
@@ -487,7 +529,7 @@ void DabMultiplexer::mux_frame(std::vector<std::shared_ptr<DabOutput> >& outputs
/* Frame Phase, 3 bit counter, tells the COFDM generator
* when to insert the TII. Is also used by the MNSC.
*/
- fc->FP = edi_tagDETI.fp = m_currentFrame & 0x7;
+ fc->FP = edi_tagDETI.fp = currentFrame & 0x7;
//****** MID ******//
//Mode Identity, 2 bits, 01 ModeI, 10 modeII, 11 ModeIII, 00 ModeIV
@@ -562,7 +604,7 @@ void DabMultiplexer::mux_frame(std::vector<std::shared_ptr<DabOutput> >& outputs
eoh->MNSC = 0;
struct tm time_tm;
- gmtime_r(&mnsc_time, &time_tm);
+ gmtime_r(&m_time.mnsc_time, &time_tm);
switch (fc->FP & 0x3) {
case 0:
@@ -574,10 +616,10 @@ void DabMultiplexer::mux_frame(std::vector<std::shared_ptr<DabOutput> >& outputs
mnsc->rfa = 0;
}
- if (mnsc_increment_time)
+ if (m_time.mnsc_increment_time)
{
- mnsc_increment_time = false;
- mnsc_time += 1;
+ m_time.mnsc_increment_time = false;
+ m_time.mnsc_time += 1;
}
break;
case 1:
@@ -621,7 +663,7 @@ void DabMultiplexer::mux_frame(std::vector<std::shared_ptr<DabOutput> >& outputs
// Insert all FIBs
const bool fib3_present = (ensemble->transmission_mode == TransmissionMode_e::TM_III);
- index += fig_carousel.write_fibs(&etiFrame[index], m_currentFrame, fib3_present);
+ index += fig_carousel.write_fibs(&etiFrame[index], currentFrame, fib3_present);
/**********************************************************************
****** Input Data Reading *******************************************
@@ -633,12 +675,13 @@ void DabMultiplexer::mux_frame(std::vector<std::shared_ptr<DabOutput> >& outputs
int sizeSubchannel = subchannel->getSizeByte();
// no need to check enableTist because we always increment the timestamp
int result = subchannel->readFrame(&etiFrame[index],
- sizeSubchannel, edi_time, tai_utc_offset, m_timestamp);
+ sizeSubchannel,
+ edi_time, tai_utc_offset, timestamp);
if (result < 0) {
etiLog.log(info,
"Subchannel %d read failed at ETI frame number: %d",
- subchannel->id, m_currentFrame);
+ subchannel->id, currentFrame);
}
// save pointer to Audio or Data Stream into correct TagESTn for EDI
@@ -678,8 +721,8 @@ void DabMultiplexer::mux_frame(std::vector<std::shared_ptr<DabOutput> >& outputs
bool enableTist = m_pt.get("general.tist", false);
if (enableTist) {
- tist->TIST = htonl(m_timestamp) | 0xff;
- edi_tagDETI.tsta = m_timestamp & 0xffffff;
+ tist->TIST = htonl(timestamp) | 0xff;
+ edi_tagDETI.tsta = timestamp & 0xffffff;
}
else {
tist->TIST = htonl(0xffffff) | 0xff;
@@ -700,7 +743,7 @@ void DabMultiplexer::mux_frame(std::vector<std::shared_ptr<DabOutput> >& outputs
output->setMetadata(md_edi_time);
shared_ptr<OutputMetadata> md_dlfc =
- make_shared<OutputMetadataDLFC>(m_currentFrame % 5000);
+ make_shared<OutputMetadataDLFC>(currentFrame % 5000);
output->setMetadata(md_dlfc);
}
}
@@ -716,8 +759,7 @@ void DabMultiplexer::mux_frame(std::vector<std::shared_ptr<DabOutput> >& outputs
Approximate 8 ms 1 ms 3,91 us 488 ns 61 ns
time resolution
*/
-
- increment_timestamp();
+ m_time.increment_timestamp();
/**********************************************************************
*********** Section FRPD *****************************************
@@ -759,6 +801,12 @@ void DabMultiplexer::mux_frame(std::vector<std::shared_ptr<DabOutput> >& outputs
}
edi_sender->write(edi_tagpacket);
+
+ for (const auto& stat : edi_sender->get_tcp_server_stats()) {
+ get_mgmt_server().update_edi_tcp_output_stat(
+ stat.listen_port,
+ stat.stats.size());
+ }
}
#if _DEBUG
@@ -769,7 +817,7 @@ void DabMultiplexer::mux_frame(std::vector<std::shared_ptr<DabOutput> >& outputs
if (enableTist) {
etiLog.log(info, "ETI frame number %i Timestamp: %d + %f",
m_currentFrame, edi_time,
- (m_timestamp & 0xFFFFFF) / 16384000.0);
+ (timestamp & 0xFFFFFF) / 16384000.0);
}
else {
etiLog.log(info, "ETI frame number %i Time: %d, no TIST",
@@ -778,7 +826,7 @@ void DabMultiplexer::mux_frame(std::vector<std::shared_ptr<DabOutput> >& outputs
}
#endif
- m_currentFrame++;
+ currentFrame++;
}
void DabMultiplexer::print_info()
@@ -799,7 +847,7 @@ void DabMultiplexer::set_parameter(const std::string& parameter,
throw ParameterError(ss.str());
}
else if (parameter == "tist_offset") {
- m_tist_offset = std::stoi(value);
+ m_time.set_tist_offset(std::stod(value));
}
else {
stringstream ss;
@@ -814,10 +862,10 @@ const std::string DabMultiplexer::get_parameter(const std::string& parameter) co
{
stringstream ss;
if (parameter == "frames") {
- ss << m_currentFrame;
+ ss << currentFrame;
}
else if (parameter == "tist_offset") {
- ss << m_tist_offset;
+ ss << m_time.tist_offset();
}
else {
ss << "Parameter '" << parameter <<
@@ -831,8 +879,8 @@ const std::string DabMultiplexer::get_parameter(const std::string& parameter) co
const json::map_t DabMultiplexer::get_all_values() const
{
json::map_t map;
- map["frames"].v = m_currentFrame;
- map["tist_offset"].v = m_tist_offset;
+ map["frames"].v = currentFrame;
+ map["tist_offset"].v = m_time.tist_offset();
return map;
}
diff --git a/src/DabMultiplexer.h b/src/DabMultiplexer.h
index 89d547e..9306eed 100644
--- a/src/DabMultiplexer.h
+++ b/src/DabMultiplexer.h
@@ -3,7 +3,7 @@
2011, 2012 Her Majesty the Queen in Right of Canada (Communications
Research Center Canada)
- Copyright (C) 2024
+ Copyright (C) 2025
Matthias P. Braendli, matthias.braendli@mpb.li
*/
/*
@@ -30,21 +30,12 @@
#endif
#include "dabOutput/dabOutput.h"
-#include "edioutput/TagItems.h"
-#include "edioutput/TagPacket.h"
-#include "edioutput/AFPacket.h"
#include "edioutput/Transport.h"
#include "fig/FIGCarousel.h"
-#include "crc.h"
-#include "utils.h"
-#include "Socket.h"
-#include "PcDebug.h"
#include "MuxElements.h"
#include "RemoteControl.h"
-#include "Eti.h"
#include "ClockTAI.h"
#include <vector>
-#include <chrono>
#include <memory>
#include <string>
#include <memory>
@@ -52,6 +43,37 @@
constexpr uint32_t ETI_FSYNC1 = 0x49C5F8;
+class MuxTime {
+ private:
+ std::time_t m_edi_time = 0;
+ uint32_t m_pps_offset_ms = 0;
+ int64_t m_tist_offset_ms = 0;
+
+ public:
+ std::pair<uint32_t, std::time_t> get_tist_seconds();
+ std::pair<uint32_t, std::time_t> get_milliseconds_seconds();
+
+
+ /* Pre v3 odr-dabmux did the MNSC calculation differently,
+ * which works with the easydabv2. The rework in odr-dabmux,
+ * deriving MNSC time from EDI time broke this.
+ *
+ * That's why we're now tracking MNSC time in separate variables,
+ * to get the same behaviour back.
+ *
+ * I'm not aware of any devices using MNSC time besides the
+ * easydab. ODR-DabMod now considers EDI seconds or ZMQ metadata.
+ */
+ bool mnsc_increment_time = false;
+ std::time_t mnsc_time = 0;
+
+ /* Setup the time and return the initial currentFrame counter value */
+ uint64_t init(uint32_t tist_at_fct0_ms, double tist_offset);
+ void increment_timestamp();
+ double tist_offset() const { return m_tist_offset_ms / 1000.0; }
+ void set_tist_offset(double new_tist_offset);
+};
+
class DabMultiplexer : public RemoteControllable {
public:
DabMultiplexer(boost::property_tree::ptree pt);
@@ -61,8 +83,6 @@ class DabMultiplexer : public RemoteControllable {
void prepare(bool require_tai_clock);
- uint64_t getCurrentFrame() const { return m_currentFrame; }
-
void mux_frame(std::vector<std::shared_ptr<DabOutput> >& outputs);
void print_info(void);
@@ -82,60 +102,19 @@ class DabMultiplexer : public RemoteControllable {
void prepare_subchannels(void);
void prepare_services_components(void);
void prepare_data_inputs(void);
- void increment_timestamp(void);
boost::property_tree::ptree m_pt;
- uint32_t m_timestamp = 0;
- std::time_t m_edi_time = 0;
-
- /* Pre v3 odr-dabmux did the MNSC calculation differently,
- * which works with the easydabv2. The rework in odr-dabmux,
- * deriving MNSC time from EDI time broke this.
- *
- * That's why we're now tracking MNSC time in separate variables,
- * to get the same behaviour back.
- *
- * I'm not aware of any devices using MNSC time besides the
- * easydab. ODR-DabMod now considers EDI seconds or ZMQ metadata.
- */
- bool mnsc_increment_time = false;
- std::time_t mnsc_time = 0;
+ MuxTime m_time;
+ uint64_t currentFrame = 0;
edi::configuration_t edi_conf;
std::shared_ptr<edi::Sender> edi_sender;
- uint64_t m_currentFrame = 0;
-
std::shared_ptr<dabEnsemble> ensemble;
- int m_tist_offset = 0;
bool m_tai_clock_required = false;
ClockTAI m_clock_tai;
- /* New FIG Carousel */
FIC::FIGCarousel fig_carousel;
};
-
-// DAB Mode
-#define DEFAULT_DAB_MODE 1
-
-// Taille de la trame de donnee, sous-canal 3, nb de paquets de 64bits,
-// STL3 * 8 = x kbytes par trame ETI
-
-// Data bitrate in kbits/s. Must be 64 kb/s multiple.
-#define DEFAULT_DATA_BITRATE 384
-#define DEFAULT_PACKET_BITRATE 32
-
-/* default ensemble parameters. Label must be max 16 chars, short label
- * a subset of the label, max 8 chars
- */
-#define DEFAULT_ENSEMBLE_LABEL "ODR Dab Mux"
-#define DEFAULT_ENSEMBLE_SHORT_LABEL "ODRMux"
-#define DEFAULT_ENSEMBLE_ID 0xc000
-#define DEFAULT_ENSEMBLE_ECC 0xa1
-
-// start value for default service IDs (if not overridden by configuration)
-#define DEFAULT_SERVICE_ID 50
-#define DEFAULT_PACKET_ADDRESS 0
-
diff --git a/src/DabMux.cpp b/src/DabMux.cpp
index 1a367da..bf525c1 100644
--- a/src/DabMux.cpp
+++ b/src/DabMux.cpp
@@ -327,6 +327,38 @@ int main(int argc, char *argv[])
if (outputuid == "edi") {
ptree pt_edi = pt_outputs.get_child("edi");
+ bool default_enable_pft = pt_edi.get<bool>("enable_pft", false);
+ edi_conf.verbose = pt_edi.get<bool>("verbose", false);
+
+ unsigned int default_fec = pt_edi.get<unsigned int>("fec", 3);
+ unsigned int default_chunk_len = pt_edi.get<unsigned int>("chunk_len", 207);
+
+ auto check_spreading_factor = [](int percent) {
+ if (percent < 0) {
+ throw std::runtime_error("EDI output: negative packet_spread value is invalid.");
+ }
+ double factor = (double)percent / 100.0;
+ if (factor > 30000) {
+ throw std::runtime_error("EDI output: interleaving set for more than 30 seconds!");
+ }
+ return factor;
+ };
+
+ double default_spreading_factor = check_spreading_factor(pt_edi.get<int>("packet_spread", 95));
+
+ using pt_t = boost::property_tree::basic_ptree<std::basic_string<char>, std::basic_string<char>>;
+ auto handle_overrides = [&](edi::pft_settings_t& pft_settings, pt_t pt) {
+ pft_settings.chunk_len = pt.get<unsigned int>("chunk_len", default_chunk_len);
+ pft_settings.enable_pft = pt.get<bool>("enable_pft", default_enable_pft);
+ pft_settings.fec = pt.get<unsigned int>("fec", default_fec);
+ pft_settings.fragment_spreading_factor = default_spreading_factor;
+ auto override_spread_percent = pt.get_optional<int>("packet_spread");
+ if (override_spread_percent) {
+ pft_settings.fragment_spreading_factor = check_spreading_factor(*override_spread_percent);
+ }
+ pft_settings.verbose = pt.get<bool>("verbose", edi_conf.verbose);
+ };
+
for (auto pt_edi_dest : pt_edi.get_child("destinations")) {
const auto proto = pt_edi_dest.second.get<string>("protocol", "udp");
if (proto == "udp") {
@@ -346,6 +378,8 @@ int main(int argc, char *argv[])
dest->dest_port = pt_edi.get<unsigned int>("port");
}
+ handle_overrides(dest->pft_settings, pt_edi_dest.second);
+
edi_conf.destinations.push_back(dest);
}
else if (proto == "tcp") {
@@ -355,6 +389,8 @@ int main(int argc, char *argv[])
double preroll = pt_edi_dest.second.get<double>("preroll-burst", 0.0);
dest->tcp_server_preroll_buffers = ceil(preroll / 24e-3);
+ handle_overrides(dest->pft_settings, pt_edi_dest.second);
+
edi_conf.destinations.push_back(dest);
}
else {
@@ -362,22 +398,6 @@ int main(int argc, char *argv[])
}
}
- edi_conf.dump = pt_edi.get<bool>("dump", false);
- edi_conf.enable_pft = pt_edi.get<bool>("enable_pft", false);
- edi_conf.verbose = pt_edi.get<bool>("verbose", false);
-
- edi_conf.fec = pt_edi.get<unsigned int>("fec", 3);
- edi_conf.chunk_len = pt_edi.get<unsigned int>("chunk_len", 207);
-
- int spread_percent = pt_edi.get<int>("packet_spread", 95);
- if (spread_percent < 0) {
- throw std::runtime_error("EDI output: negative packet_spread value is invalid.");
- }
- edi_conf.fragment_spreading_factor = (double)spread_percent / 100.0;
- if (edi_conf.fragment_spreading_factor > 30000) {
- throw std::runtime_error("EDI output: interleaving set for more than 30 seconds!");
- }
-
edi_conf.tagpacket_alignment = pt_edi.get<unsigned int>("tagpacket_alignment", 8);
mux.set_edi_config(edi_conf);
diff --git a/src/ManagementServer.cpp b/src/ManagementServer.cpp
index 568e80e..dff093a 100644
--- a/src/ManagementServer.cpp
+++ b/src/ManagementServer.cpp
@@ -2,7 +2,7 @@
Copyright (C) 2009 Her Majesty the Queen in Right of Canada (Communications
Research Center Canada)
- Copyright (C) 2018
+ Copyright (C) 2025
Matthias P. Braendli, matthias.braendli@mpb.li
http://www.opendigitalradio.org
@@ -28,13 +28,12 @@
along with ODR-DabMux. If not, see <http://www.gnu.org/licenses/>.
*/
-#include <errno.h>
-#include <string.h>
-#include <math.h>
-#include <stdint.h>
-#include <limits>
#include <sstream>
#include <algorithm>
+#include <cstring>
+#include <cmath>
+#include <cstdint>
+#include <limits>
#include <boost/version.hpp>
#include "ManagementServer.h"
#include "Log.h"
@@ -127,37 +126,42 @@ ManagementServer& get_mgmt_server()
*/
}
-void ManagementServer::registerInput(InputStat* is)
+void ManagementServer::register_input(InputStat* is)
{
unique_lock<mutex> lock(m_statsmutex);
std::string id(is->get_name());
- if (m_inputStats.count(id) == 1) {
+ if (m_input_stats.count(id) == 1) {
etiLog.level(error) <<
"Double registration in MGMT Server with id '" <<
id << "'";
return;
}
- m_inputStats[id] = is;
+ m_input_stats[id] = is;
}
-void ManagementServer::unregisterInput(std::string id)
+void ManagementServer::unregister_input(std::string id)
{
unique_lock<mutex> lock(m_statsmutex);
- if (m_inputStats.count(id) == 1) {
- m_inputStats.erase(id);
+ if (m_input_stats.count(id) == 1) {
+ m_input_stats.erase(id);
}
}
+// outputs will never disappear, no need to have a "remove" logic
+void ManagementServer::update_edi_tcp_output_stat(uint16_t listen_port, size_t num_connections)
+{
+ m_output_stats[listen_port] = num_connections;
+}
bool ManagementServer::isInputRegistered(std::string& id)
{
unique_lock<mutex> lock(m_statsmutex);
- if (m_inputStats.count(id) == 0) {
+ if (m_input_stats.count(id) == 0) {
etiLog.level(error) <<
"Management Server: id '" <<
id << "' does was not registered";
@@ -166,7 +170,7 @@ bool ManagementServer::isInputRegistered(std::string& id)
return true;
}
-std::string ManagementServer::getStatConfigJSON()
+std::string ManagementServer::get_input_config_json()
{
unique_lock<mutex> lock(m_statsmutex);
@@ -175,7 +179,7 @@ std::string ManagementServer::getStatConfigJSON()
std::map<std::string,InputStat*>::iterator iter;
int i = 0;
- for(iter = m_inputStats.begin(); iter != m_inputStats.end();
+ for (iter = m_input_stats.begin(); iter != m_input_stats.end();
++iter, i++)
{
std::string id = iter->first;
@@ -192,16 +196,15 @@ std::string ManagementServer::getStatConfigJSON()
return ss.str();
}
-std::string ManagementServer::getValuesJSON()
+std::string ManagementServer::get_input_values_json()
{
unique_lock<mutex> lock(m_statsmutex);
std::ostringstream ss;
ss << "{ \"values\" : {\n";
- std::map<std::string,InputStat*>::iterator iter;
int i = 0;
- for(iter = m_inputStats.begin(); iter != m_inputStats.end();
+ for (auto iter = m_input_stats.begin(); iter != m_input_stats.end();
++iter, i++)
{
const std::string& id = iter->first;
@@ -220,6 +223,31 @@ std::string ManagementServer::getValuesJSON()
return ss.str();
}
+std::string ManagementServer::get_output_values_json()
+{
+ unique_lock<mutex> lock(m_statsmutex);
+
+ std::ostringstream ss;
+ ss << "{ \"output_values\" : {\n";
+
+ int i = 0;
+ for (auto iter = m_output_stats.begin(); iter != m_output_stats.end();
+ ++iter, i++)
+ {
+ auto listen_port = iter->first;
+ auto num_connections = iter->second;
+ if (i > 0) {
+ ss << " ,\n";
+ }
+ ss << " \"edi_tcp_" << listen_port << "\" : { \"num_connections\": " <<
+ num_connections << "} ";
+ }
+
+ ss << "}\n}\n";
+
+ return ss.str();
+}
+
ManagementServer::ManagementServer() :
m_zmq_context(),
m_zmq_sock(m_zmq_context, ZMQ_REP),
@@ -323,10 +351,13 @@ void ManagementServer::handle_message(zmq::message_t& zmq_message)
<< "}\n";
}
else if (data == "config") {
- answer << getStatConfigJSON();
+ answer << get_input_config_json();
}
else if (data == "values") {
- answer << getValuesJSON();
+ answer << get_input_values_json();
+ }
+ else if (data == "output_values") {
+ answer << get_output_values_json();
}
else if (data == "getptree") {
unique_lock<mutex> lock(m_configmutex);
@@ -366,12 +397,12 @@ InputStat::InputStat(const std::string& name) :
InputStat::~InputStat()
{
- get_mgmt_server().unregisterInput(m_name);
+ get_mgmt_server().unregister_input(m_name);
}
void InputStat::registerAtServer()
{
- get_mgmt_server().registerInput(this);
+ get_mgmt_server().register_input(this);
}
void InputStat::notifyBuffer(long bufsize)
diff --git a/src/ManagementServer.h b/src/ManagementServer.h
index 6e39922..c7a4222 100644
--- a/src/ManagementServer.h
+++ b/src/ManagementServer.h
@@ -2,7 +2,7 @@
Copyright (C) 2009 Her Majesty the Queen in Right of Canada (Communications
Research Center Canada)
- Copyright (C) 2018
+ Copyright (C) 2025
Matthias P. Braendli, matthias.braendli@mpb.li
http://www.opendigitalradio.org
@@ -50,6 +50,7 @@
# include "config.h"
#endif
+#include "Socket.h"
#include "zmq.hpp"
#include <string>
#include <map>
@@ -167,8 +168,10 @@ class ManagementServer
void open(int listenport);
/* Un-/Register a statistics data source */
- void registerInput(InputStat* is);
- void unregisterInput(std::string id);
+ void register_input(InputStat* is);
+ void unregister_input(std::string id);
+
+ void update_edi_tcp_output_stat(uint16_t listen_port, size_t num_connections);
/* Load a ptree given by the management server.
*
@@ -205,20 +208,25 @@ class ManagementServer
std::thread m_restarter_thread;
/******* Statistics Data ********/
- std::map<std::string, InputStat*> m_inputStats;
+ std::map<std::string, InputStat*> m_input_stats;
+
+ // Holds information about EDI/TCP outputs
+ std::map<uint16_t /* port */, size_t /* num_connections */> m_output_stats;
/* Return a description of the configuration that will
* allow to define what graphs to be created
*
* returns: a JSON encoded configuration
*/
- std::string getStatConfigJSON();
+ std::string get_input_config_json();
/* Return the values for the statistics as defined in the configuration
*
* returns: JSON encoded statistics
*/
- std::string getValuesJSON();
+ std::string get_input_values_json();
+
+ std::string get_output_values_json();
// mutex for accessing the map
std::mutex m_statsmutex;
diff --git a/src/fig/FIG.h b/src/fig/FIG.h
index 9752245..eda4671 100644
--- a/src/fig/FIG.h
+++ b/src/fig/FIG.h
@@ -35,11 +35,19 @@ namespace FIC {
class FIGRuntimeInformation {
public:
- FIGRuntimeInformation(std::shared_ptr<dabEnsemble>& e) :
+
+ using dab_time_t = std::pair<uint32_t /* milliseconds */, time_t>;
+ using get_time_func_t = std::function<dab_time_t()>;
+
+ FIGRuntimeInformation(
+ std::shared_ptr<dabEnsemble>& e,
+ get_time_func_t getTimeFunc) :
+ getTimeFunc(getTimeFunc),
currentFrame(0),
ensemble(e),
factumAnalyzer(false) {}
+ get_time_func_t getTimeFunc;
unsigned long currentFrame;
std::shared_ptr<dabEnsemble> ensemble;
bool factumAnalyzer;
diff --git a/src/fig/FIG0_10.cpp b/src/fig/FIG0_10.cpp
index 56ce9fb..240aa19 100644
--- a/src/fig/FIG0_10.cpp
+++ b/src/fig/FIG0_10.cpp
@@ -3,7 +3,7 @@
2011, 2012 Her Majesty the Queen in Right of Canada (Communications
Research Center Canada)
- Copyright (C) 2016
+ Copyright (C) 2025
Matthias P. Braendli, matthias.braendli@mpb.li
*/
/*
@@ -23,7 +23,6 @@
along with ODR-DabMux. If not, see <http://www.gnu.org/licenses/>.
*/
-#include "fig/FIG0structs.h"
#include "fig/FIG0_10.h"
#include "utils.h"
@@ -89,7 +88,7 @@ FillStatus FIG0_10::fill(uint8_t *buf, size_t max_size)
return fs;
}
- //Time and country identifier
+ // Time and country identifier
auto fig0_10 = (FIGtype0_10_LongForm*)buf;
fig0_10->FIGtypeNumber = 0;
@@ -102,9 +101,9 @@ FillStatus FIG0_10::fill(uint8_t *buf, size_t max_size)
remaining -= 2;
struct tm timeData;
- time_t dab_time_seconds = 0;
- uint32_t dab_time_millis = 0;
- get_dab_time(&dab_time_seconds, &dab_time_millis);
+ const auto dab_time = m_rti->getTimeFunc();
+ time_t dab_time_seconds = dab_time.second;
+ uint32_t dab_time_millis = dab_time.first;
gmtime_r(&dab_time_seconds, &timeData);
fig0_10->RFU = 0;
diff --git a/src/fig/FIG0structs.h b/src/fig/FIG0structs.h
index 5f514b3..2e107e8 100644
--- a/src/fig/FIG0structs.h
+++ b/src/fig/FIG0structs.h
@@ -24,19 +24,17 @@
*/
#pragma once
-
#include <cstdint>
-
#include "fig/FIG.h"
-#define FIG0_13_APPTYPE_SLIDESHOW 0x2
-#define FIG0_13_APPTYPE_WEBSITE 0x3
-#define FIG0_13_APPTYPE_TPEG 0x4
-#define FIG0_13_APPTYPE_DGPS 0x5
-#define FIG0_13_APPTYPE_TMC 0x6
-#define FIG0_13_APPTYPE_SPI 0x7
-#define FIG0_13_APPTYPE_DABJAVA 0x8
-#define FIG0_13_APPTYPE_JOURNALINE 0x44a
+constexpr uint16_t FIG0_13_APPTYPE_SLIDESHOW = 0x2;
+constexpr uint16_t FIG0_13_APPTYPE_WEBSITE = 0x3;
+constexpr uint16_t FIG0_13_APPTYPE_TPEG = 0x4;
+constexpr uint16_t FIG0_13_APPTYPE_DGPS = 0x5;
+constexpr uint16_t FIG0_13_APPTYPE_TMC = 0x6;
+constexpr uint16_t FIG0_13_APPTYPE_SPI = 0x7;
+constexpr uint16_t FIG0_13_APPTYPE_DABJAVA = 0x8;
+constexpr uint16_t FIG0_13_APPTYPE_JOURNALINE = 0x44a;
struct FIGtype0 {
diff --git a/src/fig/FIGCarousel.cpp b/src/fig/FIGCarousel.cpp
index 9748dbf..ceda275 100644
--- a/src/fig/FIGCarousel.cpp
+++ b/src/fig/FIGCarousel.cpp
@@ -68,8 +68,11 @@ bool FIGCarouselElement::check_deadline()
/**************** FIGCarousel *****************/
-FIGCarousel::FIGCarousel(std::shared_ptr<dabEnsemble> ensemble) :
- m_rti(ensemble),
+FIGCarousel::FIGCarousel(
+ std::shared_ptr<dabEnsemble> ensemble,
+ FIGRuntimeInformation::get_time_func_t getTimeFunc
+ ) :
+ m_rti(ensemble, getTimeFunc),
m_fig0_0(&m_rti),
m_fig0_1(&m_rti),
m_fig0_2(&m_rti),
diff --git a/src/fig/FIGCarousel.h b/src/fig/FIGCarousel.h
index 1e33577..a2a8022 100644
--- a/src/fig/FIGCarousel.h
+++ b/src/fig/FIGCarousel.h
@@ -67,7 +67,9 @@ enum class FIBAllocation {
class FIGCarousel {
public:
- FIGCarousel(std::shared_ptr<dabEnsemble> ensemble);
+ FIGCarousel(
+ std::shared_ptr<dabEnsemble> ensemble,
+ FIGRuntimeInformation::get_time_func_t getTimeFunc);
/* Write all FIBs to the buffer, including correct padding and crc.
* Returns number of bytes written.
diff --git a/src/utils.cpp b/src/utils.cpp
index e7ef224..7ea6293 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -3,7 +3,7 @@
2011, 2012 Her Majesty the Queen in Right of Canada (Communications
Research Center Canada)
- Copyright (C) 2021
+ Copyright (C) 2025
Matthias P. Braendli, matthias.braendli@mpb.li
http://www.opendigitalradio.org
@@ -29,36 +29,13 @@
#include <iostream>
#include <memory>
#include <boost/algorithm/string/join.hpp>
-#include "DabMux.h"
#include "utils.h"
#include "fig/FIG0structs.h"
using namespace std;
-static time_t dab_time_seconds = 0;
-static int dab_time_millis = 0;
-
static void printServices(const vector<shared_ptr<DabService> >& services);
-void update_dab_time()
-{
- if (dab_time_seconds == 0) {
- dab_time_seconds = time(nullptr);
- } else {
- dab_time_millis+= 24;
- if (dab_time_millis >= 1000) {
- dab_time_millis -= 1000;
- ++dab_time_seconds;
- }
- }
-}
-
-void get_dab_time(time_t *time, uint32_t *millis)
-{
- *time = dab_time_seconds;
- *millis = dab_time_millis;
-}
-
uint32_t gregorian2mjd(int year, int month, int day)
{
diff --git a/src/utils.h b/src/utils.h
index 331a0b2..d037bb3 100644
--- a/src/utils.h
+++ b/src/utils.h
@@ -3,7 +3,7 @@
2011, 2012 Her Majesty the Queen in Right of Canada (Communications
Research Center Canada)
- Copyright (C) 2020
+ Copyright (C) 2025
Matthias P. Braendli, matthias.braendli@mpb.li
This file contains a set of utility functions that are used to show
@@ -34,10 +34,6 @@
#include <memory>
#include "MuxElements.h"
-/* Must be called once per ETI frame to update the time */
-void update_dab_time(void);
-void get_dab_time(time_t *time, uint32_t *millis);
-
/* Convert a date and time into the modified Julian date
* used in FIG 0/10
*
diff --git a/src/zmq2edi/EDISender.cpp b/src/zmq2edi/EDISender.cpp
new file mode 100644
index 0000000..06b7420
--- /dev/null
+++ b/src/zmq2edi/EDISender.cpp
@@ -0,0 +1,390 @@
+/*
+ Copyright (C) 2002, 2003, 2004, 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://www.opendigitalradio.org
+ */
+/*
+ This file is part of ODR-DabMux.
+
+ ODR-DabMux 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-DabMux 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-DabMux. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "EDISender.h"
+#include "Log.h"
+#include <cmath>
+#include <numeric>
+#include <map>
+#include <algorithm>
+
+using namespace std;
+
+EDISender::~EDISender()
+{
+ if (running.load()) {
+ running.store(false);
+
+ // Unblock thread
+ frame_t emptyframe;
+ frames.push(emptyframe);
+
+ process_thread.join();
+ }
+}
+
+void EDISender::start(const edi_configuration_t& conf,
+ int delay_ms, int max_delay_ms)
+{
+ edi_conf = conf;
+ tist_delay_ms = delay_ms;
+ tist_max_delay_ms = max_delay_ms;
+
+ if (edi_conf.verbose) {
+ etiLog.log(info, "Setup EDI");
+ }
+
+ if (edi_conf.dump) {
+ edi_debug_file.open("./edi.debug");
+ }
+
+ if (edi_conf.enabled()) {
+ for (auto& edi_destination : edi_conf.destinations) {
+ auto edi_output = make_shared<UdpSocket>(edi_destination.source_port);
+
+ if (not edi_destination.source_addr.empty()) {
+ int err = edi_output->setMulticastSource(edi_destination.source_addr.c_str());
+ if (err) {
+ throw runtime_error("EDI socket set source failed!");
+ }
+ err = edi_output->setMulticastTTL(edi_destination.ttl);
+ if (err) {
+ throw runtime_error("EDI socket set TTL failed!");
+ }
+ }
+
+ edi_destination.socket = edi_output;
+ }
+ }
+
+ if (edi_conf.verbose) {
+ etiLog.log(info, "EDI set up");
+ }
+
+ // The AF Packet will be protected with reed-solomon and split in fragments
+ edi::PFT pft(edi_conf);
+ edi_pft = pft;
+
+ if (edi_conf.interleaver_enabled()) {
+ edi_interleaver.SetLatency(edi_conf.latency_frames);
+ }
+
+ startTime = std::chrono::steady_clock::now();
+ running.store(true);
+ process_thread = thread(&EDISender::process, this);
+}
+
+void EDISender::push_frame(const frame_t& frame)
+{
+ frames.push(frame);
+}
+
+void EDISender::print_configuration()
+{
+ if (edi_conf.enabled()) {
+ etiLog.level(info) << "EDI";
+ etiLog.level(info) << " verbose " << edi_conf.verbose;
+ for (auto& edi_dest : edi_conf.destinations) {
+ etiLog.level(info) << " to " << edi_dest.dest_addr << ":" << edi_conf.dest_port;
+ if (not edi_dest.source_addr.empty()) {
+ etiLog.level(info) << " source " << edi_dest.source_addr;
+ etiLog.level(info) << " ttl " << edi_dest.ttl;
+ }
+ etiLog.level(info) << " source port " << edi_dest.source_port;
+ }
+ if (edi_conf.interleaver_enabled()) {
+ etiLog.level(info) << " interleave " << edi_conf.latency_frames * 24 << " ms";
+ }
+ }
+ else {
+ etiLog.level(info) << "EDI disabled";
+ }
+}
+
+void EDISender::send_eti_frame(uint8_t* p, metadata_t metadata)
+{
+ edi::TagDETI edi_tagDETI;
+ edi::TagStarPTR edi_tagStarPtr;
+ map<int, edi::TagESTn> edi_subchannelToTag;
+ // The above Tag Items will be assembled into a TAG Packet
+ edi::TagPacket edi_tagpacket(edi_conf.tagpacket_alignment);
+
+ // SYNC
+ edi_tagDETI.stat = p[0];
+
+ // LIDATA FCT
+ edi_tagDETI.dlfc = metadata.dlfc;
+
+ const int fct = p[4];
+ if (metadata.dlfc % 250 != fct) {
+ etiLog.level(warn) << "Frame FCT=" << fct <<
+ " does not correspond to DLFC=" << metadata.dlfc;
+ }
+
+ bool ficf = (p[5] & 0x80) >> 7;
+ edi_tagDETI.ficf = ficf;
+
+ const int nst = p[5] & 0x7F;
+
+ edi_tagDETI.fp = (p[6] & 0xE0) >> 5;
+ const int mid = (p[6] & 0x18) >> 3;
+ edi_tagDETI.mid = mid;
+ //const int fl = (p[6] & 0x07) * 256 + p[7];
+
+ int ficl = 0;
+ if (ficf == 0) {
+ etiLog.level(warn) << "Not FIC in data stream!";
+ return;
+ }
+ else if (mid == 3) {
+ ficl = 32;
+ }
+ else {
+ ficl = 24;
+ }
+
+ vector<uint32_t> sad(nst);
+ vector<uint32_t> stl(nst);
+ // Loop over STC subchannels:
+ for (int i=0; i < nst; i++) {
+ // EDI stream index is 1-indexed
+ const int edi_stream_id = i + 1;
+
+ uint32_t scid = (p[8 + 4*i] & 0xFC) >> 2;
+ sad[i] = (p[8+4*i] & 0x03) * 256 + p[9+4*i];
+ uint32_t tpl = (p[10+4*i] & 0xFC) >> 2;
+ stl[i] = (p[10+4*i] & 0x03) * 256 + \
+ p[11+4*i];
+
+ edi::TagESTn tag_ESTn;
+ tag_ESTn.id = edi_stream_id;
+ tag_ESTn.scid = scid;
+ tag_ESTn.sad = sad[i];
+ tag_ESTn.tpl = tpl;
+ tag_ESTn.rfa = 0; // two bits
+ tag_ESTn.mst_length = stl[i];
+ tag_ESTn.mst_data = nullptr;
+
+ edi_subchannelToTag[i] = tag_ESTn;
+ }
+
+ const uint16_t mnsc = p[8 + 4*nst] * 256 + \
+ p[8 + 4*nst + 1];
+ edi_tagDETI.mnsc = mnsc;
+
+ /*const uint16_t crc1 = p[8 + 4*nst + 2]*256 + \
+ p[8 + 4*nst + 3]; */
+
+ edi_tagDETI.fic_data = p + 12 + 4*nst;
+ edi_tagDETI.fic_length = ficl * 4;
+
+ // loop over MSC subchannels
+ int offset = 0;
+ for (int i=0; i < nst; i++) {
+ edi::TagESTn& tag = edi_subchannelToTag[i];
+ tag.mst_data = (p + 12 + 4*nst + ficf*ficl*4 + offset);
+
+ offset += stl[i] * 8;
+ }
+
+ /*
+ const uint16_t crc2 = p[12 + 4*nst + ficf*ficl*4 + offset] * 256 + \
+ p[12 + 4*nst + ficf*ficl*4 + offset + 1]; */
+
+ // TIST
+ const size_t tist_ix = 12 + 4*nst + ficf*ficl*4 + offset + 4;
+ uint32_t tist = (uint32_t)(p[tist_ix]) << 24 |
+ (uint32_t)(p[tist_ix+1]) << 16 |
+ (uint32_t)(p[tist_ix+2]) << 8 |
+ (uint32_t)(p[tist_ix+3]);
+
+ std::time_t posix_timestamp_1_jan_2000 = 946684800;
+
+ // Wait until our time is tist_delay after the TIST before
+ // we release that frame
+
+ using namespace std::chrono;
+
+ const auto seconds = metadata.edi_time;
+ const auto pps_offset = milliseconds(std::lrint((tist & 0xFFFFFF) / 16384.0));
+ const auto t_frame = system_clock::from_time_t(
+ seconds + posix_timestamp_1_jan_2000) + pps_offset;
+
+ const auto t_release = t_frame + milliseconds(tist_delay_ms);
+ const auto t_now = system_clock::now();
+
+ /*
+ etiLog.level(debug) << "seconds " << seconds + posix_timestamp_1_jan_2000;
+ etiLog.level(debug) << "now " << system_clock::to_time_t(t_now);
+ etiLog.level(debug) << "wait " << wait_time.count();
+ */
+
+ const auto wait_time = t_release - t_now;
+ wait_times.push_back(duration_cast<microseconds>(wait_time).count());
+
+ if (tist_max_delay_ms > 0) {
+ const auto t_latest_release = t_frame + milliseconds(tist_max_delay_ms);
+
+ if (t_now > t_latest_release) {
+ // drop frame
+ num_dropped.fetch_add(1);
+ return;
+ }
+ }
+
+ if (t_release > t_now) {
+ std::this_thread::sleep_for(wait_time);
+ }
+
+ edi_tagDETI.tsta = tist;
+ edi_tagDETI.atstf = 1;
+ edi_tagDETI.utco = metadata.utc_offset;
+ edi_tagDETI.seconds = metadata.edi_time;
+
+ if (edi_conf.enabled()) {
+ // put tags *ptr, DETI and all subchannels into one TagPacket
+ edi_tagpacket.tag_items.push_back(&edi_tagStarPtr);
+ edi_tagpacket.tag_items.push_back(&edi_tagDETI);
+
+ for (auto& tag : edi_subchannelToTag) {
+ edi_tagpacket.tag_items.push_back(&tag.second);
+ }
+
+ // Assemble into one AF Packet
+ edi::AFPacket edi_afpacket = edi_afPacketiser.Assemble(edi_tagpacket);
+
+ if (edi_conf.enable_pft) {
+ // Apply PFT layer to AF Packet (Reed Solomon FEC and Fragmentation)
+ vector<edi::PFTFragment> edi_fragments = edi_pft.Assemble(edi_afpacket);
+
+ if (edi_conf.verbose) {
+ fprintf(stderr, "EDI number of PFT fragment before interleaver %zu\n",
+ edi_fragments.size());
+ }
+
+ if (edi_conf.interleaver_enabled()) {
+ edi_fragments = edi_interleaver.Interleave(edi_fragments);
+ }
+
+ // Send over ethernet
+ for (const auto& edi_frag : edi_fragments) {
+ for (auto& dest : edi_conf.destinations) {
+ InetAddress addr;
+ addr.setAddress(dest.dest_addr.c_str());
+ addr.setPort(edi_conf.dest_port);
+
+ dest.socket->send(edi_frag, addr);
+ }
+
+ if (edi_conf.dump) {
+ std::ostream_iterator<uint8_t> debug_iterator(edi_debug_file);
+ std::copy(edi_frag.begin(), edi_frag.end(), debug_iterator);
+ }
+ }
+
+ if (edi_conf.verbose) {
+ fprintf(stderr, "EDI number of PFT fragments %zu\n",
+ edi_fragments.size());
+ }
+ }
+ else {
+ // Send over ethernet
+ for (auto& dest : edi_conf.destinations) {
+ InetAddress addr;
+ addr.setAddress(dest.dest_addr.c_str());
+ addr.setPort(edi_conf.dest_port);
+
+ dest.socket->send(edi_afpacket, addr);
+ }
+
+ if (edi_conf.dump) {
+ std::ostream_iterator<uint8_t> debug_iterator(edi_debug_file);
+ std::copy(edi_afpacket.begin(), edi_afpacket.end(), debug_iterator);
+ }
+ }
+ }
+}
+
+void EDISender::process()
+{
+ while (running.load()) {
+ frame_t frame;
+ frames.wait_and_pop(frame);
+
+ if (not running.load() or frame.first.empty()) {
+ break;
+ }
+
+ if (frame.first.size() == 6144) {
+ send_eti_frame(frame.first.data(), frame.second);
+ }
+ else {
+ etiLog.level(warn) << "Ignoring short ETI frame, "
+ "DFLC=" << frame.second.dlfc << ", len=" <<
+ frame.first.size();
+ }
+
+ if (wait_times.size() == 250) { // every six seconds
+ const double n = wait_times.size();
+
+ double sum = accumulate(wait_times.begin(), wait_times.end(), 0);
+ size_t num_late = std::count_if(wait_times.begin(), wait_times.end(),
+ [](double v){ return v < 0; });
+ double mean = sum / n;
+
+ double sq_sum = 0;
+ for (const auto t : wait_times) {
+ sq_sum += (t-mean) * (t-mean);
+ }
+ double stdev = sqrt(sq_sum / n);
+ auto min_max = minmax_element(wait_times.begin(), wait_times.end());
+
+ /* Debug code
+ stringstream ss;
+ ss << "times:";
+ for (const auto t : wait_times) {
+ ss << " " << t;
+ }
+ etiLog.level(debug) << ss.str();
+ */
+
+ const size_t dropped = num_dropped.exchange(0);
+
+ etiLog.level(info) << "Wait time statistics [microseconds]:"
+ " min: " << *min_max.first <<
+ " max: " << *min_max.second <<
+ " mean: " << mean <<
+ " stdev: " << stdev <<
+ " late: " <<
+ num_late << " of " << wait_times.size() << " (" <<
+ num_late * 100.0 / n << "%)" <<
+ " dropped: " << dropped;
+
+ wait_times.clear();
+ }
+ }
+}
diff --git a/src/zmq2edi/EDISender.h b/src/zmq2edi/EDISender.h
new file mode 100644
index 0000000..44502c1
--- /dev/null
+++ b/src/zmq2edi/EDISender.h
@@ -0,0 +1,91 @@
+/*
+ Copyright (C) 2002, 2003, 2004, 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://www.opendigitalradio.org
+ */
+/*
+ This file is part of ODR-DabMux.
+
+ ODR-DabMux 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-DabMux 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-DabMux. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#pragma once
+#include <iostream>
+#include <iterator>
+#include <thread>
+#include <vector>
+#include <chrono>
+#include <atomic>
+#include "ThreadsafeQueue.h"
+#include "dabOutput/dabOutput.h"
+#include "dabOutput/edi/TagItems.h"
+#include "dabOutput/edi/TagPacket.h"
+#include "dabOutput/edi/AFPacket.h"
+#include "dabOutput/edi/PFT.h"
+#include "dabOutput/edi/Interleaver.h"
+
+// This metadata gets transmitted in the zmq stream
+struct metadata_t {
+ uint32_t edi_time = 0;
+ int16_t utc_offset = 0;
+ uint16_t dlfc = 0;
+};
+
+using frame_t = std::pair<std::vector<uint8_t>, metadata_t>;
+
+class EDISender {
+ public:
+ EDISender() = default;
+ EDISender(const EDISender& other) = delete;
+ EDISender& operator=(const EDISender& other) = delete;
+ ~EDISender();
+ void start(const edi_configuration_t& conf, int delay_ms, int max_delay_ms);
+ void push_frame(const frame_t& frame);
+ void print_configuration(void);
+
+ private:
+ void send_eti_frame(uint8_t* p, metadata_t metadata);
+ void process(void);
+
+ int tist_delay_ms = 0;
+ int tist_max_delay_ms = 0;
+ std::atomic<bool> running = ATOMIC_VAR_INIT(false);
+ std::thread process_thread;
+ edi_configuration_t edi_conf;
+ std::chrono::steady_clock::time_point startTime;
+ ThreadsafeQueue<frame_t> frames;
+ std::ofstream edi_debug_file;
+
+ // The TagPacket will then be placed into an AFPacket
+ edi::AFPacketiser edi_afPacketiser;
+
+ // The AF Packet will be protected with reed-solomon and split in fragments
+ edi::PFT edi_pft;
+
+ // To mitigate for burst packet loss, PFT fragments can be sent out-of-order
+ edi::Interleaver edi_interleaver;
+
+ // For statistics about wait time before we transmit packets,
+ // in microseconds
+ std::vector<double> wait_times;
+
+ // Number of frames dropped because their TIST was larger than max_delay
+ std::atomic<size_t> num_dropped = ATOMIC_VAR_INIT(0);
+
+};
diff --git a/src/zmq2edi/zmq2edi.cpp b/src/zmq2edi/zmq2edi.cpp
new file mode 100644
index 0000000..63c3228
--- /dev/null
+++ b/src/zmq2edi/zmq2edi.cpp
@@ -0,0 +1,419 @@
+/*
+ Copyright (C) 2002, 2003, 2004, 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://www.opendigitalradio.org
+ */
+/*
+ This file is part of ODR-DabMux.
+
+ ODR-DabMux 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-DabMux 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-DabMux. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "Log.h"
+#include "zmq.hpp"
+#include <math.h>
+#include <getopt.h>
+#include <string.h>
+#include <iostream>
+#include <iterator>
+#include <vector>
+
+#include "EDISender.h"
+#include "dabOutput/dabOutput.h"
+
+constexpr size_t MAX_ERROR_COUNT = 10;
+constexpr long ZMQ_TIMEOUT_MS = 1000;
+
+static edi_configuration_t edi_conf;
+
+static EDISender edisender;
+
+void usage(void)
+{
+ using namespace std;
+
+ cerr << "Usage:" << endl;
+ cerr << "odr-zmq2edi [options] <source>" << endl << endl;
+
+ cerr << "Options:" << endl;
+ cerr << "The following options can be given only once:" << endl;
+ cerr << " <source> is a ZMQ URL that points to a ODR-DabMux ZMQ output." << endl;
+ cerr << " -w <delay> Keep every ETI frame until TIST is <delay> milliseconds after current system time." << endl;
+ cerr << " -W <max_delay> Drop ETI frames if TIST is <max_delay> later than current system time." << endl;
+ cerr << " -p <destination port> sets the destination port." << endl;
+ cerr << " -P Disable PFT and send AFPackets." << endl;
+ cerr << " -f <fec> sets the FEC." << endl;
+ cerr << " -i <interleave> enables the interleaved with this latency." << endl;
+ cerr << " -D dumps the EDI to edi.debug file." << endl;
+ cerr << " -v Enables verbose mode." << endl;
+ cerr << " -a <tagpacket alignement> sets the alignment of the TAG Packet (default 8)." << endl << endl;
+
+ cerr << "The following options can be given several times, when more than once destination is addressed:" << endl;
+ cerr << " -d <destination ip> sets the destination ip." << endl;
+ cerr << " -s <source port> sets the source port." << endl;
+ cerr << " -S <source ip> select the source IP in case we want to use multicast." << endl;
+ cerr << " -t <ttl> set the packet's TTL." << endl << endl;
+
+ cerr << "odr-zmq2edi will quit if it does not receive data for " <<
+ (int)(MAX_ERROR_COUNT * ZMQ_TIMEOUT_MS / 1000.0) << " seconds." << endl;
+ cerr << "It is best practice to run this tool under a process supervisor that will restart it automatically." << endl;
+}
+
+static metadata_t get_md_one_frame(uint8_t *buf, size_t size, size_t *consumed_bytes)
+{
+ size_t remaining = size;
+ if (remaining < 3) {
+ etiLog.level(warn) << "Insufficient data to parse metadata";
+ throw std::runtime_error("Insufficient data");
+ }
+
+ metadata_t md;
+ bool utc_offset_received = false;
+ bool edi_time_received = false;
+ bool dlfc_received = false;
+
+ while (remaining) {
+ uint8_t id = buf[0];
+ uint16_t len = (((uint16_t)buf[1]) << 8) + buf[2];
+
+ if (id == static_cast<uint8_t>(output_metadata_id_e::separation_marker)) {
+ if (len != 0) {
+ etiLog.level(warn) << "Invalid length " << len << " for metadata: separation_marker";
+ }
+
+ if (not utc_offset_received or not edi_time_received or not dlfc_received) {
+ throw std::runtime_error("Incomplete metadata received");
+ }
+
+ remaining -= 3;
+ *consumed_bytes = size - remaining;
+ return md;
+ }
+ else if (id == static_cast<uint8_t>(output_metadata_id_e::utc_offset)) {
+ if (len != 2) {
+ etiLog.level(warn) << "Invalid length " << len << " for metadata: utc_offset";
+ }
+ if (remaining < 2) {
+ throw std::runtime_error("Insufficient data for utc_offset");
+ }
+ uint16_t utco;
+ std::memcpy(&utco, buf + 3, sizeof(utco));
+ md.utc_offset = ntohs(utco);
+ utc_offset_received = true;
+ remaining -= 5;
+ buf += 5;
+ }
+ else if (id == static_cast<uint8_t>(output_metadata_id_e::edi_time)) {
+ if (len != 4) {
+ etiLog.level(warn) << "Invalid length " << len << " for metadata: edi_time";
+ }
+ if (remaining < 4) {
+ throw std::runtime_error("Insufficient data for edi_time");
+ }
+ uint32_t edi_time;
+ std::memcpy(&edi_time, buf + 3, sizeof(edi_time));
+ md.edi_time = ntohl(edi_time);
+ edi_time_received = true;
+ remaining -= 7;
+ buf += 7;
+ }
+ else if (id == static_cast<uint8_t>(output_metadata_id_e::dlfc)) {
+ if (len != 2) {
+ etiLog.level(warn) << "Invalid length " << len << " for metadata: dlfc";
+ }
+ if (remaining < 2) {
+ throw std::runtime_error("Insufficient data for dlfc");
+ }
+ uint16_t dlfc;
+ std::memcpy(&dlfc, buf + 3, sizeof(dlfc));
+ md.dlfc = ntohs(dlfc);
+ dlfc_received = true;
+ remaining -= 5;
+ buf += 5;
+ }
+ }
+
+ throw std::runtime_error("Insufficient data");
+}
+
+/* There is some state inside the parsing of destination arguments,
+ * because several destinations can be given. */
+
+static edi_destination_t edi_destination;
+static bool source_port_set = false;
+static bool source_addr_set = false;
+static bool ttl_set = false;
+static bool dest_addr_set = false;
+
+static void add_edi_destination(void)
+{
+ if (not dest_addr_set) {
+ throw std::runtime_error("Destination address not specified for destination number " +
+ std::to_string(edi_conf.destinations.size() + 1));
+ }
+
+ edi_conf.destinations.push_back(edi_destination);
+ edi_destination_t newdest;
+ edi_destination = newdest;
+
+ source_port_set = false;
+ source_addr_set = false;
+ ttl_set = false;
+ dest_addr_set = false;
+}
+
+static void parse_destination_args(char option)
+{
+ switch (option) {
+ case 's':
+ if (source_port_set) {
+ add_edi_destination();
+ }
+ edi_destination.source_port = std::stoi(optarg);
+ source_port_set = true;
+ break;
+ case 'S':
+ if (source_addr_set) {
+ add_edi_destination();
+ }
+ edi_destination.source_addr = optarg;
+ source_addr_set = true;
+ break;
+ case 't':
+ if (ttl_set) {
+ add_edi_destination();
+ }
+ edi_destination.ttl = std::stoi(optarg);
+ ttl_set = true;
+ break;
+ case 'd':
+ if (dest_addr_set) {
+ add_edi_destination();
+ }
+ edi_destination.dest_addr = optarg;
+ dest_addr_set = true;
+ break;
+ default:
+ throw std::logic_error("parse_destination_args invalid");
+ }
+}
+
+int start(int argc, char **argv)
+{
+ edi_conf.enable_pft = true;
+
+ if (argc == 0) {
+ usage();
+ return 1;
+ }
+
+ int delay_ms = 500;
+ int max_delay_ms = 0; // no max delay
+
+ int ch = 0;
+ while (ch != -1) {
+ ch = getopt(argc, argv, "d:p:s:S:t:Pf:i:Dva:w:W:");
+ switch (ch) {
+ case -1:
+ break;
+ case 'd':
+ case 's':
+ case 'S':
+ case 't':
+ parse_destination_args(ch);
+ break;
+ case 'p':
+ edi_conf.dest_port = std::stoi(optarg);
+ break;
+ case 'P':
+ edi_conf.enable_pft = false;
+ break;
+ case 'f':
+ edi_conf.fec = std::stoi(optarg);
+ break;
+ case 'i':
+ {
+ double interleave_ms = std::stod(optarg);
+ if (interleave_ms != 0.0) {
+ if (interleave_ms < 0) {
+ throw std::runtime_error("EDI output: negative interleave value is invalid.");
+ }
+
+ auto latency_rounded = lround(interleave_ms / 24.0);
+ if (latency_rounded * 24 > 30000) {
+ throw std::runtime_error("EDI output: interleaving set for more than 30 seconds!");
+ }
+
+ edi_conf.latency_frames = latency_rounded;
+ }
+ }
+ break;
+ case 'D':
+ edi_conf.dump = true;
+ break;
+ case 'v':
+ edi_conf.verbose = true;
+ break;
+ case 'a':
+ edi_conf.tagpacket_alignment = std::stoi(optarg);
+ break;
+ case 'w':
+ delay_ms = std::stoi(optarg);
+ break;
+ case 'W':
+ max_delay_ms = std::stoi(optarg);
+ break;
+ case 'h':
+ default:
+ usage();
+ return 1;
+ }
+ }
+
+ add_edi_destination();
+
+ if (optind >= argc) {
+ etiLog.level(error) << "source option is missing";
+ return 1;
+ }
+
+ if (edi_conf.dest_port == 0) {
+ etiLog.level(error) << "No EDI destination port defined";
+ return 1;
+ }
+
+ if (edi_conf.destinations.empty()) {
+ etiLog.level(error) << "No EDI destinations set";
+ return 1;
+ }
+
+ if (max_delay_ms > 0) {
+ etiLog.level(info) << "Setting up EDI Sender with delay " << delay_ms << " ms and max delay " << max_delay_ms << " ms";
+ }
+ else {
+ etiLog.level(info) << "Setting up EDI Sender with delay " << delay_ms << " ms";
+ }
+ edisender.start(edi_conf, delay_ms, max_delay_ms);
+ edisender.print_configuration();
+
+ const char* source_url = argv[optind];
+
+
+ size_t frame_count = 0;
+ size_t error_count = 0;
+
+ etiLog.level(info) << "Opening ZMQ input: " << source_url;
+
+ zmq::context_t zmq_ctx(1);
+ zmq::socket_t zmq_sock(zmq_ctx, ZMQ_SUB);
+ zmq_sock.connect(source_url);
+ zmq_sock.setsockopt(ZMQ_SUBSCRIBE, NULL, 0); // subscribe to all messages
+
+ while (error_count < MAX_ERROR_COUNT) {
+ zmq::message_t incoming;
+ zmq::pollitem_t items[1];
+ items[0].socket = zmq_sock;
+ items[0].events = ZMQ_POLLIN;
+ const int num_events = zmq::poll(items, 1, ZMQ_TIMEOUT_MS);
+ if (num_events == 0) { // timeout
+ error_count++;
+ }
+ else {
+ // Event received: recv will not block
+ zmq_sock.recv(&incoming);
+
+ zmq_dab_message_t* dab_msg = (zmq_dab_message_t*)incoming.data();
+
+ if (dab_msg->version != 1) {
+ etiLog.level(error) << "ZeroMQ wrong packet version " << dab_msg->version;
+ error_count++;
+ }
+
+ int offset = sizeof(dab_msg->version) +
+ NUM_FRAMES_PER_ZMQ_MESSAGE * sizeof(*dab_msg->buflen);
+
+ std::list<std::pair<std::vector<uint8_t>, metadata_t> > all_frames;
+
+ for (int i = 0; i < NUM_FRAMES_PER_ZMQ_MESSAGE; i++) {
+ if (dab_msg->buflen[i] <= 0 or dab_msg->buflen[i] > 6144) {
+ etiLog.level(error) << "ZeroMQ buffer " << i <<
+ " has invalid length " << dab_msg->buflen[i];
+ error_count++;
+ }
+ else {
+ std::vector<uint8_t> buf(6144, 0x55);
+
+ const int framesize = dab_msg->buflen[i];
+
+ memcpy(&buf.front(),
+ ((uint8_t*)incoming.data()) + offset,
+ framesize);
+
+ all_frames.emplace_back(
+ std::piecewise_construct,
+ std::make_tuple(std::move(buf)),
+ std::make_tuple());
+
+ offset += framesize;
+ }
+ }
+
+ for (auto &f : all_frames) {
+ size_t consumed_bytes = 0;
+
+ f.second = get_md_one_frame(
+ static_cast<uint8_t*>(incoming.data()) + offset,
+ incoming.size() - offset,
+ &consumed_bytes);
+
+ offset += consumed_bytes;
+ }
+
+ for (auto &f : all_frames) {
+ edisender.push_frame(f);
+ frame_count++;
+ }
+ }
+ }
+
+ etiLog.level(info) << "Quitting after " << frame_count << " frames transferred";
+
+ return 0;
+}
+
+int main(int argc, char **argv)
+{
+ etiLog.level(info) << "ZMQ2EDI converter from " <<
+ PACKAGE_NAME << " " <<
+#if defined(GITVERSION)
+ GITVERSION <<
+#else
+ PACKAGE_VERSION <<
+#endif
+ " starting up";
+
+ try {
+ return start(argc, argv);
+ }
+ catch (std::runtime_error &e) {
+ etiLog.level(error) << "Error: " << e.what();
+ }
+
+ return 1;
+}