/*
 * (C) 2010 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 2 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.
 *
 */

#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <unistd.h>
#include <sys/types.h>

#include <osmocom/core/utils.h>
#include <osmocom/gsm/gsm48.h>
#include <osmocom/core/talloc.h>
#include <osmocom/core/signal.h>
#include <osmocom/crypt/auth.h>

#include <osmocom/bb/common/osmocom_data.h>
#include <osmocom/bb/common/networks.h>
#include <osmocom/bb/common/gps.h>
#include <osmocom/bb/mobile/mncc.h>
#include <osmocom/bb/mobile/mncc_ms.h>
#include <osmocom/bb/mobile/transaction.h>
#include <osmocom/bb/mobile/vty.h>
#include <osmocom/bb/mobile/app_mobile.h>
#include <osmocom/bb/mobile/gsm480_ss.h>
#include <osmocom/bb/mobile/gsm411_sms.h>
#include <osmocom/vty/telnet_interface.h>
#include <osmocom/vty/misc.h>

extern struct llist_head ms_list;
extern struct llist_head active_connections;

struct cmd_node ms_node = {
	MS_NODE,
	"%s(ms)# ",
	1
};

struct cmd_node testsim_node = {
	TESTSIM_NODE,
	"%s(test-sim)# ",
	1
};

struct cmd_node support_node = {
	SUPPORT_NODE,
	"%s(support)# ",
	1
};

struct cmd_node audio_node = {
	AUDIO_NODE,
	"%s(audio)# ",
	1
};

static void print_vty(void *priv, const char *fmt, ...)
{
	char buffer[1000];
	struct vty *vty = priv;
	va_list args;

	va_start(args, fmt);
	vsnprintf(buffer, sizeof(buffer) - 1, fmt, args);
	buffer[sizeof(buffer) - 1] = '\0';
	va_end(args);

	if (buffer[0]) {
		if (buffer[strlen(buffer) - 1] == '\n') {
			buffer[strlen(buffer) - 1] = '\0';
			vty_out(vty, "%s%s", buffer, VTY_NEWLINE);
		} else
			vty_out(vty, "%s", buffer);
	}
}

int vty_check_number(struct vty *vty, const char *number)
{
	int i;

	for (i = 0; i < strlen(number); i++) {
		/* allow international notation with + */
		if (i == 0 && number[i] == '+')
			continue;
		if (!(number[i] >= '0' && number[i] <= '9')
		 && number[i] != '*'
		 && number[i] != '#'
		 && !(number[i] >= 'a' && number[i] <= 'c')) {
			vty_out(vty, "Invalid digit '%c' of number!%s",
				number[i], VTY_NEWLINE);
			return -EINVAL;
		}
	}
	if (number[0] == '\0' || (number[0] == '+' && number[1] == '\0')) {
		vty_out(vty, "Given number has no digits!%s", VTY_NEWLINE);
		return -EINVAL;
	}

	return 0;
}

int vty_reading = 0;
static int hide_default = 0;

static void vty_restart(struct vty *vty, struct osmocom_ms *ms)
{
	if (vty_reading)
		return;
	if (ms->shutdown != MS_SHUTDOWN_NONE)
		return;
	vty_out(vty, "You must restart MS '%s' ('shutdown / no shutdown') for "
		"change to take effect!%s", ms->name, VTY_NEWLINE);
}

static void vty_restart_if_started(struct vty *vty, struct osmocom_ms *ms)
{
	if (!ms->started)
		return;
	vty_restart(vty, ms);
}

static struct osmocom_ms *get_ms(const char *name, struct vty *vty)
{
	struct osmocom_ms *ms;

	llist_for_each_entry(ms, &ms_list, entity) {
		if (!strcmp(ms->name, name)) {
			if (ms->shutdown != MS_SHUTDOWN_NONE) {
				vty_out(vty, "MS '%s' is admin down.%s", name,
					VTY_NEWLINE);
				return NULL;
			}
			return ms;
		}
	}
	vty_out(vty, "MS name '%s' does not exits.%s", name, VTY_NEWLINE);

	return NULL;
}

static void gsm_ms_dump(struct osmocom_ms *ms, struct vty *vty)
{
	struct gsm_settings *set = &ms->settings;
	struct gsm_trans *trans;
	char *service = "";

	if (!ms->started)
		service = ", radio is not started";
	else if (ms->mmlayer.state == GSM48_MM_ST_MM_IDLE) {
		/* current MM idle state */
		switch (ms->mmlayer.substate) {
		case GSM48_MM_SST_NORMAL_SERVICE:
		case GSM48_MM_SST_PLMN_SEARCH_NORMAL:
			service = ", service is normal";
			break;
		case GSM48_MM_SST_LOC_UPD_NEEDED:
		case GSM48_MM_SST_ATTEMPT_UPDATE:
			service = ", service is limited (pending)";
			break;
		case GSM48_MM_SST_NO_CELL_AVAIL:
			service = ", service is unavailable";
			break;
		default:
			if (ms->subscr.sim_valid)
				service = ", service is limited";
			else
				service = ", service is limited "
					"(IMSI detached)";
			break;
		}
	} else
		service = ", MM connection active";

	vty_out(vty, "MS '%s' is %s%s%s%s", ms->name,
		(ms->shutdown != MS_SHUTDOWN_NONE) ? "administratively " : "",
		(ms->shutdown != MS_SHUTDOWN_NONE || !ms->started) ? "down" : "up",
		(ms->shutdown == MS_SHUTDOWN_NONE) ? service : "",
		VTY_NEWLINE);
	vty_out(vty, "  IMEI: %s%s", set->imei, VTY_NEWLINE);
	vty_out(vty, "     IMEISV: %s%s", set->imeisv, VTY_NEWLINE);
	if (set->imei_random)
		vty_out(vty, "     IMEI generation: random (%d trailing "
			"digits)%s", set->imei_random, VTY_NEWLINE);
	else
		vty_out(vty, "     IMEI generation: fixed%s", VTY_NEWLINE);

	if (ms->shutdown != MS_SHUTDOWN_NONE)
		return;

	if (set->plmn_mode == PLMN_MODE_AUTO)
		vty_out(vty, "  automatic network selection state: %s%s",
			get_a_state_name(ms->plmn.state), VTY_NEWLINE);
	else
		vty_out(vty, "  manual network selection state   : %s%s",
			get_m_state_name(ms->plmn.state), VTY_NEWLINE);
	if (ms->plmn.mcc)
		vty_out(vty, "                                     MCC=%s "
			"MNC=%s (%s, %s)%s", gsm_print_mcc(ms->plmn.mcc),
			gsm_print_mnc(ms->plmn.mnc), gsm_get_mcc(ms->plmn.mcc),
			gsm_get_mnc(ms->plmn.mcc, ms->plmn.mnc), VTY_NEWLINE);
	vty_out(vty, "  cell selection state: %s%s",
		get_cs_state_name(ms->cellsel.state), VTY_NEWLINE);
	if (ms->cellsel.sel_mcc) {
		vty_out(vty, "                        ARFCN=%s MCC=%s MNC=%s "
			"LAC=0x%04x CELLID=0x%04x%s",
			gsm_print_arfcn(ms->cellsel.sel_arfcn),
			gsm_print_mcc(ms->cellsel.sel_mcc),
			gsm_print_mnc(ms->cellsel.sel_mnc),
			ms->cellsel.sel_lac, ms->cellsel.sel_id, VTY_NEWLINE);
		vty_out(vty, "                        (%s, %s)%s",
			gsm_get_mcc(ms->cellsel.sel_mcc),
			gsm_get_mnc(ms->cellsel.sel_mcc, ms->cellsel.sel_mnc),
			VTY_NEWLINE);
	}
	vty_out(vty, "  radio resource layer state: %s%s",
		gsm48_rr_state_names[ms->rrlayer.state], VTY_NEWLINE);
	vty_out(vty, "  mobility management layer state: %s",
		gsm48_mm_state_names[ms->mmlayer.state]);
	if (ms->mmlayer.state == GSM48_MM_ST_MM_IDLE)
		vty_out(vty, ", %s",
			gsm48_mm_substate_names[ms->mmlayer.substate]);
	vty_out(vty, "%s", VTY_NEWLINE);
	llist_for_each_entry(trans, &ms->trans_list, entry) {
		vty_out(vty, "  call control state: %s%s",
			gsm48_cc_state_name(trans->cc.state), VTY_NEWLINE);
	}
}


DEFUN(show_ms, show_ms_cmd, "show ms [MS_NAME]",
	SHOW_STR "Display available MS entities\n")
{
	struct osmocom_ms *ms;

	if (argc) {
		llist_for_each_entry(ms, &ms_list, entity) {
			if (!strcmp(ms->name, argv[0])) {
				gsm_ms_dump(ms, vty);
				return CMD_SUCCESS;
			}
		}
		vty_out(vty, "MS name '%s' does not exits.%s", argv[0],
		VTY_NEWLINE);
		return CMD_WARNING;
	} else {
		llist_for_each_entry(ms, &ms_list, entity) {
			gsm_ms_dump(ms, vty);
			vty_out(vty, "%s", VTY_NEWLINE);
		}
	}

	return CMD_SUCCESS;
}

DEFUN(show_support, show_support_cmd, "show support [MS_NAME]",
	SHOW_STR "Display information about MS support\n"
	"Name of MS (see \"show ms\")")
{
	struct osmocom_ms *ms;

	if (argc) {
		ms = get_ms(argv[0], vty);
		if (!ms)
			return CMD_WARNING;
		gsm_support_dump(ms, print_vty, vty);
	} else {
		llist_for_each_entry(ms, &ms_list, entity) {
			gsm_support_dump(ms, print_vty, vty);
			vty_out(vty, "%s", VTY_NEWLINE);
		}
	}

	return CMD_SUCCESS;
}

DEFUN(show_subscr, show_subscr_cmd, "show subscriber [MS_NAME]",
	SHOW_STR "Display information about subscriber\n"
	"Name of MS (see \"show ms\")")
{
	struct osmocom_ms *ms;

	if (argc) {
		ms = get_ms(argv[0], vty);
		if (!ms)
			return CMD_WARNING;
		gsm_subscr_dump(&ms->subscr, print_vty, vty);
	} else {
		llist_for_each_entry(ms, &ms_list, entity) {
			if (ms->shutdown == MS_SHUTDOWN_NONE) {
				gsm_subscr_dump(&ms->subscr, print_vty, vty);
				vty_out(vty, "%s", VTY_NEWLINE);
			}
		}
	}

	return CMD_SUCCESS;
}

DEFUN(show_cell, show_cell_cmd, "show cell MS_NAME",
	SHOW_STR "Display information about received cells\n"
	"Name of MS (see \"show ms\")")
{
	struct osmocom_ms *ms;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	gsm322_dump_cs_list(&ms->cellsel, GSM322_CS_FLAG_SUPPORT, print_vty,
		vty);

	return CMD_SUCCESS;
}

DEFUN(show_cell_si, show_cell_si_cmd, "show cell MS_NAME <0-1023> [pcs]",
	SHOW_STR "Display information about received cell\n"
	"Name of MS (see \"show ms\")\nRadio frequency number\n"
	"Given frequency is PCS band (1900) rather than DCS band.")
{
	struct osmocom_ms *ms;
	struct gsm48_sysinfo *s;
	uint16_t arfcn = atoi(argv[1]);

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	if (argc > 2) {
		if (arfcn < 512 || arfcn > 810) {
			vty_out(vty, "Given ARFCN not in PCS band%s",
				VTY_NEWLINE);
			return CMD_WARNING;
		}
		arfcn |= ARFCN_PCS;
	}

	s = ms->cellsel.list[arfcn2index(arfcn)].sysinfo;
	if (!s) {
		vty_out(vty, "Given ARFCN '%s' has no sysinfo available%s",
			argv[1], VTY_NEWLINE);
		return CMD_SUCCESS;
	}

	gsm48_sysinfo_dump(s, arfcn, print_vty, vty, ms->settings.freq_map);

	return CMD_SUCCESS;
}

DEFUN(show_nbcells, show_nbcells_cmd, "show neighbour-cells MS_NAME",
	SHOW_STR "Display information about current neighbour cells\n"
	"Name of MS (see \"show ms\")")
{
	struct osmocom_ms *ms;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	gsm322_dump_nb_list(&ms->cellsel, print_vty, vty);

	return CMD_SUCCESS;
}

DEFUN(show_ba, show_ba_cmd, "show ba MS_NAME [MCC] [MNC]",
	SHOW_STR "Display information about band allocations\n"
	"Name of MS (see \"show ms\")\nMobile Country Code\n"
	"Mobile Network Code")
{
	struct osmocom_ms *ms;
	uint16_t mcc = 0, mnc = 0;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	if (argc >= 3) {
		mcc = gsm_input_mcc((char *)argv[1]);
		mnc = gsm_input_mnc((char *)argv[2]);
		if (mcc == GSM_INPUT_INVALID) {
			vty_out(vty, "Given MCC invalid%s", VTY_NEWLINE);
			return CMD_WARNING;
		}
		if (mnc == GSM_INPUT_INVALID) {
			vty_out(vty, "Given MNC invalid%s", VTY_NEWLINE);
			return CMD_WARNING;
		}
	}

	gsm322_dump_ba_list(&ms->cellsel, mcc, mnc, print_vty, vty);

	return CMD_SUCCESS;
}

DEFUN(show_forb_plmn, show_forb_plmn_cmd, "show forbidden plmn MS_NAME",
	SHOW_STR "Display information about forbidden cells / networks\n"
	"Display forbidden PLMNs\nName of MS (see \"show ms\")")
{
	struct osmocom_ms *ms;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	gsm_subscr_dump_forbidden_plmn(ms, print_vty, vty);

	return CMD_SUCCESS;
}

DEFUN(show_forb_la, show_forb_la_cmd, "show forbidden location-area MS_NAME",
	SHOW_STR "Display information about forbidden cells / networks\n"
	"Display forbidden location areas\nName of MS (see \"show ms\")")
{
	struct osmocom_ms *ms;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	gsm322_dump_forbidden_la(ms, print_vty, vty);

	return CMD_SUCCESS;
}

DEFUN(monitor_network, monitor_network_cmd, "monitor network MS_NAME",
	"Monitor...\nMonitor network information\nName of MS (see \"show ms\")")
{
	struct osmocom_ms *ms;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	gsm48_rr_start_monitor(ms);

	return CMD_SUCCESS;
}

DEFUN(no_monitor_network, no_monitor_network_cmd, "no monitor network MS_NAME",
	NO_STR "Monitor...\nDeactivate monitor of network information\n"
	"Name of MS (see \"show ms\")")
{
	struct osmocom_ms *ms;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	gsm48_rr_stop_monitor(ms);

	return CMD_SUCCESS;
}

static int _sim_test_cmd(struct vty *vty, int argc, const char *argv[],
	int attached)
{
	struct osmocom_ms *ms;
	struct gsm_settings *set;

	/* Initial testcard settings */
	uint16_t mcc = 0x001, mnc = 0x01f, lac = 0x0000;
	uint32_t tmsi = 0xffffffff;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	if (ms->subscr.sim_valid) {
		vty_out(vty, "SIM already attached, remove first!%s",
			VTY_NEWLINE);
		return CMD_WARNING;
	}

	set = &ms->settings;
	if (set->test_rplmn_valid) {
		mcc = set->test_rplmn_mcc;
		mnc = set->test_rplmn_mnc;

		if (set->test_lac > 0x0000 && set->test_lac < 0xfffe)
			lac = set->test_lac;

		if (set->test_tmsi != 0xffffffff)
			tmsi = set->test_tmsi;
	}

	if (argc == 2) {
		vty_out(vty, "Give MNC together with MCC%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	if (argc >= 3) {
		mcc = gsm_input_mcc((char *)argv[1]);
		mnc = gsm_input_mnc((char *)argv[2]);
		if (mcc == GSM_INPUT_INVALID) {
			vty_out(vty, "Given MCC invalid%s", VTY_NEWLINE);
			return CMD_WARNING;
		}
		if (mnc == GSM_INPUT_INVALID) {
			vty_out(vty, "Given MNC invalid%s", VTY_NEWLINE);
			return CMD_WARNING;
		}
	}

	if (argc >= 4)
		lac = strtoul(argv[3], NULL, 16);

	if (argc >= 5)
		tmsi = strtoul(argv[4], NULL, 16);

	gsm_subscr_testcard(ms, mcc, mnc, lac, tmsi, attached);

	return CMD_SUCCESS;
}

DEFUN(sim_test, sim_test_cmd,
	"sim testcard MS_NAME [MCC] [MNC] [LAC] [TMSI]",
	"SIM actions\nAttach built in test SIM\nName of MS (see \"show ms\")\n"
	"Optionally set mobile Country Code of RPLMN\n"
	"Optionally set mobile Network Code of RPLMN\n"
	"Optionally set location area code of RPLMN\n"
	"Optionally set current assigned TMSI")
{
	return _sim_test_cmd(vty, argc, argv, 0);
}

DEFUN(sim_test_att, sim_test_att_cmd,
	"sim testcard MS_NAME MCC MNC LAC TMSI attached",
	"SIM actions\nAttach built in test SIM\nName of MS (see \"show ms\")\n"
	"Set mobile Country Code of RPLMN\nSet mobile Network Code of RPLMN\n"
	"Set location area code\nSet current assigned TMSI\n"
	"Indicate to MM that card is already attached")
{
	return _sim_test_cmd(vty, argc, argv, 1);
}

DEFUN(sim_sap, sim_sap_cmd, "sim sap MS_NAME",
	"SIM actions\nAttach SIM over SAP interface\n"
	"Name of MS (see \"show ms\")\n")
{
	struct osmocom_ms *ms;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	if (ms->subscr.sim_valid) {
		vty_out(vty, "SIM already attached, remove first!%s",
			VTY_NEWLINE);
		return CMD_WARNING;
	}

	if (gsm_subscr_sapcard(ms) != 0) {
		return CMD_WARNING;
	}

	return CMD_SUCCESS;
}

DEFUN(sim_reader, sim_reader_cmd, "sim reader MS_NAME",
	"SIM actions\nAttach SIM from reader\nName of MS (see \"show ms\")")
{
	struct osmocom_ms *ms;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	if (ms->subscr.sim_valid) {
		vty_out(vty, "SIM already attached, remove first!%s",
			VTY_NEWLINE);
		return CMD_WARNING;
	}

	gsm_subscr_simcard(ms);

	return CMD_SUCCESS;
}

DEFUN(sim_remove, sim_remove_cmd, "sim remove MS_NAME",
	"SIM actions\nDetach SIM card\nName of MS (see \"show ms\")")
{
	struct osmocom_ms *ms;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	if (!ms->subscr.sim_valid) {
		vty_out(vty, "No SIM attached!%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	if (ms->subscr.sim_type == GSM_SIM_TYPE_SAP) {
		gsm_subscr_remove_sapcard(ms);
	}

	gsm_subscr_remove(ms);
	return CMD_SUCCESS;
}

DEFUN(sim_pin, sim_pin_cmd, "sim pin MS_NAME PIN",
	"SIM actions\nEnter PIN for SIM card\nName of MS (see \"show ms\")\n"
	"PIN number")
{
	struct osmocom_ms *ms;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	if (strlen(argv[1]) < 4 || strlen(argv[1]) > 8) {
		vty_out(vty, "PIN must be in range 4..8!%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	if (!ms->subscr.sim_pin_required) {
		vty_out(vty, "No PIN is required at this time!%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	gsm_subscr_sim_pin(ms, (char *)argv[1], "", 0);

	return CMD_SUCCESS;
}

DEFUN(sim_disable_pin, sim_disable_pin_cmd, "sim disable-pin MS_NAME PIN",
	"SIM actions\nDisable PIN of SIM card\nName of MS (see \"show ms\")\n"
	"PIN number")
{
	struct osmocom_ms *ms;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	if (strlen(argv[1]) < 4 || strlen(argv[1]) > 8) {
		vty_out(vty, "PIN must be in range 4..8!%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	gsm_subscr_sim_pin(ms, (char *)argv[1], "", -1);

	return CMD_SUCCESS;
}

DEFUN(sim_enable_pin, sim_enable_pin_cmd, "sim enable-pin MS_NAME PIN",
	"SIM actions\nEnable PIN of SIM card\nName of MS (see \"show ms\")\n"
	"PIN number")
{
	struct osmocom_ms *ms;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	if (strlen(argv[1]) < 4 || strlen(argv[1]) > 8) {
		vty_out(vty, "PIN must be in range 4..8!%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	gsm_subscr_sim_pin(ms, (char *)argv[1], "", 1);

	return CMD_SUCCESS;
}

DEFUN(sim_change_pin, sim_change_pin_cmd, "sim change-pin MS_NAME OLD NEW",
	"SIM actions\nChange PIN of SIM card\nName of MS (see \"show ms\")\n"
	"Old PIN number\nNew PIN number")
{
	struct osmocom_ms *ms;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	if (strlen(argv[1]) < 4 || strlen(argv[1]) > 8) {
		vty_out(vty, "Old PIN must be in range 4..8!%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	if (strlen(argv[2]) < 4 || strlen(argv[2]) > 8) {
		vty_out(vty, "New PIN must be in range 4..8!%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	gsm_subscr_sim_pin(ms, (char *)argv[1], (char *)argv[2], 2);

	return CMD_SUCCESS;
}

DEFUN(sim_unblock_pin, sim_unblock_pin_cmd, "sim unblock-pin MS_NAME PUC NEW",
	"SIM actions\nChange PIN of SIM card\nName of MS (see \"show ms\")\n"
	"Personal Unblock Key\nNew PIN number")
{
	struct osmocom_ms *ms;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	if (strlen(argv[1]) != 8) {
		vty_out(vty, "PUC must be 8 digits!%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	if (strlen(argv[2]) < 4 || strlen(argv[2]) > 8) {
		vty_out(vty, "PIN must be in range 4..8!%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	gsm_subscr_sim_pin(ms, (char *)argv[1], (char *)argv[2], 99);

	return CMD_SUCCESS;
}

DEFUN(sim_lai, sim_lai_cmd, "sim lai MS_NAME MCC MNC LAC",
	"SIM actions\nChange LAI of SIM card\nName of MS (see \"show ms\")\n"
	"Mobile Country Code\nMobile Network Code\nLocation Area Code "
	" (use 0000 to remove LAI)")
{
	struct osmocom_ms *ms;
	uint16_t mcc = gsm_input_mcc((char *)argv[1]),
		 mnc = gsm_input_mnc((char *)argv[2]),
		 lac = strtoul(argv[3], NULL, 16);

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	if (mcc == GSM_INPUT_INVALID) {
		vty_out(vty, "Given MCC invalid%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	if (mnc == GSM_INPUT_INVALID) {
		vty_out(vty, "Given MNC invalid%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	ms->subscr.mcc = mcc;
	ms->subscr.mnc = mnc;
	ms->subscr.lac = lac;
	ms->subscr.tmsi = 0xffffffff;

	gsm_subscr_write_loci(ms);

	return CMD_SUCCESS;
}

DEFUN(network_select, network_select_cmd,
	"network select MS_NAME MCC MNC [force]",
	"Select ...\nSelect Network\nName of MS (see \"show ms\")\n"
	"Mobile Country Code\nMobile Network Code\n"
	"Force selecting a network that is not in the list")
{
	struct osmocom_ms *ms;
	struct gsm322_plmn *plmn;
	struct msgb *nmsg;
	struct gsm322_msg *ngm;
	struct gsm322_plmn_list *temp;
	uint16_t mcc = gsm_input_mcc((char *)argv[1]),
		 mnc = gsm_input_mnc((char *)argv[2]);
	int found = 0;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;
	plmn = &ms->plmn;

	if (ms->settings.plmn_mode != PLMN_MODE_MANUAL) {
		vty_out(vty, "Not in manual network selection mode%s",
			VTY_NEWLINE);
		return CMD_WARNING;
	}

	if (mcc == GSM_INPUT_INVALID) {
		vty_out(vty, "Given MCC invalid%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	if (mnc == GSM_INPUT_INVALID) {
		vty_out(vty, "Given MNC invalid%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	if (argc < 4) {
		llist_for_each_entry(temp, &plmn->sorted_plmn, entry)
			if (temp->mcc == mcc &&  temp->mnc == mnc)
				found = 1;
		if (!found) {
			vty_out(vty, "Network not in list!%s", VTY_NEWLINE);
			vty_out(vty, "To force selecting this network, use "
				"'force' keyword%s", VTY_NEWLINE);
			return CMD_WARNING;
		}
	}

	nmsg = gsm322_msgb_alloc(GSM322_EVENT_CHOOSE_PLMN);
	if (!nmsg)
		return CMD_WARNING;
	ngm = (struct gsm322_msg *) nmsg->data;
	ngm->mcc = mcc;
	ngm->mnc = mnc;
	gsm322_plmn_sendmsg(ms, nmsg);

	return CMD_SUCCESS;
}

DEFUN(call, call_cmd, "call MS_NAME (NUMBER|emergency|answer|hangup|hold)",
	"Make a call\nName of MS (see \"show ms\")\nPhone number to call "
	"(Use digits '0123456789*#abc', and '+' to dial international)\n"
	"Make an emergency call\nAnswer an incoming call\nHangup a call\n"
	"Hold current active call\n")
{
	struct osmocom_ms *ms;
	struct gsm_settings *set;
	struct gsm_settings_abbrev *abbrev;
	char *number;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;
	set = &ms->settings;

	if (set->ch_cap == GSM_CAP_SDCCH) {
		vty_out(vty, "Basic call is not supported for SDCCH only "
			"mobile%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	number = (char *)argv[1];
	if (!strcmp(number, "emergency"))
		mncc_call(ms, number);
	else if (!strcmp(number, "answer"))
		mncc_answer(ms);
	else if (!strcmp(number, "hangup"))
		mncc_hangup(ms);
	else if (!strcmp(number, "hold"))
		mncc_hold(ms);
	else {
		llist_for_each_entry(abbrev, &set->abbrev, list) {
			if (!strcmp(number, abbrev->abbrev)) {
				number = abbrev->number;
				vty_out(vty, "Dialing number '%s'%s", number,
					VTY_NEWLINE);
				break;
			}
		}
		if (vty_check_number(vty, number))
			return CMD_WARNING;
		mncc_call(ms, number);
	}

	return CMD_SUCCESS;
}

DEFUN(call_retr, call_retr_cmd, "call MS_NAME retrieve [NUMBER]",
	"Make a call\nName of MS (see \"show ms\")\n"
	"Retrieve call on hold\nNumber of call to retrieve")
{
	struct osmocom_ms *ms;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	mncc_retrieve(ms, (argc > 1) ? atoi(argv[1]) : 0);

	return CMD_SUCCESS;
}

DEFUN(call_dtmf, call_dtmf_cmd, "call MS_NAME dtmf DIGITS",
	"Make a call\nName of MS (see \"show ms\")\n"
	"One or more DTMF digits to transmit")
{
	struct osmocom_ms *ms;
	struct gsm_settings *set;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;
	set = &ms->settings;

	if (!set->cc_dtmf) {
		vty_out(vty, "DTMF not supported, please enable!%s",
			VTY_NEWLINE);
		return CMD_WARNING;
	}

	mncc_dtmf(ms, (char *)argv[1]);

	return CMD_SUCCESS;
}

DEFUN(sms, sms_cmd, "sms MS_NAME NUMBER .LINE",
	"Send an SMS\nName of MS (see \"show ms\")\nPhone number to send SMS "
	"(Use digits '0123456789*#abc', and '+' to dial international)\n"
	"SMS text\n")
{
	struct osmocom_ms *ms;
	struct gsm_settings *set;
	struct gsm_settings_abbrev *abbrev;
	char *number, *sms_sca = NULL;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;
	set = &ms->settings;

	if (!set->sms_ptp) {
		vty_out(vty, "SMS not supported by this mobile, please enable "
			"SMS support%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	if (ms->subscr.sms_sca[0])
		sms_sca = ms->subscr.sms_sca;
	else if (set->sms_sca[0])
		sms_sca = set->sms_sca;

	if (!sms_sca) {
		vty_out(vty, "SMS sms-service-center not defined on SIM card, "
			"please define one at settings.%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	number = (char *)argv[1];
	llist_for_each_entry(abbrev, &set->abbrev, list) {
		if (!strcmp(number, abbrev->abbrev)) {
			number = abbrev->number;
			vty_out(vty, "Using number '%s'%s", number,
				VTY_NEWLINE);
			break;
		}
	}
	if (vty_check_number(vty, number))
		return CMD_WARNING;

	sms_send(ms, sms_sca, number, argv_concat(argv, argc, 2), 42);

	return CMD_SUCCESS;
}

DEFUN(service, service_cmd, "service MS_NAME (*#06#|*#21#|*#67#|*#61#|*#62#"
	"|*#002#|*#004#|*xx*number#|*xx#|#xx#|##xx#|STRING|hangup)",
	"Send a Supplementary Service request\nName of MS (see \"show ms\")\n"
	"Query IMSI\n"
	"Query Call Forwarding Unconditional (CFU)\n"
	"Query Call Forwarding when Busy (CFB)\n"
	"Query Call Forwarding when No Response (CFNR)\n"
	"Query Call Forwarding when Not Reachable\n"
	"Query all Call Forwardings\n"
	"Query all conditional Call Forwardings\n"
	"Set and activate Call Forwarding (xx = Service Code, see above)\n"
	"Activate Call Forwarding (xx = Service Code, see above)\n"
	"Deactivate Call Forwarding (xx = Service Code, see above)\n"
	"Erase and deactivate Call Forwarding (xx = Service Code, see above)\n"
	"Service string "
	"(Example: '*100#' requests account balace on some networks.)\n"
	"Hangup existing service connection")
{
	struct osmocom_ms *ms;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	ss_send(ms, argv[1], 0);

	return CMD_SUCCESS;
}

#define TEST_CMD_DESC	"Special commands for testing\n"

DEFUN(test_reselection, test_reselection_cmd, "test re-selection NAME",
      TEST_CMD_DESC "Manually trigger cell re-selection\n"
      "Name of MS (see \"show ms\")")
{
	struct osmocom_ms *ms;
	struct gsm_settings *set;
	struct msgb *nmsg;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;
	set = &ms->settings;

	if (set->stick) {
		vty_out(vty, "Cannot trigger cell re-selection, because we "
			"stick to a cell!%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	nmsg = gsm322_msgb_alloc(GSM322_EVENT_CELL_RESEL);
	if (!nmsg)
		return CMD_WARNING;
	gsm322_c_event(ms, nmsg);


	return CMD_SUCCESS;
}

DEFUN(delete_forbidden_plmn, delete_forbidden_plmn_cmd,
	"delete forbidden plmn NAME MCC MNC",
	"Delete\nForbidden\nplmn\nName of MS (see \"show ms\")\n"
	"Mobile Country Code\nMobile Network Code")
{
	struct osmocom_ms *ms;
	uint16_t mcc = gsm_input_mcc((char *)argv[1]),
		 mnc = gsm_input_mnc((char *)argv[2]);

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	if (mcc == GSM_INPUT_INVALID) {
		vty_out(vty, "Given MCC invalid%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	if (mnc == GSM_INPUT_INVALID) {
		vty_out(vty, "Given MNC invalid%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	gsm_subscr_del_forbidden_plmn(&ms->subscr, mcc, mnc);

	return CMD_SUCCESS;
}

DEFUN(network_show, network_show_cmd, "network show MS_NAME",
	"Network ...\nShow results of network search (again)\n"
	"Name of MS (see \"show ms\")")
{
	struct osmocom_ms *ms;
	struct gsm_settings *set;
	struct gsm322_plmn *plmn;
	struct gsm322_plmn_list *temp;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;
	set = &ms->settings;
	plmn = &ms->plmn;

	if (set->plmn_mode != PLMN_MODE_AUTO
	 && plmn->state != GSM322_M3_NOT_ON_PLMN) {
		vty_out(vty, "Start network search first!%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	llist_for_each_entry(temp, &plmn->sorted_plmn, entry)
		vty_out(vty, " Network %s, %s (%s, %s)%s",
			gsm_print_mcc(temp->mcc), gsm_print_mnc(temp->mnc),
			gsm_get_mcc(temp->mcc),
			gsm_get_mnc(temp->mcc, temp->mnc), VTY_NEWLINE);

	return CMD_SUCCESS;
}

DEFUN(network_search, network_search_cmd, "network search MS_NAME",
	"Network ...\nTrigger network search\nName of MS (see \"show ms\")")
{
	struct osmocom_ms *ms;
	struct msgb *nmsg;

	ms = get_ms(argv[0], vty);
	if (!ms)
		return CMD_WARNING;

	nmsg = gsm322_msgb_alloc(GSM322_EVENT_USER_RESEL);
	if (!nmsg)
		return CMD_WARNING;
	gsm322_plmn_sendmsg(ms, nmsg);

	return CMD_SUCCESS;
}

DEFUN(cfg_gps_enable, cfg_gps_enable_cmd, "gps enable",
	"GPS receiver")
{
	if (osmo_gps_open()) {
		g.enable = 1;
		vty_out(vty, "Failed to open GPS device!%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	g.enable = 1;

	return CMD_SUCCESS;
}

DEFUN(cfg_no_gps_enable, cfg_no_gps_enable_cmd, "no gps enable",
	NO_STR "Disable GPS receiver")
{
	if (g.enable)
		osmo_gps_close();
	g.enable = 0;

	return CMD_SUCCESS;
}

#ifdef _HAVE_GPSD
DEFUN(cfg_gps_host, cfg_gps_host_cmd, "gps host HOST:PORT",
	"GPS receiver\nSelect gpsd host and port\n"
	"IP and port (optional) of the host running gpsd")
{
	char* colon = strstr(argv[0], ":");
	if (colon != NULL) {
		memcpy(g.gpsd_host, argv[0], colon - argv[0]);
		g.gpsd_host[colon - argv[0]] = '\0';
		memcpy(g.gpsd_port, colon+1, strlen(colon+1));
		g.gpsd_port[strlen(colon+1)] = '\0';
	} else {
		snprintf(g.gpsd_host, ARRAY_SIZE(g.gpsd_host), "%s", argv[0]);
		g.gpsd_host[ARRAY_SIZE(g.gpsd_host) - 1] = '\0';
		snprintf(g.gpsd_port, ARRAY_SIZE(g.gpsd_port), "2947");
		g.gpsd_port[ARRAY_SIZE(g.gpsd_port) - 1] = '\0';
	}
	g.gps_type = GPS_TYPE_GPSD;
	if (g.enable) {
		osmo_gps_close();
		if (osmo_gps_open()) {
			vty_out(vty, "Failed to connect to gpsd host!%s",
				VTY_NEWLINE);
			return CMD_WARNING;
		}
	}

	return CMD_SUCCESS;
}
#endif

DEFUN(cfg_gps_device, cfg_gps_device_cmd, "gps device DEVICE",
	"GPS receiver\nSelect serial device\n"
	"Full path of serial device including /dev/")
{
	osmo_strlcpy(g.device, argv[0], sizeof(g.device));
	g.device[sizeof(g.device) - 1] = '\0';
	g.gps_type = GPS_TYPE_SERIAL;
	if (g.enable) {
		osmo_gps_close();
		if (osmo_gps_open()) {
			vty_out(vty, "Failed to open GPS device!%s",
				VTY_NEWLINE);
			return CMD_WARNING;
		}
	}

	return CMD_SUCCESS;
}

DEFUN(cfg_gps_baud, cfg_gps_baud_cmd, "gps baudrate "
	"(default|4800|""9600|19200|38400|57600|115200)",
	"GPS receiver\nSelect baud rate\nDefault, don't modify\n\n\n\n\n\n")
{
	if (argv[0][0] == 'd')
		g.baud = 0;
	else
		g.baud = atoi(argv[0]);
	if (g.enable) {
		osmo_gps_close();
		if (osmo_gps_open()) {
			g.enable = 0;
			vty_out(vty, "Failed to open GPS device!%s",
				VTY_NEWLINE);
			return CMD_WARNING;
		}
	}

	return CMD_SUCCESS;
}

DEFUN(cfg_hide_default, cfg_hide_default_cmd, "hide-default",
	"Hide most default values in config to make it more compact")
{
	hide_default = 1;

	return CMD_SUCCESS;
}

DEFUN(cfg_no_hide_default, cfg_no_hide_default_cmd, "no hide-default",
	NO_STR "Show default values in config")
{
	hide_default = 0;

	return CMD_SUCCESS;
}

/* per MS config */
DEFUN(cfg_ms, cfg_ms_cmd, "ms MS_NAME",
	"Select a mobile station to configure\nName of MS (see \"show ms\")")
{
	struct osmocom_ms *ms;
	int found = 0;

	llist_for_each_entry(ms, &ms_list, entity) {
		if (!strcmp(ms->name, argv[0])) {
			found = 1;
			break;
		}
	}

	if (!found) {
		if (!vty_reading) {
			vty_out(vty, "MS name '%s' does not exits, try "
				"'ms %s create'%s", argv[0], argv[0],
				VTY_NEWLINE);
			return CMD_WARNING;
		}
		ms = mobile_new((char *)argv[0]);
		if (!ms) {
			vty_out(vty, "Failed to add MS name '%s'%s", argv[0],
				VTY_NEWLINE);
			return CMD_WARNING;
		}
	}

	vty->index = ms;
	vty->node = MS_NODE;

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_create, cfg_ms_create_cmd, "ms MS_NAME create",
	"Select a mobile station to configure\nName of MS (see \"show ms\")\n"
	"Create if MS does not exists")
{
	struct osmocom_ms *ms;
	int found = 0;

	llist_for_each_entry(ms, &ms_list, entity) {
		if (!strcmp(ms->name, argv[0])) {
			found = 1;
			break;
		}
	}

	if (!found) {
		ms = mobile_new((char *)argv[0]);
		if (!ms) {
			vty_out(vty, "Failed to add MS name '%s'%s", argv[0],
				VTY_NEWLINE);
			return CMD_WARNING;
		}
	}

	vty->index = ms;
	vty->node = MS_NODE;

	vty_out(vty, "MS '%s' created, after configuration, do 'no shutdown'%s",
		argv[0], VTY_NEWLINE);
	return CMD_SUCCESS;
}

DEFUN(cfg_ms_rename, cfg_ms_rename_cmd, "ms MS_NAME rename MS_NAME",
	"Select a mobile station to configure\nName of MS (see \"show ms\")\n"
	"Rename MS\nNew name of MS")
{
	struct osmocom_ms *ms;
	int found = 0;

	llist_for_each_entry(ms, &ms_list, entity) {
		if (!strcmp(ms->name, argv[0])) {
			found = 1;
			break;
		}
	}

	if (!found) {
		vty_out(vty, "MS name '%s' does not exist%s", argv[0],
			VTY_NEWLINE);
		return CMD_WARNING;
	}

	osmo_talloc_replace_string(ms, &ms->name, argv[1]);

	return CMD_SUCCESS;
}

DEFUN(cfg_no_ms, cfg_no_ms_cmd, "no ms MS_NAME",
	NO_STR "Select a mobile station to remove\n"
	"Name of MS (see \"show ms\")")
{
	struct osmocom_ms *ms;
	int found = 0;

	llist_for_each_entry(ms, &ms_list, entity) {
		if (!strcmp(ms->name, argv[0])) {
			found = 1;
			break;
		}
	}

	if (!found) {
		vty_out(vty, "MS name '%s' does not exist%s", argv[0],
			VTY_NEWLINE);
		return CMD_WARNING;
	}

	mobile_delete(ms, 1);

	return CMD_SUCCESS;
}

#define SUP_WRITE(item, cmd) \
	if (sup->item) \
		if (!hide_default || !set->item) \
			vty_out(vty, "  %s%s%s", (set->item) ? "" : "no ", \
			cmd, VTY_NEWLINE);

static void config_write_ms(struct vty *vty, struct osmocom_ms *ms)
{
	struct gsm_settings *set = &ms->settings;
	struct gsm_support *sup = &ms->support;
	struct gsm_settings_abbrev *abbrev;

	vty_out(vty, "ms %s%s", ms->name, VTY_NEWLINE);
	vty_out(vty, " layer2-socket %s%s", set->layer2_socket_path,
		VTY_NEWLINE);
	vty_out(vty, " sap-socket %s%s", set->sap_socket_path, VTY_NEWLINE);
	vty_out(vty, " mncc-socket %s%s", set->mncc_socket_path, VTY_NEWLINE);
	switch (set->mncc_handler) {
	case MNCC_HANDLER_INTERNAL:
		vty_out(vty, " mncc-handler internal%s", VTY_NEWLINE);
		break;
	case MNCC_HANDLER_EXTERNAL:
		vty_out(vty, " mncc-handler external%s", VTY_NEWLINE);
		break;
	case MNCC_HANDLER_DUMMY:
		vty_out(vty, " mncc-handler dummy%s", VTY_NEWLINE);
	}
	switch(set->sim_type) {
		case GSM_SIM_TYPE_NONE:
		vty_out(vty, " sim none%s", VTY_NEWLINE);
		break;
		case GSM_SIM_TYPE_L1PHY:
		vty_out(vty, " sim reader%s", VTY_NEWLINE);
		break;
		case GSM_SIM_TYPE_TEST:
		vty_out(vty, " sim test%s", VTY_NEWLINE);
		break;
		case GSM_SIM_TYPE_SAP:
		vty_out(vty, " sim sap%s", VTY_NEWLINE);
		break;
	}
	vty_out(vty, " network-selection-mode %s%s", (set->plmn_mode
			== PLMN_MODE_AUTO) ? "auto" : "manual", VTY_NEWLINE);
	vty_out(vty, " imei %s %s%s", set->imei,
		set->imeisv + strlen(set->imei), VTY_NEWLINE);
	if (set->imei_random)
		vty_out(vty, " imei-random %d%s", set->imei_random,
			VTY_NEWLINE);
	else
		if (!hide_default)
			vty_out(vty, " imei-fixed%s", VTY_NEWLINE);
	if (set->emergency_imsi[0])
		vty_out(vty, " emergency-imsi %s%s", set->emergency_imsi,
			VTY_NEWLINE);
	else
		if (!hide_default)
			vty_out(vty, " no emergency-imsi%s", VTY_NEWLINE);
	if (set->sms_sca[0])
		vty_out(vty, " sms-service-center %s%s", set->sms_sca,
			VTY_NEWLINE);
	else
		if (!hide_default)
			vty_out(vty, " no sms-service-center%s", VTY_NEWLINE);
	if (!hide_default || set->cw)
		vty_out(vty, " %scall-waiting%s", (set->cw) ? "" : "no ",
			VTY_NEWLINE);
	if (!hide_default || set->auto_answer)
		vty_out(vty, " %sauto-answer%s",
			(set->auto_answer) ? "" : "no ", VTY_NEWLINE);
	if (!hide_default || set->force_rekey)
		vty_out(vty, " %sforce-rekey%s",
			(set->force_rekey) ? "" : "no ", VTY_NEWLINE);
	if (!hide_default || set->clip)
		vty_out(vty, " %sclip%s", (set->clip) ? "" : "no ",
			VTY_NEWLINE);
	if (!hide_default || set->clir)
		vty_out(vty, " %sclir%s", (set->clir) ? "" : "no ",
			VTY_NEWLINE);
	if (set->alter_tx_power)
		if (set->alter_tx_power_value)
			vty_out(vty, " tx-power %d%s",
				set->alter_tx_power_value, VTY_NEWLINE);
		else
			vty_out(vty, " tx-power full%s", VTY_NEWLINE);
	else
		if (!hide_default)
			vty_out(vty, " tx-power auto%s", VTY_NEWLINE);
	if (set->alter_delay)
		vty_out(vty, " simulated-delay %d%s", set->alter_delay,
			VTY_NEWLINE);
	else
		if (!hide_default)
			vty_out(vty, " no simulated-delay%s", VTY_NEWLINE);
	if (set->stick)
		vty_out(vty, " stick %d%s%s", set->stick_arfcn & 1023,
			(set->stick_arfcn & ARFCN_PCS) ? " pcs" : "",
			VTY_NEWLINE);
	else
		if (!hide_default)
			vty_out(vty, " no stick%s", VTY_NEWLINE);
	if (!hide_default || set->no_lupd)
		vty_out(vty, " %slocation-updating%s",
			(set->no_lupd) ? "no " : "", VTY_NEWLINE);
	if (!hide_default || set->no_neighbour)
		vty_out(vty, " %sneighbour-measurement%s",
			(set->no_neighbour) ? "no " : "", VTY_NEWLINE);
	if (set->full_v1 || set->full_v2 || set->full_v3) {
		/* mandatory anyway */
		vty_out(vty, " codec full-speed%s%s",
			(!set->half_prefer) ? " prefer" : "",
			VTY_NEWLINE);
	}
	if (set->half_v1 || set->half_v3) {
		if (set->half)
			vty_out(vty, " codec half-speed%s%s",
				(set->half_prefer) ? " prefer" : "",
				VTY_NEWLINE);
		else
			vty_out(vty, " no codec half-speed%s", VTY_NEWLINE);
	}
	if (llist_empty(&set->abbrev)) {
		if (!hide_default)
			vty_out(vty, " no abbrev%s", VTY_NEWLINE);
	} else {
		llist_for_each_entry(abbrev, &set->abbrev, list)
			vty_out(vty, " abbrev %s %s%s%s%s", abbrev->abbrev,
				abbrev->number, (abbrev->name[0]) ? " " : "",
				abbrev->name, VTY_NEWLINE);
	}
	vty_out(vty, " support%s", VTY_NEWLINE);
	SUP_WRITE(sms_ptp, "sms");
	SUP_WRITE(a5_1, "a5/1");
	SUP_WRITE(a5_2, "a5/2");
	SUP_WRITE(a5_3, "a5/3");
	SUP_WRITE(a5_4, "a5/4");
	SUP_WRITE(a5_5, "a5/5");
	SUP_WRITE(a5_6, "a5/6");
	SUP_WRITE(a5_7, "a5/7");
	SUP_WRITE(p_gsm, "p-gsm");
	SUP_WRITE(e_gsm, "e-gsm");
	SUP_WRITE(r_gsm, "r-gsm");
	SUP_WRITE(pcs, "gsm-850");
	SUP_WRITE(gsm_480, "gsm-480");
	SUP_WRITE(gsm_450, "gsm-450");
	SUP_WRITE(dcs, "dcs");
	SUP_WRITE(pcs, "pcs");
	if (sup->r_gsm || sup->e_gsm || sup->p_gsm)
		if (!hide_default || sup->class_900 != set->class_900)
			vty_out(vty, "  class-900 %d%s", set->class_900,
				VTY_NEWLINE);
	if (sup->gsm_850)
		if (!hide_default || sup->class_850 != set->class_850)
			vty_out(vty, "  class-850 %d%s", set->class_850,
				VTY_NEWLINE);
	if (sup->gsm_480 || sup->gsm_450)
		if (!hide_default || sup->class_400 != set->class_400)
			vty_out(vty, "  class-400 %d%s", set->class_400,
				VTY_NEWLINE);
	if (sup->dcs)
		if (!hide_default || sup->class_dcs != set->class_dcs)
			vty_out(vty, "  class-dcs %d%s", set->class_dcs,
				VTY_NEWLINE);
	if (sup->pcs)
		if (!hide_default || sup->class_pcs != set->class_pcs)
			vty_out(vty, "  class-pcs %d%s", set->class_pcs,
				VTY_NEWLINE);
	if (!hide_default || sup->ch_cap != set->ch_cap) {
		switch (set->ch_cap) {
		case GSM_CAP_SDCCH:
			vty_out(vty, "  channel-capability sdcch%s",
				VTY_NEWLINE);
			break;
		case GSM_CAP_SDCCH_TCHF:
			vty_out(vty, "  channel-capability sdcch+tchf%s",
				VTY_NEWLINE);
			break;
		case GSM_CAP_SDCCH_TCHF_TCHH:
			vty_out(vty, "  channel-capability sdcch+tchf+tchh%s",
				VTY_NEWLINE);
			break;
		}
	}
	SUP_WRITE(full_v1, "full-speech-v1");
	SUP_WRITE(full_v2, "full-speech-v2");
	SUP_WRITE(full_v3, "full-speech-v3");
	SUP_WRITE(half_v1, "half-speech-v1");
	SUP_WRITE(half_v3, "half-speech-v3");
	if (!hide_default || sup->min_rxlev_dbm != set->min_rxlev_dbm)
		vty_out(vty, "  min-rxlev %d%s", set->min_rxlev_dbm,
			VTY_NEWLINE);
	if (!hide_default || sup->dsc_max != set->dsc_max)
		vty_out(vty, "  dsc-max %d%s", set->dsc_max, VTY_NEWLINE);
	if (!hide_default || set->skip_max_per_band)
		vty_out(vty, "  %sskip-max-per-band%s",
			(set->skip_max_per_band) ? "" : "no ", VTY_NEWLINE);
	vty_out(vty, " test-sim%s", VTY_NEWLINE);
	vty_out(vty, "  imsi %s%s", set->test_imsi, VTY_NEWLINE);
	switch (set->test_ki_type) {
	case OSMO_AUTH_ALG_XOR:
		vty_out(vty, "  ki xor %s%s",
			osmo_hexdump(set->test_ki, 12), VTY_NEWLINE);
		break;
	case OSMO_AUTH_ALG_COMP128v1:
		vty_out(vty, "  ki comp128 %s%s",
			osmo_hexdump(set->test_ki, 16), VTY_NEWLINE);
		break;
	}
	if (!hide_default || set->test_barr)
		vty_out(vty, "  %sbarred-access%s",
			(set->test_barr) ? "" : "no ", VTY_NEWLINE);
	if (set->test_rplmn_valid) {
		vty_out(vty, "  rplmn %s %s",
			gsm_print_mcc(set->test_rplmn_mcc),
			gsm_print_mnc(set->test_rplmn_mnc));
		if (set->test_lac > 0x0000 && set->test_lac < 0xfffe) {
			vty_out(vty, " 0x%04x", set->test_lac);
			if (set->test_tmsi != 0xffffffff) {
				vty_out(vty, " 0x%08x", set->test_tmsi);
				if (set->test_imsi_attached)
					vty_out(vty, " attached");
			}
		}
		vty_out(vty, "%s", VTY_NEWLINE);
	} else
		if (!hide_default)
			vty_out(vty, "  no rplmn%s", VTY_NEWLINE);
	if (!hide_default || set->test_always)
		vty_out(vty, "  hplmn-search %s%s",
			(set->test_always) ? "everywhere" : "foreign-country",
			VTY_NEWLINE);
	if (!hide_default || set->any_timeout != MOB_C7_DEFLT_ANY_TIMEOUT)
		vty_out(vty, " c7-any-timeout %d%s",
			set->any_timeout, VTY_NEWLINE);

	vty_out(vty, " audio%s", VTY_NEWLINE);
	vty_out(vty, "  io-handler %s%s",
		audio_io_handler_name(set->audio.io_handler), VTY_NEWLINE);
	if (set->audio.io_handler == AUDIO_IOH_GAPK) {
		vty_out(vty, "  io-tch-format %s%s",
			audio_io_format_name(set->audio.io_format), VTY_NEWLINE);
		vty_out(vty, "  alsa-output-dev %s%s",
			set->audio.alsa_output_dev, VTY_NEWLINE);
		vty_out(vty, "  alsa-input-dev %s%s",
			set->audio.alsa_input_dev, VTY_NEWLINE);
	}

	/* no shutdown must be written to config, because shutdown is default */
	vty_out(vty, " %sshutdown%s", (ms->shutdown != MS_SHUTDOWN_NONE) ? "" : "no ",
		VTY_NEWLINE);
	if (ms->lua_script)
		vty_out(vty, " lua-script %s%s", ms->lua_script, VTY_NEWLINE);
	vty_out(vty, "!%s", VTY_NEWLINE);
}

static int config_write(struct vty *vty)
{
	struct osmocom_ms *ms;

#ifdef _HAVE_GPSD
	vty_out(vty, "gps host %s:%s%s", g.gpsd_host, g.gpsd_port, VTY_NEWLINE);
#endif
	vty_out(vty, "gps device %s%s", g.device, VTY_NEWLINE);
	if (g.baud)
		vty_out(vty, "gps baudrate %d%s", g.baud, VTY_NEWLINE);
	else
		vty_out(vty, "gps baudrate default%s", VTY_NEWLINE);
	vty_out(vty, "%sgps enable%s", (g.enable) ? "" : "no ", VTY_NEWLINE);
	vty_out(vty, "!%s", VTY_NEWLINE);

	vty_out(vty, "%shide-default%s", (hide_default) ? "": "no ",
		VTY_NEWLINE);
	vty_out(vty, "!%s", VTY_NEWLINE);

	llist_for_each_entry(ms, &ms_list, entity)
		config_write_ms(vty, ms);

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_show_this, cfg_ms_show_this_cmd, "show this",
	SHOW_STR "Show config of this MS")
{
	struct osmocom_ms *ms = vty->index;

	config_write_ms(vty, ms);

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_layer2, cfg_ms_layer2_cmd, "layer2-socket PATH",
	"Define socket path to connect between layer 2 and layer 1\n"
	"Unix socket, default '/tmp/osmocom_l2'")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	OSMO_STRLCPY_ARRAY(set->layer2_socket_path, argv[0]);

	vty_restart(vty, ms);
	return CMD_SUCCESS;
}

DEFUN(cfg_ms_sap, cfg_ms_sap_cmd, "sap-socket PATH",
	"Define socket path to connect to SIM reader\n"
	"Unix socket, default '/tmp/osmocom_sap'")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	OSMO_STRLCPY_ARRAY(set->sap_socket_path, argv[0]);

	vty_restart(vty, ms);
	return CMD_SUCCESS;
}

DEFUN(cfg_ms_mncc_sock, cfg_ms_mncc_sock_cmd, "mncc-socket PATH",
	"Define socket path for MNCC interface\n"
	"UNIX socket path (default '/tmp/ms_mncc_' + MS_NAME)")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	OSMO_STRLCPY_ARRAY(set->mncc_socket_path, argv[0]);

	vty_restart(vty, ms);
	return CMD_SUCCESS;
}

DEFUN(cfg_ms_mncc_handler, cfg_ms_mncc_handler_cmd,
      "mncc-handler (internal|external|dummy)",
      "Set MNCC (Call Control) handler\n"
      "Built-in MNCC handler (default)\n"
      "External MNCC application via UNIX-socket (e.g. LCR)\n"
      "Dummy MNCC handler (no Call Control)\n")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	switch (argv[0][0]) {
	case 'i':
		if (set->ch_cap == GSM_CAP_SDCCH) { /* SDCCH only */
			vty_out(vty, "TCH support is disabled, "
				"check 'channel-capability' param%s", VTY_NEWLINE);
			return CMD_WARNING;
		}
		set->mncc_handler = MNCC_HANDLER_INTERNAL;
		break;
	case 'e':
		set->mncc_handler = MNCC_HANDLER_EXTERNAL;
		break;
	case 'd':
		set->mncc_handler = MNCC_HANDLER_DUMMY;
		break;
	default:
		/* Shall not happen */
		OSMO_ASSERT(0);
	}

	vty_restart_if_started(vty, ms);
	return CMD_SUCCESS;
}

DEFUN(cfg_ms_no_mncc_handler, cfg_ms_no_mncc_handler_cmd,
      "no mncc-handler", NO_STR "Disable Call Control")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->mncc_handler = MNCC_HANDLER_DUMMY;

	vty_restart_if_started(vty, ms);
	return CMD_SUCCESS;
}

DEFUN(cfg_ms_sim, cfg_ms_sim_cmd, "sim (none|reader|test|sap)",
	"Set SIM card to attach when powering on\nAttach no SIM\n"
	"Attach SIM from reader\nAttach build in test SIM\n"
	"Attach SIM over SAP interface")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	switch (argv[0][0]) {
	case 'n':
		set->sim_type = GSM_SIM_TYPE_NONE;
		break;
	case 'r':
		set->sim_type = GSM_SIM_TYPE_L1PHY;
		break;
	case 't':
		set->sim_type = GSM_SIM_TYPE_TEST;
		break;
	case 's':
		set->sim_type = GSM_SIM_TYPE_SAP;
		break;
	default:
		vty_out(vty, "unknown SIM type%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	vty_restart_if_started(vty, ms);

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_mode, cfg_ms_mode_cmd, "network-selection-mode (auto|manual)",
	"Set network selection mode\nAutomatic network selection\n"
	"Manual network selection")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;
	struct msgb *nmsg;

	if (!ms->started) {
		if (argv[0][0] == 'a')
			set->plmn_mode = PLMN_MODE_AUTO;
		else
			set->plmn_mode = PLMN_MODE_MANUAL;
	} else {
		if (argv[0][0] == 'a')
			nmsg = gsm322_msgb_alloc(GSM322_EVENT_SEL_AUTO);
		else
			nmsg = gsm322_msgb_alloc(GSM322_EVENT_SEL_MANUAL);
		if (!nmsg)
			return CMD_WARNING;
		gsm322_plmn_sendmsg(ms, nmsg);
	}

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_imei, cfg_ms_imei_cmd, "imei IMEI [SV]",
	"Set IMEI (enter without control digit)\n15 Digits IMEI\n"
	"Software version digit")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;
	char *error, *sv = "0";

	if (argc >= 2)
		sv = (char *)argv[1];

	error = gsm_check_imei(argv[0], sv);
	if (error) {
		vty_out(vty, "%s%s", error, VTY_NEWLINE);
		return CMD_WARNING;
	}

	strcpy(set->imei, argv[0]);
	strcpy(set->imeisv, argv[0]);
	strcpy(set->imeisv + 15, sv);

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_imei_fixed, cfg_ms_imei_fixed_cmd, "imei-fixed",
	"Use fixed IMEI on every power on")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->imei_random = 0;

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_imei_random, cfg_ms_imei_random_cmd, "imei-random <0-15>",
	"Use random IMEI on every power on\n"
	"Number of trailing digits to randomize")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->imei_random = atoi(argv[0]);

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_emerg_imsi, cfg_ms_emerg_imsi_cmd, "emergency-imsi IMSI",
	"Use special IMSI for emergency calls\n15 digits IMSI")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;
	char *error;

	error = gsm_check_imsi(argv[0]);
	if (error) {
		vty_out(vty, "%s%s", error, VTY_NEWLINE);
		return CMD_WARNING;
	}
	strcpy(set->emergency_imsi, argv[0]);

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_no_emerg_imsi, cfg_ms_no_emerg_imsi_cmd, "no emergency-imsi",
	NO_STR "Use IMSI of SIM or IMEI for emergency calls")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->emergency_imsi[0] = '\0';

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_sms_sca, cfg_ms_sms_sca_cmd, "sms-service-center NUMBER",
	"Use Service center address for outgoing SMS\nNumber of service center "
	"(Use digits '0123456789*#abc', and '+' to dial international)")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;
	const char *number = argv[0];

	if ((strlen(number) > 20 && number[0] != '+') || strlen(number) > 21) {
		vty_out(vty, "Number too long%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	if (vty_check_number(vty, number))
		return CMD_WARNING;

	strcpy(set->sms_sca, number);

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_no_sms_sca, cfg_ms_no_sms_sca_cmd, "no sms-service-center",
	NO_STR "Use Service center address for outgoing SMS")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->sms_sca[0] = '\0';

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_no_sms_store, cfg_ms_no_sms_store_cmd, "no sms-store",
	NO_STR "Store SMS in the home directory")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->store_sms = false;
	return CMD_SUCCESS;
}

DEFUN(cfg_ms_sms_store, cfg_ms_sms_store_cmd, "sms-store",
	"Store SMS in the home directory")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->store_sms = true;
	return CMD_SUCCESS;
}

DEFUN(cfg_no_cw, cfg_ms_no_cw_cmd, "no call-waiting",
	NO_STR "Disallow waiting calls")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->cw = 0;

	return CMD_SUCCESS;
}

DEFUN(cfg_cw, cfg_ms_cw_cmd, "call-waiting",
	"Allow waiting calls")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->cw = 1;

	return CMD_SUCCESS;
}

DEFUN(cfg_no_auto_answer, cfg_ms_no_auto_answer_cmd, "no auto-answer",
	NO_STR "Disable auto-answering calls")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->auto_answer = 0;

	return CMD_SUCCESS;
}

DEFUN(cfg_auto_answer, cfg_ms_auto_answer_cmd, "auto-answer",
	"Enable auto-answering calls")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->auto_answer = 1;

	return CMD_SUCCESS;
}

DEFUN(cfg_no_force_rekey, cfg_ms_no_force_rekey_cmd, "no force-rekey",
	NO_STR "Disable key renew forcing after every event")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->force_rekey = 0;

	return CMD_SUCCESS;
}

DEFUN(cfg_force_rekey, cfg_ms_force_rekey_cmd, "force-rekey",
	"Enable key renew forcing after every event")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->force_rekey = 1;

	return CMD_SUCCESS;
}

DEFUN(cfg_clip, cfg_ms_clip_cmd, "clip",
	"Force caller ID presentation")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->clip = 1;
	set->clir = 0;

	return CMD_SUCCESS;
}

DEFUN(cfg_clir, cfg_ms_clir_cmd, "clir",
	"Force caller ID restriction")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->clip = 0;
	set->clir = 1;

	return CMD_SUCCESS;
}

DEFUN(cfg_no_clip, cfg_ms_no_clip_cmd, "no clip",
	NO_STR "Disable forcing of caller ID presentation")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->clip = 0;

	return CMD_SUCCESS;
}

DEFUN(cfg_no_clir, cfg_ms_no_clir_cmd, "no clir",
	NO_STR "Disable forcing of caller ID restriction")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->clir = 0;

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_tx_power, cfg_ms_tx_power_cmd, "tx-power (auto|full)",
	"Set the way to choose transmit power\nControlled by BTS\n"
	"Always full power\nFixed GSM power value if supported")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	switch (argv[0][0]) {
	case 'a':
		set->alter_tx_power = 0;
		break;
	case 'f':
		set->alter_tx_power = 1;
		set->alter_tx_power_value = 0;
		break;
	}

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_tx_power_val, cfg_ms_tx_power_val_cmd, "tx-power <0-31>",
	"Set the way to choose transmit power\n"
	"Fixed GSM power value if supported")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->alter_tx_power = 1;
	set->alter_tx_power_value = atoi(argv[0]);

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_sim_delay, cfg_ms_sim_delay_cmd, "simulated-delay <-128-127>",
	"Simulate a lower or higher distance from the BTS\n"
	"Delay in half bits (distance in 553.85 meter steps)")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->alter_delay = atoi(argv[0]);
	gsm48_rr_alter_delay(ms);

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_no_sim_delay, cfg_ms_no_sim_delay_cmd, "no simulated-delay",
	NO_STR "Do not simulate a lower or higher distance from the BTS")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->alter_delay = 0;
	gsm48_rr_alter_delay(ms);

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_stick, cfg_ms_stick_cmd, "stick <0-1023> [pcs]",
	"Stick to the given cell\nARFCN of the cell to stick to\n"
	"Given frequency is PCS band (1900) rather than DCS band.")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;
	uint16_t arfcn = atoi(argv[0]);

	if (argc > 1) {
		if (arfcn < 512 || arfcn > 810) {
			vty_out(vty, "Given ARFCN not in PCS band%s",
				VTY_NEWLINE);
			return CMD_WARNING;
		}
		arfcn |= ARFCN_PCS;
	}
	set->stick = 1;
	set->stick_arfcn = arfcn;

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_no_stick, cfg_ms_no_stick_cmd, "no stick",
	NO_STR "Do not stick to any cell")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->stick = 0;

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_lupd, cfg_ms_lupd_cmd, "location-updating",
	"Allow location updating")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->no_lupd = 0;

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_no_lupd, cfg_ms_no_lupd_cmd, "no location-updating",
	NO_STR "Do not allow location updating")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->no_lupd = 1;

	return CMD_SUCCESS;
}

DEFUN(cfg_codec_full, cfg_ms_codec_full_cmd, "codec full-speed",
	"Enable codec\nFull speed speech codec")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	if (!set->full_v1 && !set->full_v2 && !set->full_v3) {
		vty_out(vty, "Full-rate codec not supported%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	return CMD_SUCCESS;
}

DEFUN(cfg_codec_full_pref, cfg_ms_codec_full_pref_cmd, "codec full-speed "
	"prefer",
	"Enable codec\nFull speed speech codec\nPrefer this codec")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	if (!set->full_v1 && !set->full_v2 && !set->full_v3) {
		vty_out(vty, "Full-rate codec not supported%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	set->half_prefer = 0;

	return CMD_SUCCESS;
}

DEFUN(cfg_codec_half, cfg_ms_codec_half_cmd, "codec half-speed",
	"Enable codec\nHalf speed speech codec")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	if (!set->half_v1 && !set->half_v3) {
		vty_out(vty, "Half-rate codec not supported%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	set->half = 1;

	return CMD_SUCCESS;
}

DEFUN(cfg_codec_half_pref, cfg_ms_codec_half_pref_cmd, "codec half-speed "
	"prefer",
	"Enable codec\nHalf speed speech codec\nPrefer this codec")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	if (!set->half_v1 && !set->half_v3) {
		vty_out(vty, "Half-rate codec not supported%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	set->half = 1;
	set->half_prefer = 1;

	return CMD_SUCCESS;
}

DEFUN(cfg_no_codec_half, cfg_ms_no_codec_half_cmd, "no codec half-speed",
	NO_STR "Disable codec\nHalf speed speech codec")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	if (!set->half_v1 && !set->half_v3) {
		vty_out(vty, "Half-rate codec not supported%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	set->half = 0;
	set->half_prefer = 0;

	return CMD_SUCCESS;
}

DEFUN(cfg_abbrev, cfg_ms_abbrev_cmd, "abbrev ABBREVIATION NUMBER [NAME]",
	"Store given abbreviation number\n1-3 digits abbreviation\n"
	"Number to store for the abbreviation "
	"(Use digits '0123456789*#abc', and '+' to dial international)\n"
	"Name of the abbreviation")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;
	struct gsm_settings_abbrev *abbrev;
	int i;

	llist_for_each_entry(abbrev, &set->abbrev, list) {
		if (!strcmp(argv[0], abbrev->abbrev)) {
			vty_out(vty, "Given abbreviation '%s' already stored, "
				"delete first!%s", argv[0], VTY_NEWLINE);
			return CMD_WARNING;
		}
	}

	if (strlen(argv[0]) >= sizeof(abbrev->abbrev)) {
		vty_out(vty, "Given abbreviation too long%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	for (i = 0; i < strlen(argv[0]); i++) {
		if (argv[0][i] < '0' || argv[0][i] > '9') {
			vty_out(vty, "Given abbreviation must have digits "
				"0..9 only!%s", VTY_NEWLINE);
			return CMD_WARNING;
		}
	}

	if (vty_check_number(vty, argv[1]))
		return CMD_WARNING;

	abbrev = talloc_zero(ms, struct gsm_settings_abbrev);
	if (!abbrev) {
		vty_out(vty, "No Memory!%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	llist_add_tail(&abbrev->list, &set->abbrev);
	OSMO_STRLCPY_ARRAY(abbrev->abbrev, argv[0]);
	OSMO_STRLCPY_ARRAY(abbrev->number, argv[1]);
	if (argc >= 3)
		OSMO_STRLCPY_ARRAY(abbrev->name, argv[2]);

	return CMD_SUCCESS;
}

DEFUN(cfg_no_abbrev, cfg_ms_no_abbrev_cmd, "no abbrev [ABBREVIATION]",
	NO_STR "Remove given abbreviation number or all numbers\n"
	"Abbreviation number to remove")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;
	struct gsm_settings_abbrev *abbrev, *abbrev2;
	uint8_t deleted = 0;

	llist_for_each_entry_safe(abbrev, abbrev2, &set->abbrev, list) {
		if (argc < 1 || !strcmp(argv[0], abbrev->abbrev)) {
			llist_del(&abbrev->list);
			deleted = 1;
		}
	}

	if (argc >= 1 && !deleted) {
		vty_out(vty, "Given abbreviation '%s' not found!%s",
			argv[0], VTY_NEWLINE);
		return CMD_WARNING;
	}

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_neighbour, cfg_ms_neighbour_cmd, "neighbour-measurement",
	"Allow neighbour cell measurement in idle mode")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->no_neighbour = 0;

	vty_restart_if_started(vty, ms);


	return CMD_SUCCESS;
}

DEFUN(cfg_ms_no_neighbour, cfg_ms_no_neighbour_cmd, "no neighbour-measurement",
	NO_STR "Do not allow neighbour cell measurement in idle mode")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->no_neighbour = 1;

	vty_restart_if_started(vty, ms);

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_any_timeout, cfg_ms_any_timeout_cmd, "c7-any-timeout <0-255>",
	"Seconds to wait in C7 before doing a PLMN search")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->any_timeout = atoi(argv[0]);

	vty_restart_if_started(vty, ms);

	return CMD_SUCCESS;
}

static int config_write_dummy(struct vty *vty)
{
	return CMD_SUCCESS;
}

/* per support config */
DEFUN(cfg_ms_support, cfg_ms_support_cmd, "support",
	"Define supported features")
{
	vty->node = SUPPORT_NODE;

	return CMD_SUCCESS;
}

#define SUP_EN(cfg, cfg_cmd, item, cmd, desc, restart) \
DEFUN(cfg, cfg_cmd, cmd, "Enable " desc "support") \
{ \
	struct osmocom_ms *ms = vty->index; \
	struct gsm_settings *set = &ms->settings; \
	struct gsm_support *sup = &ms->support; \
	if (!sup->item) { \
		vty_out(vty, desc " not supported%s", VTY_NEWLINE); \
		if (vty_reading) \
			return CMD_SUCCESS; \
		return CMD_WARNING; \
	} \
	if (restart) \
		vty_restart(vty, ms); \
	set->item = 1; \
	return CMD_SUCCESS; \
}

#define SUP_DI(cfg, cfg_cmd, item, cmd, desc, restart) \
DEFUN(cfg, cfg_cmd, "no " cmd, NO_STR "Disable " desc " support") \
{ \
	struct osmocom_ms *ms = vty->index; \
	struct gsm_settings *set = &ms->settings; \
	struct gsm_support *sup = &ms->support; \
	if (!sup->item) { \
		vty_out(vty, desc " not supported%s", VTY_NEWLINE); \
		if (vty_reading) \
			return CMD_SUCCESS; \
		return CMD_WARNING; \
	} \
	if (restart) \
		vty_restart(vty, ms); \
	set->item = 0; \
	return CMD_SUCCESS; \
}

#define SET_EN(cfg, cfg_cmd, item, cmd, desc, restart) \
DEFUN(cfg, cfg_cmd, cmd, "Enable " desc "support") \
{ \
	struct osmocom_ms *ms = vty->index; \
	struct gsm_settings *set = &ms->settings; \
	if (restart) \
		vty_restart(vty, ms); \
	set->item = 1; \
	return CMD_SUCCESS; \
}

#define SET_DI(cfg, cfg_cmd, item, cmd, desc, restart) \
DEFUN(cfg, cfg_cmd, "no " cmd, NO_STR "Disable " desc " support") \
{ \
	struct osmocom_ms *ms = vty->index; \
	struct gsm_settings *set = &ms->settings; \
	if (restart) \
		vty_restart(vty, ms); \
	set->item = 0; \
	return CMD_SUCCESS; \
}

SET_EN(cfg_ms_sup_dtmf, cfg_ms_sup_dtmf_cmd, cc_dtmf, "dtmf", "DTMF", 0);
SET_DI(cfg_ms_sup_no_dtmf, cfg_ms_sup_no_dtmf_cmd, cc_dtmf, "dtmf", "DTMF", 0);
SUP_EN(cfg_ms_sup_sms, cfg_ms_sup_sms_cmd, sms_ptp, "sms", "SMS", 0);
SUP_DI(cfg_ms_sup_no_sms, cfg_ms_sup_no_sms_cmd, sms_ptp, "sms", "SMS", 0);
SUP_EN(cfg_ms_sup_a5_1, cfg_ms_sup_a5_1_cmd, a5_1, "a5/1", "A5/1", 0);
SUP_DI(cfg_ms_sup_no_a5_1, cfg_ms_sup_no_a5_1_cmd, a5_1, "a5/1", "A5/1", 0);
SUP_EN(cfg_ms_sup_a5_2, cfg_ms_sup_a5_2_cmd, a5_2, "a5/2", "A5/2", 0);
SUP_DI(cfg_ms_sup_no_a5_2, cfg_ms_sup_no_a5_2_cmd, a5_2, "a5/2", "A5/2", 0);
SUP_EN(cfg_ms_sup_a5_3, cfg_ms_sup_a5_3_cmd, a5_3, "a5/3", "A5/3", 0);
SUP_DI(cfg_ms_sup_no_a5_3, cfg_ms_sup_no_a5_3_cmd, a5_3, "a5/3", "A5/3", 0);
SUP_EN(cfg_ms_sup_a5_4, cfg_ms_sup_a5_4_cmd, a5_4, "a5/4", "A5/4", 0);
SUP_DI(cfg_ms_sup_no_a5_4, cfg_ms_sup_no_a5_4_cmd, a5_4, "a5/4", "A5/4", 0);
SUP_EN(cfg_ms_sup_a5_5, cfg_ms_sup_a5_5_cmd, a5_5, "a5/5", "A5/5", 0);
SUP_DI(cfg_ms_sup_no_a5_5, cfg_ms_sup_no_a5_5_cmd, a5_5, "a5/5", "A5/5", 0);
SUP_EN(cfg_ms_sup_a5_6, cfg_ms_sup_a5_6_cmd, a5_6, "a5/6", "A5/6", 0);
SUP_DI(cfg_ms_sup_no_a5_6, cfg_ms_sup_no_a5_6_cmd, a5_6, "a5/6", "A5/6", 0);
SUP_EN(cfg_ms_sup_a5_7, cfg_ms_sup_a5_7_cmd, a5_7, "a5/7", "A5/7", 0);
SUP_DI(cfg_ms_sup_no_a5_7, cfg_ms_sup_no_a5_7_cmd, a5_7, "a5/7", "A5/7", 0);
SUP_EN(cfg_ms_sup_p_gsm, cfg_ms_sup_p_gsm_cmd, p_gsm, "p-gsm", "P-GSM (900)",
	1);
SUP_DI(cfg_ms_sup_no_p_gsm, cfg_ms_sup_no_p_gsm_cmd, p_gsm, "p-gsm",
	"P-GSM (900)", 1);
SUP_EN(cfg_ms_sup_e_gsm, cfg_ms_sup_e_gsm_cmd, e_gsm, "e-gsm", "E-GSM (850)",
	1);
SUP_DI(cfg_ms_sup_no_e_gsm, cfg_ms_sup_no_e_gsm_cmd, e_gsm, "e-gsm",
	"E-GSM (850)", 1);
SUP_EN(cfg_ms_sup_r_gsm, cfg_ms_sup_r_gsm_cmd, r_gsm, "r-gsm", "R-GSM (850)",
	1);
SUP_DI(cfg_ms_sup_no_r_gsm, cfg_ms_sup_no_r_gsm_cmd, r_gsm, "r-gsm",
	"R-GSM (850)", 1);
SUP_EN(cfg_ms_sup_dcs, cfg_ms_sup_dcs_cmd, dcs, "dcs", "DCS (1800)", 1);
SUP_DI(cfg_ms_sup_no_dcs, cfg_ms_sup_no_dcs_cmd, dcs, "dcs", "DCS (1800)", 1);
SUP_EN(cfg_ms_sup_gsm_850, cfg_ms_sup_gsm_850_cmd, gsm_850, "gsm-850",
	"GSM 850", 1);
SUP_DI(cfg_ms_sup_no_gsm_850, cfg_ms_sup_no_gsm_850_cmd, gsm_850, "gsm-850",
	"GSM 850", 1);
SUP_EN(cfg_ms_sup_pcs, cfg_ms_sup_pcs_cmd, pcs, "pcs", "PCS (1900)", 1);
SUP_DI(cfg_ms_sup_no_pcs, cfg_ms_sup_no_pcs_cmd, pcs, "pcs", "PCS (1900)", 1);
SUP_EN(cfg_ms_sup_gsm_480, cfg_ms_sup_gsm_480_cmd, gsm_480, "gsm-480",
	"GSM 480", 1);
SUP_DI(cfg_ms_sup_no_gsm_480, cfg_ms_sup_no_gsm_480_cmd, gsm_480, "gsm-480",
	"GSM 480", 1);
SUP_EN(cfg_ms_sup_gsm_450, cfg_ms_sup_gsm_450_cmd, gsm_450, "gsm-450",
	"GSM 450", 1);
SUP_DI(cfg_ms_sup_no_gsm_450, cfg_ms_sup_no_gsm_450_cmd, gsm_450, "gsm-450",
	"GSM 450", 1);

DEFUN(cfg_ms_sup_class_900, cfg_ms_sup_class_900_cmd, "class-900 (1|2|3|4|5)",
	"Select power class for GSM 900\n"
	"20 Watts\n"
	"8 Watts\n"
	"5 Watts\n"
	"2 Watts\n"
	"0.8 Watts")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;
	struct gsm_support *sup = &ms->support;

	set->class_900 = atoi(argv[0]);

	if (set->class_900 < sup->class_900 && !vty_reading)
		vty_out(vty, "Note: You selected a higher class than supported "
			" by hardware!%s", VTY_NEWLINE);

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_sup_class_850, cfg_ms_sup_class_850_cmd, "class-850 (1|2|3|4|5)",
	"Select power class for GSM 850\n"
	"20 Watts\n"
	"8 Watts\n"
	"5 Watts\n"
	"2 Watts\n"
	"0.8 Watts")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;
	struct gsm_support *sup = &ms->support;

	set->class_850 = atoi(argv[0]);

	if (set->class_850 < sup->class_850 && !vty_reading)
		vty_out(vty, "Note: You selected a higher class than supported "
			" by hardware!%s", VTY_NEWLINE);

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_sup_class_400, cfg_ms_sup_class_400_cmd, "class-400 (1|2|3|4|5)",
	"Select power class for GSM 400 (480 and 450)\n"
	"20 Watts\n"
	"8 Watts\n"
	"5 Watts\n"
	"2 Watts\n"
	"0.8 Watts")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;
	struct gsm_support *sup = &ms->support;

	set->class_400 = atoi(argv[0]);

	if (set->class_400 < sup->class_400 && !vty_reading)
		vty_out(vty, "Note: You selected a higher class than supported "
			" by hardware!%s", VTY_NEWLINE);

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_sup_class_dcs, cfg_ms_sup_class_dcs_cmd, "class-dcs (1|2|3)",
	"Select power class for DCS 1800\n"
	"1 Watt\n"
	"0.25 Watts\n"
	"4 Watts")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;
	struct gsm_support *sup = &ms->support;

	set->class_dcs = atoi(argv[0]);

	if (((set->class_dcs + 1) & 3) < ((sup->class_dcs + 1) & 3)
	 && !vty_reading)
		vty_out(vty, "Note: You selected a higher class than supported "
			" by hardware!%s", VTY_NEWLINE);

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_sup_class_pcs, cfg_ms_sup_class_pcs_cmd, "class-pcs (1|2|3)",
	"Select power class for PCS 1900\n"
	"1 Watt\n"
	"0.25 Watts\n"
	"2 Watts")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;
	struct gsm_support *sup = &ms->support;

	set->class_pcs = atoi(argv[0]);

	if (((set->class_pcs + 1) & 3) < ((sup->class_pcs + 1) & 3)
	 && !vty_reading)
		vty_out(vty, "Note: You selected a higher class than supported "
			" by hardware!%s", VTY_NEWLINE);

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_sup_ch_cap, cfg_ms_sup_ch_cap_cmd,
	"channel-capability (sdcch|sdcch+tchf|sdcch+tchf+tchh)",
	"Select channel capability\n"
	"SDCCH only\n"
	"SDCCH + TCH/F\n"
	"SDCCH + TCH/F + TCH/H")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;
	struct gsm_support *sup = &ms->support;
	uint8_t ch_cap;

	if (!strcmp(argv[0], "sdcch+tchf+tchh"))
		ch_cap = GSM_CAP_SDCCH_TCHF_TCHH;
	else if (!strcmp(argv[0], "sdcch+tchf"))
		ch_cap = GSM_CAP_SDCCH_TCHF;
	else
		ch_cap = GSM_CAP_SDCCH;

	if (ch_cap > sup->ch_cap && !vty_reading) {
		vty_out(vty, "You selected an higher capability than supported "
			" by hardware!%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	if (ms->started && ch_cap != set->ch_cap
	 && (ch_cap == GSM_CAP_SDCCH || set->ch_cap == GSM_CAP_SDCCH))
		vty_restart_if_started(vty, ms);

	set->ch_cap = ch_cap;

	return CMD_SUCCESS;
}

SUP_EN(cfg_ms_sup_full_v1, cfg_ms_sup_full_v1_cmd, full_v1, "full-speech-v1",
	"Full rate speech V1", 0);
SUP_DI(cfg_ms_sup_no_full_v1, cfg_ms_sup_no_full_v1_cmd, full_v1,
	"full-speech-v1", "Full rate speech V1", 0);
SUP_EN(cfg_ms_sup_full_v2, cfg_ms_sup_full_v2_cmd, full_v2, "full-speech-v2",
	"Full rate speech V2 (EFR)", 0);
SUP_DI(cfg_ms_sup_no_full_v2, cfg_ms_sup_no_full_v2_cmd, full_v2,
	"full-speech-v2", "Full rate speech V2 (EFR)", 0);
SUP_EN(cfg_ms_sup_full_v3, cfg_ms_sup_full_v3_cmd, full_v3, "full-speech-v3",
	"Full rate speech V3 (AMR)", 0);
SUP_DI(cfg_ms_sup_no_full_v3, cfg_ms_sup_no_full_v3_cmd, full_v3,
	"full-speech-v3", "Full rate speech V3 (AMR)", 0);
SUP_EN(cfg_ms_sup_half_v1, cfg_ms_sup_half_v1_cmd, half_v1, "half-speech-v1",
	"Half rate speech V1", 0);
SUP_DI(cfg_ms_sup_no_half_v1, cfg_ms_sup_no_half_v1_cmd, half_v1,
	"half-speech-v1", "Half rate speech V1", 0);
SUP_EN(cfg_ms_sup_half_v3, cfg_ms_sup_half_v3_cmd, half_v3, "half-speech-v3",
	"Half rate speech V3 (AMR)", 0);
SUP_DI(cfg_ms_sup_no_half_v3, cfg_ms_sup_no_half_v3_cmd, half_v3,
	"half-speech-v3", "Half rate speech V3 (AMR)", 0);

DEFUN(cfg_ms_sup_min_rxlev, cfg_ms_sup_min_rxlev_cmd, "min-rxlev <-110--47>",
	"Set the minimum receive level to select a cell\n"
	"Minimum receive level from -110 dBm to -47 dBm")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->min_rxlev_dbm = atoi(argv[0]);

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_sup_dsc_max, cfg_ms_sup_dsc_max_cmd, "dsc-max <90-500>",
	"Set the maximum DSC value. Standard is 90. Increase to make mobile "
	"more reliable against bad RX signal. This increase the propability "
	"of missing a paging requests\n"
	"DSC initial and maximum value (standard is 90)")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->dsc_max = atoi(argv[0]);

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_sup_skip_max_per_band, cfg_ms_sup_skip_max_per_band_cmd,
	"skip-max-per-band",
	"Scan all frequencies per band, not only a maximum number")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->skip_max_per_band = 1;

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_sup_no_skip_max_per_band, cfg_ms_sup_no_skip_max_per_band_cmd,
	"no skip-max-per-band",
	NO_STR "Scan only a maximum number of frequencies per band")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->skip_max_per_band = 0;

	return CMD_SUCCESS;
}

/* per testsim config */
DEFUN(cfg_ms_testsim, cfg_ms_testsim_cmd, "test-sim",
	"Configure test SIM emulation")
{
	vty->node = TESTSIM_NODE;

	return CMD_SUCCESS;
}

DEFUN(cfg_test_imsi, cfg_test_imsi_cmd, "imsi IMSI",
	"Set IMSI on test card\n15 digits IMSI")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;
	char *error = gsm_check_imsi(argv[0]);

	if (error) {
		vty_out(vty, "%s%s", error, VTY_NEWLINE);
		return CMD_WARNING;
	}

	strcpy(set->test_imsi, argv[0]);

	vty_restart_if_started(vty, ms);

	return CMD_SUCCESS;
}

#define HEX_STR "\nByte as two digits hexadecimal"
DEFUN(cfg_test_ki_xor, cfg_test_ki_xor_cmd, "ki xor HEX HEX HEX HEX HEX HEX "
	"HEX HEX HEX HEX HEX HEX",
	"Set Key (Ki) on test card\nUse XOR algorithm" HEX_STR HEX_STR HEX_STR
	HEX_STR HEX_STR HEX_STR HEX_STR HEX_STR HEX_STR HEX_STR HEX_STR HEX_STR)
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;
	uint8_t ki[12];
	const char *p;
	int i;

	for (i = 0; i < 12; i++) {
		p = argv[i];
		if (!strncmp(p, "0x", 2))
			p += 2;
		if (strlen(p) != 2) {
			vty_out(vty, "Expecting two digits hex value (with or "
				"without 0x in front)%s", VTY_NEWLINE);
			return CMD_WARNING;
		}
		ki[i] = strtoul(p, NULL, 16);
	}

	set->test_ki_type = OSMO_AUTH_ALG_XOR;
	memcpy(set->test_ki, ki, 12);
	return CMD_SUCCESS;
}

DEFUN(cfg_test_ki_comp128, cfg_test_ki_comp128_cmd, "ki comp128 HEX HEX HEX "
	"HEX HEX HEX HEX HEX HEX HEX HEX HEX HEX HEX HEX HEX",
	"Set Key (Ki) on test card\nUse XOR algorithm" HEX_STR HEX_STR HEX_STR
	HEX_STR HEX_STR HEX_STR HEX_STR HEX_STR HEX_STR HEX_STR HEX_STR HEX_STR
	HEX_STR HEX_STR HEX_STR HEX_STR)
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;
	uint8_t ki[16];
	const char *p;
	int i;

	for (i = 0; i < 16; i++) {
		p = argv[i];
		if (!strncmp(p, "0x", 2))
			p += 2;
		if (strlen(p) != 2) {
			vty_out(vty, "Expecting two digits hex value (with or "
				"without 0x in front)%s", VTY_NEWLINE);
			return CMD_WARNING;
		}
		ki[i] = strtoul(p, NULL, 16);
	}

	set->test_ki_type = OSMO_AUTH_ALG_COMP128v1;
	memcpy(set->test_ki, ki, 16);
	return CMD_SUCCESS;
}

DEFUN(cfg_test_barr, cfg_test_barr_cmd, "barred-access",
	"Allow access to barred cells")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->test_barr = 1;

	return CMD_SUCCESS;
}

DEFUN(cfg_test_no_barr, cfg_test_no_barr_cmd, "no barred-access",
	NO_STR "Deny access to barred cells")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->test_barr = 0;

	return CMD_SUCCESS;
}

DEFUN(cfg_test_no_rplmn, cfg_test_no_rplmn_cmd, "no rplmn",
	NO_STR "Unset Registered PLMN")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	set->test_rplmn_valid = 0;

	vty_restart_if_started(vty, ms);

	return CMD_SUCCESS;
}

static int _test_rplmn_cmd(struct vty *vty, int argc, const char *argv[],
	int attached)
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;
	uint16_t mcc = gsm_input_mcc((char *)argv[0]),
		 mnc = gsm_input_mnc((char *)argv[1]);

	if (mcc == GSM_INPUT_INVALID) {
		vty_out(vty, "Given MCC invalid%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	if (mnc == GSM_INPUT_INVALID) {
		vty_out(vty, "Given MNC invalid%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	set->test_rplmn_valid = 1;
	set->test_rplmn_mcc = mcc;
	set->test_rplmn_mnc = mnc;

	if (argc >= 3)
		set->test_lac = strtoul(argv[2], NULL, 16);
	else
		set->test_lac = 0xfffe;

	if (argc >= 4)
		set->test_tmsi = strtoul(argv[3], NULL, 16);
	else
		set->test_tmsi = 0xffffffff;

	if (attached)
		set->test_imsi_attached = 1;
	else
		set->test_imsi_attached = 0;

	vty_restart_if_started(vty, ms);

	return CMD_SUCCESS;
}

DEFUN(cfg_test_rplmn, cfg_test_rplmn_cmd,
	"rplmn MCC MNC [LAC] [TMSI]",
	"Set Registered PLMN\nMobile Country Code\nMobile Network Code\n"
	"Optionally set location area code\n"
	"Optionally set current assigned TMSI")
{
	return _test_rplmn_cmd(vty, argc, argv, 0);
}

DEFUN(cfg_test_rplmn_att, cfg_test_rplmn_att_cmd,
	"rplmn MCC MNC LAC TMSI attached",
	"Set Registered PLMN\nMobile Country Code\nMobile Network Code\n"
	"Set location area code\nSet current assigned TMSI\n"
	"Indicate to MM that card is already attached")
{
	return _test_rplmn_cmd(vty, argc, argv, 1);
}

DEFUN(cfg_test_hplmn, cfg_test_hplmn_cmd, "hplmn-search (everywhere|foreign-country)",
	"Set Home PLMN search mode\n"
	"Search for HPLMN when on any other network\n"
	"Search for HPLMN when in a different country")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	switch (argv[0][0]) {
	case 'e':
		set->test_always = 1;
		break;
	case 'f':
		set->test_always = 0;
		break;
	}

	vty_restart_if_started(vty, ms);

	return CMD_SUCCESS;
}

/* per audio config */
DEFUN(cfg_ms_audio, cfg_ms_audio_cmd, "audio",
	"Configure audio settings")
{
	vty->node = AUDIO_NODE;
	return CMD_SUCCESS;
}

static int set_audio_io_handler(struct vty *vty, enum audio_io_handler val)
{
	struct osmocom_ms *ms = (struct osmocom_ms *) vty->index;
	struct gsm_settings *set = &ms->settings;

	/* Don't restart on unchanged value */
	if (val == set->audio.io_handler)
		return CMD_SUCCESS;
	set->audio.io_handler = val;

	/* Restart required */
	vty_restart_if_started(vty, ms);

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_audio_io_handler, cfg_ms_audio_io_handler_cmd,
	"io-handler (none|gapk|l1phy|mncc-sock|loopback)",
	"Set TCH frame I/O handler\n"
	"No handler, drop TCH frames (default)\n"
	"libosmo-gapk based I/O handler (requires ALSA)\n"
	"L1 PHY (e.g. Calypso DSP in Motorola C1xx phones)\n"
	"External MNCC application (e.g. LCR) via MNCC socket\n"
	"Return TCH frame payload back to sender\n")
{
	struct osmocom_ms *ms = (struct osmocom_ms *) vty->index;
	int val = get_string_value(audio_io_handler_names, argv[0]);

	if (val == AUDIO_IOH_MNCC_SOCK) {
		if (ms->settings.mncc_handler != MNCC_HANDLER_INTERNAL) {
			vty_out(vty, "Audio I/O handler 'mncc-sock' can only be used "
				"with MNCC handler 'external'%s", VTY_NEWLINE);
			return CMD_WARNING;
		}
	}

#ifndef WITH_GAPK_IO
	if (val == AUDIO_IOH_GAPK) {
		vty_out(vty, "GAPK I/O is not compiled in (--with-gapk-io)%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
#endif

	return set_audio_io_handler(vty, val);
}

DEFUN(cfg_ms_audio_no_io_handler, cfg_ms_audio_no_io_handler_cmd,
	"no io-handler", NO_STR "Disable TCH frame processing")
{
	return set_audio_io_handler(vty, AUDIO_IOH_NONE);
}

DEFUN(cfg_ms_audio_io_tch_format, cfg_ms_audio_io_tch_format_cmd,
	"io-tch-format (rtp|ti)",
	"Set TCH I/O frame format used by the L1 PHY (for GAPK only)\n"
	"RTP format (RFC3551 for FR/EFR, RFC5993 for HR, RFC4867 for AMR)\n"
	"Texas Instruments format, used by Calypso based phones (e.g. Motorola C1xx)\n")
{
	int val = get_string_value(audio_io_format_names, argv[0]);
	struct osmocom_ms *ms = (struct osmocom_ms *) vty->index;
	struct gsm_settings *set = &ms->settings;

	if (set->audio.io_handler != AUDIO_IOH_GAPK) {
		vty_out(vty, "This parameter is only valid for GAPK%s", VTY_NEWLINE);
		return CMD_WARNING;
	}

	OSMO_ASSERT(val >= 0);
	set->audio.io_format = val;

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_audio_alsa_out_dev, cfg_ms_audio_alsa_out_dev_cmd,
	"alsa-output-dev (default|NAME)",
	"Set ALSA output (playback) device name (for GAPK only)\n"
	"Default system playback device (default)\n"
	"Name of a custom playback device")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	OSMO_STRLCPY_ARRAY(set->audio.alsa_output_dev, argv[0]);

	return CMD_SUCCESS;
}

DEFUN(cfg_ms_audio_alsa_in_dev, cfg_ms_audio_alsa_in_dev_cmd,
	"alsa-input-dev (default|NAME)",
	"Set ALSA input (capture) device name (for GAPK only)\n"
	"Default system recording device (default)\n"
	"Name of a custom recording device")
{
	struct osmocom_ms *ms = vty->index;
	struct gsm_settings *set = &ms->settings;

	OSMO_STRLCPY_ARRAY(set->audio.alsa_input_dev, argv[0]);

	return CMD_SUCCESS;
}

DEFUN(cfg_no_shutdown, cfg_ms_no_shutdown_cmd, "no shutdown",
	NO_STR "Activate and run MS")
{
	struct osmocom_ms *ms = vty->index;
	char *other_name = NULL;
	int rc;

	rc = mobile_start(ms, &other_name);
	switch (rc) {
	case -1:
		vty_out(vty, "Cannot start MS '%s', because MS '%s' "
			"use the same layer2-socket.%sPlease shutdown "
			"MS '%s' first.%s", ms->name, other_name,
			VTY_NEWLINE, other_name, VTY_NEWLINE);
		return CMD_WARNING;
	case -2:
		vty_out(vty, "Cannot start MS '%s', because MS '%s' "
			"use the same sap-socket.%sPlease shutdown "
			"MS '%s' first.%s", ms->name, other_name,
			VTY_NEWLINE, other_name, VTY_NEWLINE);
		return CMD_WARNING;
	case -3:
		vty_out(vty, "Connection to layer 1 failed!%s",
			VTY_NEWLINE);
		return CMD_WARNING;
	}

	return CMD_SUCCESS;
}

DEFUN(cfg_shutdown, cfg_ms_shutdown_cmd, "shutdown",
	"Shut down and deactivate MS")
{
	struct osmocom_ms *ms = vty->index;
	mobile_stop(ms, 0);
	return CMD_SUCCESS;
}

DEFUN(cfg_shutdown_force, cfg_ms_shutdown_force_cmd, "shutdown force",
	"Shut down and deactivate MS\nDo not perform IMSI detach")
{
	struct osmocom_ms *ms = vty->index;

	mobile_stop(ms, 1);
	return CMD_SUCCESS;
}

DEFUN(cfg_ms_script_load_run, cfg_ms_script_load_run_cmd, "lua-script FILENAME",
	"Load and execute a LUA script\nFilename for lua script")
{
	struct osmocom_ms *ms = vty->index;

	osmo_talloc_replace_string(ms, &ms->lua_script, argv[0]);
	if (!ms->lua_script)
		return CMD_WARNING;

	script_lua_load(vty, ms, ms->lua_script);
	return CMD_SUCCESS;
}

DEFUN(cfg_ms_no_script_load_run, cfg_ms_no_script_load_run_cmd, "no lua-script",
	NO_STR "Load and execute LUA script")
{
	struct osmocom_ms *ms = vty->index;

	script_lua_close(ms);
	talloc_free(ms->lua_script);
	ms->lua_script = NULL;
	return CMD_SUCCESS;
}

int ms_vty_go_parent(struct vty *vty)
{
	switch (vty->node) {
	case MS_NODE:
		vty->node = CONFIG_NODE;
		vty->index = NULL;
		break;
	case TESTSIM_NODE:
	case SUPPORT_NODE:
	case AUDIO_NODE:
		vty->node = MS_NODE;
		break;
	default:
		vty->node = CONFIG_NODE;
	}

	return vty->node;
}

DEFUN(off, off_cmd, "off",
	"Turn mobiles off (shutdown) and exit")
{
	osmo_signal_dispatch(SS_GLOBAL, S_GLOBAL_SHUTDOWN, NULL);

	return CMD_SUCCESS;
}

#define SUP_NODE(item) \
	install_element(SUPPORT_NODE, &cfg_ms_sup_item_cmd);

int ms_vty_init(void)
{
	install_element_ve(&show_ms_cmd);
	install_element_ve(&show_subscr_cmd);
	install_element_ve(&show_support_cmd);
	install_element_ve(&show_cell_cmd);
	install_element_ve(&show_cell_si_cmd);
	install_element_ve(&show_nbcells_cmd);
	install_element_ve(&show_ba_cmd);
	install_element_ve(&show_forb_la_cmd);
	install_element_ve(&show_forb_plmn_cmd);
	install_element_ve(&monitor_network_cmd);
	install_element_ve(&no_monitor_network_cmd);
	install_element(ENABLE_NODE, &off_cmd);

	install_element(ENABLE_NODE, &sim_test_cmd);
	install_element(ENABLE_NODE, &sim_test_att_cmd);
	install_element(ENABLE_NODE, &sim_sap_cmd);
	install_element(ENABLE_NODE, &sim_reader_cmd);
	install_element(ENABLE_NODE, &sim_remove_cmd);
	install_element(ENABLE_NODE, &sim_pin_cmd);
	install_element(ENABLE_NODE, &sim_disable_pin_cmd);
	install_element(ENABLE_NODE, &sim_enable_pin_cmd);
	install_element(ENABLE_NODE, &sim_change_pin_cmd);
	install_element(ENABLE_NODE, &sim_unblock_pin_cmd);
	install_element(ENABLE_NODE, &sim_lai_cmd);
	install_element(ENABLE_NODE, &network_search_cmd);
	install_element(ENABLE_NODE, &network_show_cmd);
	install_element(ENABLE_NODE, &network_select_cmd);
	install_element(ENABLE_NODE, &call_cmd);
	install_element(ENABLE_NODE, &call_retr_cmd);
	install_element(ENABLE_NODE, &call_dtmf_cmd);
	install_element(ENABLE_NODE, &sms_cmd);
	install_element(ENABLE_NODE, &service_cmd);
	install_element(ENABLE_NODE, &test_reselection_cmd);
	install_element(ENABLE_NODE, &delete_forbidden_plmn_cmd);

#ifdef _HAVE_GPSD
	install_element(CONFIG_NODE, &cfg_gps_host_cmd);
#endif
	install_element(CONFIG_NODE, &cfg_gps_device_cmd);
	install_element(CONFIG_NODE, &cfg_gps_baud_cmd);
	install_element(CONFIG_NODE, &cfg_gps_enable_cmd);
	install_element(CONFIG_NODE, &cfg_no_gps_enable_cmd);

	install_element(CONFIG_NODE, &cfg_hide_default_cmd);
	install_element(CONFIG_NODE, &cfg_no_hide_default_cmd);

	install_element(CONFIG_NODE, &cfg_ms_cmd);
	install_element(CONFIG_NODE, &cfg_ms_create_cmd);
	install_element(CONFIG_NODE, &cfg_ms_rename_cmd);
	install_element(CONFIG_NODE, &cfg_no_ms_cmd);
	install_node(&ms_node, config_write);
	install_element(MS_NODE, &cfg_ms_show_this_cmd);
	install_element(MS_NODE, &cfg_ms_layer2_cmd);
	install_element(MS_NODE, &cfg_ms_sap_cmd);
	install_element(MS_NODE, &cfg_ms_mncc_sock_cmd);
	install_element(MS_NODE, &cfg_ms_mncc_handler_cmd);
	install_element(MS_NODE, &cfg_ms_no_mncc_handler_cmd);
	install_element(MS_NODE, &cfg_ms_sim_cmd);
	install_element(MS_NODE, &cfg_ms_mode_cmd);
	install_element(MS_NODE, &cfg_ms_imei_cmd);
	install_element(MS_NODE, &cfg_ms_imei_fixed_cmd);
	install_element(MS_NODE, &cfg_ms_imei_random_cmd);
	install_element(MS_NODE, &cfg_ms_no_emerg_imsi_cmd);
	install_element(MS_NODE, &cfg_ms_emerg_imsi_cmd);
	install_element(MS_NODE, &cfg_ms_no_sms_sca_cmd);
	install_element(MS_NODE, &cfg_ms_sms_sca_cmd);
	install_element(MS_NODE, &cfg_ms_cw_cmd);
	install_element(MS_NODE, &cfg_ms_no_cw_cmd);
	install_element(MS_NODE, &cfg_ms_auto_answer_cmd);
	install_element(MS_NODE, &cfg_ms_no_auto_answer_cmd);
	install_element(MS_NODE, &cfg_ms_force_rekey_cmd);
	install_element(MS_NODE, &cfg_ms_no_force_rekey_cmd);
	install_element(MS_NODE, &cfg_ms_clip_cmd);
	install_element(MS_NODE, &cfg_ms_clir_cmd);
	install_element(MS_NODE, &cfg_ms_no_clip_cmd);
	install_element(MS_NODE, &cfg_ms_no_clir_cmd);
	install_element(MS_NODE, &cfg_ms_tx_power_cmd);
	install_element(MS_NODE, &cfg_ms_tx_power_val_cmd);
	install_element(MS_NODE, &cfg_ms_sim_delay_cmd);
	install_element(MS_NODE, &cfg_ms_no_sim_delay_cmd);
	install_element(MS_NODE, &cfg_ms_stick_cmd);
	install_element(MS_NODE, &cfg_ms_no_stick_cmd);
	install_element(MS_NODE, &cfg_ms_lupd_cmd);
	install_element(MS_NODE, &cfg_ms_no_lupd_cmd);
	install_element(MS_NODE, &cfg_ms_codec_full_cmd);
	install_element(MS_NODE, &cfg_ms_codec_full_pref_cmd);
	install_element(MS_NODE, &cfg_ms_codec_half_cmd);
	install_element(MS_NODE, &cfg_ms_codec_half_pref_cmd);
	install_element(MS_NODE, &cfg_ms_no_codec_half_cmd);
	install_element(MS_NODE, &cfg_ms_abbrev_cmd);
	install_element(MS_NODE, &cfg_ms_no_abbrev_cmd);
	install_element(MS_NODE, &cfg_ms_testsim_cmd);
	install_element(MS_NODE, &cfg_ms_audio_cmd);
	install_element(MS_NODE, &cfg_ms_neighbour_cmd);
	install_element(MS_NODE, &cfg_ms_no_neighbour_cmd);
	install_element(MS_NODE, &cfg_ms_any_timeout_cmd);
	install_element(MS_NODE, &cfg_ms_sms_store_cmd);
	install_element(MS_NODE, &cfg_ms_no_sms_store_cmd);
	install_element(MS_NODE, &cfg_ms_support_cmd);
	install_node(&support_node, config_write_dummy);
	install_element(SUPPORT_NODE, &cfg_ms_sup_dtmf_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_no_dtmf_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_sms_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_no_sms_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_a5_1_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_no_a5_1_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_a5_2_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_no_a5_2_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_a5_3_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_no_a5_3_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_a5_4_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_no_a5_4_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_a5_5_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_no_a5_5_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_a5_6_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_no_a5_6_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_a5_7_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_no_a5_7_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_p_gsm_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_no_p_gsm_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_e_gsm_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_no_e_gsm_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_r_gsm_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_no_r_gsm_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_dcs_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_no_dcs_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_gsm_850_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_no_gsm_850_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_pcs_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_no_pcs_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_gsm_480_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_no_gsm_480_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_gsm_450_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_no_gsm_450_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_class_900_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_class_dcs_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_class_850_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_class_pcs_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_class_400_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_ch_cap_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_full_v1_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_no_full_v1_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_full_v2_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_no_full_v2_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_full_v3_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_no_full_v3_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_half_v1_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_no_half_v1_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_half_v3_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_no_half_v3_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_min_rxlev_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_dsc_max_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_skip_max_per_band_cmd);
	install_element(SUPPORT_NODE, &cfg_ms_sup_no_skip_max_per_band_cmd);
	install_node(&testsim_node, config_write_dummy);
	install_element(TESTSIM_NODE, &cfg_test_imsi_cmd);
	install_element(TESTSIM_NODE, &cfg_test_ki_xor_cmd);
	install_element(TESTSIM_NODE, &cfg_test_ki_comp128_cmd);
	install_element(TESTSIM_NODE, &cfg_test_barr_cmd);
	install_element(TESTSIM_NODE, &cfg_test_no_barr_cmd);
	install_element(TESTSIM_NODE, &cfg_test_no_rplmn_cmd);
	install_element(TESTSIM_NODE, &cfg_test_rplmn_cmd);
	install_element(TESTSIM_NODE, &cfg_test_rplmn_att_cmd);
	install_element(TESTSIM_NODE, &cfg_test_hplmn_cmd);
	install_element(MS_NODE, &cfg_ms_shutdown_cmd);
	install_element(MS_NODE, &cfg_ms_shutdown_force_cmd);
	install_element(MS_NODE, &cfg_ms_no_shutdown_cmd);
	install_element(MS_NODE, &cfg_ms_script_load_run_cmd);
	install_element(MS_NODE, &cfg_ms_no_script_load_run_cmd);

	install_node(&audio_node, config_write_dummy);
	install_element(AUDIO_NODE, &cfg_ms_audio_io_handler_cmd);
	install_element(AUDIO_NODE, &cfg_ms_audio_no_io_handler_cmd);
	install_element(AUDIO_NODE, &cfg_ms_audio_io_tch_format_cmd);
	install_element(AUDIO_NODE, &cfg_ms_audio_alsa_out_dev_cmd);
	install_element(AUDIO_NODE, &cfg_ms_audio_alsa_in_dev_cmd);

	/* Register the talloc context introspection command */
	osmo_talloc_vty_add_cmds();

	return 0;
}

void vty_notify(struct osmocom_ms *ms, const char *fmt, ...)
{
	struct telnet_connection *connection;
	char buffer[1000];
	va_list args;
	struct vty *vty;

	if (fmt) {
		va_start(args, fmt);
		vsnprintf(buffer, sizeof(buffer) - 1, fmt, args);
		buffer[sizeof(buffer) - 1] = '\0';
		va_end(args);

		if (!buffer[0])
			return;
	}

	llist_for_each_entry(connection, &active_connections, entry) {
		vty = connection->vty;
		if (!vty)
			continue;
		if (!fmt) {
			vty_out(vty, "%s%% (MS %s)%s", VTY_NEWLINE, ms->name,
				VTY_NEWLINE);
			continue;
		}
		if (buffer[strlen(buffer) - 1] == '\n') {
			buffer[strlen(buffer) - 1] = '\0';
			vty_out(vty, "%% %s%s", buffer, VTY_NEWLINE);
			buffer[strlen(buffer)] = '\n';
		} else
			vty_out(vty, "%% %s", buffer);
	}
}

