commit c354814c97bd206e872c7dd91c3bb9f1363fc1eb Author: Andreas Eversberg Date: Sat Nov 14 15:45:09 2020 +0100 Initial GIT import diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea5f9b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +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/libfilter/libfilter.a +src/ss5/osmo-cc-ss5-endpoint diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..689d568 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,5 @@ +AUTOMAKE_OPTIONS = foreign dist-bzip2 1.6 +ACLOCAL_AMFLAGS = -I m4 + +SUBDIRS = src + diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..70ec0ba --- /dev/null +++ b/configure.ac @@ -0,0 +1,92 @@ +AC_INIT([osmo-cc-ss5-endpoint], + 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/libfilter/Makefile + src/ss5/Makefile + src/Makefile + Makefile) diff --git a/git-version-gen b/git-version-gen new file mode 100755 index 0000000..42cf3d2 --- /dev/null +++ b/git-version-gen @@ -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 . + +# 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: diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..84aaade --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,13 @@ +AUTOMAKE_OPTIONS = foreign + +SUBDIRS = \ + liboptions \ + libdebug \ + libsample \ + libtimer \ + libjitter \ + libosmocc \ + libg711 \ + libfilter \ + ss5 + diff --git a/src/ss5/Makefile.am b/src/ss5/Makefile.am new file mode 100644 index 0000000..03f78a0 --- /dev/null +++ b/src/ss5/Makefile.am @@ -0,0 +1,23 @@ +AM_CPPFLAGS = -Wall -Wextra -g $(all_includes) + +bin_PROGRAMS = \ + osmo-cc-ss5-endpoint + +osmo_cc_ss5_endpoint_SOURCES = \ + mf.c \ + dsp.c \ + ss5.c \ + display_status.c \ + main.c + +osmo_cc_ss5_endpoint_LDADD = \ + $(COMMON_LA) \ + ../libdebug/libdebug.a \ + ../liboptions/liboptions.a \ + ../libsample/libsample.a \ + ../libtimer/libtimer.a \ + ../libjitter/libjitter.a \ + ../libosmocc/libosmocc.a \ + ../libg711/libg711.a \ + ../libfilter/libfilter.a + diff --git a/src/ss5/display.h b/src/ss5/display.h new file mode 100644 index 0000000..0f1ca1b --- /dev/null +++ b/src/ss5/display.h @@ -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 *if_name, int link_count, const char *from_id, const char *to_id, enum ss5_state state); +void display_status_end(void); + diff --git a/src/ss5/display_status.c b/src/ss5/display_status.c new file mode 100644 index 0000000..70201c4 --- /dev/null +++ b/src/ss5/display_status.c @@ -0,0 +1,169 @@ +/* display status functions + * + * (C) 2020 by Andreas Eversberg + * 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 . + */ + +#include +#include +#include +#include +#include "../libdebug/debug.h" +#include "ss5.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 *if_name, int link_count, const char *from_id, const char *to_id, enum ss5_state state) +{ + char line[MAX_DISPLAY_WIDTH + 4096]; + char color[MAX_DISPLAY_WIDTH + 4096]; + int index, from_len, to_len; + + memset(color, 7, sizeof(color)); // make valgrind happy + + if (line_count == MAX_HEIGHT_STATUS) + return; + + /* at first interface or when it changes */ + if (!link_count) + line_count++; + + /* check line count again */ + if (line_count == MAX_HEIGHT_STATUS) + return; + + if (!link_count) { + /* interface */ + strcpy(line, if_name); + memset(color, 3, strlen(if_name)); + } else { + memset(line, ' ', strlen(if_name)); + } + index = strlen(if_name); + + if (from_id && to_id) { + /* '' */ + sprintf(line + index, " \'%s\' -> \'%s\' ", from_id, to_id); + from_len = strlen(from_id); + to_len = strlen(to_id); + memset(color + index, 1, 4 + from_len); + memset(color + index + 4 + from_len, 7, 2); + memset(color + index + 4 + from_len + 2, 2, 4 + to_len); + index += 4 + from_len + 2 + 4 + to_len; + line[index] = '\0'; + + strcpy(line + index, ss5_state_names[state]); + memset(color + index, 7, strlen(ss5_state_names[state])); + } + + /* 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; +} + diff --git a/src/ss5/dsp.c b/src/ss5/dsp.c new file mode 100644 index 0000000..4a15937 --- /dev/null +++ b/src/ss5/dsp.c @@ -0,0 +1,552 @@ +/* DSP functions + * + * (C) 2020 by Andreas Eversberg + * 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 . + */ + +#define CHAN ((ss5_t *)(dsp->priv))->name + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../libdebug/debug.h" +#include "ss5.h" + +//#define DEBUG_DEMODULATOR + +#define NUM_TONES 8 +#define db2level(db) pow(10, (double)(db) / 20.0) +#define level2db(level) (20 * log10(level)) + +static double tone_dbm[NUM_TONES] = { -7, -7, -7, -7, -7, -7, -9, -9 }; +static double tone_freq[NUM_TONES] = { 700, 900, 1100, 1300, 1500, 1700, 2400, 2600 }; +static double tone_width[NUM_TONES] = { 25, 25, 25, 25, 25, 25, 25, 25 }; + +static double tone_min_dbm[NUM_TONES] = { -14, -14, -14, -14, -14, -14, -16, -16 }; +static double tone_min_ampl_sq[NUM_TONES]; +static double tone_diff_db[NUM_TONES] = { 4, 4, 4, 4, 4, 4, 5, 5 }; +static double tone_diff_ampl_sq[NUM_TONES]; + +#define INTERRUPT_DURATION 0.015 +#define SPLIT_DURATION 0.030 +#define MF_RECOGNITION 0.025 + +int dsp_init_inst(dsp_t *dsp, void *priv, double samplerate, double sense_db) +{ + double tone_amplitude[NUM_TONES]; + int t; + + PDEBUG(DDSP, DEBUG_DEBUG, "Init DSP for SS5 instance.\n"); + + memset(dsp, 0, sizeof(*dsp)); + dsp->priv = priv; + dsp->samplerate = samplerate; + dsp->interrupt_duration = (int)(1000.0 * INTERRUPT_DURATION); + dsp->split_duration = (int)(1000.0 * SPLIT_DURATION); + dsp->mf_detect_duration = (int)(1000.0 * MF_RECOGNITION); + dsp->ms_per_sample = 1000.0 / samplerate; + dsp->detect_tone = ' '; + + /* convert dbm of tones to speech level */ + for (t = 0; t < NUM_TONES; t++) { + tone_amplitude[t] = db2level(tone_dbm[t]) / SPEECH_LEVEL; + tone_min_ampl_sq[t] = pow(db2level(tone_min_dbm[t] - sense_db) / SPEECH_LEVEL, 2); + tone_diff_ampl_sq[t] = pow(db2level(tone_diff_db[t]), 2); + } + + /* init MF modulator */ + dsp->mf_mod = mf_mod_init(samplerate, NUM_TONES, tone_freq, tone_amplitude); + if (!dsp->mf_mod) + return -EINVAL; + + /* init MF demodulator */ + dsp->mf_demod = mf_demod_init(samplerate, NUM_TONES, tone_freq, tone_width); + if (!dsp->mf_mod) + return -EINVAL; + + return 0; +} + +void dsp_cleanup_inst(dsp_t *dsp) +{ + PDEBUG(DDSP, DEBUG_DEBUG, "Cleanup DSP of SS5 instance.\n"); + + if (dsp->mf_mod) { + mf_mod_exit(dsp->mf_mod); + dsp->mf_mod = NULL; + } + + if (dsp->mf_demod) { + mf_demod_exit(dsp->mf_demod); + dsp->mf_demod = NULL; + } +} + +/* + * tone encoder + */ + +static struct dsp_digits { + char tone; + uint32_t mask; +} dsp_digits[] = { + { '1', 0x01 + 0x02 }, + { '2', 0x01 + 0x04 }, + { '3', 0x02 + 0x04 }, + { '4', 0x01 + 0x08 }, + { '5', 0x02 + 0x08 }, + { '6', 0x04 + 0x08 }, + { '7', 0x01 + 0x10 }, + { '8', 0x02 + 0x10 }, + { '9', 0x04 + 0x10 }, + { '0', 0x08 + 0x10 }, + { '*', 0x01 + 0x20 }, /* code 11 */ + { '#', 0x02 + 0x20 }, /* code 12 */ + { 'a', 0x04 + 0x20 }, /* KP1 */ + { 'b', 0x08 + 0x20 }, /* KP2 */ + { 'c', 0x10 + 0x20 }, /* ST */ + { 'A', 0x40 }, /* 2400 answer, acknowledge */ + { 'B', 0x80 }, /* 2600 busy */ + { 'C', 0x40 + 0x80 }, /* 2600+2400 clear forward */ + { ' ', 0 }, /* silence */ + { 0 , 0 }, +}; + +#define KP_DIGIT_DURATION 0.100 +#define OTHER_DIGIT_DURATION 0.055 +#define DIGIT_PAUSE 0.055 + +/* set signaling tone duration threshold */ +void set_sig_detect_duration(dsp_t *dsp, double duration_AB, double duration_C) +{ + dsp->detect_count = 0; + dsp->sig_detect_duration_AB = (int)(1000.0 * duration_AB); + dsp->sig_detect_duration_C = (int)(1000.0 * duration_C); +} + +/* set given tone with duration (ms) or continuously (0) */ +void set_tone(dsp_t *dsp, char tone, double duration) +{ + int i; + + dsp->tone = 0; + + if (!tone) { + PDEBUG_CHAN(DDSP, DEBUG_DEBUG, "Remove tone\n"); + return; + } + + for (i = 0; dsp_digits[i].tone; i++) { + if (dsp_digits[i].tone == tone) { + dsp->tone_mask = dsp_digits[i].mask; + dsp->tone = tone; + dsp->tone_duration = (int)(dsp->samplerate * duration); + PDEBUG_CHAN(DDSP, DEBUG_DEBUG, "Set tone=\'%c\' duration=%.0fms (mask = 0x%02x)\n", dsp->tone, 1000.0 * duration, dsp->tone_mask); + return; + } + } +} + +/* get next tone from dial string, if any */ +static void get_tone_from_dial_string(dsp_t *dsp) +{ + char tone; + double duration; + + dsp->tone = 0; + + if (dsp->dial_index == dsp->dial_length) { + dsp->dial_length = 0; + dialing_complete(dsp->priv); + return; + } + + /* get alternating tone/pause from dial string */ + if (!dsp->digit_pause) { + /* digit on */ + tone = dsp->dial_string[dsp->dial_index++]; + dsp->digit_pause = 1; + if (tone == 'a' || tone == 'b') + duration = KP_DIGIT_DURATION; + else + duration = OTHER_DIGIT_DURATION; + PDEBUG_CHAN(DDSP, DEBUG_DEBUG, "Send digit \'%c\' from dial string\n", tone); + } else { + /* digit pause */ + tone = ' '; + dsp->digit_pause = 0; + duration = DIGIT_PAUSE; + PDEBUG_CHAN(DDSP, DEBUG_DEBUG, "Send pause after digit from dial string\n"); + } + + set_tone(dsp, tone, duration); +} + +/* set given dial string */ +void set_dial_string(dsp_t *dsp, const char *dial) +{ + dsp->digit_pause = 0; + strncpy(dsp->dial_string, dial, sizeof(dsp->dial_string) - 1); + dsp->dial_index = 0; + dsp->dial_length = strlen(dsp->dial_string); +} + +/* determine which tones to be modulated, get next tone, if elapsed */ +static int assemble_tones(dsp_t *dsp, uint32_t *mask, int length) +{ + int i; + + for (i = 0; i < length; i++) { + /* if tone was done, try to get next digit */ + if (!dsp->tone) { + if (!dsp->dial_length) + return i; + get_tone_from_dial_string(dsp); + if (!dsp->tone) + return i; + } + *mask++ = dsp->tone_mask; + if (dsp->tone_duration) { + /* count down duration, if tones is not continuous */ + if (!(--dsp->tone_duration)) + dsp->tone = 0; + } + } + + return i; +} + +/* + * tone deencoder + */ + +/* detection array for one frequency */ +static char decode_one[8] = + { ' ', ' ', ' ', ' ', ' ', ' ', 'A', 'B' }; /* A = 2400, B = 2600 */ + +/* detection matrix for two frequencies */ +static char decode_two[8][8] = +{ + { ' ', '1', '2', '4', '7', '*', ' ', ' ' }, /* * = code 11 */ + { '1', ' ', '3', '5', '8', '#', ' ', ' ' }, /* # = code 12 */ + { '2', '3', ' ', '6', '9', 'a', ' ', ' ' }, /* a = KP1 */ + { '4', '5', '6', ' ', '0', 'b', ' ', ' ' }, /* b = KP2 */ + { '7', '8', '9', '0', ' ', 'c', ' ', ' ' }, /* c = ST */ + { '*', '#', 'a', 'b', 'c', ' ', ' ', ' ' }, + { ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'C' }, /* C = 2600+2400 */ + { ' ', ' ', ' ', ' ', ' ', ' ', 'C', ' ' } +}; + +#define NONE_MIN_LEVEL_SQUARED + +/* determine which tone is played */ +static void detect_tones(dsp_t *dsp, sample_t *samples, sample_t **levels_squared, int length, int incoming) +{ + int f1, f2; + double f1_level_squared, f2_level_squared; + char tone; + int s, t; + + for (s = 0; s < length; s++) { + /* mute if split duration reached */ + if (dsp->split_duration && dsp->split_count == dsp->split_duration) + samples[s] = 0.0; + /* only perform tone detection every millisecond */ + dsp->detect_interval += dsp->ms_per_sample; + if (dsp->detect_interval < 1.0) + continue; + dsp->detect_interval -= 1.0; + + if (incoming) { +#ifdef DEBUG_DEMODULATOR + for (t = 0; t < dsp->mf_demod->tones; t++) { + char level[20]; + int db; + memset(level, 32, sizeof(level)); + db = roundf(level2db(sqrt(levels_squared[t][s]) * SPEECH_LEVEL) + 25); + if (db >= 0 && db < (int)sizeof(level)) + level[db] = '*'; + level[sizeof(level)-1]=0; + printf("%s|", level); + } + printf("\n"); +#endif + } + + /* find the tone which is the loudest */ + f1 = -1; + f1_level_squared = -1.0; + for (t = 0; t < dsp->mf_demod->tones; t++) { + if (levels_squared[t][s] > f1_level_squared) { + f1_level_squared = levels_squared[t][s]; + f1 = t; + } + } + /* find the tone which is the second loudest */ + f2 = -1; + f2_level_squared = -1.0; + for (t = 0; t < dsp->mf_demod->tones; t++) { + if (t == f1) + continue; + if (levels_squared[t][s] > f2_level_squared) { + f2_level_squared = levels_squared[t][s]; + f2 = t; + } + } + /* now check if the minimum level is reached */ + if (f1 >= 0 && f1_level_squared < tone_min_ampl_sq[f1]) + f1 = -1; + if (f2 >= 0 && f2_level_squared < tone_min_ampl_sq[f2]) + f2 = -1; + +// printf("%s f1=%.0f (%.1f dBm) f2=%.0f (%.1f dBm)\n", CHAN, (f1 >= 0) ? tone_freq[f1] : 0, level2db(sqrt(f1_level_squared) * SPEECH_LEVEL), (f2 >= 0) ? tone_freq[f2] : 0, level2db(sqrt(f2_level_squared) * SPEECH_LEVEL)); + /* check if no, one or two tones are detected */ + if (f1 < 0) + tone = ' '; + else if (f2 < 0) + tone = decode_one[f1]; + else { + if (f2_level_squared * tone_diff_ampl_sq[f2] < f1_level_squared) + tone = ' '; + else + tone = decode_two[f1][f2]; + } + //printf("tone=%c\n", tone); + + /* process interrupt counting, keep tone until interrupt counter expires */ + if (dsp->detect_tone != ' ' && tone != dsp->detect_tone) { + if (dsp->interrupt_count < dsp->interrupt_duration) { + dsp->interrupt_count++; + tone = dsp->detect_tone; + } + } else + dsp->interrupt_count = 0; + + /* split audio, after minimum duration of detecting a tone */ + if (tone >= 'A' && tone <= 'C') { + if (dsp->split_count < dsp->split_duration) + dsp->split_count++; + } else + dsp->split_count = 0; + + + /* some change in tone */ + if (dsp->detect_tone != tone) { + if (dsp->detect_count == 0) + PDEBUG_CHAN(DDSP, DEBUG_DEBUG, "Detected new tone '%c' (%.1f dBm)\n", tone, level2db(sqrt(f1_level_squared) * SPEECH_LEVEL)); + switch (tone) { + case 'A': + case 'B': + /* tone appears, wait some time */ + if (dsp->detect_count < dsp->sig_detect_duration_AB) + dsp->detect_count++; + else { + /* sign tone detected */ + dsp->detect_count = 0; + dsp->detect_tone = tone; + receive_digit(dsp->priv, tone, level2db(sqrt(f1_level_squared) * SPEECH_LEVEL)); + } + break; + case 'C': + /* tone appears, wait some time */ + if (dsp->detect_count < dsp->sig_detect_duration_C) + dsp->detect_count++; + else { + /* sign tone detected */ + dsp->detect_count = 0; + dsp->detect_tone = tone; + receive_digit(dsp->priv, tone, level2db(sqrt(f1_level_squared) * SPEECH_LEVEL)); + } + break; + case ' ': + /* tone appears or ceases */ + dsp->detect_count = 0; + dsp->detect_tone = tone; + receive_digit(dsp->priv, tone, 0.0); + break; + default: + /* tone appears, wait some time */ + if (dsp->detect_count < dsp->mf_detect_duration) + dsp->detect_count++; + else { + /* sign tone detected */ + dsp->detect_count = 0; + dsp->detect_tone = tone; + receive_digit(dsp->priv, tone, level2db(sqrt(f1_level_squared) * SPEECH_LEVEL)); + } + } + } else + dsp->detect_count = 0; + } +} + +/* process audio from one link (source) to another (destination) */ +static void process_audio(ss5_t *ss5_a, ss5_t *ss5_b, int length) +{ + sample_t samples[2][length], s; + sample_t b1[length], b2[length], b3[length], b4[length], b5[length], b6[length], b7[length], b8[length]; + sample_t *levels_squared[NUM_TONES] = { b1, b2, b3, b4, b5, b6, b7, b8 }; + uint32_t mask[length]; + int16_t data[160]; + int count1, count2; + int i; + + /* trigger reception of RTP stuff */ + if (ss5_a->cc_session) + osmo_cc_session_handle(ss5_a->cc_session); + if (ss5_b->cc_session) + osmo_cc_session_handle(ss5_b->cc_session); + + /* get audio from jitter buffer */ + jitter_load(&ss5_a->dejitter, samples[0], length); + jitter_load(&ss5_b->dejitter, samples[1], length); + + /* optionally add comfort noise */ + if (!ss5_a->cc_callref && ss5_a->ss5_ep->comfort_noise) { + for (i = 0; i < length; i++) + samples[0][i] += (double)((int8_t)random()) / 2000.0; + } + if (!ss5_b->cc_callref && ss5_b->ss5_ep->comfort_noise) { + for (i = 0; i < length; i++) + samples[1][i] += (double)((int8_t)random()) / 2000.0; + } + + /* modulate tone/digit. if no tone has to be played (or it stopped), count is less than length */ + count1 = assemble_tones(&ss5_a->dsp, mask, length); + mf_mod(ss5_a->dsp.mf_mod, mask, samples[0], count1); + count2 = assemble_tones(&ss5_b->dsp, mask, length); + mf_mod(ss5_b->dsp.mf_mod, mask, samples[1], count2); + + /* optionally add some crosstalk */ + if (ss5_a->ss5_ep->crosstalk) { + /* use count, since it carries number of samples with signalling */ + for (i = 0; i < count1; i++) + samples[1][i] += samples[0][i] / 70.0; + } + if (ss5_b->ss5_ep->crosstalk) { + /* use count, since it carries number of samples with signalling */ + for (i = 0; i < count2; i++) + samples[0][i] += samples[1][i] / 70.0; + } + + /* ! here is the bridge from a to b and from b to a ! */ + + /* optionally add one way delay */ + if (ss5_b->delay_buffer) { + for (i = 0; i < length; i++) { + s = ss5_b->delay_buffer[ss5_b->delay_index]; + ss5_b->delay_buffer[ss5_b->delay_index] = samples[0][i]; + if (++(ss5_b->delay_index) == ss5_b->delay_length) + ss5_b->delay_index = 0; + samples[0][i] = s; + } + } + if (ss5_a->delay_buffer) { + for (i = 0; i < length; i++) { + s = ss5_a->delay_buffer[ss5_a->delay_index]; + ss5_a->delay_buffer[ss5_a->delay_index] = samples[1][i]; + if (++(ss5_a->delay_index) == ss5_a->delay_length) + ss5_a->delay_index = 0; + samples[1][i] = s; + } + } + + /* demodulate and call tone detector */ + mf_demod(ss5_b->dsp.mf_demod, samples[0], length, levels_squared); + detect_tones(&ss5_b->dsp, samples[0], levels_squared, length, 1); + mf_demod(ss5_a->dsp.mf_demod, samples[1], length, levels_squared); + detect_tones(&ss5_a->dsp, samples[1], levels_squared, length, 0); + + /* forward audio to CC if call exists */ + if (ss5_b->cc_callref && ss5_b->codec) { + samples_to_int16(data, samples[0], length); + osmo_cc_rtp_send(ss5_b->codec, (uint8_t *)data, length * 2, 1, length); + } + if (ss5_a->cc_callref && ss5_a->codec) { + samples_to_int16(data, samples[1], length); + osmo_cc_rtp_send(ss5_a->codec, (uint8_t *)data, length * 2, 1, length); + } +} + +/* clock is called every given number of samples (20ms) */ +void audio_clock(ss5_endpoint_t *ss5_ep_sunset, ss5_endpoint_t *ss5_ep_sunrise, int len) +{ + ss5_t *ss5_a, *ss5_b; + + if (!ss5_ep_sunset) + return; + + if (!ss5_ep_sunrise) { + /* each pair of links on the same endpoint are bridged */ + for (ss5_b = ss5_ep_sunset->link_list; ss5_b; ss5_b = ss5_b->next) { + ss5_a = ss5_b; + ss5_b = ss5_b->next; + if (!ss5_b) + break; + process_audio(ss5_a, ss5_b, len); + } + } else { + /* each link on two endpoints are bridged */ + for (ss5_a = ss5_ep_sunset->link_list, ss5_b = ss5_ep_sunrise->link_list; ss5_a && ss5_b; ss5_a = ss5_a->next, ss5_b = ss5_b->next) { + process_audio(ss5_a, ss5_b, len); + } + } +} + +/* take audio from CC and store in jitter buffer */ +void down_audio(struct osmo_cc_session_codec *codec, uint16_t __attribute__((unused)) sequence_number, uint32_t __attribute__((unused)) timestamp, uint8_t *data, int len) +{ + ss5_t *ss5 = codec->media->session->priv; + sample_t samples[len / 2]; + + int16_to_samples(samples, (int16_t *)data, len / 2); + jitter_save(&ss5->dejitter, samples, len / 2); +} + +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; +} + diff --git a/src/ss5/dsp.h b/src/ss5/dsp.h new file mode 100644 index 0000000..aa58dea --- /dev/null +++ b/src/ss5/dsp.h @@ -0,0 +1,46 @@ + +struct ss5_endpoint; + +typedef struct dsp { + void *priv; + double samplerate; + + /* tone generation */ + mf_mod_t *mf_mod; /* MF modulator */ + char tone; /* current digit playing or 0 */ + uint32_t tone_mask; /* bit-mask of which MF tones to play */ + int tone_duration; /* counter of samples for length of tone */ + char dial_string[33]; /* stores digits when dialing number sequence */ + int dial_length; /* length of dial string, or 0 */ + int dial_index; /* current position at dial string */ + int digit_pause; /* flag that states if pause is played after digit */ + + /* tone detection */ + mf_demod_t *mf_demod; /* MF demodulator */ + double ms_per_sample; /* how many milliseconds between two samples */ + double detect_interval; /* counts milliseconds */ + int interrupt_duration; /* number of milliseconds until interruption is detected */ + int interrupt_count; /* counter for interrupt condition */ + int split_duration; /* number of milliseconds until split (mute) condition meats */ + int split_count; /* counter for split condition */ + int sig_detect_duration_AB; /* signaling tone duration in milliseconds (TONE A/B) */ + int sig_detect_duration_C; /* signaling tone duration in milliseconds (TONE C) */ + int mf_detect_duration; /* MF tone duration in milliseconds */ + int detect_count; /* counter for tone detection */ + char detect_tone; /* current tone detected or ' ' for no tone */ +} dsp_t; + + +int dsp_init_inst(dsp_t *dsp, void *priv, double samplerate, double sense_db); +void dsp_cleanup_inst(dsp_t *dsp); +void set_sig_detect_duration(dsp_t *dsp, double duration_AB, double duration_C); +void set_tone(dsp_t *dsp, char tone, double duration); +void set_dial_string(dsp_t *dsp, const char *dial); +void audio_clock(struct ss5_endpoint *ss5_ep_sunset, struct ss5_endpoint *ss5_ep_sunrise, int len); +void down_audio(struct osmo_cc_session_codec *codec, uint16_t __attribute__((unused)) sequence_number, uint32_t __attribute__((unused)) timestamp, uint8_t *data, 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); + +void receive_digit(void *priv, char digit, double dbm); +void dialing_complete(void *priv); + diff --git a/src/ss5/main.c b/src/ss5/main.c new file mode 100644 index 0000000..e4ce31a --- /dev/null +++ b/src/ss5/main.c @@ -0,0 +1,346 @@ +/* osmo-cc-ss5-endpoint main + * + * (C) 2020 by Andreas Eversberg + * 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 . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "../libdebug/debug.h" +#include "../liboptions/options.h" +#include "../libg711/g711.h" +#include "ss5.h" +#include "display.h" + +ss5_endpoint_t *ss5_ep_sunset = NULL, *ss5_ep_sunrise = NULL; +int num_kanal = 2; + +int endpoints = 1; +int links = 0; +int prevent_blueboxing = 0; +int suppress_disconnect = 1; +int crosstalk = 1; +int delay_ms = 300; +int comfort_noise = 1; +double sense_db = 5; +#define MAX_CC_ARGS 1024 +static int cc_argc_sunset, cc_argc_sunrise = 0; +static const char *cc_argv_sunset[MAX_CC_ARGS], *cc_argv_sunrise[MAX_CC_ARGS]; + +static void print_usage(const char *app) +{ + printf("Usage: %s [--port ] [--nt] []\n", app); + printf("This will create pairs of SS5 channels that are bridged together, so that\n"); + printf("calls from one link to the other can be made using SS5. The a bluebox can be\n"); + printf("used to play with it.\n"); + printf("If one endpoint is used (default), its name is 'sunset' and each pair of\n"); + printf("channels are bridged together. If two endpoints are used, their names are\n"); + printf("'sunset' and 'sunrise' and same channel index of both endpoints are bridged\n"); + printf("together.\n"); +} + +static void print_help() +{ + /* - - */ + printf(" -h --help\n"); + printf(" This help\n"); + printf(" -v --verbose | ,[,[,...]] | 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(" -> If no category is specified, all categories are selected\n"); + printf(" -2 --two\n"); + printf(" Create two Osmo-CC endpoints instead of one.\n"); + printf(" -c --channels\n"); + printf(" Give number of channels per endpoint. If you use a single endpoint,\n"); + printf(" you must define an even number. By default this is '2' for one\n"); + printf(" endpoint and '1' for two endpoints.\n"); + printf(" -s --suppress-disconnect 1 | 0\n"); + printf(" When a 'busy-flash' or 'release-guard' is received a disconnect is\n"); + printf(" forwarded towards OsmoCC. Set to 1 to suppress this. (Default is %d.)\n", suppress_disconnect); + printf(" -p --prevent-blueboxing 1 | 0\n"); + printf(" Prevent blueboxing by making 'release-guard' 200 ms minimum length.\n"); + printf(" -x --crosstalk 1 | 0\n"); + printf(" Enable or disable some minor crosstalk. This allows you to hear\n"); + printf(" transmitted tones at a low volume. (Default is %d.).\n", crosstalk); + printf(" -d --delay | 0\n"); + printf(" Add one-way delay to the connection between two SS5 links. This allows\n"); + printf(" to hear 'acknowlege' tones delayed. (Default is %d ms.).\n", delay_ms); + printf(" -n --comfort-noise 1 | 0\n"); + printf(" Add comfort noise whenever there is no audio from the remote link\n"); + printf(" (before or after call). (Default is %d ms.).\n", comfort_noise); + printf(" --sense 0 | \n"); + printf(" Change sensitivity (level) of tone detector. A bluebox must not be\n"); + printf(" that loud. (Default is %.0f dB.).\n", sense_db); + printf(" -C --cc \"\" [--cc ...]\n"); + printf(" --cc2 \"\" [--cc2 ...]\n"); + printf(" Pass arguments to Osmo-CC endpoint. Use '-cc help' for description.\n"); + printf(" If you select two endpoints, use '--cc2' to pass arguments to the\n"); + printf(" second endpoint.\n"); +} + +#define OPT_SENSE 256 +#define OPT_CC2 257 + +static void add_options(void) +{ + option_add('h', "help", 0); + option_add('v', "verbose", 1); + option_add('2', "two", 0); + option_add('c', "channels", 1); + option_add('s', "suppress-disconnect", 1); + option_add('p', "prevent-blueboxing", 1); + option_add('x', "crosstalk", 1); + option_add('d', "delay", 1); + option_add('n', "comfort-noise", 1); + option_add(OPT_SENSE, "sense", 1); + option_add('C', "cc", 1); + option_add(OPT_CC2, "cc2", 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 '2': + endpoints = 2; + break; + case 'c': + links = atoi(argv[argi]); + break; + case 's': + suppress_disconnect = atoi(argv[argi]); + break; + case 'p': + prevent_blueboxing = atoi(argv[argi]); + break; + case 'x': + crosstalk = atoi(argv[argi]); + break; + case 'd': + delay_ms = atoi(argv[argi]); + break; + case 'n': + comfort_noise = atoi(argv[argi]); + break; + case OPT_SENSE: + sense_db = (double)atoi(argv[argi]); + break; + case 'C': + if (!strcasecmp(argv[argi], "help")) { + osmo_cc_help(); + return 0; + } + if (cc_argc_sunset == MAX_CC_ARGS) { + fprintf(stderr, "Too many osmo-cc args!\n"); + break; + } + cc_argv_sunset[cc_argc_sunset++] = strdup(argv[argi]); + break; + case OPT_CC2: + if (!strcasecmp(argv[argi], "help")) { + osmo_cc_help(); + return 0; + } + if (cc_argc_sunrise == MAX_CC_ARGS) { + fprintf(stderr, "Too many osmo-cc args!\n"); + break; + } + cc_argv_sunrise[cc_argc_sunrise++] = 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[]) +{ + int argi, rc; + struct termios term, term_orig; + double now, last_time_call = 0.0; + int c; + + /* init MF */ + mf_init(0); + + /* init codecs (for recording) */ + g711_init(); + + cc_argv_sunset[cc_argc_sunset++] = strdup("remote auto"); + cc_argv_sunrise[cc_argc_sunrise++] = strdup("remote auto"); + + /* handle options / config file */ + add_options(); + rc = options_config_file("~/.osmocom/ss5/ss5.conf", handle_options); + if (rc < 0) + return 0; + argi = options_command_line(argc, argv, handle_options); + if (argi <= 0) + return argi; + + /* check links (per endpoint) */ + if (!links) + links = (endpoints == 2) ? 1 : 2; + if (links == 1 && (endpoints % 1)) { + PDEBUG(DSS5, DEBUG_ERROR, "You must define an even number of channels on a single endpoint!\n"); + goto error; + } + + /* create sunset and (optionally) sunrise */ + ss5_ep_sunset = ss5_ep_create("sunset", links, prevent_blueboxing, crosstalk, delay_ms, comfort_noise, suppress_disconnect, sense_db); + if (!ss5_ep_sunset) + goto error; + rc = osmo_cc_new(&ss5_ep_sunset->cc_ep, OSMO_CC_VERSION, "sunset", OSMO_CC_LOCATION_BEYOND_INTERWORKING, cc_message, NULL, ss5_ep_sunset, cc_argc_sunset, cc_argv_sunset); + if (rc < 0) + goto error; + if (endpoints == 2) { + ss5_ep_sunrise = ss5_ep_create("sunrise", links, prevent_blueboxing, crosstalk, delay_ms, comfort_noise, suppress_disconnect, sense_db); + if (!ss5_ep_sunrise) + goto error; + rc = osmo_cc_new(&ss5_ep_sunrise->cc_ep, OSMO_CC_VERSION, "sunrise", OSMO_CC_LOCATION_BEYOND_INTERWORKING, cc_message, NULL, ss5_ep_sunrise, cc_argc_sunrise, cc_argv_sunrise); + if (rc < 0) + goto error; + PDEBUG(DSS5, DEBUG_NOTICE, "Created endpoints 'sunset' and 'sunrise' with %d links that connect these endpoints.\n", links); + } else + PDEBUG(DSS5, DEBUG_NOTICE, "Created endpoint 'sunset' with %d links, each pair connected.\n", links); + refresh_status(); + + /* 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); + + 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 */ + audio_clock(ss5_ep_sunset, ss5_ep_sunrise, 160); + } + + process_timer(); + do { + w = 0; + w |= osmo_cc_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: + /* destroy endpoints */ + if (ss5_ep_sunset) { + osmo_cc_delete(&ss5_ep_sunset->cc_ep); + ss5_ep_destroy(ss5_ep_sunset); + } + if (ss5_ep_sunrise) { + osmo_cc_delete(&ss5_ep_sunrise->cc_ep); + ss5_ep_destroy(ss5_ep_sunrise); + } + + /* exit MF */ + mf_exit(); + + return 0; +} + diff --git a/src/ss5/mf.c b/src/ss5/mf.c new file mode 100644 index 0000000..273146e --- /dev/null +++ b/src/ss5/mf.c @@ -0,0 +1,202 @@ +/* multi frequency coder and decoder + * + * (C) 2020 by Andreas Eversberg + * 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 . + */ + +#include +#include +#include +#include +#include +#include +#include "../libsample/sample.h" +#include "mf.h" + +static int has_init = 0; +static int fast_math = 0; +static float *sin_tab = NULL, *cos_tab = NULL; + +/* global init */ +int mf_init(int _fast_math) +{ + fast_math = _fast_math; + + if (fast_math) { + int i; + + sin_tab = calloc(65536+16384, sizeof(*sin_tab)); + if (!sin_tab) { + fprintf(stderr, "No mem!\n"); + return -ENOMEM; + } + cos_tab = sin_tab + 16384; + + /* generate sine and cosine */ + for (i = 0; i < 65536+16384; i++) + sin_tab[i] = sin(2.0 * M_PI * (double)i / 65536.0); + } + + has_init = 1; + + return 0; +} + +/* global exit */ +void mf_exit(void) +{ + if (sin_tab) { + free(sin_tab); + sin_tab = cos_tab = NULL; + } + + has_init = 0; +} + +mf_mod_t *mf_mod_init(double samplerate, int tones, double *freq, double *amplitude) +{ + mf_mod_t *mod; + int t; + + if (!has_init) { + fprintf(stderr, "libmf was not initialized, plese fix!\n"); + abort(); + } + + mod = calloc(1, sizeof(*mod) + sizeof(*mod->tone) * tones); + if (!mod) { + fprintf(stderr, "No mem!\n"); + return NULL; + } + mod->tones = tones; + + for (t = 0; t < tones; t++) { + /* set rotation of IQ vector */ + if (fast_math) + mod->tone[t].rot = 65536.0 * freq[t] / samplerate; + else + mod->tone[t].rot = 2 * M_PI * freq[t] / samplerate; + mod->tone[t].amplitude = amplitude[t]; + } + + return mod; +} + +void mf_mod_exit(mf_mod_t *mod) +{ + free(mod); +} + +void mf_mod(mf_mod_t *mod, uint32_t *mask, sample_t *samples, int length) +{ + int t, s; + double phase; + + for (s = 0; s < length; s++) { + samples[s] = 0; + for (t = 0; t < mod->tones; t++) { + /* continue phase even on muted tones */ + phase = (mod->tone[t].phase += mod->tone[t].rot); + if (!((1 << t) & mask[s])) + continue; + if (fast_math) { + if (phase >= 65536.0) + phase -= 65536.0; + samples[s] += sin_tab[(uint16_t)phase] * mod->tone[t].amplitude; + } else { + if (phase >= 2.0 * M_PI) + phase -= 2.0 * M_PI; + samples[s] += cos(phase) * mod->tone[t].amplitude; + } + } + } +} + +mf_demod_t *mf_demod_init(double samplerate, int tones, double *freq, double *width) +{ + mf_demod_t *demod; + int t; + + if (!has_init) { + fprintf(stderr, "libmf was not initialized, plese fix!\n"); + abort(); + } + + demod = calloc(1, sizeof(*demod) + sizeof(*demod->tone) * tones); + if (!demod) { + fprintf(stderr, "No mem!\n"); + return NULL; + } + demod->tones = tones; + + for (t = 0; t < tones; t++) { + /* set rotation of IQ vector */ + if (fast_math) + demod->tone[t].rot = 65536.0 * -freq[t] / samplerate; + else + demod->tone[t].rot = 2 * M_PI * -freq[t] / samplerate; + + /* use fourth order (2 iter) filter, since it is as fast as second order (1 iter) filter */ + iir_lowpass_init(&demod->tone[t].lp[0], width[t], samplerate, 2); + iir_lowpass_init(&demod->tone[t].lp[1], width[t], samplerate, 2); + } + + return demod; +} + +void mf_demod_exit(mf_demod_t *demod) +{ + free(demod); +} + +void mf_demod(mf_demod_t *demod, sample_t *samples, int length, sample_t **levels_squared) +{ + sample_t I[length], Q[length]; + double phase, rot; + int t, s; + double i, _sin, _cos; + sample_t *level_squared; + + /* we apply filters and get the squared levels as a result */ + for (t = 0; t < demod->tones; t++) { + phase = demod->tone[t].phase; + rot = demod->tone[t].rot; + for (s = 0; s < length; s++) { + phase += rot; + i = samples[s]; + if (fast_math) { + if (phase >= 65536.0) + phase -= 65536.0; + _sin = sin_tab[(uint16_t)phase]; + _cos = cos_tab[(uint16_t)phase]; + } else { + if (phase >= 2.0 * M_PI) + phase -= 2.0 * M_PI; + _sin = sin(phase); + _cos = cos(phase); + } + I[s] = i * _cos; + Q[s] = i * _sin; + } + demod->tone[t].phase = phase; + iir_process(&demod->tone[t].lp[0], I, length); /* filter single side band */ + iir_process(&demod->tone[t].lp[1], Q, length); + level_squared = levels_squared[t]; + for (s = 0; s < length; s++) + level_squared[s] = 4.0 * (I[s] * I[s] + Q[s] * Q[s]); /* x2 because of single side band */ + } +} + diff --git a/src/ss5/mf.h b/src/ss5/mf.h new file mode 100644 index 0000000..ca7d304 --- /dev/null +++ b/src/ss5/mf.h @@ -0,0 +1,34 @@ + +#include "../libfilter/iir_filter.h" + +struct mf_mod_tone { + double rot; + double phase; + double amplitude; +}; + +typedef struct mf_mod { + int tones; + struct mf_mod_tone tone[0]; +} mf_mod_t; + +struct mf_demod_tone { + double rot; + double phase; + iir_filter_t lp[2]; +}; + +typedef struct mf_demod { + int tones; + struct mf_demod_tone tone[0]; +} mf_demod_t; + +int mf_init(int _fast_math); +void mf_exit(void); +mf_mod_t *mf_mod_init(double samplerate, int tones, double *freq, double *amplitude); +void mf_mod_exit(mf_mod_t *mod); +void mf_mod(mf_mod_t *mod, uint32_t *mask, sample_t *samples, int length); +mf_demod_t *mf_demod_init(double samplerate, int tones, double *freq, double *width); +void mf_demod_exit(mf_demod_t *demod); +void mf_demod(mf_demod_t *demod, sample_t *samples, int length, sample_t **levels_squared); + diff --git a/src/ss5/ss5.c b/src/ss5/ss5.c new file mode 100644 index 0000000..93bdd9f --- /dev/null +++ b/src/ss5/ss5.c @@ -0,0 +1,1128 @@ +/* SS5 process + * + * (C) 2020 by Andreas Eversberg + * 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 . + */ + +#define CHAN ss5->name + +#include +#include +#include +#include +#include +#include +#include +#include +#include "../libdebug/debug.h" +#include "../libg711/g711.h" +#include "ss5.h" +#include "display.h" + +/* names of all SS5 states */ +const char *ss5_state_names[] = { + "NULL", + /* idle line */ + "IDLE", + /* outgoing call */ + "SEND SEIZE", + "RECV PROCEED-TO-SEND", + "SEND DIGITS", + "OUT INACTIVE", + "SEND ACKNOWLEDGE (to answer)", + "SEND ACKNOWLEDGE (to busy-flash)", + "SEND ACKNOWLEDGE (to clear-back)", + "OUT ACTIVE", + "SEND CLEAR-FORWARD", + "RECV RELEASE-GUARD", + "SEND FORWARD-TRANSFER", + /* incoming call */ + "SEND PROCEED-TO-SEND", + "RECV DIGIT", + "RECV SPACE", + "IN INACTIVE", + "SEND ANSWER", + "IN ACTIVE", + "SEND BUSY-FLASH", + "SEND CLEAR-BACK", + "SEND RELEASE-GUARD", + "SEND RELEASE-GUARD (waiting)", + /* seize collision */ + "DOUBLE-SEIZURE", +}; + +/* timers and durations */ +#define SIGN_RECOGNITION_FAST 0.040 /* 40 ms for seize and proceed-to-send */ +#define SIGN_RECOGNITION_NORMAL 0.125 /* 125 ms for all other signals */ +#define MIN_RELEASE_GUARD 0.200 /* minimum 200 ms, in case we prevent blueboxing */ +#define MAX_SEIZE 10.0 +#define MAX_PROCEED_TO_SEND 4.0 +#define MAX_ANSWER 10.0 +#define MAX_BUSY_FLASH 10.0 +#define MAX_CLEAR_BACK 10.0 +#define MAX_ACKNOWLEDGE 4.0 +#define MAX_CLEAR_FORWARD 10.0 +#define MAX_RELEASE_GUARD 4.0 +#define DUR_DOUBLE_SEIZURE 0.850 /* 850 ms to be sure the other end recognizes the double seizure */ +#define DUR_FORWARD_TRANSFER 0.850 /* 850 ms forward-transfer */ +#define PAUSE_BEFORE_DIALING 0.080 /* pause before dialing after cease of tone */ +#define TO_DIALING 10.0 /* as defined in clause about releasing the incoming register when number is incomplete */ + +static struct osmo_cc_helper_audio_codecs codecs[] = { + { "L16", 8000, 1, encode_l16, decode_l16 }, + { "PCMA", 8000, 1, g711_encode_alaw, g711_decode_alaw }, + { "PCMU", 8000, 1, g711_encode_ulaw, g711_decode_ulaw }, + { NULL, 0, 0, NULL, NULL}, +}; + +void refresh_status(void) +{ + osmo_cc_endpoint_t *ep; + ss5_endpoint_t *ss5_ep; + ss5_t *ss5; + int i; + + display_status_start(); + + for (ep = osmo_cc_endpoint_list; ep; ep = ep->next) { + ss5_ep = ep->priv; + if (!ss5_ep->link_list) + display_status_line(ep->local_name, 0, NULL, NULL, 0); + for (i = 0, ss5 = ss5_ep->link_list; ss5; i++, ss5 = ss5->next) + display_status_line(ep->local_name, i, ss5->callerid, ss5->dialing, ss5->state); + } + + display_status_end(); +} + +void ss5_new_state(ss5_t *ss5, enum ss5_state state) +{ + PDEBUG_CHAN(DSS5, DEBUG_DEBUG, "Changing state '%s' -> '%s'\n", ss5_state_names[ss5->state], ss5_state_names[state]); + ss5->state = state; + + /* must update (new state) */ + refresh_status(); +} + +/* + * endpoints & links + */ + +/* reset ss5 link, but keep states, so audio generation/processing can continue */ +static void link_reset(ss5_t *ss5) +{ + /* unlink callref */ + ss5->cc_callref = 0; + + /* stop timer */ + timer_stop(&ss5->timer); + + /* free session description */ + if (ss5->cc_session) { + osmo_cc_free_session(ss5->cc_session); + ss5->cc_session = NULL; + ss5->codec = NULL; + } + + /* reset jitter buffer */ + jitter_reset(&ss5->dejitter); + + /* set recognition time */ + set_sig_detect_duration(&ss5->dsp, SIGN_RECOGNITION_FAST, SIGN_RECOGNITION_NORMAL); + + /* reset all other states */ + ss5->callerid[0] = '\0'; + ss5->dialing[0] = '\0'; + + /* must update (e.g. caller and dialing) */ + refresh_status(); +} + +static void ss5_timeout(struct timer *timer); + +static ss5_t *link_create(ss5_endpoint_t *ss5_ep, const char *ep_name, int linkid) +{ + ss5_t *ss5, **ss5_p; + int rc; + + ss5 = calloc(1, sizeof(*ss5)); + if (!ss5) { + PDEBUG(DSS5, DEBUG_ERROR, "No memory!\n"); + abort(); + } + ss5->ss5_ep = ss5_ep; + + ss5_p = &ss5_ep->link_list; + while (*ss5_p) + ss5_p = &((*ss5_p)->next); + *ss5_p = ss5; + + /* debug name */ + snprintf(ss5->name, sizeof(ss5->name) - 1, "%s/%d", ep_name, linkid); + + /* init dsp instance */ + dsp_init_inst(&ss5->dsp, ss5, ss5_ep->samplerate, ss5_ep->sense_db); + + /* init timer */ + timer_init(&ss5->timer, ss5_timeout, ss5); + + /* allocate jitter buffer */ + rc = jitter_create(&ss5->dejitter, 8000 / 10); // FIXME: size + if (rc < 0) + abort(); + + /* alloc delay buffer */ + if (ss5_ep->delay_ms) { + ss5->delay_length = (int)(ss5_ep->samplerate * (double)ss5_ep->delay_ms / 1000.0); + ss5->delay_buffer = calloc(ss5->delay_length, sizeof(*ss5->delay_buffer)); + } + + /* reset instance */ + link_reset(ss5); + + /* state idle */ + ss5_new_state(ss5, SS5_STATE_IDLE); + + PDEBUG_CHAN(DSS5, DEBUG_DEBUG, "created ss5 instance\n"); + + return ss5; +} + +static void link_destroy(ss5_t *ss5) +{ + ss5_t **ss5_p; + + /* state idle */ + ss5_new_state(ss5, SS5_STATE_IDLE); + + /* reset instance */ + link_reset(ss5); + + /* exit timer */ + timer_exit(&ss5->timer); + + /* free jitter buffer */ + jitter_destroy(&ss5->dejitter); + + /* free delay buffer */ + if (ss5->delay_buffer) { + free(ss5->delay_buffer); + ss5->delay_buffer = NULL; + } + + /* cleanup dsp instance */ + dsp_cleanup_inst(&ss5->dsp); + + /* detach */ + ss5_p = &ss5->ss5_ep->link_list; + while (*ss5_p) { + if (*ss5_p == ss5) + break; + ss5_p = &((*ss5_p)->next); + } + *ss5_p = ss5->next; + + PDEBUG_CHAN(DSS5, DEBUG_DEBUG, "destroyed ss5 instance\n"); + + free(ss5); +} + +ss5_endpoint_t *ss5_ep_create(const char *ep_name, int links, int prevent_blueboxing, int crosstalk, int delay_ms, int comfort_noise, int suppress_disconnect, double sense_db) +{ + ss5_endpoint_t *ss5_ep; + int i; + + ss5_ep = calloc(1, sizeof(*ss5_ep)); + if (!ss5_ep) { + PDEBUG(DSS5, DEBUG_ERROR, "No memory!\n"); + abort(); + } + + ss5_ep->samplerate = 8000; + ss5_ep->prevent_blueboxing = prevent_blueboxing; + ss5_ep->crosstalk = crosstalk; + ss5_ep->delay_ms = delay_ms; + ss5_ep->comfort_noise = comfort_noise; + ss5_ep->suppress_disconnect = suppress_disconnect; + ss5_ep->sense_db = sense_db; + + for (i = 0; i < links; i++) + link_create(ss5_ep, ep_name, i + 1); + + PDEBUG(DSS5, DEBUG_DEBUG, "SS5 endpoint instance created\n"); + + return ss5_ep; +} + +void ss5_ep_destroy(ss5_endpoint_t *ss5_ep) +{ + /* destroy all calls */ + while (ss5_ep->link_list) + link_destroy(ss5_ep->link_list); + + free(ss5_ep); + + PDEBUG(DSS5, DEBUG_DEBUG, "SS5 endpoint instance destroyed\n"); +} + +/* + * several messages towards CC + */ + +static void reject_call(ss5_endpoint_t *ss5_ep, uint32_t callref, uint8_t isdn_cause) + +{ + osmo_cc_msg_t *new_msg; + + /* create osmo-cc message */ + new_msg = osmo_cc_new_msg(OSMO_CC_MSG_REJ_IND); + /* cause */ + osmo_cc_add_ie_cause(new_msg, OSMO_CC_LOCATION_BEYOND_INTERWORKING, isdn_cause, 0, 0); + /* send message to osmo-cc */ + osmo_cc_ll_msg(&ss5_ep->cc_ep, callref, new_msg); +} + +static void release_call(ss5_t *ss5, uint8_t isdn_cause) +{ + osmo_cc_msg_t *new_msg; + + /* create osmo-cc message */ + new_msg = osmo_cc_new_msg(OSMO_CC_MSG_REL_IND); + /* cause */ + osmo_cc_add_ie_cause(new_msg, OSMO_CC_LOCATION_BEYOND_INTERWORKING, isdn_cause, 0, 0); + /* send message to osmo-cc */ + osmo_cc_ll_msg(&ss5->ss5_ep->cc_ep, ss5->cc_callref, new_msg); +} + +static void disconnect_call(ss5_t *ss5, uint8_t isdn_cause) +{ + osmo_cc_msg_t *new_msg; + + if (ss5->ss5_ep->suppress_disconnect) + return; + /* create osmo-cc message */ + new_msg = osmo_cc_new_msg(OSMO_CC_MSG_DISC_IND); + /* progress */ + osmo_cc_add_ie_progress(new_msg, OSMO_CC_CODING_ITU_T, OSMO_CC_LOCATION_BEYOND_INTERWORKING, OSMO_CC_PROGRESS_INBAND_INFO_AVAILABLE); + /* cause */ + osmo_cc_add_ie_cause(new_msg, OSMO_CC_LOCATION_BEYOND_INTERWORKING, isdn_cause, 0, 0); + /* send message to osmo-cc */ + osmo_cc_ll_msg(&ss5->ss5_ep->cc_ep, ss5->cc_callref, new_msg); +} + +static void proceed_call(ss5_t *ss5, const char *sdp) +{ + osmo_cc_msg_t *new_msg; + + /* create osmo-cc message */ + new_msg = osmo_cc_new_msg(OSMO_CC_MSG_PROC_IND); + /* progress */ + osmo_cc_add_ie_progress(new_msg, OSMO_CC_CODING_ITU_T, OSMO_CC_LOCATION_BEYOND_INTERWORKING, OSMO_CC_PROGRESS_INBAND_INFO_AVAILABLE); + /* sdp */ + osmo_cc_add_ie_sdp(new_msg, sdp); + /* send message to osmo-cc */ + osmo_cc_ll_msg(&ss5->ss5_ep->cc_ep, ss5->cc_callref, new_msg); +} + +static void alert_call(ss5_t *ss5) +{ + osmo_cc_msg_t *new_msg; + + /* create osmo-cc message */ + new_msg = osmo_cc_new_msg(OSMO_CC_MSG_ALERT_IND); + /* send message to osmo-cc */ + osmo_cc_ll_msg(&ss5->ss5_ep->cc_ep, ss5->cc_callref, new_msg); +} + +static void answer_call(ss5_t *ss5) +{ + osmo_cc_msg_t *new_msg; + + /* create osmo-cc message */ + new_msg = osmo_cc_new_msg(OSMO_CC_MSG_SETUP_CNF); + /* send message to osmo-cc */ + osmo_cc_ll_msg(&ss5->ss5_ep->cc_ep, ss5->cc_callref, new_msg); +} + +static void setup_ack_call(ss5_t *ss5) +{ + osmo_cc_msg_t *new_msg; + + /* create osmo-cc message */ + new_msg = osmo_cc_new_msg(OSMO_CC_MSG_SETUP_ACK_IND); + /* send message to osmo-cc */ + osmo_cc_ll_msg(&ss5->ss5_ep->cc_ep, ss5->cc_callref, new_msg); +} + +/* + * dial string generation and parsing + */ + +static char *prefix_1_digit[] = { "1", "7", NULL }; +static char *prefix_2_digit[] = { + "20", "27", "28", "30", "31", "32", "33", "34", "36", "39", "40", "41", + "43", "44", "45", "46", "47", "48", "49", "51", "52", "53", "54", "55", + "56", "57", "58", "60", "61", "62", "63", "64", "65", "66", "81", "82", + "83", "84", "86", "89", "90", "91", "92", "93", "94", "95", "98", + NULL }; + +/* use number and number type to generate an SS5 dial string + * the digits are checked if they can be dialed + * if the number is already in SS5 format, only digits are checked + */ +static int generate_dial_string(uint8_t type, const char *dialing, char *string, int string_size) +{ + int full_string_given = 0; + int i, ii; + + if ((int)strlen(dialing) + 4 > string_size) { + PDEBUG(DSS5, DEBUG_NOTICE, "Dial string is too long for our digit register, call is rejected!\n"); + return -EINVAL; + } + + /* check for correct digits */ + for (i = 0, ii = strlen(dialing); i < ii; i++) { + /* string may start with 'a' or 'b', but then 'c' must be the last digit */ + if (dialing[i] == 'a' || dialing[i] == 'b') { + full_string_given = 1; + if (dialing[ii - 1] != 'c') { + PDEBUG(DSS5, DEBUG_NOTICE, "Number starts with 'a' (KP1) or 'b' (KP2) but missing 'c' (ST) at the end, call is rejected!\n"); + return -EINVAL; + } + /* remove check of last digit 'c' */ + --ii; + continue; + } + /* string must only consist of numerical digits and '*' (code 11) and '#' (code 12) */ + if (!strchr("0123456789*#", dialing[i])) { + PDEBUG(DSS5, DEBUG_NOTICE, "Number has invalid digits, call is rejected!\n"); + return -EINVAL; + } + } + + /* if full string with 'a'/'b' and 'c' is given, we have complete dial string */ + if (full_string_given) { + strcpy(string, dialing); + return 0; + } + + /* if number is not of international type, create national dial string */ + if (type != OSMO_CC_TYPE_INTERNATIONAL) { + // make GCC happy + strcpy(string, "a0"); + strcat(string, dialing); + strcat(string, "c"); + return 0; + } + + /* check international prefix with length of 1 digit */ + if ((int)strlen(dialing) < 1) { + PDEBUG(DSS5, DEBUG_NOTICE, "International number too short to get country code from, call is rejected!\n"); + return -EINVAL; + } + for (i = 0; prefix_1_digit[i]; i++) { + if (prefix_1_digit[i][0] == dialing[0]) + break; + } + /* if number is of international type, create international dial string */ + if (prefix_1_digit[i]) { + sprintf(string, "b%c0%sc", dialing[0], dialing + 1); + return 0; + } + + /* check international prefix with length of 2 digits */ + if ((int)strlen(dialing) < 2) { + PDEBUG(DSS5, DEBUG_NOTICE, "International number too short to get country code from, call is rejected!\n"); + return -EINVAL; + } + for (i = 0; prefix_2_digit[i]; i++) { + if (prefix_2_digit[i][0] == dialing[0] + && prefix_2_digit[i][1] == dialing[1]) + break; + } + /* if number is of international type, create international dial string */ + if (prefix_2_digit[i]) { + sprintf(string, "b%c%c0%sc", dialing[0], dialing[1], dialing + 2); + return 0; + } + + /* check international prefix with length of 3 digits */ + if ((int)strlen(dialing) < 3) { + PDEBUG(DSS5, DEBUG_NOTICE, "International number too short to get country code from, call is rejected!\n"); + return -EINVAL; + } + /* if number is of international type, create international dial string */ + sprintf(string, "b%c%c%c0%sc", dialing[0], dialing[1], dialing[2], dialing + 3); + return 0; +} + +/* parse received SS5 dial string and convert it into a national or international number */ +static int parse_dial_string(uint8_t *type, char *dialing, int dialing_size, const char *string) +{ + char kp_digit; + const char *prefix; + int length; + int i; + + /* remove start and stop digits, set string after start digit and set length to digits between start and stop */ + if (string[0] != 'a' && string[0] != 'b') { + PDEBUG(DSS5, DEBUG_NOTICE, "Received digits do not start with 'a' (KP1) nor 'b' (KP2), call is rejected!\n"); + return -EINVAL; + } + kp_digit = *string++; + length = strlen(string) - 1; + if (string[length] != 'c') { + PDEBUG(DSS5, DEBUG_NOTICE, "Received digits do end with 'c' (ST), call is rejected!\n"); + return -EINVAL; + } + if (length > dialing_size - 1) { + PDEBUG(DSS5, DEBUG_NOTICE, "Received dial string is too long, call is rejected!\n"); + return -EINVAL; + } + + /* received national call */ + if (kp_digit == 'a') { + /* remove discriminaing digit */ + string++; + --length; + *type = OSMO_CC_TYPE_NATIONAL; + strncpy(dialing, string, length); + dialing[length] = '\0'; + return 0; + } + + /* check international prefix with length of 1 digit */ + if (length < 2) { + PDEBUG(DSS5, DEBUG_NOTICE, "International number too short to get country code from, call is rejected!\n"); + return -EINVAL; + } + for (i = 0; prefix_1_digit[i]; i++) { + if (prefix_1_digit[i][0] == string[0]) + break; + } + /* if number is of international type, create international dial string */ + if (prefix_1_digit[i]) { + prefix = string; + string += 1; + length -= 1; + /* remove discriminaing digit */ + string++; + --length; + *type = OSMO_CC_TYPE_INTERNATIONAL; + dialing[0] = prefix[0]; + strncpy(dialing + 1, string, length); + dialing[1 + length] = '\0'; + return 0; + } + + /* check international prefix with length of 2 digits */ + if (length < 3) { + PDEBUG(DSS5, DEBUG_NOTICE, "International number too short to get country code from, call is rejected!\n"); + return -EINVAL; + } + for (i = 0; prefix_2_digit[i]; i++) { + if (prefix_2_digit[i][0] == string[0] + && prefix_2_digit[i][1] == string[1]) + break; + } + /* if number is of international type, create international dial string */ + if (prefix_2_digit[i]) { + prefix = string; + string += 2; + length -= 2; + /* remove discriminaing digit */ + string++; + --length; + *type = OSMO_CC_TYPE_INTERNATIONAL; + dialing[0] = prefix[0]; + dialing[1] = prefix[1]; + strncpy(dialing + 2, string, length); + dialing[2 + length] = '\0'; + return 0; + } + + /* check international prefix with length of 3 digits */ + if (length < 4) { + PDEBUG(DSS5, DEBUG_NOTICE, "International number too short to get country code from, call is rejected!\n"); + return -EINVAL; + } + /* if number is of international type, create international dial string */ + prefix = string; + string += 3; + length -= 3; + /* remove discriminaing digit */ + string++; + --length; + *type = OSMO_CC_TYPE_INTERNATIONAL; + dialing[0] = prefix[0]; + dialing[1] = prefix[1]; + dialing[2] = prefix[2]; + strncpy(dialing + 3, string, length); + dialing[3 + length] = '\0'; + return 0; +} + +/* + * event handling + */ + +/* function that receives the digit or the cease of it (' ' or different digit) */ +void receive_digit(void *priv, char digit, double dbm) +{ + ss5_t *ss5 = priv; + int i; + int rc; + + if (digit > ' ') + PDEBUG_CHAN(DSS5, DEBUG_DEBUG, "Received digit '%c' in '%s' state. (%.1f dBm)\n", digit, ss5_state_names[ss5->state], dbm); + else + PDEBUG_CHAN(DSS5, DEBUG_DEBUG, "Received cease of digit in '%s' state.\n", ss5_state_names[ss5->state]); + + /* a clear forward (not release guard) at any state (including idle state) */ + if (ss5->state != SS5_STATE_SEND_CLR_FWD && digit == 'C') { + PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received 'clear-forward' signal in '%s' state, sending 'release-guard' and clearing call.\n", ss5_state_names[ss5->state]); + /* release outgoing call */ + if (ss5->cc_callref) { + /* send release indication towards CC */ + release_call(ss5, OSMO_CC_ISDN_CAUSE_NORM_CALL_CLEAR); + /* remove ref */ + ss5->cc_callref = 0; + } + /* stop timer */ + timer_stop(&ss5->timer); + /* change state */ + ss5_new_state(ss5, SS5_STATE_SEND_RELEASE); + /* unset dial string, if set */ + set_dial_string(&ss5->dsp, ""); + /* send release-guard */ + set_tone(&ss5->dsp, 'C', 0); + /* to prevent blueboxing */ + if (ss5->ss5_ep->prevent_blueboxing) { + PDEBUG_CHAN(DSS5, DEBUG_INFO, "Starting release-guard timer to prevent blueboxing.\n"); + /* start timer */ + timer_start(&ss5->timer, MIN_RELEASE_GUARD); + } + return; + } + + switch (ss5->state) { + /* release guard */ + case SS5_STATE_SEND_RELEASE: + if (digit != 'C') { + /* wait at least the minimum release-guard time, to prevent blueboxing */ + if (timer_running(&ss5->timer)) { + PDEBUG_CHAN(DSS5, DEBUG_INFO, "'clear-forward' is ceased in '%s' state, must wait to prevent blueboxing.\n", ss5_state_names[ss5->state]); + /* state idle */ + ss5_new_state(ss5, SS5_STATE_SEND_REL_WAIT); + break; + } + PDEBUG_CHAN(DSS5, DEBUG_INFO, "'clear-forward' is ceased in '%s' state, going idle.\n", ss5_state_names[ss5->state]); + /* cease */ + set_tone(&ss5->dsp, 0, 0); + /* state idle */ + ss5_new_state(ss5, SS5_STATE_IDLE); + /* reset instance */ + link_reset(ss5); + } + break; + /* outgoing call sends seize */ + case SS5_STATE_SEND_SEIZE: + if (digit == 'A') { + PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received 'seize' signal in '%s' state, this is double seizure.\n", ss5_state_names[ss5->state]); + /* set timeout */ + timer_start(&ss5->timer, DUR_DOUBLE_SEIZURE); + /* change state */ + ss5_new_state(ss5, SS5_STATE_DOUBLE_SEIZE); + } + if (digit == 'B') { + PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received 'proceed-to-send' signal in '%s' state, ceasing 'seize' signal.\n", ss5_state_names[ss5->state]); + /* set recognition time to normal */ + set_sig_detect_duration(&ss5->dsp, SIGN_RECOGNITION_NORMAL, SIGN_RECOGNITION_NORMAL); + /* cease */ + set_tone(&ss5->dsp, 0, 0); + /* stop timer */ + timer_stop(&ss5->timer); + /* change state */ + ss5_new_state(ss5, SS5_STATE_RECV_PROCEED); + } + break; + /* both ends send a seize, waiting for timeout */ + case SS5_STATE_DOUBLE_SEIZE: + break; + /* outgoing call receives proceed-to-send */ + case SS5_STATE_RECV_PROCEED: + if (digit != 'B') { + PDEBUG_CHAN(DSS5, DEBUG_INFO, "proceed-to-send' is ceased in '%s' state, sendig digits.\n", ss5_state_names[ss5->state]); + /* cease */ + set_tone(&ss5->dsp, 0, 0); + /* dial */ + set_tone(&ss5->dsp, ' ', PAUSE_BEFORE_DIALING); + set_dial_string(&ss5->dsp, ss5->dialing); + /* change state */ + ss5_new_state(ss5, SS5_STATE_SEND_DIGITS); + } + break; + /* outgoing call receives answer or busy-flash */ + case SS5_STATE_OUT_INACTIVE: + if (digit == 'A') { + PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received 'answer' signal in '%s' state, sending 'acknowledge'.\n", ss5_state_names[ss5->state]); + /* send acknowledge */ + set_tone(&ss5->dsp, 'A', MAX_ACKNOWLEDGE); + /* change state */ + ss5_new_state(ss5, SS5_STATE_SEND_ACK_ANS); + /* indicate answer to upper layer */ + answer_call(ss5); + } + if (digit == 'B') { + PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received 'busy-flash' signal in '%s' state, sending 'acknowledge'.\n", ss5_state_names[ss5->state]); + /* send acknowledge */ + set_tone(&ss5->dsp, 'A', MAX_ACKNOWLEDGE); + /* change state */ + ss5_new_state(ss5, SS5_STATE_SEND_ACK_BUS); + /* indicate disconnect w/tones to upper layer */ + disconnect_call(ss5, OSMO_CC_ISDN_CAUSE_USER_BUSY); + } + break; + /* outgoing call receives clear-back */ + case SS5_STATE_OUT_ACTIVE: + if (digit == 'A') { + PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received 'answer' signal in '%s' state, sending 'acknowledge'.\n", ss5_state_names[ss5->state]); + /* send acknowledge */ + set_tone(&ss5->dsp, 'A', MAX_ACKNOWLEDGE); + /* change state */ + ss5_new_state(ss5, SS5_STATE_SEND_ACK_ANS); + /* indicate answer to upper layer */ + answer_call(ss5); + } + if (digit == 'B') { + PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received 'clear-back' signal in '%s' state, sending 'acknowledge'.\n", ss5_state_names[ss5->state]); + /* send acknowledge */ + set_tone(&ss5->dsp, 'A', MAX_ACKNOWLEDGE); + /* change state */ + ss5_new_state(ss5, SS5_STATE_SEND_ACK_CLR); + /* indicate disconnect w/tones to upper layer */ + disconnect_call(ss5, OSMO_CC_ISDN_CAUSE_NORM_CALL_CLEAR); + } + break; + /* outgoing call receives answer */ + case SS5_STATE_SEND_ACK_ANS: + if (digit != 'A') { + PDEBUG_CHAN(DSS5, DEBUG_INFO, "'answer' is ceased in '%s' state, call is established.\n", ss5_state_names[ss5->state]); + /* stop timer */ + timer_stop(&ss5->timer); + /* cease */ + set_tone(&ss5->dsp, 0, 0); + /* change state */ + ss5_new_state(ss5, SS5_STATE_OUT_ACTIVE); + } + break; + /* outgoing call receives busy-flash */ + case SS5_STATE_SEND_ACK_BUS: + case SS5_STATE_SEND_DIGITS: + if (digit != 'B') { + PDEBUG_CHAN(DSS5, DEBUG_INFO, "'busy-flash' is ceased in '%s' state, call is disconnected.\n", ss5_state_names[ss5->state]); + /* stop timer */ + timer_stop(&ss5->timer); + /* unset dial string, if set */ + set_dial_string(&ss5->dsp, ""); + /* cease */ + set_tone(&ss5->dsp, 0, 0); + /* change state */ + ss5_new_state(ss5, SS5_STATE_OUT_INACTIVE); + } + break; + /* outgoing call receives clear-back */ + case SS5_STATE_SEND_ACK_CLR: + if (digit != 'B') { + PDEBUG_CHAN(DSS5, DEBUG_INFO, "'clear-back' is ceased in '%s' state, call is disconnected.\n", ss5_state_names[ss5->state]); + /* stop timer */ + timer_stop(&ss5->timer); + /* cease */ + set_tone(&ss5->dsp, 0, 0); + /* change state */ + ss5_new_state(ss5, SS5_STATE_OUT_INACTIVE); + } + break; + /* outgoing call sends clear forward */ + case SS5_STATE_SEND_CLR_FWD: + if (digit == 'C') { + PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received 'release-guard' signal in '%s' state, ceasing 'clear-forward' signal.\n", ss5_state_names[ss5->state]); + /* cease */ + set_tone(&ss5->dsp, 0, 0); + /* stop timer */ + timer_stop(&ss5->timer); + /* change state */ + ss5_new_state(ss5, SS5_STATE_RECV_RELEASE); + } + break; + /* outgoing call receives release guard */ + case SS5_STATE_RECV_RELEASE: + if (digit != 'C') { + PDEBUG_CHAN(DSS5, DEBUG_INFO, "'release-guard' is ceased in '%s' state, going idle.\n", ss5_state_names[ss5->state]); + /* state idle */ + ss5_new_state(ss5, SS5_STATE_IDLE); + /* reset instance */ + link_reset(ss5); + } + break; + /* incoming call receives seize */ + case SS5_STATE_IDLE: + if (digit == 'A') { + PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received 'seize' signal in '%s' state, sending 'proceed-to-send'.\n", ss5_state_names[ss5->state]); + /* set recognition time to normal */ + set_sig_detect_duration(&ss5->dsp, SIGN_RECOGNITION_NORMAL, SIGN_RECOGNITION_NORMAL); + /* change state */ + ss5_new_state(ss5, SS5_STATE_SEND_PROCEED); + /* send proceed-to-send */ + set_tone(&ss5->dsp, 'B', MAX_PROCEED_TO_SEND); + } + break; + /* incoming call sends proceed-to-send */ + case SS5_STATE_SEND_PROCEED: + if (digit != 'A') { + PDEBUG_CHAN(DSS5, DEBUG_INFO, "'seize' is ceased in '%s' state, receiving digits.\n", ss5_state_names[ss5->state]); + /* cease */ + set_tone(&ss5->dsp, 0, 0); + /* change state */ + ss5_new_state(ss5, SS5_STATE_RECV_DIGIT); + /* start timer */ + timer_start(&ss5->timer, TO_DIALING); + } + break; + /* incoming call receives digits */ + case SS5_STATE_RECV_DIGIT: + if (!(digit >= '0' && digit <= '9') && !(digit >= 'a' && digit <= 'c')) { + break; + } + PDEBUG_CHAN(DSS5, DEBUG_INFO, "Digit '%c' is ceased in '%s' state.\n", digit, ss5_state_names[ss5->state]); + /* add digit */ + i = strlen(ss5->dialing); + if (i + 1 == sizeof(ss5->dialing)) + break; + ss5->dialing[i++] = digit; + ss5->dialing[i] = '\0'; + /* change state */ + ss5_new_state(ss5, SS5_STATE_RECV_SPACE); + /* restart timer */ + timer_start(&ss5->timer, TO_DIALING); + break; + case SS5_STATE_RECV_SPACE: + if (digit != ' ') + break; + /* check for end of dialing */ + i = strlen(ss5->dialing) - 1; + if (ss5->dialing[i] == 'c') { + osmo_cc_msg_t *msg; + uint8_t type; + char dialing[sizeof(ss5->dialing)]; + PDEBUG_CHAN(DSS5, DEBUG_INFO, "Dialing '%s' is complete, sending setup message towards call control.\n", ss5->dialing); + /* stop timer */ + timer_stop(&ss5->timer); + /* check dial string */ + rc = parse_dial_string(&type, dialing, sizeof(dialing), ss5->dialing); + if (rc < 0) { + /* send clear-back */ + set_tone(&ss5->dsp, 'B', MAX_CLEAR_BACK); + /* change state */ + ss5_new_state(ss5, SS5_STATE_SEND_CLR_BAK); + break; + } + /* setup message */ + msg = osmo_cc_new_msg(OSMO_CC_MSG_SETUP_IND); + /* network type */ + osmo_cc_add_ie_calling_network(msg, OSMO_CC_NETWORK_SS5_NONE, ""); + /* called number */ + osmo_cc_add_ie_called(msg, type, OSMO_CC_PLAN_TELEPHONY, dialing); + /* bearer capability */ + osmo_cc_add_ie_bearer(msg, OSMO_CC_CODING_ITU_T, OSMO_CC_CAPABILITY_AUDIO, OSMO_CC_MODE_CIRCUIT); + /* sdp offer */ + ss5->cc_session = osmo_cc_helper_audio_offer(ss5, codecs, down_audio, msg, 1); + if (!ss5->cc_session) { + osmo_cc_free_msg(msg); + PDEBUG_CHAN(DSS5, DEBUG_NOTICE, "Failed to offer audio, sending 'clear-back'.\n"); + /* send clear-back */ + set_tone(&ss5->dsp, 'B', MAX_CLEAR_BACK); + /* change state */ + ss5_new_state(ss5, SS5_STATE_SEND_CLR_BAK); + break; + } + /* create new call */ + osmo_cc_call_t *cc_call = osmo_cc_call_new(&ss5->ss5_ep->cc_ep); + ss5->cc_callref = cc_call->callref; + /* send message to CC */ + osmo_cc_ll_msg(&ss5->ss5_ep->cc_ep, ss5->cc_callref, msg); + /* change state */ + ss5_new_state(ss5, SS5_STATE_IN_INACTIVE); + break; + } + /* change state */ + ss5_new_state(ss5, SS5_STATE_RECV_DIGIT); + break; + /* incoming call sends answer */ + case SS5_STATE_SEND_ANSWER: + if (digit == 'A') { + PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received 'acknowledge' to 'answer' in '%s' state, call is now active.\n", ss5_state_names[ss5->state]); + /* stop timer */ + timer_stop(&ss5->timer); + /* cease */ + set_tone(&ss5->dsp, 0, 0); + /* change state */ + ss5_new_state(ss5, SS5_STATE_IN_ACTIVE); + } + break; + /* incoming call sends busy-flash */ + case SS5_STATE_SEND_BUSY: + if (digit == 'A') { + PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received 'acknowledge' to 'busy-flash' in '%s' state, call is now inactive.\n", ss5_state_names[ss5->state]); + /* stop timer */ + timer_stop(&ss5->timer); + /* cease */ + set_tone(&ss5->dsp, 0, 0); + /* change state */ + ss5_new_state(ss5, SS5_STATE_IN_INACTIVE); + } + break; + /* incoming call sends clear-back */ + case SS5_STATE_SEND_CLR_BAK: + if (digit == 'A') { + PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received 'acknowledge' to 'clear-back' in '%s' state, call is now inactive.\n", ss5_state_names[ss5->state]); + /* stop timer */ + timer_stop(&ss5->timer); + /* cease */ + set_tone(&ss5->dsp, 0, 0); + /* change state */ + ss5_new_state(ss5, SS5_STATE_IN_INACTIVE); + } + break; + default: + PDEBUG_CHAN(DSS5, DEBUG_ERROR, "Received digit '%c' in '%s' state is not handled, please fix!\n", digit, ss5_state_names[ss5->state]); + } +} + +/* dialing was completed */ +void dialing_complete(void *priv) +{ + ss5_t *ss5 = priv; + + if (ss5->state == SS5_STATE_SEND_DIGITS) { + PDEBUG_CHAN(DSS5, DEBUG_INFO, "Dialing is complete in '%s' state, waiting for remote party to answer.\n", ss5_state_names[ss5->state]); + /* change state */ + ss5_new_state(ss5, SS5_STATE_OUT_INACTIVE); + } + + /* indicate alerting */ + alert_call(ss5); +} + +/* timeouts */ +static void ss5_timeout(struct timer *timer) +{ + ss5_t *ss5 = timer->priv; + + switch (ss5->state) { + case SS5_STATE_RECV_DIGIT: + case SS5_STATE_RECV_SPACE: + PDEBUG_CHAN(DSS5, DEBUG_INFO, "Received timeout in '%s' state, sending 'clear-back'.\n", ss5_state_names[ss5->state]); + /* send clear-back */ + set_tone(&ss5->dsp, 'B', MAX_CLEAR_BACK); + /* change state */ + ss5_new_state(ss5, SS5_STATE_SEND_CLR_BAK); + break; + case SS5_STATE_DOUBLE_SEIZE: + /* cease */ + set_tone(&ss5->dsp, 0, 0); + /* state idle */ + ss5_new_state(ss5, SS5_STATE_IDLE); + /* send release indication towards CC */ + release_call(ss5, OSMO_CC_ISDN_CAUSE_NO_CIRCUIT_CHAN); + /* reset inst */ + link_reset(ss5); + break; + case SS5_STATE_SEND_REL_WAIT: + PDEBUG_CHAN(DSS5, DEBUG_INFO, "'release-guard' timer expired, going idle.\n"); + /* cease */ + set_tone(&ss5->dsp, 0, 0); + /* state idle */ + ss5_new_state(ss5, SS5_STATE_IDLE); + /* reset instance */ + link_reset(ss5); + break; + default: + ; + } +} + +/* message from call contol */ +void cc_message(osmo_cc_endpoint_t *ep, uint32_t callref, osmo_cc_msg_t *msg) +{ + ss5_endpoint_t *ss5_ep = ep->priv; + ss5_t *ss5; + osmo_cc_msg_t *new_msg; + uint8_t type, plan, present, screen; + char dialing[64]; + const char *sdp; + int rc; + + /* hunt for callref */ + ss5 = ss5_ep->link_list; + while (ss5) { + if (ss5->cc_callref == callref) + break; + ss5 = ss5->next; + } + + /* process SETUP */ + if (!ss5) { + if (msg->type != OSMO_CC_MSG_SETUP_REQ) { + PDEBUG(DSS5, DEBUG_ERROR, "received message without ss5 instance, please fix!\n"); + goto done; + } + /* hunt free ss5 instance */ + ss5 = ss5_ep->link_list; + while (ss5) { + if (ss5->state == SS5_STATE_IDLE) + break; + ss5 = ss5->next; + } + if (!ss5) { + PDEBUG(DSS5, DEBUG_NOTICE, "No free ss5 instance, rejecting.\n"); + reject_call(ss5_ep, callref, OSMO_CC_ISDN_CAUSE_NO_CIRCUIT_CHAN); + goto done; + } + /* link with cc */ + ss5->cc_callref = callref; + } + + switch (msg->type) { + case OSMO_CC_MSG_SETUP_REQ: /* dial-out command received from epoint */ + PDEBUG_CHAN(DSS5, DEBUG_INFO, "Outgoing call in '%s' state, sending 'seize'.\n", ss5_state_names[ss5->state]); + /* caller id */ + rc = osmo_cc_get_ie_calling(msg, 0, &type, &plan, &present, &screen, ss5->callerid, sizeof(ss5->callerid)); + /* called number */ + rc = osmo_cc_get_ie_called(msg, 0, &type, &plan, dialing, sizeof(dialing)); + if (rc < 0 || !dialing[0]) { + PDEBUG_CHAN(DSS5, DEBUG_NOTICE, "No number given, call is rejected!\n"); + inv_nr: + reject_call(ss5->ss5_ep, ss5->cc_callref, OSMO_CC_ISDN_CAUSE_INV_NR_FORMAT); + link_reset(ss5); + goto done; + } + rc = generate_dial_string(type, dialing, ss5->dialing, sizeof(ss5->dialing)); + if (rc < 0) + goto inv_nr; + /* sdp accept */ + sdp = osmo_cc_helper_audio_accept(ss5, codecs, down_audio, msg, &ss5->cc_session, &ss5->codec, 0); + if (!sdp) { + reject_call(ss5->ss5_ep, ss5->cc_callref, OSMO_CC_ISDN_CAUSE_RESOURCE_UNAVAIL); + link_reset(ss5); + goto done; + } + /* proceed */ + proceed_call(ss5, sdp); + /* send seize */ + set_tone(&ss5->dsp, 'A', MAX_SEIZE); + /* change state */ + ss5_new_state(ss5, SS5_STATE_SEND_SEIZE); + break; + case OSMO_CC_MSG_SETUP_ACK_REQ: /* more information is needed */ + case OSMO_CC_MSG_PROC_REQ: /* call of endpoint is proceeding */ + case OSMO_CC_MSG_ALERT_REQ: /* call of endpoint is ringing */ + case OSMO_CC_MSG_PROGRESS_REQ: /* progress */ + rc = osmo_cc_helper_audio_negotiate(msg, &ss5->cc_session, &ss5->codec); + if (rc < 0) { + codec_failed: + PDEBUG_CHAN(DSS5, DEBUG_NOTICE, "Releasing, because codec negotiation failed.\n"); + /* send busy-flash */ + set_tone(&ss5->dsp, 'B', MAX_BUSY_FLASH); + /* change state */ + ss5_new_state(ss5, SS5_STATE_SEND_BUSY); + /* release call */ + release_call(ss5, OSMO_CC_ISDN_CAUSE_RESOURCE_UNAVAIL); + /* reset inst */ + link_reset(ss5); + goto done; + } + break; + case OSMO_CC_MSG_SETUP_RSP: /* call of endpoint is connected */ + PDEBUG_CHAN(DSS5, DEBUG_INFO, "Incoming call has answered in '%s' state, sending 'answer'.\n", ss5_state_names[ss5->state]); + rc = osmo_cc_helper_audio_negotiate(msg, &ss5->cc_session, &ss5->codec); + if (rc < 0) + goto codec_failed; + /* setup acknowledge */ + setup_ack_call(ss5); + /* not in right state, which should never happen anyway */ + if (ss5->state != SS5_STATE_IN_INACTIVE) + break; + /* send answer */ + set_tone(&ss5->dsp, 'A', MAX_ANSWER); + /* change state */ + ss5_new_state(ss5, SS5_STATE_SEND_ANSWER); + break; + case OSMO_CC_MSG_REJ_REQ: /* call has been rejected */ + case OSMO_CC_MSG_REL_REQ: /* call has been released */ + case OSMO_CC_MSG_DISC_REQ: /* call has been disconnected */ + rc = osmo_cc_helper_audio_negotiate(msg, &ss5->cc_session, &ss5->codec); + if (rc < 0) + goto codec_failed; + /* right state */ + if (ss5->state == SS5_STATE_IN_INACTIVE) { + PDEBUG_CHAN(DSS5, DEBUG_INFO, "Incoming call has disconnected in '%s' state, sending 'busy-flash'.\n", ss5_state_names[ss5->state]); + /* send busy-flash */ + set_tone(&ss5->dsp, 'B', MAX_BUSY_FLASH); + /* change state */ + ss5_new_state(ss5, SS5_STATE_SEND_BUSY); + } else + if (ss5->state == SS5_STATE_IN_ACTIVE) { + PDEBUG_CHAN(DSS5, DEBUG_INFO, "Incoming call has disconnected in '%s' state, sending 'clear-back'.\n", ss5_state_names[ss5->state]); + /* send clear-back */ + set_tone(&ss5->dsp, 'B', MAX_CLEAR_BACK); + /* change state */ + ss5_new_state(ss5, SS5_STATE_SEND_CLR_BAK); + } else { + PDEBUG_CHAN(DSS5, DEBUG_INFO, "Outgoing call has disconnected in '%s' state, sending 'clear-forward'.\n", ss5_state_names[ss5->state]); + /* send clear-forward */ + set_tone(&ss5->dsp, 'C', MAX_CLEAR_FORWARD); + /* change state */ + ss5_new_state(ss5, SS5_STATE_SEND_CLR_FWD); + if (msg->type == OSMO_CC_MSG_DISC_REQ) { + /* clone osmo-cc message to preserve cause */ + new_msg = osmo_cc_clone_msg(msg); + new_msg->type = OSMO_CC_MSG_REL_IND; + /* send message to osmo-cc */ + osmo_cc_ll_msg(&ss5->ss5_ep->cc_ep, ss5->cc_callref, new_msg); + /* reset */ + link_reset(ss5); + break; + } + } + /* on release, we confirm */ + if (msg->type == OSMO_CC_MSG_REL_REQ) { + /* clone osmo-cc message to preserve cause */ + new_msg = osmo_cc_clone_msg(msg); + new_msg->type = OSMO_CC_MSG_REL_CNF; + /* send message to osmo-cc */ + osmo_cc_ll_msg(&ss5->ss5_ep->cc_ep, ss5->cc_callref, new_msg); + } + /* on reject/release we reset/unlink call */ + if (msg->type != OSMO_CC_MSG_DISC_REQ) + link_reset(ss5); + break; + } + +done: + osmo_cc_free_msg(msg); +} + diff --git a/src/ss5/ss5.h b/src/ss5/ss5.h new file mode 100644 index 0000000..91acdc9 --- /dev/null +++ b/src/ss5/ss5.h @@ -0,0 +1,85 @@ + +#include "../libtimer/timer.h" +#include "../libosmocc/endpoint.h" +#include "../libosmocc/helper.h" +#include "../libsample/sample.h" +#include "../libjitter/jitter.h" +#include "mf.h" +#include "dsp.h" + +enum ss5_state { + SS5_STATE_NULL = 0, + /* idle line */ + SS5_STATE_IDLE, + /* outgoing call */ + SS5_STATE_SEND_SEIZE, /* sending seize, waiting for proceed-to-send */ + SS5_STATE_RECV_PROCEED, /* detected proceed-to-send, waiting to cease */ + SS5_STATE_SEND_DIGITS, /* proceed-to-send is ceased, sending digits */ + SS5_STATE_OUT_INACTIVE, /* waiting for called party to answer or being busy */ + SS5_STATE_SEND_ACK_ANS, /* detected answer, sending acknowledge, waiting to cease */ + SS5_STATE_SEND_ACK_BUS, /* detected busy-flash, sending acknowledge, waiting to cease */ + SS5_STATE_SEND_ACK_CLR, /* detected clear-back, sending acknowledge, waiting to cease */ + SS5_STATE_OUT_ACTIVE, /* detected answer is ceased */ + SS5_STATE_SEND_CLR_FWD, /* sending clear-forward, waiting for release-guard */ + SS5_STATE_RECV_RELEASE, /* receiving release-guard, waiting to cease */ + SS5_STATE_SEND_FORWARD, /* sending forward-transfer for a while */ + /* incoming call */ + SS5_STATE_SEND_PROCEED, /* detected seize, sending procced-to-send, waiting to cease */ + SS5_STATE_RECV_DIGIT, /* seize is ceased, waiting for digits */ + SS5_STATE_RECV_SPACE, /* digit received, waiting to cease */ + SS5_STATE_IN_INACTIVE, /* waiting for called party to answer or disconnect */ + SS5_STATE_SEND_ANSWER, /* sending answer, waiting for for acknowledge */ + SS5_STATE_IN_ACTIVE, /* detected acknowledge of answer */ + SS5_STATE_SEND_BUSY, /* sending busy-flash, waiting for acknowledge */ + SS5_STATE_SEND_CLR_BAK, /* sending clear back (hangup) */ + SS5_STATE_SEND_RELEASE, /* detected clear-forward, sending release-guard, waiting for cease */ + SS5_STATE_SEND_REL_WAIT,/* clear forward ceased, but waiting to prevent blueboxing */ + /* seize collision */ + SS5_STATE_DOUBLE_SEIZE, /* seize is detected, sending for while, waiting for cease */ +}; + +extern const char *ss5_state_names[]; + +struct ss5_endpoint; + +/* SS5 link definition */ +typedef struct ss5 { + struct ss5 *next; + struct ss5_endpoint *ss5_ep; + char name[32]; /* name of link for debugging */ + + /* call states */ + enum ss5_state state; /* state of link */ + struct timer timer; /* for several timeouts */ + uint32_t cc_callref; /* ref to CC call */ + osmo_cc_session_t *cc_session; /* audio session description */ + osmo_cc_session_codec_t *codec; /* selected codec */ + char callerid[65]; /* current caller id (outgoing only) */ + char dialing[33]; /* current dial string (send or receive) */ + + /* audio processing */ + jitter_t dejitter; /* jitter buffer for audio from CC */ + dsp_t dsp; /* all dsp processing */ + sample_t *delay_buffer; /* buffer for delaying audio */ + int delay_length; + int delay_index; +} ss5_t; + +/* SS5 endpoint definition */ +typedef struct ss5_endpoint { + osmo_cc_endpoint_t cc_ep; + ss5_t *link_list; + double samplerate; + int suppress_disconnect; /* do not forward disconnect towards CC */ + int prevent_blueboxing; /* extend release-guard, so outgoing exchange releases */ + int crosstalk; /* mix crosstalk from TX to RX */ + int comfort_noise; /* add comfort noise before answer and after disconnect */ + int delay_ms; /* add delay to simulate long distance lines */ + double sense_db; /* increase sensitivity of decoder by this value */ +} ss5_endpoint_t; + +void refresh_status(void); +ss5_endpoint_t *ss5_ep_create(const char *ep_name, int links, int prevent_blueboxing, int crosstalk, int delay_ms, int comfort_noise, int suppress_disconnect, double sense_db); +void ss5_ep_destroy(ss5_endpoint_t *ss5_ep); +void cc_message(osmo_cc_endpoint_t *ep, uint32_t callref, osmo_cc_msg_t *msg); +