/* built-in console to talk to a phone * * (C) 2017 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 G* along with this program. If not, see . */ #include #include #include #include #include #include #include #include "../libsample/sample.h" #include "../libsamplerate/samplerate.h" #include "../libjitter/jitter.h" #include "../libdebug/debug.h" #include "testton.h" #include "mncc.h" #include "mncc_console.h" #include "cause.h" #include "../libmobile/call.h" #ifdef HAVE_ALSA #include "../libsound/sound.h" #endif static int new_callref = 0; /* toward mobile */ enum console_state { CONSOLE_IDLE = 0, /* IDLE */ CONSOLE_SETUP_RO, /* call from radio to console */ CONSOLE_SETUP_RT, /* call from console to radio */ CONSOLE_ALERTING_RO, /* call from radio to console */ CONSOLE_ALERTING_RT, /* call from console to radio */ CONSOLE_CONNECT, CONSOLE_DISCONNECT, }; static const char *console_state_name[] = { "IDLE", "SETUP_RO", "SETUP_RT", "ALERTING_RO", "ALERTING_RT", "CONNECT", "DISCONNECT", }; /* console call instance */ typedef struct console { uint32_t callref; enum console_state state; int disc_cause; /* cause that has been sent by transceiver instance for release */ char station_id[16]; char dialing[16]; char audiodev[64]; /* headphone interface, if used */ int samplerate; /* sample rate of headphone interface */ void *sound; /* headphone interface */ int latspl; /* sample latency at headphone interface */ samplerate_t srstate; /* patterns/announcement upsampling */ jitter_t dejitter; /* headphone audio dejittering */ int test_audio_pos; /* position for test tone toward mobile */ sample_t tx_buffer[160];/* transmit audio buffer */ int tx_buffer_pos; /* current position in transmit audio buffer */ int dial_digits; /* number of digits to be dialed */ int loopback; /* loopback test for echo */ int echo_test; /* send echo back to mobile phone */ } console_t; static console_t console; /* stream test music */ int16_t *test_spl = NULL; int test_size = 0; int test_max = 0; static void get_test_patterns(int16_t *samples, int length) { const int16_t *spl; int size, max, pos; spl = test_spl; size = test_size; max = test_max; /* stream sample */ pos = console.test_audio_pos; while(length--) { if (pos >= size) *samples++ = 0; else *samples++ = spl[pos] >> 2; if (++pos == max) pos = 0; } console.test_audio_pos = pos; } static void console_new_state(enum console_state state) { PDEBUG(DMNCC, DEBUG_DEBUG, "Call state '%s' -> '%s'\n", console_state_name[console.state], console_state_name[state]); console.state = state; console.test_audio_pos = 0; } static int console_mncc_up(uint8_t *buf, int length) { struct gsm_mncc *mncc = (struct gsm_mncc *)buf; if (mncc->msg_type == ANALOG_8000HZ) { struct gsm_data_frame *data = (struct gsm_data_frame *)buf; int count = 160; sample_t samples[count]; /* save audio from transceiver to jitter buffer */ if (console.sound) { sample_t up[(int)((double)count * console.srstate.factor + 0.5) + 10]; int16_to_samples(samples, (int16_t *)data->data, count); count = samplerate_upsample(&console.srstate, samples, count, up); jitter_save(&console.dejitter, up, count); return 0; } /* if echo test is used, send echo back to mobile */ if (console.echo_test) { /* send down reused MNCC */ mncc_down(buf, length); return 0; } /* if no sound is used, send test tone to mobile */ if (console.state == CONSOLE_CONNECT) { /* send down reused MNCC */ get_test_patterns((int16_t *)data->data, count); mncc_down(buf, length); return 0; } return 0; } if (mncc->msg_type != MNCC_SETUP_IND && console.callref != mncc->callref) { PDEBUG(DMNCC, DEBUG_ERROR, "invalid call ref.\n"); /* send down reused MNCC */ mncc->msg_type = MNCC_REL_REQ; mncc->fields |= MNCC_F_CAUSE; mncc->cause.location = LOCATION_USER; mncc->cause.value = CAUSE_INVALCALLREF; mncc_down(buf, length); return 0; } switch(mncc->msg_type) { case MNCC_SETUP_IND: PDEBUG(DMNCC, DEBUG_INFO, "Incoming call from '%s'\n", mncc->calling.number); /* setup is also allowed on disconnected call */ if (console.state == CONSOLE_DISCONNECT) { PDEBUG(DMNCC, DEBUG_INFO, "Releasing pending disconnected call\n"); if (console.callref) { uint8_t buf[sizeof(struct gsm_mncc)]; struct gsm_mncc *mncc = (struct gsm_mncc *)buf; memset(buf, 0, sizeof(buf)); mncc->msg_type = MNCC_REL_REQ; mncc->callref = console.callref; mncc->fields |= MNCC_F_CAUSE; mncc->cause.location = LOCATION_USER; mncc->cause.value = CAUSE_NORMAL; mncc_down(buf, sizeof(struct gsm_mncc)); console.callref = 0; } console_new_state(CONSOLE_IDLE); } if (console.state != CONSOLE_IDLE) { PDEBUG(DMNCC, DEBUG_NOTICE, "We are busy, rejecting.\n"); return -CAUSE_BUSY; } console.callref = mncc->callref; if (mncc->calling.number[0]) { strncpy(console.station_id, mncc->calling.number, console.dial_digits); console.station_id[console.dial_digits] = '\0'; } strncpy(console.dialing, mncc->called.number, sizeof(console.dialing) - 1); console.dialing[sizeof(console.dialing) - 1] = '\0'; console_new_state(CONSOLE_CONNECT); PDEBUG(DMNCC, DEBUG_INFO, "Call automatically answered\n"); /* send down reused MNCC */ mncc->msg_type = MNCC_SETUP_RSP; mncc_down(buf, length); break; case MNCC_ALERT_IND: PDEBUG(DMNCC, DEBUG_INFO, "Call alerting\n"); console_new_state(CONSOLE_ALERTING_RT); break; case MNCC_SETUP_CNF: PDEBUG(DMNCC, DEBUG_INFO, "Call connected to '%s'\n", mncc->connected.number); console_new_state(CONSOLE_CONNECT); strncpy(console.station_id, mncc->connected.number, console.dial_digits); console.station_id[console.dial_digits] = '\0'; /* send down reused MNCC */ mncc->msg_type = MNCC_SETUP_COMPL_REQ; mncc_down(buf, length); break; case MNCC_DISC_IND: PDEBUG(DMNCC, DEBUG_INFO, "Call disconnected (%s)\n", cause_name(mncc->cause.value)); console_new_state(CONSOLE_DISCONNECT); console.disc_cause = mncc->cause.value; break; case MNCC_REL_IND: PDEBUG(DMNCC, DEBUG_INFO, "Call released (%s)\n", cause_name(mncc->cause.value)); console_new_state(CONSOLE_IDLE); console.callref = 0; break; } return 0; } static char console_text[256]; static char console_clear[256]; static int console_len = 0; static void _clear_console_text(void) { if (!console_len) return; fwrite(console_clear, console_len, 1, stdout); // note: fflused by user of this function console_len = 0; } static void _print_console_text(void) { if (!console_len) return; printf("\033[1;37m"); fwrite(console_text, console_len, 1, stdout); printf("\033[0;39m"); } int console_init(const char *station_id, const char *audiodev, int samplerate, int latency, int dial_digits, int loopback, int echo_test) { int rc = 0; init_testton(); clear_console_text = _clear_console_text; print_console_text = _print_console_text; memset(&console, 0, sizeof(console)); strncpy(console.station_id, station_id, sizeof(console.station_id) - 1); strncpy(console.audiodev, audiodev, sizeof(console.audiodev) - 1); console.samplerate = samplerate; console.latspl = latency * samplerate / 1000; console.dial_digits = dial_digits; console.loopback = loopback; console.echo_test = echo_test; mncc_up = console_mncc_up; if (!audiodev[0]) return 0; rc = init_samplerate(&console.srstate, 8000.0, (double)samplerate, 3300.0); if (rc < 0) { PDEBUG(DSENDER, DEBUG_ERROR, "Failed to init sample rate conversion!\n"); goto error; } rc = jitter_create(&console.dejitter, samplerate / 5); if (rc < 0) { PDEBUG(DSENDER, DEBUG_ERROR, "Failed to create and init dejitter buffer!\n"); goto error; } return 0; error: console_cleanup(); return rc; } int console_open_audio(int __attribute__((unused)) latspl) { if (!console.audiodev[0]) return 0; #ifdef HAVE_ALSA /* open sound device for call control */ /* use factor 1.4 of speech level for complete range of sound card */ console.sound = sound_open(console.audiodev, NULL, NULL, NULL, 1, 0.0, console.samplerate, latspl, 1.4, 4000.0, 2.0); if (!console.sound) { PDEBUG(DSENDER, DEBUG_ERROR, "No sound device!\n"); return -EIO; } #else PDEBUG(DSENDER, DEBUG_ERROR, "No sound card support compiled in!\n"); return -ENOTSUP; #endif return 0; } int console_start_audio(void) { if (!console.audiodev[0]) return 0; #ifdef HAVE_ALSA return sound_start(console.sound); #else return -EINVAL; #endif } void console_cleanup(void) { #ifdef HAVE_ALSA /* close sound devoice */ if (console.sound) sound_close(console.sound); #endif jitter_destroy(&console.dejitter); } static void process_ui(int c) { char text[256]; int len; switch (console.state) { case CONSOLE_IDLE: if (c > 0) { if (c >= '0' && c <= '9' && (int)strlen(console.station_id) < console.dial_digits) { console.station_id[strlen(console.station_id) + 1] = '\0'; console.station_id[strlen(console.station_id)] = c; } if ((c == 8 || c == 127) && strlen(console.station_id)) console.station_id[strlen(console.station_id) - 1] = '\0'; dial_after_hangup: if (c == 'd' && (int)strlen(console.station_id) == console.dial_digits) { int callref = ++new_callref; uint8_t buf[sizeof(struct gsm_mncc)]; struct gsm_mncc *mncc = (struct gsm_mncc *)buf; PDEBUG(DMNCC, DEBUG_INFO, "Outgoing call to '%s'\n", console.station_id); console.dialing[0] = '\0'; console_new_state(CONSOLE_SETUP_RT); console.callref = callref; memset(buf, 0, sizeof(buf)); mncc->msg_type = MNCC_SETUP_REQ; mncc->callref = callref; mncc->fields |= MNCC_F_CALLED; strncpy(mncc->called.number, console.station_id, sizeof(mncc->called.number) - 1); mncc->called.type = 0; /* dialing is of type 'unknown' */ mncc->lchan_type = GSM_LCHAN_TCH_F; mncc->fields |= MNCC_F_BEARER_CAP; mncc->bearer_cap.speech_ver[0] = BCAP_ANALOG_8000HZ; mncc->bearer_cap.speech_ver[1] = -1; mncc_down(buf, sizeof(struct gsm_mncc)); } } if (console.dial_digits != (int)strlen(console.station_id)) sprintf(text, "on-hook: %s%s (enter digits 0..9)\r", console.station_id, "..............." + 15 - console.dial_digits + strlen(console.station_id)); else sprintf(text, "on-hook: %s (press d=dial)\r", console.station_id); break; case CONSOLE_SETUP_RO: case CONSOLE_SETUP_RT: case CONSOLE_ALERTING_RO: case CONSOLE_ALERTING_RT: case CONSOLE_CONNECT: case CONSOLE_DISCONNECT: if (c > 0) { if (c == 'h' || (c == 'd' && console.state == CONSOLE_DISCONNECT)) { PDEBUG(DMNCC, DEBUG_INFO, "Call hangup\n"); console_new_state(CONSOLE_IDLE); if (console.callref) { uint8_t buf[sizeof(struct gsm_mncc)]; struct gsm_mncc *mncc = (struct gsm_mncc *)buf; memset(buf, 0, sizeof(buf)); mncc->msg_type = MNCC_REL_REQ; mncc->callref = console.callref; mncc->fields |= MNCC_F_CAUSE; mncc->cause.location = LOCATION_USER; mncc->cause.value = CAUSE_NORMAL; mncc_down(buf, sizeof(struct gsm_mncc)); console.callref = 0; } if (c == 'd') goto dial_after_hangup; } } if (console.state == CONSOLE_SETUP_RT) sprintf(text, "call setup: %s (press h=hangup)\r", console.station_id); if (console.state == CONSOLE_ALERTING_RT) sprintf(text, "call ringing: %s (press h=hangup)\r", console.station_id); if (console.state == CONSOLE_CONNECT) { if (console.dialing[0]) sprintf(text, "call active: %s->%s (press h=hangup)\r", console.station_id, console.dialing); else sprintf(text, "call active: %s (press h=hangup)\r", console.station_id); } if (console.state == CONSOLE_DISCONNECT) sprintf(text, "call disconnected: %s (press h=hangup d=redial)\r", cause_name(console.disc_cause)); break; } /* skip if nothing has changed */ len = strlen(text); if (console_len == len && !memcmp(console_text, text, len)) return; clear_console_text(); console_len = len; memcpy(console_text, text, len); memset(console_clear, ' ', len - 1); console_clear[len - 1] = '\r'; print_console_text(); fflush(stdout); } /* get keys from keyboad to control call via console * returns 1 on exit (ctrl+c) */ void process_console(int c) { if (!console.loopback) process_ui(c); if (!console.sound) return; #ifdef HAVE_ALSA /* handle audio, if sound device is used */ sample_t samples[console.latspl + 10], *samples_list[1]; uint8_t *power_list[1]; int count; int rc; count = sound_get_tosend(console.sound, console.latspl); if (count < 0) { PDEBUG(DSENDER, DEBUG_ERROR, "Failed to get samples in buffer (rc = %d)!\n", count); if (count == -EPIPE) PDEBUG(DSENDER, DEBUG_ERROR, "Trying to recover.\n"); return; } if (count > 0) { jitter_load(&console.dejitter, samples, count); samples_list[0] = samples; power_list[0] = NULL; rc = sound_write(console.sound, samples_list, power_list, count, NULL, NULL, 1); if (rc < 0) { PDEBUG(DSENDER, DEBUG_ERROR, "Failed to write TX data to sound device (rc = %d)\n", rc); if (rc == -EPIPE) PDEBUG(DSENDER, DEBUG_ERROR, "Trying to recover.\n"); return; } } samples_list[0] = samples; count = sound_read(console.sound, samples_list, console.latspl, 1, NULL); if (count < 0) { PDEBUG(DSENDER, DEBUG_ERROR, "Failed to read from sound device (rc = %d)!\n", count); if (count == -EPIPE) PDEBUG(DSENDER, DEBUG_ERROR, "Trying to recover.\n"); return; } if (count) { int i; if (console.loopback == 3) jitter_save(&console.dejitter, samples, count); count = samplerate_downsample(&console.srstate, samples, count); /* put samples into ring buffer */ for (i = 0; i < count; i++) { console.tx_buffer[console.tx_buffer_pos] = samples[i]; /* if ring buffer wrapps, deliver data down to call process */ if (++console.tx_buffer_pos == 160) { console.tx_buffer_pos = 0; /* only if we have a call */ if (console.callref) { uint8_t buf[sizeof(struct gsm_data_frame) + 160 * sizeof(int16_t)]; struct gsm_data_frame *data = (struct gsm_data_frame *)buf; data->msg_type = ANALOG_8000HZ; data->callref = console.callref; samples_to_int16((int16_t *)data->data, console.tx_buffer, 160); mncc_down(buf, sizeof(struct gsm_mncc)); } } } } #endif }