Initial GIT import
This commit is contained in:
commit
fde7cc2ce3
|
@ -0,0 +1,47 @@
|
|||
Makefile
|
||||
Makefile.in
|
||||
.deps
|
||||
.libs
|
||||
*.o
|
||||
*.lo
|
||||
*.la
|
||||
*.pc
|
||||
aclocal.m4
|
||||
acinclude.m4
|
||||
aminclude.am
|
||||
m4/*.m4
|
||||
autom4te.cache
|
||||
compile
|
||||
config.h*
|
||||
config.sub
|
||||
config.log
|
||||
config.status
|
||||
config.guess
|
||||
configure
|
||||
depcomp
|
||||
missing
|
||||
ltmain.sh
|
||||
install-sh
|
||||
stamp-h1
|
||||
libtool
|
||||
|
||||
.tarball-version
|
||||
.version
|
||||
.dirstamp
|
||||
|
||||
Doxyfile
|
||||
|
||||
.*.sw?
|
||||
|
||||
src/libdebug/libdebug.a
|
||||
src/libg711/libg711.a
|
||||
src/libjitter/libjitter.a
|
||||
src/liboptions/liboptions.a
|
||||
src/libosmocc/libosmocc.a
|
||||
src/libsample/libsample.a
|
||||
src/libtimer/libtimer.a
|
||||
src/libwave/libwave.a
|
||||
src/libdtmf/libdtmf.a
|
||||
src/libfm/libfm.a
|
||||
src/libfilter/libfilter.a
|
||||
src/router/osmo-cc-router
|
|
@ -0,0 +1,5 @@
|
|||
AUTOMAKE_OPTIONS = foreign dist-bzip2 1.6
|
||||
ACLOCAL_AMFLAGS = -I m4
|
||||
|
||||
SUBDIRS = src
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
AC_INIT([osmo-cc-router],
|
||||
m4_esyscmd([./git-version-gen .tarball-version]),
|
||||
[jolly@eversberg.eu])
|
||||
|
||||
dnl *This* is the root dir, even if an install-sh exists in ../ or ../../
|
||||
AC_CONFIG_AUX_DIR([.])
|
||||
|
||||
AM_INIT_AUTOMAKE([subdir-objects dist-bzip2])
|
||||
AC_CONFIG_TESTDIR(tests)
|
||||
|
||||
dnl kernel style compile messages
|
||||
m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])])
|
||||
|
||||
dnl include release helper
|
||||
RELMAKE='-include osmo-release.mk'
|
||||
AC_SUBST([RELMAKE])
|
||||
|
||||
dnl checks for programs
|
||||
AC_PROG_MAKE_SET
|
||||
AC_PROG_CC
|
||||
AC_PROG_INSTALL
|
||||
LT_INIT
|
||||
AC_PROG_LIBTOOL
|
||||
|
||||
AC_CONFIG_MACRO_DIR([m4])
|
||||
|
||||
AC_ARG_ENABLE(sanitize,
|
||||
[AS_HELP_STRING(
|
||||
[--enable-sanitize],
|
||||
[Compile with address sanitizer enabled],
|
||||
)],
|
||||
[sanitize=$enableval], [sanitize="no"])
|
||||
if test x"$sanitize" = x"yes"
|
||||
then
|
||||
CFLAGS="$CFLAGS -fsanitize=address -fsanitize=undefined"
|
||||
CPPFLAGS="$CPPFLAGS -fsanitize=address -fsanitize=undefined"
|
||||
fi
|
||||
|
||||
AC_ARG_ENABLE(werror,
|
||||
[AS_HELP_STRING(
|
||||
[--enable-werror],
|
||||
[Turn all compiler warnings into errors, with exceptions:
|
||||
a) deprecation (allow upstream to mark deprecation without breaking builds);
|
||||
b) "#warning" pragmas (allow to remind ourselves of errors without breaking builds)
|
||||
]
|
||||
)],
|
||||
[werror=$enableval], [werror="no"])
|
||||
if test x"$werror" = x"yes"
|
||||
then
|
||||
WERROR_FLAGS="-Werror"
|
||||
WERROR_FLAGS+=" -Wno-error=deprecated -Wno-error=deprecated-declarations"
|
||||
WERROR_FLAGS+=" -Wno-error=cpp" # "#warning"
|
||||
CFLAGS="$CFLAGS $WERROR_FLAGS"
|
||||
CPPFLAGS="$CPPFLAGS $WERROR_FLAGS"
|
||||
fi
|
||||
|
||||
CFLAGS="$CFLAGS -Wall"
|
||||
CPPFLAGS="$CPPFLAGS -Wall"
|
||||
|
||||
dnl checks for header files
|
||||
AC_HEADER_STDC
|
||||
AC_CHECK_HEADERS(execinfo.h sys/select.h sys/socket.h syslog.h ctype.h)
|
||||
|
||||
# The following test is taken from WebKit's webkit.m4
|
||||
saved_CFLAGS="$CFLAGS"
|
||||
CFLAGS="$CFLAGS -fvisibility=hidden "
|
||||
AC_MSG_CHECKING([if ${CC} supports -fvisibility=hidden])
|
||||
AC_COMPILE_IFELSE([AC_LANG_SOURCE([char foo;])],
|
||||
[ AC_MSG_RESULT([yes])
|
||||
SYMBOL_VISIBILITY="-fvisibility=hidden"],
|
||||
AC_MSG_RESULT([no]))
|
||||
CFLAGS="$saved_CFLAGS"
|
||||
AC_SUBST(SYMBOL_VISIBILITY)
|
||||
|
||||
dnl Generate the output
|
||||
AM_CONFIG_HEADER(config.h)
|
||||
|
||||
AC_CHECK_LIB([m], [main])
|
||||
AC_CHECK_LIB([pthread], [main])
|
||||
|
||||
AC_OUTPUT(
|
||||
src/liboptions/Makefile
|
||||
src/libdebug/Makefile
|
||||
src/libsample/Makefile
|
||||
src/libtimer/Makefile
|
||||
src/libjitter/Makefile
|
||||
src/libosmocc/Makefile
|
||||
src/libg711/Makefile
|
||||
src/libwave/Makefile
|
||||
src/libdtmf/Makefile
|
||||
src/libfm/Makefile
|
||||
src/libfilter/Makefile
|
||||
src/router/Makefile
|
||||
src/Makefile
|
||||
Makefile)
|
|
@ -0,0 +1,151 @@
|
|||
#!/bin/sh
|
||||
# Print a version string.
|
||||
scriptversion=2010-01-28.01
|
||||
|
||||
# Copyright (C) 2007-2010 Free Software Foundation, Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This script is derived from GIT-VERSION-GEN from GIT: http://git.or.cz/.
|
||||
# It may be run two ways:
|
||||
# - from a git repository in which the "git describe" command below
|
||||
# produces useful output (thus requiring at least one signed tag)
|
||||
# - from a non-git-repo directory containing a .tarball-version file, which
|
||||
# presumes this script is invoked like "./git-version-gen .tarball-version".
|
||||
|
||||
# In order to use intra-version strings in your project, you will need two
|
||||
# separate generated version string files:
|
||||
#
|
||||
# .tarball-version - present only in a distribution tarball, and not in
|
||||
# a checked-out repository. Created with contents that were learned at
|
||||
# the last time autoconf was run, and used by git-version-gen. Must not
|
||||
# be present in either $(srcdir) or $(builddir) for git-version-gen to
|
||||
# give accurate answers during normal development with a checked out tree,
|
||||
# but must be present in a tarball when there is no version control system.
|
||||
# Therefore, it cannot be used in any dependencies. GNUmakefile has
|
||||
# hooks to force a reconfigure at distribution time to get the value
|
||||
# correct, without penalizing normal development with extra reconfigures.
|
||||
#
|
||||
# .version - present in a checked-out repository and in a distribution
|
||||
# tarball. Usable in dependencies, particularly for files that don't
|
||||
# want to depend on config.h but do want to track version changes.
|
||||
# Delete this file prior to any autoconf run where you want to rebuild
|
||||
# files to pick up a version string change; and leave it stale to
|
||||
# minimize rebuild time after unrelated changes to configure sources.
|
||||
#
|
||||
# It is probably wise to add these two files to .gitignore, so that you
|
||||
# don't accidentally commit either generated file.
|
||||
#
|
||||
# Use the following line in your configure.ac, so that $(VERSION) will
|
||||
# automatically be up-to-date each time configure is run (and note that
|
||||
# since configure.ac no longer includes a version string, Makefile rules
|
||||
# should not depend on configure.ac for version updates).
|
||||
#
|
||||
# AC_INIT([GNU project],
|
||||
# m4_esyscmd([build-aux/git-version-gen .tarball-version]),
|
||||
# [bug-project@example])
|
||||
#
|
||||
# Then use the following lines in your Makefile.am, so that .version
|
||||
# will be present for dependencies, and so that .tarball-version will
|
||||
# exist in distribution tarballs.
|
||||
#
|
||||
# BUILT_SOURCES = $(top_srcdir)/.version
|
||||
# $(top_srcdir)/.version:
|
||||
# echo $(VERSION) > $@-t && mv $@-t $@
|
||||
# dist-hook:
|
||||
# echo $(VERSION) > $(distdir)/.tarball-version
|
||||
|
||||
case $# in
|
||||
1) ;;
|
||||
*) echo 1>&2 "Usage: $0 \$srcdir/.tarball-version"; exit 1;;
|
||||
esac
|
||||
|
||||
tarball_version_file=$1
|
||||
nl='
|
||||
'
|
||||
|
||||
# First see if there is a tarball-only version file.
|
||||
# then try "git describe", then default.
|
||||
if test -f $tarball_version_file
|
||||
then
|
||||
v=`cat $tarball_version_file` || exit 1
|
||||
case $v in
|
||||
*$nl*) v= ;; # reject multi-line output
|
||||
[0-9]*) ;;
|
||||
*) v= ;;
|
||||
esac
|
||||
test -z "$v" \
|
||||
&& echo "$0: WARNING: $tarball_version_file seems to be damaged" 1>&2
|
||||
fi
|
||||
|
||||
if test -n "$v"
|
||||
then
|
||||
: # use $v
|
||||
elif
|
||||
v=`git describe --abbrev=4 --match='v*' HEAD 2>/dev/null \
|
||||
|| git describe --abbrev=4 HEAD 2>/dev/null` \
|
||||
&& case $v in
|
||||
[0-9]*) ;;
|
||||
v[0-9]*) ;;
|
||||
*) (exit 1) ;;
|
||||
esac
|
||||
then
|
||||
# Is this a new git that lists number of commits since the last
|
||||
# tag or the previous older version that did not?
|
||||
# Newer: v6.10-77-g0f8faeb
|
||||
# Older: v6.10-g0f8faeb
|
||||
case $v in
|
||||
*-*-*) : git describe is okay three part flavor ;;
|
||||
*-*)
|
||||
: git describe is older two part flavor
|
||||
# Recreate the number of commits and rewrite such that the
|
||||
# result is the same as if we were using the newer version
|
||||
# of git describe.
|
||||
vtag=`echo "$v" | sed 's/-.*//'`
|
||||
numcommits=`git rev-list "$vtag"..HEAD | wc -l`
|
||||
v=`echo "$v" | sed "s/\(.*\)-\(.*\)/\1-$numcommits-\2/"`;
|
||||
;;
|
||||
esac
|
||||
|
||||
# Change the first '-' to a '.', so version-comparing tools work properly.
|
||||
# Remove the "g" in git describe's output string, to save a byte.
|
||||
v=`echo "$v" | sed 's/-/./;s/\(.*\)-g/\1-/'`;
|
||||
else
|
||||
v=UNKNOWN
|
||||
fi
|
||||
|
||||
v=`echo "$v" |sed 's/^v//'`
|
||||
|
||||
# Don't declare a version "dirty" merely because a time stamp has changed.
|
||||
git status > /dev/null 2>&1
|
||||
|
||||
dirty=`sh -c 'git diff-index --name-only HEAD' 2>/dev/null` || dirty=
|
||||
case "$dirty" in
|
||||
'') ;;
|
||||
*) # Append the suffix only if there isn't one already.
|
||||
case $v in
|
||||
*-dirty) ;;
|
||||
*) v="$v-dirty" ;;
|
||||
esac ;;
|
||||
esac
|
||||
|
||||
# Omit the trailing newline, so that m4_esyscmd can use the result directly.
|
||||
echo "$v" | tr -d '\012'
|
||||
|
||||
# Local variables:
|
||||
# eval: (add-hook 'write-file-hooks 'time-stamp)
|
||||
# time-stamp-start: "scriptversion="
|
||||
# time-stamp-format: "%:y-%02m-%02d.%02H"
|
||||
# time-stamp-end: "$"
|
||||
# End:
|
|
@ -0,0 +1,16 @@
|
|||
AUTOMAKE_OPTIONS = foreign
|
||||
|
||||
SUBDIRS = \
|
||||
liboptions \
|
||||
libdebug \
|
||||
libsample \
|
||||
libtimer \
|
||||
libjitter \
|
||||
libosmocc \
|
||||
libg711 \
|
||||
libwave \
|
||||
libdtmf \
|
||||
libfm \
|
||||
libfilter \
|
||||
router
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
AM_CPPFLAGS = -Wall -Wextra -g $(all_includes)
|
||||
|
||||
bin_PROGRAMS = \
|
||||
osmo-cc-router
|
||||
|
||||
osmo_cc_router_SOURCES = \
|
||||
call.c \
|
||||
routing.c \
|
||||
audio.c \
|
||||
display_status.c \
|
||||
main.c
|
||||
|
||||
osmo_cc_router_LDADD = \
|
||||
$(COMMON_LA) \
|
||||
../libdebug/libdebug.a \
|
||||
../liboptions/liboptions.a \
|
||||
../libsample/libsample.a \
|
||||
../libtimer/libtimer.a \
|
||||
../libjitter/libjitter.a \
|
||||
../libosmocc/libosmocc.a \
|
||||
../libg711/libg711.a \
|
||||
../libdtmf/libdtmf.a \
|
||||
../libfm/libfm.a \
|
||||
../libfilter/libfilter.a \
|
||||
../libwave/libwave.a
|
||||
|
|
@ -0,0 +1,286 @@
|
|||
/* audio handling
|
||||
*
|
||||
* (C) 2020 by Andreas Eversberg <jolly@eversberg.eu>
|
||||
* All Rights Reserved
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Audio flow diagram:
|
||||
*
|
||||
* This diagrams shows the audio processing. The function for each processing
|
||||
* segment is given by the names ending with "()".
|
||||
*
|
||||
* ORIGINATOR
|
||||
*
|
||||
* receive_originator()
|
||||
* | /|\
|
||||
* | |
|
||||
* \|/ |
|
||||
* +-------+ +-------+
|
||||
* |int to | |samples|
|
||||
* |samples| |to int |
|
||||
* +-------+ +-------+
|
||||
* | /|\
|
||||
* +------+ | |
|
||||
* | |/ | |
|
||||
* | DTMF |---| |
|
||||
* | |\ | |
|
||||
* +------+ | |
|
||||
* \|/ |
|
||||
* +-------+ +-------+
|
||||
* | TX- | | RX- |
|
||||
* | GAIN | | GAIN |
|
||||
* +-------+ +-------+
|
||||
* | /|\
|
||||
* | |
|
||||
* | |
|
||||
* +------+ | | +------+
|
||||
* | TX- |/ | | \| RX- |
|
||||
* | |---| |---| |
|
||||
* |JITTER|\ | | /|JITTER|
|
||||
* +------+ | | +------+
|
||||
* | |
|
||||
* +------+ | |
|
||||
* | WAVE | | |
|
||||
* | |_ | |
|
||||
* | PLAY | \ | |
|
||||
* +------+ \| |
|
||||
* | |
|
||||
* \|/ send_originator()
|
||||
*-----------------------------------
|
||||
* send_terminator() /|\
|
||||
* | | +------+
|
||||
* | |\ | WAVE |
|
||||
* | | \_| | call_clock()
|
||||
* | | | PLAY |
|
||||
* \|/ | +------+
|
||||
* +-------+ +-------+
|
||||
* |samples| |int to |
|
||||
* |to int | |samples|
|
||||
* +-------+ +-------+
|
||||
* | /|\
|
||||
* | |
|
||||
* \|/ |
|
||||
* receive_terminator()
|
||||
*
|
||||
* TERMINATOR
|
||||
*
|
||||
* In recording mode:
|
||||
* Data is stored into jitter buffer of each endpoint.
|
||||
* The clock triggers dejittering of TX and RX data and writes it to wave file.
|
||||
*
|
||||
* In playback mode:
|
||||
* The clock triggers read from wave file and forwards it to the originator.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <math.h>
|
||||
#include <sys/types.h>
|
||||
#include <arpa/inet.h>
|
||||
#include "../libdebug/debug.h"
|
||||
#include "call.h"
|
||||
#include "audio.h"
|
||||
|
||||
#define db2level(db) pow(10, (double)db / 20.0)
|
||||
|
||||
static void gain_samples(sample_t *samples, int length, double gain)
|
||||
{
|
||||
double level = db2level(gain);
|
||||
int i;
|
||||
|
||||
for (i = 0; i < length; i++)
|
||||
*samples++ *= level;
|
||||
}
|
||||
|
||||
static void send_terminator(call_relation_t *relation, sample_t *samples, int len)
|
||||
{
|
||||
int16_t spl[len];
|
||||
|
||||
/* convert samples to int16 */
|
||||
samples_to_int16(spl, samples, len);
|
||||
|
||||
/* encode and send via RTP */
|
||||
osmo_cc_rtp_send(relation->codec, (uint8_t *)spl, len * sizeof(*spl), 1, len);
|
||||
}
|
||||
|
||||
void receive_originator(struct osmo_cc_session_codec *codec, uint16_t __attribute__((unused)) sequence_number, uint32_t __attribute__((unused)) timestamp, uint8_t *data, int len)
|
||||
{
|
||||
call_relation_t *relation = codec->media->session->priv;
|
||||
len = len / 2;
|
||||
sample_t samples[len];
|
||||
|
||||
/* convert int16 to samples */
|
||||
int16_to_samples(samples, (int16_t *)data, len);
|
||||
|
||||
/* dtmf decoding */
|
||||
if (relation->dtmf_dec_enable)
|
||||
dtmf_decode(&relation->dtmf_dec, samples, len);
|
||||
|
||||
/* adjust gain */
|
||||
if (relation->call->tx_gain)
|
||||
gain_samples(samples, len, relation->call->tx_gain);
|
||||
|
||||
/* store to originator jitter buffer */
|
||||
jitter_save(&relation->orig_dejitter, samples, len);
|
||||
|
||||
/* forward to terminators */
|
||||
for (relation = relation->next; relation; relation = relation->next) {
|
||||
if (relation->cc_session && relation->codec && !relation->play.fp)
|
||||
send_terminator(relation, samples, len);
|
||||
}
|
||||
}
|
||||
|
||||
static void send_originator(call_relation_t *relation, sample_t *samples, int len)
|
||||
{
|
||||
int16_t spl[len];
|
||||
|
||||
/* store to terminator jitter buffer */
|
||||
jitter_save(&relation->term_dejitter, samples, len);
|
||||
|
||||
if (relation->call->rx_gain)
|
||||
gain_samples(samples, len, relation->call->rx_gain);
|
||||
|
||||
samples_to_int16(spl, samples, len);
|
||||
|
||||
osmo_cc_rtp_send(relation->codec, (uint8_t *)spl, len * sizeof(*spl), 1, len);
|
||||
}
|
||||
|
||||
void receive_terminator(struct osmo_cc_session_codec *codec, uint16_t __attribute__((unused)) sequence_number, uint32_t __attribute__((unused)) timestamp, uint8_t *data, int len)
|
||||
{
|
||||
call_relation_t *relation = codec->media->session->priv;
|
||||
len = len / 2;
|
||||
sample_t samples[len];
|
||||
|
||||
int16_to_samples(samples, (int16_t *)data, len);
|
||||
|
||||
/* forward to originator, if not a forking call */
|
||||
if (!relation->call->forking) {
|
||||
relation = relation->call->relation_list;
|
||||
if (relation->cc_session && relation->codec && !relation->play.fp)
|
||||
send_originator(relation, samples, len);
|
||||
}
|
||||
}
|
||||
|
||||
void call_media_handle(void)
|
||||
{
|
||||
call_t *call;
|
||||
call_relation_t *relation;
|
||||
|
||||
for (call = call_list; call; call = call->next) {
|
||||
for (relation = call->relation_list; relation; relation = relation->next) {
|
||||
if (relation->cc_session)
|
||||
osmo_cc_session_handle(relation->cc_session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void call_clock(int len)
|
||||
{
|
||||
call_t *call;
|
||||
call_relation_t *relation;
|
||||
sample_t buffer[len], buffer2[len], *samples[2];
|
||||
int i;
|
||||
int rc;
|
||||
|
||||
for (call = call_list; call; call = call->next) {
|
||||
relation = call->relation_list;
|
||||
if (!relation->cc_session || !relation->codec)
|
||||
continue;
|
||||
/* play */
|
||||
if (relation->play.fp) {
|
||||
int got = 0;
|
||||
read_again:
|
||||
samples[0] = buffer + got;
|
||||
samples[1] = buffer2 + got;
|
||||
rc = wave_read(&relation->play, samples, len - got);
|
||||
got += rc;
|
||||
/* we have a short read (hit the end) or nothing to play left (hit the end without short read) */
|
||||
if (!relation->play.left) {
|
||||
wave_destroy_playback(&relation->play);
|
||||
if (relation->play_loop) {
|
||||
int samplerate = 0, channels = 0;
|
||||
int rc;
|
||||
rc = wave_create_playback(&relation->play, relation->play_filename, &samplerate, &channels, relation->play_deviation);
|
||||
if (rc >= 0)
|
||||
goto read_again;
|
||||
} else {
|
||||
/* notify routing about finished playback */
|
||||
if (call->routing.routing)
|
||||
routing_send(&call->routing, "wave-finished");
|
||||
}
|
||||
}
|
||||
/* in case wie do not get all samples filled, append silence */
|
||||
while (got < len)
|
||||
buffer[got++] = 0;
|
||||
/* convert stereo to mono */
|
||||
if (relation->play.channels == 2) {
|
||||
for (i = 0; i < len; i++)
|
||||
buffer[i] += buffer2[i];
|
||||
}
|
||||
/* forward audio */
|
||||
if (relation == call->relation_list)
|
||||
send_originator(relation, buffer, len);
|
||||
else
|
||||
send_terminator(relation, buffer, len);
|
||||
}
|
||||
/* record
|
||||
* NOTE: jitter buffer is recorded at send_originator() or send_terminator, so it already includes wave playback */
|
||||
if (relation->rec.fp) {
|
||||
samples[0] = buffer;
|
||||
samples[1] = buffer2;
|
||||
jitter_load(&relation->orig_dejitter, samples[0], len);
|
||||
if (!call->forking && relation->next)
|
||||
jitter_load(&relation->term_dejitter, samples[1], len);
|
||||
else
|
||||
memset(samples[1], 0, len * sizeof(sample_t));
|
||||
wave_write(&relation->rec, samples, len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void encode_l16(uint8_t *src_data, int src_len, uint8_t **dst_data, int *dst_len)
|
||||
{
|
||||
uint16_t *src = (uint16_t *)src_data, *dst;
|
||||
int len = src_len / 2, i;
|
||||
|
||||
dst = malloc(len * 2);
|
||||
if (!dst)
|
||||
return;
|
||||
for (i = 0; i < len; i++)
|
||||
dst[i] = htons(src[i]);
|
||||
*dst_data = (uint8_t *)dst;
|
||||
*dst_len = len * 2;
|
||||
}
|
||||
|
||||
void decode_l16(uint8_t *src_data, int src_len, uint8_t **dst_data, int *dst_len)
|
||||
{
|
||||
uint16_t *src = (uint16_t *)src_data, *dst;
|
||||
int len = src_len / 2, i;
|
||||
|
||||
dst = malloc(len * 2);
|
||||
if (!dst)
|
||||
return;
|
||||
for (i = 0; i < len; i++)
|
||||
dst[i] = ntohs(src[i]);
|
||||
*dst_data = (uint8_t *)dst;
|
||||
*dst_len = len * 2;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
void receive_originator(struct osmo_cc_session_codec *codec, uint16_t sequence_number, uint32_t timestamp, uint8_t *data, int len);
|
||||
void receive_terminator(struct osmo_cc_session_codec *codec, uint16_t sequence_number, uint32_t timestamp, uint8_t *data, int len);
|
||||
void call_media_handle(void);
|
||||
void call_clock(int len);
|
||||
void encode_l16(uint8_t *src_data, int src_len, uint8_t **dst_data, int *dst_len);
|
||||
void decode_l16(uint8_t *src_data, int src_len, uint8_t **dst_data, int *dst_len);
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,84 @@
|
|||
|
||||
#include "../libtimer/timer.h"
|
||||
#include "../libosmocc/endpoint.h"
|
||||
#include "../libosmocc/helper.h"
|
||||
#include "../libsample/sample.h"
|
||||
#include "../libjitter/jitter.h"
|
||||
#include "../libwave/wave.h"
|
||||
#include "../libdtmf/dtmf_decode.h"
|
||||
|
||||
|
||||
enum call_state {
|
||||
CALL_STATE_IDLE = 0,
|
||||
CALL_STATE_SETUP,
|
||||
CALL_STATE_OVERLAP,
|
||||
CALL_STATE_PROCEEDING,
|
||||
CALL_STATE_ALERTING,
|
||||
CALL_STATE_CONNECT,
|
||||
CALL_STATE_DISC_FROM_ORIG,
|
||||
CALL_STATE_DISC_FROM_TERM,
|
||||
};
|
||||
|
||||
#include "routing.h"
|
||||
|
||||
/* relation to upper layer */
|
||||
typedef struct call_relation {
|
||||
struct call_relation *next;
|
||||
int num; /* number counter for debugging */
|
||||
struct call *call; /* points to the call */
|
||||
uint32_t cc_callref; /* callref for each releation */
|
||||
const char *sdp; /* received SDP? */
|
||||
int tones_recv; /* are inband tones available? */
|
||||
jitter_t orig_dejitter; /* jitter buffer for call recording (originating source) */
|
||||
jitter_t term_dejitter; /* jitter buffer for call recording (terminating source) */
|
||||
|
||||
char interface[256]; /* interface */
|
||||
char id[256]; /* caller ID / dialing*/
|
||||
enum call_state state; /* state for status display */
|
||||
|
||||
int rtp_proxy;
|
||||
osmo_cc_session_t *cc_session;
|
||||
int codec_negotiated;
|
||||
osmo_cc_session_codec_t *codec;
|
||||
|
||||
wave_play_t play; /* play a wave file */
|
||||
int play_loop; /* set to play loop */
|
||||
char play_filename[256];/* stored for reopen on loop */
|
||||
double play_deviation; /* stored for reopen on loop */
|
||||
wave_rec_t rec; /* record a wave file */
|
||||
dtmf_dec_t dtmf_dec; /* dtmf decoder */
|
||||
int dtmf_dec_enable;/* feed decoder with data */
|
||||
} call_relation_t;
|
||||
|
||||
/* call instance */
|
||||
typedef struct call {
|
||||
struct call *next;
|
||||
int num; /* number counter for debugging */
|
||||
|
||||
/* call */
|
||||
enum call_state state; /* current state of call */
|
||||
osmo_cc_msg_t *setup_msg; /* stored setup message for later IE forwarding */
|
||||
char dialing_number[256]; /* accumulated dial string (setup + info) */
|
||||
char dialing_keypad[256]; /* accumulated keypad string (setup + info) */
|
||||
int forking; /* set, if call is forked (started with 2 or more calls) */
|
||||
uint8_t collect_cause; /* cause from forking calls */
|
||||
call_relation_t *relation_list; /* list of all upper layer relations */
|
||||
/* NOTE: the first relation is always the originator */
|
||||
int tones_sent; /* did we announce inband tones? */
|
||||
int sdp_forwarded; /* if set, we cannot do RTP-Proxy anymore */
|
||||
|
||||
/* routing */
|
||||
routing_t routing;
|
||||
|
||||
/* audio */
|
||||
double tx_gain; /* gain of what is received from originator */
|
||||
double rx_gain; /* gain of what is transmitted to the originator */
|
||||
} call_t;
|
||||
|
||||
extern call_t *call_list;
|
||||
|
||||
int call_init(osmo_cc_endpoint_t *ep, const char *_routing_script, const char *_routing_shell);
|
||||
void call_exit(void);
|
||||
int call_handle(void);
|
||||
void cc_message(osmo_cc_endpoint_t *ep, uint32_t callref, osmo_cc_msg_t *msg);
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
#define MAX_DISPLAY_WIDTH 1024
|
||||
#define MAX_HEIGHT_STATUS 64
|
||||
|
||||
void display_status_on(int on);
|
||||
void display_status_start(void);
|
||||
void display_status_line(const char *from_if, int from_count, const char *from_id, int to_count, const char *to_if, const char *to_id, enum call_state to_state);
|
||||
void display_status_end(void);
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
/* display status functions
|
||||
*
|
||||
* (C) 2020 by Andreas Eversberg <jolly@eversberg.eu>
|
||||
* All Rights Reserved
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
#include "../libdebug/debug.h"
|
||||
#include "call.h"
|
||||
#include "display.h"
|
||||
|
||||
static int status_on = 0;
|
||||
static int line_count = 0;
|
||||
static int lines_total = 0;
|
||||
static char screen[MAX_HEIGHT_STATUS][MAX_DISPLAY_WIDTH];
|
||||
static char screen_color[MAX_HEIGHT_STATUS][MAX_DISPLAY_WIDTH];
|
||||
|
||||
static void print_status(int on)
|
||||
{
|
||||
int i, j;
|
||||
int w, h;
|
||||
char color, last_color = -1;
|
||||
|
||||
get_win_size(&w, &h);
|
||||
if (w > MAX_DISPLAY_WIDTH - 1)
|
||||
w = MAX_DISPLAY_WIDTH - 1;
|
||||
|
||||
if (w > MAX_DISPLAY_WIDTH)
|
||||
w = MAX_DISPLAY_WIDTH;
|
||||
h--;
|
||||
if (h > lines_total)
|
||||
h = lines_total;
|
||||
|
||||
printf("\0337\033[H");
|
||||
for (i = 0; i < h; i++) {
|
||||
j = 0;
|
||||
if (on) {
|
||||
for (j = 0; j < w; j++) {
|
||||
color = screen_color[i][j];
|
||||
if (screen[i][j] > ' ' && last_color != color) {
|
||||
printf("\033[%d;3%dm", color / 10, color % 10);
|
||||
last_color = color;
|
||||
}
|
||||
putchar(screen[i][j]);
|
||||
}
|
||||
} else {
|
||||
for (j = 0; j < w; j++)
|
||||
putchar(' ');
|
||||
}
|
||||
putchar('\n');
|
||||
}
|
||||
printf("\033[0;39m\0338"); fflush(stdout);
|
||||
}
|
||||
|
||||
void display_status_on(int on)
|
||||
{
|
||||
if (status_on)
|
||||
print_status(0);
|
||||
|
||||
if (on < 0)
|
||||
status_on = 1 - status_on;
|
||||
else
|
||||
status_on = on;
|
||||
|
||||
if (status_on)
|
||||
print_status(1);
|
||||
|
||||
if (status_on)
|
||||
debug_limit_scroll = lines_total;
|
||||
else
|
||||
debug_limit_scroll = 0;
|
||||
}
|
||||
|
||||
/* start status display */
|
||||
void display_status_start(void)
|
||||
{
|
||||
memset(screen, ' ', sizeof(screen));
|
||||
memset(screen_color, 7, sizeof(screen_color));
|
||||
memset(screen[0], '-', sizeof(screen[0]));
|
||||
memcpy(screen[0] + 4, "Call Status", 11);
|
||||
line_count = 1;
|
||||
}
|
||||
|
||||
void display_status_line(const char *from_if, int from_count, const char *from_id, int to_count, const char *to_if, const char *to_id, enum call_state to_state)
|
||||
{
|
||||
char line[MAX_DISPLAY_WIDTH + 4096];
|
||||
char color[MAX_DISPLAY_WIDTH + 4096];
|
||||
static int from_id_pos, to_if_pos;
|
||||
|
||||
memset(color, 7, sizeof(color)); // make valgrind happy
|
||||
|
||||
if (line_count == MAX_HEIGHT_STATUS)
|
||||
return;
|
||||
|
||||
if (!from_if)
|
||||
from_if = "<unknown>";
|
||||
|
||||
/* at first interface or when it changes */
|
||||
if (!from_count && !to_count) {
|
||||
from_id_pos = strlen(from_if) + 1;
|
||||
line_count++;
|
||||
}
|
||||
|
||||
/* at first call */
|
||||
if (from_id && !to_count) {
|
||||
to_if_pos = from_id_pos + 1 + strlen(from_id) + 1 + 4; /* quote,id,quote,arrow */
|
||||
}
|
||||
|
||||
/* check line count again */
|
||||
if (line_count == MAX_HEIGHT_STATUS)
|
||||
return;
|
||||
|
||||
if (!from_id) {
|
||||
/* only interface is given, since there is no call */
|
||||
strcpy(line, from_if);
|
||||
memset(color, 3, strlen(from_if));
|
||||
} else {
|
||||
/* originating call */
|
||||
memset(line, ' ', to_if_pos);
|
||||
if (!from_count && !to_count) {
|
||||
/* <if> */
|
||||
memcpy(line, from_if, strlen(from_if));
|
||||
memset(color, 3, strlen(from_if));
|
||||
}
|
||||
if (!to_count) {
|
||||
/* '<id>' */
|
||||
line[from_id_pos] = '\'';
|
||||
memcpy(line + from_id_pos + 1, from_id, strlen(from_id));
|
||||
line[from_id_pos + 1 + strlen(from_id)] = '\'';
|
||||
memset(color + from_id_pos, 1, 1 + strlen(from_id) + 1);
|
||||
}
|
||||
line[to_if_pos] = '\0';
|
||||
/* terminating call */
|
||||
if (to_if && to_id) {
|
||||
int to_id_pos, to_state_pos;
|
||||
/* arrow in the first line of a call */
|
||||
if (!to_count) {
|
||||
/* -> <if> '<id>' */
|
||||
line[to_if_pos - 3] = '-';
|
||||
line[to_if_pos - 2] = '>';
|
||||
color[to_if_pos - 3] = 7;
|
||||
color[to_if_pos - 2] = 7;
|
||||
}
|
||||
sprintf(line + to_if_pos, "%s '%s' ", to_if, to_id);
|
||||
memset(color + to_if_pos, 3, strlen(to_if));
|
||||
to_id_pos = to_if_pos + strlen(to_if) + 1;
|
||||
memset(color + to_id_pos, 2, 1 + strlen(to_id) + 1);
|
||||
to_state_pos = to_id_pos + 1 + strlen(to_id) + 1 + 1;
|
||||
|
||||
switch (to_state) {
|
||||
case CALL_STATE_SETUP:
|
||||
strcpy(line + to_state_pos, "[setup]");
|
||||
/* magenta */
|
||||
memset(color + to_state_pos + 1, 15, 5);
|
||||
break;
|
||||
case CALL_STATE_OVERLAP:
|
||||
strcpy(line + to_state_pos, "[overlap]");
|
||||
/* green */
|
||||
memset(color + to_state_pos + 1, 12, 7);
|
||||
break;
|
||||
case CALL_STATE_PROCEEDING:
|
||||
strcpy(line + to_state_pos, "[proceeding]");
|
||||
/* cyan */
|
||||
memset(color + to_state_pos + 1, 16, 10);
|
||||
break;
|
||||
case CALL_STATE_ALERTING:
|
||||
strcpy(line + to_state_pos, "[alerting]");
|
||||
/* yellow */
|
||||
memset(color + to_state_pos + 1, 13, 8);
|
||||
break;
|
||||
case CALL_STATE_CONNECT:
|
||||
strcpy(line + to_state_pos, "[connect]");
|
||||
/* white */
|
||||
memset(color + to_state_pos + 1, 17, 7);
|
||||
break;
|
||||
case CALL_STATE_DISC_FROM_ORIG:
|
||||
strcpy(line + to_state_pos, "[out disconnect]");
|
||||
/* red */
|
||||
memset(color + to_state_pos + 1, 11, 14);
|
||||
break;
|
||||
case CALL_STATE_DISC_FROM_TERM:
|
||||
strcpy(line + to_state_pos, "[in disconnect]");
|
||||
/* red */
|
||||
memset(color + to_state_pos + 1, 11, 13);
|
||||
break;
|
||||
default:
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* store line without CR, but not more than MAX_DISPLAY_WIDTH - 1 */
|
||||
line[MAX_DISPLAY_WIDTH - 1] = '\0';
|
||||
memcpy(screen[line_count], line, strlen(line));
|
||||
memcpy(screen_color[line_count], color, strlen(line));
|
||||
line_count++;
|
||||
}
|
||||
|
||||
void display_status_end(void)
|
||||
{
|
||||
if (line_count < MAX_HEIGHT_STATUS)
|
||||
line_count++;
|
||||
|
||||
if (line_count < MAX_HEIGHT_STATUS) {
|
||||
memset(screen[line_count], '-', sizeof(screen[line_count]));
|
||||
line_count++;
|
||||
}
|
||||
/* if last total lines exceed current line count, keep it, so removed lines are overwritten with spaces */
|
||||
if (line_count > lines_total)
|
||||
lines_total = line_count;
|
||||
if (status_on)
|
||||
print_status(1);
|
||||
/* set new total lines */
|
||||
lines_total = line_count;
|
||||
if (status_on)
|
||||
debug_limit_scroll = lines_total;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,260 @@
|
|||
/* osmo-cc-router main
|
||||
*
|
||||
* (C) 2020 by Andreas Eversberg <jolly@eversberg.eu>
|
||||
* All Rights Reserved
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <termios.h>
|
||||
#include "../libdebug/debug.h"
|
||||
#include "../liboptions/options.h"
|
||||
#include "../libg711/g711.h"
|
||||
#include "call.h"
|
||||
#include "audio.h"
|
||||
#include "display.h"
|
||||
|
||||
int num_kanal = 1;
|
||||
osmo_cc_endpoint_t *cc_ep = NULL;
|
||||
|
||||
static char *routing_script = "~/.osmocom/router/routing.sh";
|
||||
static char *routing_shell = "bash";
|
||||
#define MAX_CC_ARGS 1024
|
||||
static int cc_argc = 0;
|
||||
static const char *cc_argv[MAX_CC_ARGS];
|
||||
|
||||
static void print_usage(const char *app)
|
||||
{
|
||||
printf("Usage: %s [<options>]\n", app);
|
||||
}
|
||||
|
||||
static void print_help()
|
||||
{
|
||||
/* - - */
|
||||
printf(" -h --help\n");
|
||||
printf(" This help\n");
|
||||
printf(" -v --verbose <level> | <level>,<category>[,<category>[,...]] | list\n");
|
||||
printf(" Use 'list' to get a list of all levels and categories\n");
|
||||
printf(" Verbose level: digit of debug level (default = '%d')\n", debuglevel);
|
||||
printf(" Verbose level+category: level digit followed by one or more categories\n");
|
||||
printf(" -r --routing-script <script>\n");
|
||||
printf(" Define the script/executable that is executed to perform routing\n");
|
||||
printf(" decision for each call. (default = %s)\n", routing_script);
|
||||
printf(" -s --routing-shell <>\n");
|
||||
printf(" Define the shell to run the routing scrip. (default = %s)\n", routing_shell);
|
||||
printf(" -C --cc \"<osmo-cc arg>\" [--cc ...]\n");
|
||||
printf(" Pass arguments to Osmo-CC endpoint. Use '-cc help' for description.\n");
|
||||
}
|
||||
|
||||
#define OPT_XXX 256
|
||||
|
||||
static void add_options(void)
|
||||
{
|
||||
option_add('h', "help", 0);
|
||||
option_add('v', "verbose", 1);
|
||||
option_add('r', "routing-script", 1);
|
||||
option_add('s', "routing-shell", 1);
|
||||
option_add('C', "cc", 1);
|
||||
}
|
||||
|
||||
static int handle_options(int short_option, int argi, char **argv)
|
||||
{
|
||||
int rc;
|
||||
|
||||
switch (short_option) {
|
||||
case 'h':
|
||||
print_usage(argv[0]);
|
||||
print_help();
|
||||
return 0;
|
||||
case 'v':
|
||||
if (!strcasecmp(argv[argi], "list")) {
|
||||
debug_list_cat();
|
||||
return 0;
|
||||
}
|
||||
rc = parse_debug_opt(argv[argi]);
|
||||
if (rc < 0) {
|
||||
fprintf(stderr, "Failed to parse debug option, please use -h for help.\n");
|
||||
return rc;
|
||||
}
|
||||
break;
|
||||
case 'r':
|
||||
routing_script = strdup(argv[argi]);
|
||||
break;
|
||||
case 's':
|
||||
routing_shell = strdup(argv[argi]);
|
||||
break;
|
||||
case 'C':
|
||||
if (!strcasecmp(argv[argi], "help")) {
|
||||
osmo_cc_help();
|
||||
return 0;
|
||||
}
|
||||
if (cc_argc == MAX_CC_ARGS) {
|
||||
fprintf(stderr, "Too many osmo-cc args!\n");
|
||||
break;
|
||||
}
|
||||
cc_argv[cc_argc++] = strdup(argv[argi]);
|
||||
break;
|
||||
default:
|
||||
return -EINVAL;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int quit = 0;
|
||||
void sighandler(int sigset)
|
||||
{
|
||||
if (sigset == SIGHUP || sigset == SIGPIPE)
|
||||
return;
|
||||
|
||||
fprintf(stderr, "\nSignal %d received.\n", sigset);
|
||||
|
||||
quit = 1;
|
||||
}
|
||||
|
||||
static int get_char()
|
||||
{
|
||||
struct timeval tv = {0, 0};
|
||||
fd_set fds;
|
||||
char c = 0;
|
||||
int __attribute__((__unused__)) rc;
|
||||
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(0, &fds);
|
||||
select(0+1, &fds, NULL, NULL, &tv);
|
||||
if (FD_ISSET(0, &fds)) {
|
||||
rc = read(0, &c, 1);
|
||||
return c;
|
||||
} else
|
||||
return -1;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
double last_time_call = 0, now;
|
||||
int argi, rc;
|
||||
struct termios term, term_orig;
|
||||
int c;
|
||||
|
||||
/* init FM */
|
||||
fm_init(0);
|
||||
|
||||
/* init codecs (for recording) */
|
||||
g711_init();
|
||||
|
||||
/* handle options / config file */
|
||||
add_options();
|
||||
rc = options_config_file("~/.osmocom/router/router.conf", handle_options);
|
||||
if (rc < 0)
|
||||
return 0;
|
||||
argi = options_command_line(argc, argv, handle_options);
|
||||
if (argi <= 0)
|
||||
return argi;
|
||||
|
||||
/* init osmo-cc endpoint */
|
||||
cc_ep = calloc(1, sizeof(*cc_ep));
|
||||
if (!cc_ep)
|
||||
goto error;
|
||||
rc = osmo_cc_new(cc_ep, OSMO_CC_VERSION, NULL, OSMO_CC_LOCATION_PRIV_SERV_LOC_USER, cc_message, NULL, NULL, cc_argc, cc_argv);
|
||||
if (rc < 0)
|
||||
goto error;
|
||||
|
||||
/* init call handling */
|
||||
call_init(cc_ep, routing_script, routing_shell);
|
||||
|
||||
/* prepare terminal */
|
||||
tcgetattr(0, &term_orig);
|
||||
term = term_orig;
|
||||
term.c_lflag &= ~(ISIG|ICANON|ECHO);
|
||||
term.c_cc[VMIN]=1;
|
||||
term.c_cc[VTIME]=2;
|
||||
tcsetattr(0, TCSANOW, &term);
|
||||
|
||||
/* catch signals */
|
||||
signal(SIGINT, sighandler);
|
||||
signal(SIGHUP, sighandler);
|
||||
signal(SIGTERM, sighandler);
|
||||
signal(SIGPIPE, sighandler);
|
||||
|
||||
printf("\nRouter is ready to process calls.\n\n");
|
||||
|
||||
while (!quit) {
|
||||
int w;
|
||||
|
||||
/* send clock calls to play/record audio files */
|
||||
now = get_time();
|
||||
if (now - last_time_call >= 0.1)
|
||||
last_time_call = now;
|
||||
if (now - last_time_call >= 0.020) {
|
||||
last_time_call += 0.020;
|
||||
/* call clock every 20ms */
|
||||
call_clock(160);
|
||||
}
|
||||
|
||||
process_timer();
|
||||
call_media_handle();
|
||||
do {
|
||||
w = 0;
|
||||
w |= osmo_cc_handle();
|
||||
w |= call_handle();
|
||||
} while (w);
|
||||
usleep(1000);
|
||||
|
||||
/* process keyboard input */
|
||||
next_char:
|
||||
c = get_char();
|
||||
switch (c) {
|
||||
case 3:
|
||||
printf("CTRL+c received, quitting!\n");
|
||||
quit = 1;
|
||||
goto next_char;
|
||||
case 'c':
|
||||
display_status_on(-1);
|
||||
goto next_char;
|
||||
}
|
||||
}
|
||||
|
||||
/* reset signals */
|
||||
signal(SIGINT, SIG_DFL);
|
||||
signal(SIGTSTP, SIG_DFL);
|
||||
signal(SIGHUP, SIG_DFL);
|
||||
signal(SIGTERM, SIG_DFL);
|
||||
signal(SIGPIPE, SIG_DFL);
|
||||
|
||||
/* reset terminal */
|
||||
tcsetattr(0, TCSANOW, &term_orig);
|
||||
|
||||
|
||||
error:
|
||||
if (cc_ep) {
|
||||
/* exit call handling */
|
||||
call_exit();
|
||||
|
||||
/* exit osmo-cc endpoint */
|
||||
osmo_cc_delete(cc_ep);
|
||||
free(cc_ep);
|
||||
}
|
||||
|
||||
/* exit FM */
|
||||
fm_exit();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,490 @@
|
|||
/* call routing
|
||||
*
|
||||
* (C) 2020 by Andreas Eversberg <jolly@eversberg.eu>
|
||||
* All Rights Reserved
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/resource.h>
|
||||
#include <sys/types.h>
|
||||
#include <signal.h>
|
||||
#include "../libdebug/debug.h"
|
||||
#include "call.h"
|
||||
|
||||
extern char **environ;
|
||||
|
||||
static const char *env_int(const char *name, int value)
|
||||
{
|
||||
char *string = malloc(strlen(name) + 20);
|
||||
|
||||
sprintf(string, "CC_%s=%d", name, value);
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
static const char *env_string(const char *name, const char *value)
|
||||
{
|
||||
char *string = malloc(strlen(name) + strlen(value) + 8);
|
||||
|
||||
sprintf(string, "CC_%s=%s", name, value);
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
static void enqueue_string(struct string_queue **queue_p, const char *string, const char *suffix)
|
||||
{
|
||||
struct string_queue *queue;
|
||||
|
||||
queue = calloc(1, sizeof(*queue));
|
||||
queue->string = malloc(strlen(string) + strlen(suffix) + 1);
|
||||
strcpy(queue->string, string);
|
||||
strcat(queue->string, suffix);
|
||||
|
||||
while (*queue_p)
|
||||
queue_p = &((*queue_p)->next);
|
||||
*queue_p = queue;
|
||||
}
|
||||
|
||||
static char *dequeue_string(struct string_queue **queue_p)
|
||||
{
|
||||
struct string_queue *queue = *queue_p;
|
||||
char *string;
|
||||
|
||||
if (!queue)
|
||||
return NULL;
|
||||
|
||||
string = queue->string;
|
||||
*queue_p = queue->next;
|
||||
free(queue);
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
/* prepare environment with setup info */
|
||||
void routing_env_msg(routing_t *routing, osmo_cc_msg_t *msg)
|
||||
{
|
||||
uint8_t coding, capability, mode;
|
||||
uint8_t type, plan, present, screen, reason;
|
||||
char number[256];
|
||||
int rc, i;
|
||||
|
||||
for (i = 0; environ[i]; i++) {
|
||||
routing->envp[routing->envc++] = strdup(environ[i]);
|
||||
}
|
||||
|
||||
rc = osmo_cc_get_ie_bearer(msg, 0, &coding, &capability, &mode);
|
||||
if (rc >= 0) {
|
||||
routing->envp[routing->envc++] = env_int("BEARER_CODING", coding);
|
||||
routing->envp[routing->envc++] = env_int("BEARER_CAPABILITY", capability);
|
||||
routing->envp[routing->envc++] = env_int("BEARER_MODE", mode);
|
||||
}
|
||||
|
||||
rc = osmo_cc_get_ie_calling(msg, 0, &type, &plan, &present, &screen, number, sizeof(number));
|
||||
if (rc >= 0) {
|
||||
routing->envp[routing->envc++] = env_int("CALLING_TYPE", type);
|
||||
routing->envp[routing->envc++] = env_int("CALLING_PLAN", plan);
|
||||
routing->envp[routing->envc++] = env_int("CALLING_PRESENT", present);
|
||||
routing->envp[routing->envc++] = env_int("CALLING_SCREEN", screen);
|
||||
routing->envp[routing->envc++] = env_string("CALLING", number);
|
||||
}
|
||||
rc = osmo_cc_get_ie_calling_interface(msg, 0, number, sizeof(number));
|
||||
if (rc >= 0) {
|
||||
routing->envp[routing->envc++] = env_string("CALLING_INTERFACE", number);
|
||||
}
|
||||
|
||||
rc = osmo_cc_get_ie_calling(msg, 2, &type, &plan, &present, &screen, number, sizeof(number));
|
||||
if (rc >= 0) {
|
||||
routing->envp[routing->envc++] = env_int("CALLING2_TYPE", type);
|
||||
routing->envp[routing->envc++] = env_int("CALLING2_PLAN", plan);
|
||||
routing->envp[routing->envc++] = env_int("CALLING2_PRESENT", present);
|
||||
routing->envp[routing->envc++] = env_int("CALLING2_SCREEN", screen);
|
||||
routing->envp[routing->envc++] = env_string("CALLING2", number);
|
||||
}
|
||||
|
||||
rc = osmo_cc_get_ie_redir(msg, 0, &type, &plan, &present, &screen, &reason, number, sizeof(number));
|
||||
if (rc >= 0) {
|
||||
routing->envp[routing->envc++] = env_int("REDIRECTING_TYPE", type);
|
||||
routing->envp[routing->envc++] = env_int("REDIRECTING_PLAN", plan);
|
||||
routing->envp[routing->envc++] = env_int("REDIRECTING_PRESENT", present);
|
||||
routing->envp[routing->envc++] = env_int("REDIRECTING_SCREEN", screen);
|
||||
routing->envp[routing->envc++] = env_int("REDIRECTING_REASON", reason);
|
||||
routing->envp[routing->envc++] = env_string("REDIRECTING", number);
|
||||
}
|
||||
|
||||
rc = osmo_cc_get_ie_called(msg, 0, &type, &plan, number, sizeof(number));
|
||||
if (rc >= 0) {
|
||||
routing->envp[routing->envc++] = env_int("DIALING_TYPE", type);
|
||||
routing->envp[routing->envc++] = env_int("DIALING_PLAN", plan);
|
||||
} else
|
||||
number[0] = 0;
|
||||
/* variable must always be present, so it can be updated when overlap dialing */
|
||||
routing->envc_dialing = routing->envc;
|
||||
routing->envp[routing->envc++] = env_string("DIALING", number);
|
||||
|
||||
rc = osmo_cc_get_ie_keypad(msg, 0, number, sizeof(number));
|
||||
if (rc < 0)
|
||||
number[0] = 0;
|
||||
/* variable must always be present, so it can be updated when overlap dialing */
|
||||
routing->envc_keypad = routing->envc;
|
||||
routing->envp[routing->envc++] = env_string("KEYPAD", number);
|
||||
|
||||
rc = osmo_cc_get_ie_complete(msg, 0);
|
||||
if (rc >= 0)
|
||||
routing->envp[routing->envc++] = env_int("COMPLETE", 1);
|
||||
|
||||
routing->envp[routing->envc++] = NULL;
|
||||
}
|
||||
|
||||
/* update environment with info message */
|
||||
void routing_env_dialing(routing_t *routing, const char *number, const char *keypad)
|
||||
{
|
||||
free((char *)routing->envp[routing->envc_dialing]);
|
||||
routing->envp[routing->envc_dialing] = env_string("DIALING", number);
|
||||
|
||||
free((char *)routing->envp[routing->envc_keypad]);
|
||||
routing->envp[routing->envc_keypad] = env_string("KEYPAD", keypad);
|
||||
}
|
||||
|
||||
void routing_env_free(routing_t *routing)
|
||||
{
|
||||
/* remove env */
|
||||
while (routing->envc) {
|
||||
free((char *)routing->envp[--(routing->envc)]);
|
||||
routing->envp[routing->envc] = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* run script */
|
||||
void routing_start(routing_t *routing, const char *script, const char *shell)
|
||||
{
|
||||
int in_pipe[2], out_pipe[2], err_pipe[2];
|
||||
pid_t pid;
|
||||
char x;
|
||||
int rc, flags;
|
||||
|
||||
rc = pipe(in_pipe);
|
||||
if (rc < 0) {
|
||||
epipe:
|
||||
PDEBUG(DROUTER, DEBUG_ERROR, "pipe() failed: errno=%d\n", errno);
|
||||
abort();
|
||||
}
|
||||
rc = pipe(out_pipe);
|
||||
if (rc < 0)
|
||||
goto epipe;
|
||||
rc = pipe(err_pipe);
|
||||
if (rc < 0)
|
||||
goto epipe;
|
||||
|
||||
pid = fork();
|
||||
if (pid < 0) {
|
||||
PDEBUG(DROUTER, DEBUG_ERROR, "fork() failed: errno=%d\n", errno);
|
||||
abort();
|
||||
}
|
||||
if (pid == 0) {
|
||||
const char *argv[] = { shell, "-c", script, NULL };
|
||||
struct rlimit rlim;
|
||||
int i;
|
||||
|
||||
/* CHILD */
|
||||
/* redirect pipes to stdio */
|
||||
close(STDIN_FILENO);
|
||||
close(STDOUT_FILENO);
|
||||
close(STDERR_FILENO);
|
||||
dup2(in_pipe[0], STDIN_FILENO);
|
||||
dup2(out_pipe[1], STDOUT_FILENO);
|
||||
dup2(err_pipe[1], STDERR_FILENO);
|
||||
|
||||
/* close all file descriptors except std* fss */
|
||||
getrlimit(RLIMIT_NOFILE, &rlim);
|
||||
for (i = 3; i < (int)rlim.rlim_cur; i++)
|
||||
close(i);
|
||||
|
||||
/* tell father that we closed all fds */
|
||||
putchar('x');
|
||||
fflush(stdout);
|
||||
|
||||
/* become script */
|
||||
rc = execvpe(argv[0], (char * const*)argv, (char * const*)routing->envp);
|
||||
if (rc < 0) {
|
||||
printf("error \"%s\"\n", strerror(errno));
|
||||
}
|
||||
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
/* PARENT */
|
||||
PDEBUG(DROUTER, DEBUG_DEBUG, "Running script '%s' as child %d\n", script, pid);
|
||||
|
||||
/* wait for clild to complete closing file descriptors */
|
||||
rc = read(out_pipe[0], &x, 1);
|
||||
if (rc != 1 || x != 'x') {
|
||||
PDEBUG(DROUTER, DEBUG_ERROR, "communication with child failed!\n");
|
||||
kill(pid, SIGKILL);
|
||||
abort();
|
||||
}
|
||||
|
||||
/* close unused file descriptors, except the child's side
|
||||
* this is because we need to keep it open to get all data from stdout/stderr
|
||||
* after the child exitted. for some reason the pipe is flused on close.
|
||||
*/
|
||||
close(in_pipe[0]);
|
||||
|
||||
/* make nonblocking IO */
|
||||
flags = fcntl(in_pipe[1], F_GETFL);
|
||||
flags |= O_NONBLOCK;
|
||||
fcntl(in_pipe[1], F_SETFL, flags);
|
||||
flags = fcntl(out_pipe[0], F_GETFL);
|
||||
flags |= O_NONBLOCK;
|
||||
fcntl(out_pipe[0], F_SETFL, flags);
|
||||
flags = fcntl(err_pipe[0], F_GETFL);
|
||||
flags |= O_NONBLOCK;
|
||||
fcntl(err_pipe[0], F_SETFL, flags);
|
||||
|
||||
/* attach pipes and pid to routing */
|
||||
routing->script_pid = pid;
|
||||
routing->script_stdin = in_pipe[1];
|
||||
routing->script_stdout = out_pipe[0];
|
||||
routing->script_stdout_child = out_pipe[1];
|
||||
routing->script_stderr = err_pipe[0];
|
||||
routing->script_stderr_child = err_pipe[1];
|
||||
|
||||
routing->routing = 1;
|
||||
|
||||
PDEBUG(DROUTER, DEBUG_DEBUG, "Routing script started.\n");
|
||||
}
|
||||
|
||||
|
||||
void routing_stop(routing_t *routing)
|
||||
{
|
||||
char *string;
|
||||
|
||||
/* kill or at least try to */
|
||||
if (routing->script_pid) {
|
||||
kill(routing->script_pid, SIGTERM);
|
||||
routing->script_pid = 0;
|
||||
}
|
||||
|
||||
/* close files towards script */
|
||||
if (routing->script_stdin) {
|
||||
close(routing->script_stdin);
|
||||
routing->script_stdin = 0;
|
||||
}
|
||||
if (routing->script_stdout) {
|
||||
close(routing->script_stdout);
|
||||
routing->script_stdout = 0;
|
||||
close(routing->script_stdout_child);
|
||||
routing->script_stdout_child = 0;
|
||||
}
|
||||
if (routing->script_stderr) {
|
||||
close(routing->script_stderr);
|
||||
routing->script_stderr = 0;
|
||||
close(routing->script_stderr_child);
|
||||
routing->script_stderr_child = 0;
|
||||
}
|
||||
|
||||
/* flush queues */
|
||||
while (routing->stdin_queue) {
|
||||
string = dequeue_string(&routing->stdin_queue);
|
||||
free(string);
|
||||
}
|
||||
while (routing->stdout_queue) {
|
||||
string = dequeue_string(&routing->stdout_queue);
|
||||
free(string);
|
||||
}
|
||||
routing->stdout_pos = 0;
|
||||
while (routing->stderr_queue) {
|
||||
string = dequeue_string(&routing->stderr_queue);
|
||||
free(string);
|
||||
}
|
||||
routing->stderr_pos = 0;
|
||||
|
||||
routing->routing = 0;
|
||||
|
||||
PDEBUG(DROUTER, DEBUG_DEBUG, "Routing script stopped.\n");
|
||||
}
|
||||
|
||||
|
||||
void routing_send(routing_t *routing, const char *string)
|
||||
{
|
||||
PDEBUG(DROUTER, DEBUG_DEBUG, "Sending line to routing script: '%s'\n", string);
|
||||
enqueue_string(&routing->stdin_queue, string, "\n");
|
||||
}
|
||||
|
||||
static int routing_handle_stdin(routing_t *routing)
|
||||
{
|
||||
char *string;
|
||||
int rc;
|
||||
|
||||
/* write to script */
|
||||
if (routing->stdin_queue && routing->script_stdin) {
|
||||
rc = write(routing->script_stdin, routing->stdin_queue->string, strlen(routing->stdin_queue->string));
|
||||
if (rc < 0) {
|
||||
if (errno == EAGAIN)
|
||||
return -EAGAIN;
|
||||
}
|
||||
if (rc <= 0) {
|
||||
close(routing->script_stdin);
|
||||
routing->script_stdin = 0;
|
||||
return 0;
|
||||
}
|
||||
string = dequeue_string(&routing->stdin_queue);
|
||||
free(string);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
static int routing_handle_stdout(routing_t *routing)
|
||||
{
|
||||
int rc, i;
|
||||
|
||||
/* read from script */
|
||||
if (routing->script_stdout) {
|
||||
rc = read(routing->script_stdout, routing->stdout_buffer + routing->stdout_pos, sizeof(routing->stdout_buffer) - routing->stdout_pos);
|
||||
if (rc < 0) {
|
||||
if (errno == EAGAIN) {
|
||||
/* if script has terminated (pid is set to 0) and stdout/stderr queue is empty */
|
||||
if (!routing->script_pid && !routing->stdout_queue && !routing->stderr_queue) {
|
||||
routing_stop(routing);
|
||||
routing_close(routing);
|
||||
return 0;
|
||||
}
|
||||
return -EAGAIN;
|
||||
}
|
||||
}
|
||||
if (rc <= 0) {
|
||||
close(routing->script_stdout);
|
||||
routing->script_stdout = 0;
|
||||
return 0;
|
||||
}
|
||||
routing->stdout_pos += rc;
|
||||
i = 0;
|
||||
while (i < routing->stdout_pos) {
|
||||
if (routing->stdout_buffer[i] != '\n') {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
routing->stdout_buffer[i] = '\0';
|
||||
enqueue_string(&routing->stdout_queue, routing->stdout_buffer, "");
|
||||
i++;
|
||||
if (i < routing->stdout_pos)
|
||||
memcpy(routing->stdout_buffer, routing->stdout_buffer + i, routing->stdout_pos - i);
|
||||
routing->stdout_pos -= i;
|
||||
i = 0;
|
||||
}
|
||||
if (i == sizeof(routing->stdout_buffer)) {
|
||||
PDEBUG(DROUTER, DEBUG_ERROR, "Output line from script too long, please fix!\n");
|
||||
routing->stdout_pos = 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
static int routing_handle_stderr(routing_t *routing)
|
||||
{
|
||||
int rc, i;
|
||||
|
||||
/* read from script */
|
||||
if (routing->script_stderr) {
|
||||
rc = read(routing->script_stderr, routing->stderr_buffer + routing->stderr_pos, sizeof(routing->stderr_buffer) - routing->stderr_pos);
|
||||
if (rc < 0) {
|
||||
if (errno == EAGAIN) {
|
||||
/* if script has terminated (pid is set to 0) and stdout/stderr queue is empty */
|
||||
if (!routing->script_pid && !routing->stdout_queue && !routing->stderr_queue) {
|
||||
routing_stop(routing);
|
||||
routing_close(routing);
|
||||
return 0;
|
||||
}
|
||||
return -EAGAIN;
|
||||
}
|
||||
}
|
||||
if (rc <= 0) {
|
||||
close(routing->script_stderr);
|
||||
routing->script_stderr = 0;
|
||||
return 0;
|
||||
}
|
||||
routing->stderr_pos += rc;
|
||||
i = 0;
|
||||
while (i < routing->stderr_pos) {
|
||||
if (routing->stderr_buffer[i] != '\n') {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
routing->stderr_buffer[i] = '\0';
|
||||
enqueue_string(&routing->stderr_queue, routing->stderr_buffer, "");
|
||||
i++;
|
||||
if (i < routing->stderr_pos)
|
||||
memcpy(routing->stderr_buffer, routing->stderr_buffer + i, routing->stderr_pos - i);
|
||||
routing->stderr_pos -= i;
|
||||
i = 0;
|
||||
}
|
||||
if (i == sizeof(routing->stderr_buffer)) {
|
||||
PDEBUG(DROUTER, DEBUG_ERROR, "Output line from script too long, please fix!\n");
|
||||
routing->stderr_pos = 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
/* handle everything that has to do with routing
|
||||
* note that work may cause the call to be destroyed
|
||||
*/
|
||||
int routing_handle(routing_t *routing)
|
||||
{
|
||||
char *string;
|
||||
int rc;
|
||||
|
||||
rc = routing_handle_stdin(routing);
|
||||
if (rc == 0)
|
||||
return 1;
|
||||
rc = routing_handle_stdout(routing);
|
||||
if (rc == 0)
|
||||
return 1;
|
||||
rc = routing_handle_stderr(routing);
|
||||
if (rc == 0)
|
||||
return 1;
|
||||
|
||||
if (routing->stdout_queue) {
|
||||
string = dequeue_string(&routing->stdout_queue);
|
||||
PDEBUG(DROUTER, DEBUG_DEBUG, "Routing script returned line from stdout: '%s'\n", string);
|
||||
routing_receive_stdout(routing, string);
|
||||
free(string);
|
||||
return 1;
|
||||
}
|
||||
if (routing->stderr_queue) {
|
||||
string = dequeue_string(&routing->stderr_queue);
|
||||
routing_receive_stderr(routing, string);
|
||||
free(string);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#warning achja: beim router darf rtp-proxy und call nur erfolgen, wenn noch kein codec negoitiated wurde. call darf aber nach call erfolgen, wenn rtp-proxy verwendet wird. tones und record darf erfolgen wemm rtp-proxy verwendet wird
|
|
@ -0,0 +1,47 @@
|
|||
|
||||
struct string_queue {
|
||||
struct string_queue *next;
|
||||
char *string;
|
||||
};
|
||||
|
||||
struct call;
|
||||
|
||||
typedef struct routing {
|
||||
struct call *call;
|
||||
|
||||
int routing; /* set, if routing is performed (script runs) */
|
||||
|
||||
int envc; /* number of environment variables */
|
||||
const char *envp[256]; /* environment variables */
|
||||
int envc_dialing; /* envc index for dialing number */
|
||||
int envc_keypad; /* envc index for keypad */
|
||||
|
||||
pid_t script_pid; /* pid of routing script */
|
||||
int script_stdin; /* pipe to stdin */
|
||||
int script_stdout; /* pipe from stdout */
|
||||
int script_stdout_child; /* child side of pipe */
|
||||
int script_stderr; /* pipe from stderr */
|
||||
int script_stderr_child; /* child side of pipe */
|
||||
|
||||
struct string_queue *stdin_queue; /* strings to write to script */
|
||||
char stdout_buffer[1024]; /* line buffer when reading from script */
|
||||
int stdout_pos; /* number of characters in buffer */
|
||||
struct string_queue *stdout_queue; /* strings read from script */
|
||||
char stderr_buffer[1024]; /* line buffer when reading from script */
|
||||
int stderr_pos; /* number of characters in buffer */
|
||||
struct string_queue *stderr_queue; /* strings read from script */
|
||||
} routing_t;
|
||||
|
||||
void routing_env_msg(routing_t *routing, osmo_cc_msg_t *msg);
|
||||
void routing_env_dialing(routing_t *routing, const char *number, const char *keypad);
|
||||
void routing_env_free(routing_t *routing);
|
||||
void routing_start(routing_t *routing, const char *script, const char *shell);
|
||||
void routing_stop(routing_t *routing);
|
||||
void routing_send(routing_t *routing, const char *string);
|
||||
int routing_handle(routing_t *routing);
|
||||
|
||||
/* callbacks */
|
||||
void routing_receive_stdout(routing_t *routing, const char *string);
|
||||
void routing_receive_stderr(routing_t *routing, const char *string);
|
||||
void routing_close(routing_t *routing);
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/* RTP bridging and recording
|
||||
*
|
||||
* (C) 2020 by Andreas Eversberg <jolly@eversberg.eu>
|
||||
* All Rights Reserved
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
Loading…
Reference in New Issue