diff options
Diffstat (limited to 'src/ss5/ss5.c')
-rw-r--r-- | src/ss5/ss5.c | 1128 |
1 files changed, 1128 insertions, 0 deletions
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 <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 CHAN ss5->name + +#include <stdio.h> +#include <string.h> +#include <unistd.h> +#include <stdlib.h> +#include <stdint.h> +#include <math.h> +#include <errno.h> +#include <sys/types.h> +#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); +} + |