/* GSM TS 12.21 O&M / OML, BTS side */

/* (C) 2011 by Andreas Eversberg <jolly@eversberg.eu>
 * (C) 2011-2013 by Harald Welte <laforge@gnumonks.org>
 *
 * All Rights Reserved
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero 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 Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 */

/*
 * Operation and Maintenance Messages
 */

#include "btsconfig.h"

#include <errno.h>
#include <stdarg.h>
#include <string.h>
#include <sys/types.h>
#include <arpa/inet.h>

#include <osmocom/core/talloc.h>
#include <osmocom/core/msgb.h>
#include <osmocom/gsm/protocol/gsm_12_21.h>
#include <osmocom/gsm/abis_nm.h>
#include <osmocom/gsm/tlv.h>
#include <osmocom/abis/e1_input.h>
#include <osmocom/abis/ipaccess.h>

#include <osmo-bts/logging.h>
#include <osmo-bts/gsm_data.h>
#include <osmo-bts/abis.h>
#include <osmo-bts/oml.h>
#include <osmo-bts/bts_model.h>
#include <osmo-bts/bts.h>
#include <osmo-bts/signal.h>
#include <osmo-bts/phy_link.h>

#define LOGPFOH(ss, lvl, foh, fmt, args ...) LOGP(ss, lvl, "%s: " fmt, abis_nm_dump_foh(foh), ## args)
#define DEBUGPFOH(ss, foh, fmt, args ...) LOGPFOH(ss, LOGL_DEBUG, foh, fmt, ## args)

static int oml_ipa_set_attr(struct gsm_bts *bts, struct msgb *msg);

static struct tlv_definition abis_nm_att_tlvdef_ipa_local = {};

/*
 * support
 */

static int oml_tlv_parse(struct tlv_parsed *tp, const uint8_t *buf, int len)
{
	return tlv_parse(tp, &abis_nm_att_tlvdef_ipa_local, buf, len, 0, 0);
}

struct msgb *oml_msgb_alloc(void)
{
	return msgb_alloc_headroom(1024, 128, "OML");
}

/* FIXME: move to gsm_data_shared */
static char mo_buf[128];
char *gsm_abis_mo_name(const struct gsm_abis_mo *mo)
{
	snprintf(mo_buf, sizeof(mo_buf), "OC=%s INST=(%02x,%02x,%02x)",
		 get_value_string(abis_nm_obj_class_names, mo->obj_class),
		 mo->obj_inst.bts_nr, mo->obj_inst.trx_nr, mo->obj_inst.ts_nr);
	return mo_buf;
}

/* 3GPP TS 12.21 § 8.8.2 */
int oml_tx_failure_event_rep(const struct gsm_abis_mo *mo, enum abis_nm_severity severity,
			     uint16_t cause_value, const char *fmt, ...)
{
	struct msgb *nmsg;
	va_list ap;

	LOGP(DOML, LOGL_NOTICE, "%s: Sending %s to BSC: ", gsm_abis_mo_name(mo),
		get_value_string(abis_mm_event_cause_names, cause_value));
	va_start(ap, fmt);
	osmo_vlogp(DOML, LOGL_NOTICE, __FILE__, __LINE__, 1, fmt, ap);
	nmsg = abis_nm_fail_evt_vrep(NM_EVT_PROC_FAIL, severity,
				     NM_PCAUSE_T_MANUF, cause_value, fmt, ap);
	va_end(ap);
	LOGPC(DOML, LOGL_NOTICE, "\n");

	if (!nmsg)
		return -ENOMEM;

	return oml_mo_send_msg(mo, nmsg, NM_MT_FAILURE_EVENT_REP);
}

/* Push OM header in front of msgb and send it */
int oml_send_msg(struct msgb *msg, int is_manuf)
{
	struct abis_om_hdr *omh;

	if (is_manuf) {
		/* length byte, string + 0 termination */
		uint8_t *manuf = msgb_push(msg, LV_GROSS_LEN(sizeof(abis_nm_ipa_magic)));
		lv_put(manuf, sizeof(abis_nm_ipa_magic), (const uint8_t *) abis_nm_ipa_magic);
	}

	/* Push the main OML header and send it off */
	omh = (struct abis_om_hdr *) msgb_push(msg, sizeof(*omh));
	if (is_manuf)
		omh->mdisc = ABIS_OM_MDISC_MANUF;
	else
		omh->mdisc = ABIS_OM_MDISC_FOM;
	omh->placement = ABIS_OM_PLACEMENT_ONLY;
	omh->sequence = 0;
	omh->length = msgb_l3len(msg);

	msg->l2h = (uint8_t *)omh;

	return abis_oml_sendmsg(msg);
}

int oml_mo_send_msg(const struct gsm_abis_mo *mo, struct msgb *msg, uint8_t msg_type)
{
	struct abis_om_fom_hdr *foh;

	msg->l3h = msgb_push(msg, sizeof(*foh));
	foh = (struct abis_om_fom_hdr *) msg->l3h;
	foh->msg_type = msg_type;
	foh->obj_class = mo->obj_class;
	memcpy(&foh->obj_inst, &mo->obj_inst, sizeof(foh->obj_inst));

	/* FIXME: This assumption may not always be correct */
	msg->trx = mo->bts->c0;

	DEBUGPFOH(DOML, foh, "Tx %s\n", get_value_string(abis_nm_msgtype_names, foh->msg_type));

	return oml_send_msg(msg, 0);
}

static inline void add_bts_attrs(struct msgb *msg, const struct gsm_bts *bts)
{
	uint16_t total_len = 0;
	uint8_t *len;

	/* Put NM_ATT_SW_CONFIG as per 9.4.61 "SW Configuration". */
	msgb_v_put(msg, NM_ATT_SW_CONFIG);

	/* We don't know the length yet, so we update it later. */
	len = msgb_put(msg, 2);

	total_len += abis_nm_put_sw_file(msg, "osmobts", PACKAGE_VERSION, true);
	total_len += abis_nm_put_sw_file(msg, btsatttr2str(BTS_TYPE_VARIANT),
					 btsvariant2str(bts->variant), true);

	if (strlen(bts->sub_model)) {
		total_len += abis_nm_put_sw_file(msg, btsatttr2str(BTS_SUB_MODEL),
						 bts->sub_model, true);
	}

	/* Finally, update the length */
	osmo_store16be(total_len, len);
}

/* Add BTS features as 3GPP TS 52.021 §9.4.30 Manufacturer Id */
static inline void add_bts_feat(struct msgb *msg, const struct gsm_bts *bts)
{
	unsigned int len = OSMO_BYTES_FOR_BITS(_NUM_BTS_FEAT);
	msgb_tl16v_put(msg, NM_ATT_MANUF_ID, len, bts->features->data);
}

static inline void add_trx_attr(struct msgb *msg, const struct gsm_bts_trx *trx)
{
	const struct phy_instance *pinst = trx_phy_instance(trx);
	const char *phy_version;
	uint16_t total_len;
	uint8_t *len;

	/* Put NM_ATT_SW_CONFIG as per 9.4.61 "SW Configuration". */
	msgb_v_put(msg, NM_ATT_SW_CONFIG);

	/* We don't know the length yet, so we update it later. */
	len = msgb_put(msg, 2);

	phy_version = pinst && strlen(pinst->version) ? pinst->version : "Unknown";
	total_len = abis_nm_put_sw_file(msg, btsatttr2str(TRX_PHY_VERSION), phy_version, true);

	/* Finally, update the length */
	osmo_store16be(total_len, len);
}

/* Handle a list of attributes requested by the BSC, compose
 * TRX-specific Get Attribute Response IE as per 9.4.64. */
static inline int handle_attrs_trx(struct msgb *out_msg, const struct gsm_bts_trx *trx,
				   const uint8_t *attr, uint16_t attr_len)
{
	uint8_t num_unsupported = 0;
	uint8_t *buf;
	int i;

	if (!trx) {
		LOGP(DOML, LOGL_ERROR, "%s: O&M Get Attributes for unknown TRX\n", gsm_trx_name(trx));
		return -NM_NACK_TRXNR_UNKN;
	}

	for (i = 0; i < attr_len; i++) {
		switch (attr[i]) {
		case NM_ATT_SW_CONFIG:
			add_trx_attr(out_msg, trx);
			break;
		default:
			LOGP(DOML, LOGL_ERROR, "%s: O&M Get Attributes [%u], %s is unsupported by TRX.\n",
			     gsm_trx_name(trx), i, get_value_string(abis_nm_att_names, attr[i]));
			/* Push this tag to the list of unsupported attributes */
			buf = msgb_push(out_msg, 1);
			*buf = attr[i];
			num_unsupported++;
		}
	}

	/* Push the amount of unsupported attributes */
	buf = msgb_push(out_msg, 1);
	*buf = num_unsupported;

	return 0;
}

/* Handle a list of attributes requested by the BSC, compose
 * BTS-specific Get Attribute Response IE as per 9.4.64. */
static inline int handle_attrs_bts(struct msgb *out_msg, const struct gsm_bts *bts,
				   const uint8_t *attr, uint16_t attr_len)
{
	uint8_t num_unsupported = 0;
	uint8_t *buf;
	int i;

	if (!bts) {
		LOGP(DOML, LOGL_ERROR, "O&M Get Attributes for unknown BTS\n");
		return -NM_NACK_BTSNR_UNKN;
	}

	for (i = 0; i < attr_len; i++) {
		switch (attr[i]) {
		case NM_ATT_SW_CONFIG:
			add_bts_attrs(out_msg, bts);
			break;
		case NM_ATT_MANUF_ID:
			add_bts_feat(out_msg, bts);
			break;
		default:
			LOGP(DOML, LOGL_ERROR, "O&M Get Attributes [%u], %s is unsupported by BTS.\n", i,
			     get_value_string(abis_nm_att_names, attr[i]));
			/* Push this tag to the list of unsupported attributes */
			buf = msgb_push(out_msg, 1);
			*buf = attr[i];
			num_unsupported++;
		}
	}

	/* Push the amount of unsupported attributes */
	buf = msgb_push(out_msg, 1);
	*buf = num_unsupported;

	return 0;
}

/* send 3GPP TS 52.021 §8.11.2 Get Attribute Response */
static int oml_tx_attr_resp(const struct gsm_abis_mo *mo,
			    const uint8_t *attr, uint16_t attr_len)
{
	struct msgb *nmsg = oml_msgb_alloc();
	const char *mo_name = gsm_abis_mo_name(mo);
	int rc;

	if (!nmsg)
		return -NM_NACK_CANT_PERFORM;

	switch (mo->obj_class) {
	case NM_OC_BTS:
		rc = handle_attrs_bts(nmsg, mo->bts, attr, attr_len);
		break;
	case NM_OC_BASEB_TRANSC:
		rc = handle_attrs_trx(nmsg, gsm_bts_trx_num(mo->bts, mo->obj_inst.trx_nr), attr, attr_len);
		break;
	default:
		LOGP(DOML, LOGL_ERROR, "%s: Unsupported MO class in Get Attribute Response\n",
		     mo_name);
		rc = -NM_NACK_OBJCLASS_NOTSUPP;
	}

	if (rc < 0) {
		LOGP(DOML, LOGL_ERROR, "%s: Tx Get Attribute Response FAILED with rc=%d\n",
		     mo_name, rc);
		msgb_free(nmsg);
		return rc;
	}

	/* Push Get Attribute Response Info TL (actually TV where V is L) */
	msgb_tv16_push(nmsg, NM_ATT_GET_ARI, msgb_length(nmsg));

	rc = oml_mo_send_msg(mo, nmsg, NM_MT_GET_ATTR_RESP);
	return (rc < 0) ? -NM_NACK_CANT_PERFORM : rc;
}

/* 8.8.1 sending State Changed Event Report */
int oml_tx_state_changed(const struct gsm_abis_mo *mo)
{
	struct msgb *nmsg;
	uint8_t avail_state;

	nmsg = oml_msgb_alloc();
	if (!nmsg)
		return -ENOMEM;

	/* 9.4.38 Operational State */
	msgb_tv_put(nmsg, NM_ATT_OPER_STATE, mo->nm_state.operational);

	/* 9.4.7 Availability Status */
	avail_state = (uint8_t) mo->nm_state.availability;
	msgb_tl16v_put(nmsg, NM_ATT_AVAIL_STATUS, 1, &avail_state);

	/* 9.4.4 Administrative Status -- not in spec but also sent by nanobts */
	msgb_tv_put(nmsg, NM_ATT_ADM_STATE, mo->nm_state.administrative);

	return oml_mo_send_msg(mo, nmsg, NM_MT_STATECHG_EVENT_REP);
}

/* First initialization of MO, does _not_ generate state changes */
void oml_mo_state_init(struct gsm_abis_mo *mo, int op_state, int avail_state)
{
	mo->nm_state.availability = avail_state;
	mo->nm_state.operational = op_state;
}

int oml_mo_state_chg(struct gsm_abis_mo *mo, int op_state, int avail_state, int adm_state)
{
	int rc = 0;

	if ((op_state != -1 && mo->nm_state.operational != op_state) ||
	    (avail_state != -1 && mo->nm_state.availability != avail_state) ||
	    (adm_state != -1 && mo->nm_state.administrative != adm_state)) {
		if (avail_state != -1) {
			LOGP(DOML, LOGL_INFO, "%s AVAIL STATE %s -> %s\n",
				gsm_abis_mo_name(mo),
				abis_nm_avail_name(mo->nm_state.availability),
				abis_nm_avail_name(avail_state));
			mo->nm_state.availability = avail_state;
		}
		if (op_state != -1) {
			struct nm_statechg_signal_data nsd;
			LOGP(DOML, LOGL_INFO, "%s OPER STATE %s -> %s\n",
				gsm_abis_mo_name(mo),
				abis_nm_opstate_name(mo->nm_state.operational),
				abis_nm_opstate_name(op_state));
			nsd.mo = mo;
			nsd.old_state = mo->nm_state.operational;
			nsd.new_state = op_state;
			mo->nm_state.operational = op_state;
			osmo_signal_dispatch(SS_GLOBAL, S_NEW_OP_STATE, &nsd);
		}
		if (adm_state != -1) {
			LOGP(DOML, LOGL_INFO, "%s ADMIN STATE %s -> %s\n",
				gsm_abis_mo_name(mo),
				abis_nm_admin_name(mo->nm_state.administrative),
				abis_nm_admin_name(adm_state));
			mo->nm_state.administrative = adm_state;
		}


		/* send state change report */
		rc = oml_tx_state_changed(mo);
	}
	return rc;
}

/* Send an ACK or NACK response from 'mo' to BSC, deriving message
 * type from 'orig_msg_type'. ACK is sent if cause == 0; NACK otherwise */
int oml_mo_fom_ack_nack(const struct gsm_abis_mo *mo, uint8_t orig_msg_type,
			uint8_t cause)
{
	struct msgb *msg;
	uint8_t new_msg_type;

	msg = oml_msgb_alloc();
	if (!msg)
		return -ENOMEM;

	if (cause) {
		new_msg_type = orig_msg_type + 2;
		msgb_tv_put(msg, NM_ATT_NACK_CAUSES, cause);
	} else {
		new_msg_type = orig_msg_type + 1;
	}

	return oml_mo_send_msg(mo, msg, new_msg_type);
}

int oml_mo_statechg_ack(const struct gsm_abis_mo *mo)
{
	struct msgb *msg;
	int rc = 0;

	msg = oml_msgb_alloc();
	if (!msg)
		return -ENOMEM;

	msgb_tv_put(msg, NM_ATT_ADM_STATE, mo->nm_state.administrative);

	rc = oml_mo_send_msg(mo, msg, NM_MT_CHG_ADM_STATE_ACK);
	if (rc != 0)
		return rc;

	/* Emulate behaviour of ipaccess nanobts: Send a 'State Changed Event Report' as well. */
	return oml_tx_state_changed(mo);
}

int oml_mo_statechg_nack(const struct gsm_abis_mo *mo, uint8_t nack_cause)
{
	return oml_mo_fom_ack_nack(mo, NM_MT_CHG_ADM_STATE, nack_cause);
}

int oml_mo_opstart_ack(const struct gsm_abis_mo *mo)
{
	return oml_mo_fom_ack_nack(mo, NM_MT_OPSTART, 0);
}

int oml_mo_opstart_nack(const struct gsm_abis_mo *mo, uint8_t nack_cause)
{
	return oml_mo_fom_ack_nack(mo, NM_MT_OPSTART, nack_cause);
}

/* Send an ACK or NACK response to BSC for the given OML message,
 * reusing it.  ACK is sent if cause == 0; NACK otherwise. */
int oml_fom_ack_nack(struct msgb *msg, uint8_t cause)
{
	struct abis_om_fom_hdr *foh;

	/* remove any l2/l1 that may be already present */
	msgb_pull_to_l2(msg);

	foh = (struct abis_om_fom_hdr *) msg->l3h;

	/* alter message type */
	if (cause) {
		LOGPFOH(DOML, LOGL_NOTICE, foh, "Sending FOM NACK with cause %s.\n",
			abis_nm_nack_cause_name(cause));
		foh->msg_type += 2; /* nack */
		/* add cause */
		msgb_tv_put(msg, NM_ATT_NACK_CAUSES, cause);
	} else {
		LOGPFOH(DOML, LOGL_DEBUG, foh, "Sending FOM ACK.\n");
		foh->msg_type++; /* ack */
	}

	/* ensure that the message length is up to date */
	struct abis_om_hdr *omh = (struct abis_om_hdr *) msgb_l2(msg);
	omh->length = msgb_l3len(msg);

	/* we cannot use oml_send_msg() as we already have the OML header */
	if (abis_oml_sendmsg(msg) != 0)
		LOGPFOH(DOML, LOGL_ERROR, foh, "Failed to send ACK/NACK\n");

	/* msgb was reused, do not free() */
	return 1;
}

/*
 * Formatted O&M messages
 */

/* 8.3.7 sending SW Activated Report */
int oml_mo_tx_sw_act_rep(const struct gsm_abis_mo *mo)
{
	struct msgb *nmsg;

	nmsg = oml_msgb_alloc();
	if (!nmsg)
		return -ENOMEM;

	return oml_mo_send_msg(mo, nmsg, NM_MT_SW_ACTIVATED_REP);
}

/* The defaults below correspond to various sources/recommendations that could be found online.
 * The BSC should override this via OML anyway. */
const unsigned int oml_default_t200_ms[7] = {
        [T200_SDCCH]		= 1000,
        [T200_FACCH_F]		= 1000,
        [T200_FACCH_H]		= 1000,
        [T200_SACCH_TCH_SAPI0]	= 2000,
        [T200_SACCH_SDCCH]	= 2000,
        [T200_SDCCH_SAPI3]	= 1000,
        [T200_SACCH_TCH_SAPI3]	= 2000,
};

/* 3GPP TS 52.021 §8.11.1 Get Attributes has been received */
static int oml_rx_get_attr(struct gsm_bts *bts, struct msgb *msg)
{
	struct abis_om_fom_hdr *foh = msgb_l3(msg);
	const struct gsm_abis_mo *mo;
	struct tlv_parsed tp;
	int rc;

	if (!foh || !bts)
		return -EINVAL;

	DEBUGPFOH(DOML, foh, "Rx GET ATTR\n");

	/* Determine which OML object is addressed */
	mo = gsm_objclass2mo(bts, foh->obj_class, &foh->obj_inst);
	if (!mo) {
		LOGPFOH(DOML, LOGL_ERROR, foh, "Get Attributes for unknown Object Instance\n");
		return oml_fom_ack_nack(msg, NM_NACK_OBJINST_UNKN);
	}

	rc = oml_tlv_parse(&tp, foh->data, msgb_l3len(msg) - sizeof(*foh));
	if (rc < 0) {
		oml_tx_failure_event_rep(mo, NM_SEVER_MINOR, OSMO_EVT_MAJ_UNSUP_ATTR,
					 "Get Attribute parsing failure");
		return oml_mo_fom_ack_nack(mo, NM_MT_GET_ATTR, NM_NACK_INCORR_STRUCT);
	}

	if (!TLVP_PRES_LEN(&tp, NM_ATT_LIST_REQ_ATTR, 1)) {
		oml_tx_failure_event_rep(mo, NM_SEVER_MINOR, OSMO_EVT_MAJ_UNSUP_ATTR,
					 "Get Attribute without Attribute List");
		return oml_mo_fom_ack_nack(mo, NM_MT_GET_ATTR, NM_NACK_INCORR_STRUCT);
	}

	rc = oml_tx_attr_resp(mo, TLVP_VAL(&tp, NM_ATT_LIST_REQ_ATTR), TLVP_LEN(&tp, NM_ATT_LIST_REQ_ATTR));
	if (rc < 0) {
		LOGPFOH(DOML, LOGL_ERROR, foh, "Responding to O&M Get Attributes message with NACK 0%x\n", -rc);
		return oml_mo_fom_ack_nack(mo, NM_MT_GET_ATTR,  -rc);
	}

	return 0;
}

/* 8.6.1 Set BTS Attributes has been received */
static int oml_rx_set_bts_attr(struct gsm_bts *bts, struct msgb *msg)
{
	struct abis_om_fom_hdr *foh = msgb_l3(msg);
	struct tlv_parsed tp, *tp_merged;
	int rc, i;
	const uint8_t *payload;

	DEBUGPFOH(DOML, foh, "Rx SET BTS ATTR\n");

	rc = oml_tlv_parse(&tp, foh->data, msgb_l3len(msg) - sizeof(*foh));
	if (rc < 0) {
		oml_tx_failure_event_rep(&bts->mo, NM_SEVER_MAJOR, OSMO_EVT_MAJ_UNSUP_ATTR,
					 "New value for Attribute not supported");
		return oml_fom_ack_nack(msg, NM_NACK_INCORR_STRUCT);
	}

	/* Test for globally unsupported stuff here */
	if (TLVP_PRES_LEN(&tp, NM_ATT_BCCH_ARFCN, 2)) {
		uint16_t arfcn = ntohs(tlvp_val16_unal(&tp, NM_ATT_BCCH_ARFCN));
		if (arfcn >= 1024) { /* 0 .. 1023 (1024 channels total) */
			oml_tx_failure_event_rep(&bts->mo, NM_SEVER_MAJOR, OSMO_EVT_WARN_SW_WARN,
						 "Given ARFCN %u is not supported",
						 arfcn);
			LOGPFOH(DOML, LOGL_ERROR, foh, "Given ARFCN %u is not supported.\n", arfcn);
			return oml_fom_ack_nack(msg, NM_NACK_FREQ_NOTAVAIL);
		}
	}
	/* 9.4.52 Starting Time */
	if (TLVP_PRESENT(&tp, NM_ATT_START_TIME)) {
		oml_tx_failure_event_rep(&bts->mo, NM_SEVER_MAJOR, OSMO_EVT_MAJ_UNSUP_ATTR,
					 "NM_ATT_START_TIME Attribute not "
					 "supported");
		return oml_fom_ack_nack(msg, NM_NACK_SPEC_IMPL_NOTSUPP);
	}

	/* merge existing BTS attributes with new attributes */
	tp_merged = osmo_tlvp_copy(bts->mo.nm_attr, bts);
	osmo_tlvp_merge(tp_merged, &tp);

	/* Ask BTS driver to validate new merged attributes */
	rc = bts_model_check_oml(bts, foh->msg_type, bts->mo.nm_attr, tp_merged, bts);
	if (rc < 0) {
		talloc_free(tp_merged);
		return oml_fom_ack_nack(msg, -rc);
	}

	/* Success: replace old BTS attributes with new */
	talloc_free(bts->mo.nm_attr);
	bts->mo.nm_attr = tp_merged;

	/* ... and actually still parse them */

	/* 9.4.25 Interference Level Boundaries */
	if (TLVP_PRES_LEN(&tp, NM_ATT_INTERF_BOUND, 6)) {
		payload = TLVP_VAL(&tp, NM_ATT_INTERF_BOUND);
		for (i = 0; i < ARRAY_SIZE(bts->interference.boundary); i++) {
			const int16_t boundary = payload[i];
			bts->interference.boundary[i] = -1 * boundary;
		}
	}
	/* 9.4.24 Intave Parameter */
	if (TLVP_PRES_LEN(&tp, NM_ATT_INTAVE_PARAM, 1))
		bts->interference.intave = *TLVP_VAL(&tp, NM_ATT_INTAVE_PARAM);

	/* 9.4.14 Connection Failure Criterion */
	if (TLVP_PRES_LEN(&tp, NM_ATT_CONN_FAIL_CRIT, 1)) {
		const uint8_t *val = TLVP_VAL(&tp, NM_ATT_CONN_FAIL_CRIT);

		switch (val[0]) {
		case 0xFF: /* Osmocom specific Extension of TS 12.21 */
			LOGPFOH(DOML, LOGL_NOTICE, foh, "WARNING: Radio Link Timeout "
				"explicitly disabled, only use this for lab testing!\n");
			bts->radio_link_timeout.oml = -1;
			if (!bts->radio_link_timeout.vty_override)
				bts->radio_link_timeout.current = bts->radio_link_timeout.oml;
			break;
		case 0x01: /* Based on uplink SACCH (radio link timeout) */
			if (TLVP_LEN(&tp, NM_ATT_CONN_FAIL_CRIT) >= 2 &&
			    val[1] >= 4 && val[1] <= 64) {
				bts->radio_link_timeout.oml = val[1];
				if (!bts->radio_link_timeout.vty_override)
					bts->radio_link_timeout.current = bts->radio_link_timeout.oml;
				break;
			}
			/* fall-through */
		case 0x02: /* Based on RXLEV/RXQUAL measurements */
		default:
			LOGPFOH(DOML, LOGL_NOTICE, foh, "Given Conn. Failure Criterion "
				"not supported. Please use criterion 0x01 with "
				"RADIO_LINK_TIMEOUT value of 4..64\n");
			return oml_fom_ack_nack(msg, NM_NACK_PARAM_RANGE);
		}
	}

	/* 9.4.53 T200 */
	if (TLVP_PRES_LEN(&tp, NM_ATT_T200, ARRAY_SIZE(bts->t200_ms))) {
		payload = TLVP_VAL(&tp, NM_ATT_T200);
		for (i = 0; i < ARRAY_SIZE(bts->t200_ms); i++) {
			uint32_t t200_ms = payload[i] * abis_nm_t200_ms[i];
#if 0
			bts->t200_ms[i] = t200_ms;
			DEBUGPFOH(DOML, foh, "T200[%u]: OML=%u, mult=%u => %u ms\n",
				  i, payload[i], abis_nm_t200_ms[i],
				  bts->t200_ms[i]);
#else
                        /* we'd rather use the 1s/2s (long) defaults by
                         * libosmocore, as we appear to have some bug(s)
                         * related to handling T200 expiration in
                         * libosmogsm lapd(m) code? */
                        LOGPFOH(DOML, LOGL_NOTICE, foh, "Ignoring T200[%u] (%u ms) "
                                "as sent by BSC due to suspected LAPDm bug!\n",
                                i, t200_ms);
#endif
		}
	}

	/* 9.4.31 Maximum Timing Advance */
	if (TLVP_PRES_LEN(&tp, NM_ATT_MAX_TA, 1))
		bts->max_ta = *TLVP_VAL(&tp, NM_ATT_MAX_TA);

	/* 9.4.39 Overload Period */
	if (TLVP_PRES_LEN(&tp, NM_ATT_OVERL_PERIOD, 1))
		bts->load.overload_period = *TLVP_VAL(&tp, NM_ATT_OVERL_PERIOD);

	/* 9.4.12 CCCH Load Threshold */
	if (TLVP_PRES_LEN(&tp, NM_ATT_CCCH_L_T, 1))
		bts->load.ccch.load_ind_thresh = *TLVP_VAL(&tp, NM_ATT_CCCH_L_T);

	/* 9.4.11 CCCH Load Indication Period */
	if (TLVP_PRES_LEN(&tp, NM_ATT_CCCH_L_I_P, 1)) {
		bts->load.ccch.load_ind_period = *TLVP_VAL(&tp, NM_ATT_CCCH_L_I_P);
		if (load_timer_is_running(bts)) {
			load_timer_stop(bts);
			load_timer_start(bts);
		}
	}

	/* 9.4.44 RACH Busy Threshold */
	if (TLVP_PRES_LEN(&tp, NM_ATT_RACH_B_THRESH, 1)) {
		int16_t thresh = *TLVP_VAL(&tp, NM_ATT_RACH_B_THRESH);
		bts->load.rach.busy_thresh = -1 * thresh;
	}

	/* 9.4.45 RACH Load Averaging Slots */
	if (TLVP_PRES_LEN(&tp, NM_ATT_LDAVG_SLOTS, 2)) {
		bts->load.rach.averaging_slots =
			ntohs(tlvp_val16_unal(&tp, NM_ATT_LDAVG_SLOTS));
	}

	/* 9.4.10 BTS Air Timer */
	if (TLVP_PRES_LEN(&tp, NM_ATT_BTS_AIR_TIMER, 1)) {
		uint8_t t3105 = *TLVP_VAL(&tp, NM_ATT_BTS_AIR_TIMER);
		if (t3105 == 0) {
			LOGPFOH(DOML, LOGL_NOTICE, foh, "T3105 must have a value != 0.\n");
			return oml_fom_ack_nack(msg, NM_NACK_PARAM_RANGE);
		}
		bts->t3105_ms = t3105 * 10;
	}

	/* 9.4.37 NY1 */
	if (TLVP_PRES_LEN(&tp, NM_ATT_NY1, 1))
		bts->ny1 = *TLVP_VAL(&tp, NM_ATT_NY1);

	/* 9.4.8 BCCH ARFCN */
	if (TLVP_PRES_LEN(&tp, NM_ATT_BCCH_ARFCN, 2))
		bts->c0->arfcn = ntohs(tlvp_val16_unal(&tp, NM_ATT_BCCH_ARFCN));

	/* 9.4.9 BSIC */
	if (TLVP_PRES_LEN(&tp, NM_ATT_BSIC, 1))
		bts->bsic = *TLVP_VAL(&tp, NM_ATT_BSIC);

	/* call into BTS driver to apply new attributes to hardware */
	return bts_model_apply_oml(bts, msg, tp_merged, NM_OC_BTS, bts);
}

/* 8.6.2 Set Radio Attributes has been received */
static int oml_rx_set_radio_attr(struct gsm_bts_trx *trx, struct msgb *msg)
{
	struct abis_om_fom_hdr *foh = msgb_l3(msg);
	struct tlv_parsed tp, *tp_merged;
	int rc;

	DEBUGPFOH(DOML, foh, "Rx SET RADIO CARRIER ATTR\n");

	rc = oml_tlv_parse(&tp, foh->data, msgb_l3len(msg) - sizeof(*foh));
	if (rc < 0) {
		oml_tx_failure_event_rep(&trx->mo, NM_SEVER_MAJOR, OSMO_EVT_MAJ_UNSUP_ATTR,
					 "New value for Set Radio Attribute not"
					 " supported");
		return oml_fom_ack_nack(msg, NM_NACK_INCORR_STRUCT);
	}

	/* merge existing BTS attributes with new attributes */
	tp_merged = osmo_tlvp_copy(trx->mo.nm_attr, trx->bts);
	osmo_tlvp_merge(tp_merged, &tp);

	/* Ask BTS driver to validate new merged attributes */
	rc = bts_model_check_oml(trx->bts, foh->msg_type, trx->mo.nm_attr, tp_merged, trx);
	if (rc < 0) {
		talloc_free(tp_merged);
		return oml_fom_ack_nack(msg, -rc);
	}

	/* Success: replace old BTS attributes with new */
	talloc_free(trx->mo.nm_attr);
	trx->mo.nm_attr = tp_merged;

	/* ... and actually still parse them */

	/* 9.4.47 RF Max Power Reduction */
	if (TLVP_PRES_LEN(&tp, NM_ATT_RF_MAXPOWR_R, 1)) {
		trx->max_power_red = *TLVP_VAL(&tp, NM_ATT_RF_MAXPOWR_R) * 2;
		LOGPFOH(DOML, LOGL_INFO, foh, "Set RF Max Power Reduction to "
			"%d dBm\n", trx->max_power_red);
	}
	/* 9.4.5 ARFCN List */
#if 0
	if (TLVP_PRESENT(&tp, NM_ATT_ARFCN_LIST)) {
		uint8_t *value = TLVP_VAL(&tp, NM_ATT_ARFCN_LIST);
		uint16_t _value;
		uint16_t length = TLVP_LEN(&tp, NM_ATT_ARFCN_LIST);
		uint16_t arfcn;
		int i;
		for (i = 0; i < length; i++) {
			memcpy(&_value, value, 2);
			arfcn = ntohs(_value);
			value += 2;
			if (arfcn > 1024)
				return oml_fom_ack_nack(msg, NM_NACK_FREQ_NOTAVAIL);
			trx->arfcn_list[i] = arfcn;
			LOGPFOH(DOML, LOGL_INFO, foh, " ARFCN list = %d\n", trx->arfcn_list[i]);
		}
		trx->arfcn_num = length;
	} else
		trx->arfcn_num = 0;
#else
	if (trx != trx->bts->c0 && TLVP_PRESENT(&tp, NM_ATT_ARFCN_LIST)) {
		const uint8_t *value = TLVP_VAL(&tp, NM_ATT_ARFCN_LIST);
		uint16_t _value;
		uint16_t length = TLVP_LEN(&tp, NM_ATT_ARFCN_LIST);
		uint16_t arfcn;
		if (length != 2) {
			LOGPFOH(DOML, LOGL_ERROR, foh, "Expecting only one ARFCN, "
				"because hopping not supported\n");
			return oml_fom_ack_nack(msg, NM_NACK_MSGINCONSIST_PHYSCFG);
		}
		memcpy(&_value, value, 2);
		arfcn = ntohs(_value);
		value += 2;
		if (arfcn >= 1024) { /* 0 .. 1023 (1024 channels total) */
			oml_tx_failure_event_rep(&trx->bts->mo, NM_SEVER_MAJOR, OSMO_EVT_WARN_SW_WARN,
						 "Given ARFCN %u is unsupported", arfcn);
			LOGPFOH(DOML, LOGL_NOTICE, foh, "Given ARFCN %u is unsupported.\n", arfcn);
			return oml_fom_ack_nack(msg, NM_NACK_FREQ_NOTAVAIL);
		}
		trx->arfcn = arfcn;
	}
#endif
	/* call into BTS driver to apply new attributes to hardware */
	return bts_model_apply_oml(trx->bts, msg, tp_merged, NM_OC_RADIO_CARRIER, trx);
}

static int handle_chan_comb(struct gsm_bts_trx_ts *ts, const uint8_t comb)
{
	enum gsm_phys_chan_config pchan;

	pchan = abis_nm_pchan4chcomb(comb);
	ts->pchan = pchan;

	/* RSL_MT_IPAC_PDCH_ACT style dyn PDCH */
	if (pchan == GSM_PCHAN_TCH_F_PDCH)
		pchan = ts->flags & TS_F_PDCH_ACTIVE? GSM_PCHAN_PDCH
						    : GSM_PCHAN_TCH_F;

	/* Osmocom RSL CHAN ACT style dyn TS */
	if (pchan == GSM_PCHAN_OSMO_DYN) {
		pchan = ts->dyn.pchan_is;

		/* If the dyn TS doesn't have a pchan yet, do nothing. */
		if (pchan == GSM_PCHAN_NONE)
			return 0;
	}

	return conf_lchans_as_pchan(ts, pchan);
}

static inline void lchans_type_set(struct gsm_bts_trx_ts *ts,
				   enum gsm_chan_t lchan_type,
				   unsigned int num_lchans)
{
	unsigned int i;

	for (i = 0; i < num_lchans; i++)
		ts->lchan[i].type = lchan_type;
}

int conf_lchans_as_pchan(struct gsm_bts_trx_ts *ts,
			 enum gsm_phys_chan_config pchan)
{
	/* Initialize all lchans with GSM_LCHAN_NONE first */
	lchans_type_set(ts, GSM_LCHAN_NONE, ARRAY_SIZE(ts->lchan));

	switch (pchan) {
	case GSM_PCHAN_CCCH_SDCCH4_CBCH:
	case GSM_PCHAN_CCCH_SDCCH4:
		lchans_type_set(ts, GSM_LCHAN_SDCCH, 4);
		if (pchan == GSM_PCHAN_CCCH_SDCCH4_CBCH)
			ts->lchan[2].type = GSM_LCHAN_CBCH;
		/* fallthrough */
	case GSM_PCHAN_CCCH:
		ts->lchan[CCCH_LCHAN].type = GSM_LCHAN_CCCH;
		break;
	case GSM_PCHAN_TCH_F:
		if (ts->vamos.peer != NULL) { /* VAMOS: enable shadow lchans */
			lchans_type_set(ts->vamos.peer, GSM_LCHAN_TCH_F, 1);
			ts->vamos.peer->pchan = GSM_PCHAN_TCH_F;
		}
		lchans_type_set(ts, GSM_LCHAN_TCH_F, 1);
		break;
	case GSM_PCHAN_TCH_H:
		if (ts->vamos.peer != NULL) { /* VAMOS: enable shadow lchans */
			lchans_type_set(ts->vamos.peer, GSM_LCHAN_TCH_H, 2);
			ts->vamos.peer->pchan = GSM_PCHAN_TCH_H;
		}
		lchans_type_set(ts, GSM_LCHAN_TCH_H, 2);
		break;
	case GSM_PCHAN_SDCCH8_SACCH8C_CBCH:
	case GSM_PCHAN_SDCCH8_SACCH8C:
		lchans_type_set(ts, GSM_LCHAN_SDCCH, 8);
		if (pchan == GSM_PCHAN_SDCCH8_SACCH8C_CBCH)
			ts->lchan[2].type = GSM_LCHAN_CBCH;
		break;
	case GSM_PCHAN_PDCH:
		if (ts->vamos.peer != NULL) { /* VAMOS: disable shadow lchans */
			lchans_type_set(ts->vamos.peer, GSM_LCHAN_NONE, 1);
			ts->vamos.peer->pchan = GSM_PCHAN_NONE;
		}
		lchans_type_set(ts, GSM_LCHAN_PDTCH, 1);
		break;
	default:
		LOGP(DOML, LOGL_ERROR, "Unknown/unhandled PCHAN type: %u %s\n",
		     ts->pchan, gsm_pchan_name(ts->pchan));
		return -NM_NACK_PARAM_RANGE;
	}

	return 0;
}

/* 8.6.3 Set Channel Attributes has been received */
static int oml_rx_set_chan_attr(struct gsm_bts_trx_ts *ts, struct msgb *msg)
{
	struct abis_om_fom_hdr *foh = msgb_l3(msg);
	struct gsm_bts *bts = ts->trx->bts;
	struct tlv_parsed tp, *tp_merged;
	int rc, i;

	DEBUGPFOH(DOML, foh, "Rx SET CHAN ATTR\n");

	rc = oml_tlv_parse(&tp, foh->data, msgb_l3len(msg) - sizeof(*foh));
	if (rc < 0) {
		oml_tx_failure_event_rep(&ts->mo, NM_SEVER_MAJOR, OSMO_EVT_MAJ_UNSUP_ATTR,
					 "New value for Set Channel Attribute not supported");
		return oml_fom_ack_nack(msg, NM_NACK_INCORR_STRUCT);
	}

	/* Check frequency hopping parameters (HSN, MAIO, ARFCN list) */
	if (TLVP_PRESENT(&tp, NM_ATT_HSN) || TLVP_PRESENT(&tp, NM_ATT_MAIO)) {
		if (!osmo_bts_has_feature(bts->features, BTS_FEAT_HOPPING)) {
			LOGPFOH(DOML, LOGL_ERROR, foh, "SET CHAN ATTR: Frequency hopping not supported.\n");
			return oml_fom_ack_nack(msg, NM_NACK_SPEC_IMPL_NOTSUPP);
		}

		if (!TLVP_PRES_LEN(&tp, NM_ATT_HSN, 1) || !TLVP_PRES_LEN(&tp, NM_ATT_MAIO, 1)) {
			LOGPFOH(DOML, LOGL_ERROR, foh, "SET CHAN ATTR: HSN and/or MAIO is missing: "
				"hsn=%u, maio=%u\n", TLVP_LEN(&tp, NM_ATT_HSN), TLVP_LEN(&tp, NM_ATT_MAIO));
			return oml_fom_ack_nack(msg, NM_NACK_ATTRLIST_INCONSISTENT);
		}

		if (!TLVP_PRES_LEN(&tp, NM_ATT_ARFCN_LIST, 2)) { /* At least one ARFCN */
			LOGPFOH(DOML, LOGL_ERROR, foh, "SET CHAN ATTR: ARFCN list is missing\n");
			return oml_fom_ack_nack(msg, NM_NACK_ATTRLIST_INCONSISTENT);
		}

		if (TLVP_LEN(&tp, NM_ATT_ARFCN_LIST) > sizeof(ts->hopping.arfcn_list)) {
			LOGPFOH(DOML, LOGL_ERROR, foh, "SET CHAN ATTR: ARFCN list is too long\n");
			return oml_fom_ack_nack(msg, NM_NACK_ATTRLIST_INCONSISTENT);
		} else if (TLVP_LEN(&tp, NM_ATT_ARFCN_LIST) % 2 != 0) {
			LOGPFOH(DOML, LOGL_ERROR, foh, "SET CHAN ATTR: ARFCN list has odd length\n");
			return oml_fom_ack_nack(msg, NM_NACK_ATTRLIST_INCONSISTENT);
		}

		ts->hopping.enabled = true;
		ts->hopping.hsn = *TLVP_VAL(&tp, NM_ATT_HSN);
		ts->hopping.maio = *TLVP_VAL(&tp, NM_ATT_MAIO);

		ts->hopping.arfcn_num = TLVP_LEN(&tp, NM_ATT_ARFCN_LIST) / sizeof(uint16_t);
		for (i = 0; i < ts->hopping.arfcn_num; i++)
			ts->hopping.arfcn_list[i] = osmo_load16be(TLVP_VAL(&tp, NM_ATT_ARFCN_LIST) + i * 2);
	}

	/* 9.4.52 Starting Time */
	if (TLVP_PRESENT(&tp, NM_ATT_START_TIME)) {
		LOGPFOH(DOML, LOGL_NOTICE, foh, "SET CHAN ATTR: Starting time not supported.\n");
		return oml_fom_ack_nack(msg, NM_NACK_SPEC_IMPL_NOTSUPP);
	}

	/* merge existing BTS attributes with new attributes */
	tp_merged = osmo_tlvp_copy(ts->mo.nm_attr, bts);
	osmo_tlvp_merge(tp_merged, &tp);

	/* Call into BTS driver to check attribute values */
	rc = bts_model_check_oml(bts, foh->msg_type, ts->mo.nm_attr, tp_merged, ts);
	if (rc < 0) {
		LOGPFOH(DOML, LOGL_ERROR, foh, "SET CHAN ATTR: invalid attribute value, rc=%d\n", rc);
		talloc_free(tp_merged);
		/* Send NACK */
		return oml_fom_ack_nack(msg, -rc);
	}

	/* Success: replace old BTS attributes with new */
	talloc_free(ts->mo.nm_attr);
	ts->mo.nm_attr = tp_merged;

	/* 9.4.13 Channel Combination */
	if (TLVP_PRES_LEN(&tp, NM_ATT_CHAN_COMB, 1)) {
		const uint8_t comb = *TLVP_VAL(&tp, NM_ATT_CHAN_COMB);
		if ((rc = handle_chan_comb(ts, comb)) != 0) {
			LOGPFOH(DOML, LOGL_ERROR, foh, "SET CHAN ATTR: invalid Chan Comb 0x%x"
				" (pchan=%s, handle_chan_comb() returns %d)\n",
				comb, gsm_pchan_name(ts->pchan), rc);
			talloc_free(tp_merged);
			/* Send NACK */
			return oml_fom_ack_nack(msg, -rc);
		}
	}

	/* 9.4.60 TSC */
	if (TLVP_PRES_LEN(&tp, NM_ATT_TSC, 1)) {
		ts->tsc_oml = ts->tsc = *TLVP_VAL(&tp, NM_ATT_TSC);
		if (ts->tsc != BTS_TSC(bts) &&
		    !osmo_bts_has_feature(bts->features, BTS_FEAT_MULTI_TSC)) {
			LOGPFOH(DOML, LOGL_ERROR, foh, "SET CHAN ATTR: this BTS model does not "
				"support TSC %u != BSIC-BCC %u\n", ts->tsc, BTS_TSC(bts));
			talloc_free(tp_merged);
			return oml_fom_ack_nack(msg, NM_NACK_PARAM_RANGE);
		}
	} else {
		/* If there is no TSC specified, use the BCC */
		ts->tsc_oml = ts->tsc = BTS_TSC(bts);
	}
	LOGPFOH(DOML, LOGL_INFO, foh, "SET CHAN ATTR (TSC=%u pchan=%s",
		ts->tsc, gsm_pchan_name(ts->pchan));
	if (ts->hopping.enabled)
		LOGPC(DOML, LOGL_INFO, " hsn=%u maio=%u chan_num=%u",
		      ts->hopping.hsn, ts->hopping.maio, ts->hopping.arfcn_num);
	LOGPC(DOML, LOGL_INFO, ")\n");

	/* call into BTS driver to apply new attributes to hardware */
	return bts_model_apply_oml(bts, msg, tp_merged, NM_OC_CHANNEL, ts);
}

/* 8.9.2 Opstart has been received */
static int oml_rx_opstart(struct gsm_bts *bts, struct msgb *msg)
{
	struct abis_om_fom_hdr *foh = msgb_l3(msg);
	struct gsm_abis_mo *mo;
	void *obj;

	DEBUGPFOH(DOML, foh, "Rx OPSTART\n");

	/* Step 1: Resolve MO by obj_class/obj_inst */
	mo = gsm_objclass2mo(bts, foh->obj_class, &foh->obj_inst);
	obj = gsm_objclass2obj(bts, foh->obj_class, &foh->obj_inst);
	if (!mo || !obj)
		return oml_fom_ack_nack(msg, NM_NACK_OBJINST_UNKN);

	/* Step 2: Do some global dependency/consistency checking */
	if (mo->nm_state.operational == NM_OPSTATE_ENABLED) {
		DEBUGPFOH(DOML, foh, "... automatic ACK, OP state already was Enabled\n");
		return oml_mo_opstart_ack(mo);
	}

	/* Step 3: Ask BTS driver to apply the opstart */
	return bts_model_opstart(bts, mo, obj);
}

static int oml_rx_chg_adm_state(struct gsm_bts *bts, struct msgb *msg)
{
	struct abis_om_fom_hdr *foh = msgb_l3(msg);
	struct tlv_parsed tp;
	struct gsm_abis_mo *mo;
	uint8_t adm_state;
	void *obj;
	int rc;

	DEBUGPFOH(DOML, foh, "Rx CHG ADM STATE\n");

	rc = oml_tlv_parse(&tp, foh->data, msgb_l3len(msg) - sizeof(*foh));
	if (rc < 0) {
		LOGPFOH(DOML, LOGL_ERROR, foh, "Rx CHG ADM STATE: error during TLV parse\n");
		return oml_fom_ack_nack(msg, NM_NACK_INCORR_STRUCT);
	}

	if (!TLVP_PRESENT(&tp, NM_ATT_ADM_STATE)) {
		LOGPFOH(DOML, LOGL_ERROR, foh, "Rx CHG ADM STATE: no ADM state attribute\n");
		return oml_fom_ack_nack(msg, NM_NACK_INCORR_STRUCT);
	}

	adm_state = *TLVP_VAL(&tp, NM_ATT_ADM_STATE);

	/* Step 1: Resolve MO by obj_class/obj_inst */
	mo = gsm_objclass2mo(bts, foh->obj_class, &foh->obj_inst);
	obj = gsm_objclass2obj(bts, foh->obj_class, &foh->obj_inst);
	if (!mo || !obj)
		return oml_fom_ack_nack(msg, NM_NACK_OBJINST_UNKN);

	/* Step 2: Do some global dependency/consistency checking */
	if (mo->nm_state.administrative == adm_state) {
		LOGPFOH(DOML, LOGL_NOTICE, foh, "ADM state already was %s\n",
			get_value_string(abis_nm_adm_state_names, adm_state));
		return oml_mo_statechg_ack(mo);
	}
	LOGPFOH(DOML, LOGL_NOTICE, foh, "ADM STATE %s -> %s\n",
		get_value_string(abis_nm_adm_state_names, mo->nm_state.administrative),
		get_value_string(abis_nm_adm_state_names, adm_state));

	/* Step 3: Ask BTS driver to apply the state chg */
	return bts_model_chg_adm_state(bts, mo, obj, adm_state);
}

/* Check and report if the BTS number received via OML is incorrect:
   according to 3GPP TS 52.021 §9.3 BTS number is used to distinguish between different BTS of the same Site Manager.
   As we always have only single BTS per Site Manager (in case of Abis/IP with each BTS having dedicated OML connection
   to BSC), the only valid values are 0 and 0xFF (means all BTS' of a given Site Manager). */
static inline bool report_bts_number_incorrect(struct gsm_bts *bts, const struct abis_om_fom_hdr *foh, bool is_formatted)
{
	struct gsm_bts_trx *trx;
	const struct gsm_abis_mo *mo = &bts->mo;
	const char *form = is_formatted ?
		"Unexpected BTS %d in formatted O&M %s (exp. 0 or 0xFF)" :
		"Unexpected BTS %d in manufacturer O&M %s (exp. 0 or 0xFF)";

	if (foh->obj_inst.bts_nr != 0 && foh->obj_inst.bts_nr != 0xff) {
		trx = gsm_bts_trx_num(bts, foh->obj_inst.trx_nr);
		if (trx)
			mo = &trx->mo;
		oml_tx_failure_event_rep(mo, NM_SEVER_MAJOR, OSMO_EVT_MAJ_UKWN_MSG, form,
					 foh->obj_inst.bts_nr,
					 get_value_string(abis_nm_msgtype_names, foh->msg_type));

		return true;
	}

	return false;
}

static int down_fom(struct gsm_bts *bts, struct msgb *msg)
{
	struct abis_om_hdr *oh = msgb_l2(msg);
	struct abis_om_fom_hdr *foh = msgb_l3(msg);
	struct gsm_bts_trx *trx;
	const struct gsm_abis_mo *mo = &bts->mo;
	int ret;

	if (msgb_l2len(msg) < sizeof(*foh)) {
		trx = gsm_bts_trx_num(bts, foh->obj_inst.trx_nr);
		if (trx)
			mo = &trx->mo;
		oml_tx_failure_event_rep(mo, NM_SEVER_MAJOR, OSMO_EVT_MAJ_UKWN_MSG,
					 "Formatted O&M message too short");
		return -EIO;
	}

	if (msgb_l3len(msg) > oh->length) {
		if (oh->mdisc == ABIS_OM_MDISC_FOM && oh->data[0] == NM_MT_GET_ATTR &&
		    msgb_l3len(msg) == oh->length + 3) {
			/* work-around a bug present in OsmoBSC before February 2019 */
			DEBUGPFOH(DOML, foh, "GET ATTR with off-by-3 length: Fixing up for OS#3799\n");
			oh->length += 3;
		} else {
			LOGPFOH(DOML, LOGL_NOTICE, foh, "OML message with %u extraneous bytes at end: %s\n",
				msgb_l3len(msg) - oh->length, msgb_hexdump(msg));
			/* remove extra bytes at end */
			msgb_l3trim(msg, oh->length);
		}
	}

	if (report_bts_number_incorrect(bts, foh, true))
		return oml_fom_ack_nack(msg, NM_NACK_BTSNR_UNKN);

	switch (foh->msg_type) {
	case NM_MT_SET_BTS_ATTR:
		ret = oml_rx_set_bts_attr(bts, msg);
		break;
	case NM_MT_SET_RADIO_ATTR:
		trx = gsm_bts_trx_num(bts, foh->obj_inst.trx_nr);
		if (!trx)
			return oml_fom_ack_nack(msg, NM_NACK_TRXNR_UNKN);
		ret = oml_rx_set_radio_attr(trx, msg);
		break;
	case NM_MT_SET_CHAN_ATTR:
		trx = gsm_bts_trx_num(bts, foh->obj_inst.trx_nr);
		if (!trx)
			return oml_fom_ack_nack(msg, NM_NACK_TRXNR_UNKN);
		if (foh->obj_inst.ts_nr >= ARRAY_SIZE(trx->ts))
			return oml_fom_ack_nack(msg, NM_NACK_OBJINST_UNKN);
		ret = oml_rx_set_chan_attr(&trx->ts[foh->obj_inst.ts_nr], msg);
		break;
	case NM_MT_OPSTART:
		ret = oml_rx_opstart(bts, msg);
		break;
	case NM_MT_CHG_ADM_STATE:
		ret = oml_rx_chg_adm_state(bts, msg);
		break;
	case NM_MT_IPACC_SET_ATTR:
		ret = oml_ipa_set_attr(bts, msg);
		break;
	case NM_MT_GET_ATTR:
		ret = oml_rx_get_attr(bts, msg);
		break;
	default:
		trx = gsm_bts_trx_num(bts, foh->obj_inst.trx_nr);
		if (trx)
			mo = &trx->mo;
		oml_tx_failure_event_rep(mo, NM_SEVER_MINOR, OSMO_EVT_MAJ_UKWN_MSG,
					 "unknown Formatted O&M msg_type 0x%02x", foh->msg_type);
		ret = oml_fom_ack_nack(msg, NM_NACK_MSGTYPE_INVAL);
	}

	return ret;
}

/*
 * manufacturer related messages
 */

static int oml_ipa_mo_set_attr_nse(void *obj, const struct tlv_parsed *tp)
{
	struct gsm_bts *bts = container_of(obj, struct gsm_bts, gprs.nse);

	if (TLVP_PRES_LEN(tp, NM_ATT_IPACC_NSEI, 2))
		bts->gprs.nse.nsei =
			ntohs(tlvp_val16_unal(tp, NM_ATT_IPACC_NSEI));

	if (TLVP_PRES_LEN(tp, NM_ATT_IPACC_NS_CFG, 7)) {
		memcpy(&bts->gprs.nse.timer,
		       TLVP_VAL(tp, NM_ATT_IPACC_NS_CFG), 7);
	}

	if (TLVP_PRES_LEN(tp, NM_ATT_IPACC_BSSGP_CFG, 11)) {
		memcpy(&bts->gprs.cell.timer,
		       TLVP_VAL(tp, NM_ATT_IPACC_BSSGP_CFG), 11);
	}

	osmo_signal_dispatch(SS_GLOBAL, S_NEW_NSE_ATTR, bts);

	return 0;
}

static int oml_ipa_mo_set_attr_cell(void *obj, const struct tlv_parsed *tp)
{
	struct gsm_bts *bts = container_of(obj, struct gsm_bts, gprs.cell);
	struct gprs_rlc_cfg *rlcc = &bts->gprs.cell.rlc_cfg;
	const uint8_t *cur;
	uint16_t _cur_s;

	if (TLVP_PRES_LEN(tp, NM_ATT_IPACC_RAC, 1))
		bts->gprs.rac = *TLVP_VAL(tp, NM_ATT_IPACC_RAC);

	if (TLVP_PRES_LEN(tp, NM_ATT_IPACC_GPRS_PAGING_CFG, 2)) {
		cur = TLVP_VAL(tp, NM_ATT_IPACC_GPRS_PAGING_CFG);
		rlcc->paging.repeat_time = cur[0] * 50;
		rlcc->paging.repeat_count = cur[1];
	}

	if (TLVP_PRES_LEN(tp, NM_ATT_IPACC_BVCI, 2))
		bts->gprs.cell.bvci =
			ntohs(tlvp_val16_unal(tp, NM_ATT_IPACC_BVCI));

	if (TLVP_PRES_LEN(tp, NM_ATT_IPACC_RLC_CFG, 9)) {
		cur = TLVP_VAL(tp, NM_ATT_IPACC_RLC_CFG);
		rlcc->parameter[RLC_T3142] = cur[0];
		rlcc->parameter[RLC_T3169] = cur[1];
		rlcc->parameter[RLC_T3191] = cur[2];
		rlcc->parameter[RLC_T3193] = cur[3];
		rlcc->parameter[RLC_T3195] = cur[4];
		rlcc->parameter[RLC_N3101] = cur[5];
		rlcc->parameter[RLC_N3103] = cur[6];
		rlcc->parameter[RLC_N3105] = cur[7];
		rlcc->parameter[CV_COUNTDOWN] = cur[8];
	}

	if (TLVP_PRES_LEN(tp, NM_ATT_IPACC_CODING_SCHEMES, 2)) {
		int i;
		rlcc->cs_mask = 0;
		cur = TLVP_VAL(tp, NM_ATT_IPACC_CODING_SCHEMES);

		for (i = 0; i < 4; i++) {
			if (cur[0] & (1 << i))
				rlcc->cs_mask |= (1 << (GPRS_CS1+i));
		}
		if (cur[0] & 0x80)
			rlcc->cs_mask |= (1 << GPRS_MCS9);
		for (i = 0; i < 8; i++) {
			if (cur[1] & (1 << i))
				rlcc->cs_mask |= (1 << (GPRS_MCS1+i));
		}
	}

	if (TLVP_PRES_LEN(tp, NM_ATT_IPACC_RLC_CFG_2, 5)) {
		cur = TLVP_VAL(tp, NM_ATT_IPACC_RLC_CFG_2);
		memcpy(&_cur_s, cur, 2);
		rlcc->parameter[T_DL_TBF_EXT] = ntohs(_cur_s) * 10;
		cur += 2;
		memcpy(&_cur_s, cur, 2);
		rlcc->parameter[T_UL_TBF_EXT] = ntohs(_cur_s) * 10;
		cur += 2;
		rlcc->initial_cs = *cur;
	}

	if (TLVP_PRES_LEN(tp, NM_ATT_IPACC_RLC_CFG_3, 1)) {
		rlcc->initial_mcs = *TLVP_VAL(tp, NM_ATT_IPACC_RLC_CFG_3);
	}

	osmo_signal_dispatch(SS_GLOBAL, S_NEW_CELL_ATTR, bts);

	return 0;
}

static int oml_ipa_mo_set_attr_nsvc(struct gsm_bts_gprs_nsvc *nsvc,
				    const struct tlv_parsed *tp)
{
	if (TLVP_PRES_LEN(tp, NM_ATT_IPACC_NSVCI, 2))
		nsvc->nsvci = ntohs(tlvp_val16_unal(tp, NM_ATT_IPACC_NSVCI));

	if (TLVP_PRES_LEN(tp, NM_ATT_IPACC_NS_LINK_CFG, 8)) {
		const uint8_t *cur = TLVP_VAL(tp, NM_ATT_IPACC_NS_LINK_CFG);
		uint16_t _cur_s;
		uint32_t _cur_l;

		memset(&nsvc->local, 0, sizeof(nsvc->local));
		memset(&nsvc->remote, 0, sizeof(nsvc->remote));

		nsvc->local.u.sin.sin_family = AF_INET;
		nsvc->remote.u.sin.sin_family = AF_INET;

		memcpy(&_cur_s, cur, 2);
		nsvc->remote.u.sin.sin_port = _cur_s;
		cur += 2;
		memcpy(&_cur_l, cur, 4);
		nsvc->remote.u.sin.sin_addr.s_addr = _cur_l;
		cur += 4;
		memcpy(&_cur_s, cur, 2);
		nsvc->local.u.sin.sin_port = _cur_s;
	}

	if (TLVP_PRES_LEN(tp, NM_ATT_OSMO_NS_LINK_CFG, 10)) {
		const uint8_t *cur = TLVP_VAL(tp, NM_ATT_OSMO_NS_LINK_CFG);
		uint8_t address_family;

		memset(&nsvc->local, 0, sizeof(nsvc->local));
		memset(&nsvc->remote, 0, sizeof(nsvc->remote));

		address_family = *cur;
		/* 1byte padding */
		cur += 2;

		memcpy(&nsvc->local.u.sin.sin_port, cur, 2);
		cur += 2;

		memcpy(&nsvc->remote.u.sin.sin_port, cur, 2);
		cur += 2;

		switch (address_family) {
		case OSMO_NSVC_ADDR_IPV4:
			/* we already checked for 10 bytes */
			nsvc->remote.u.sas.ss_family = AF_INET;
			nsvc->local.u.sas.ss_family = AF_INET;
			memcpy(&nsvc->remote.u.sin.sin_addr.s_addr, cur, sizeof(in_addr_t));
			break;
		case OSMO_NSVC_ADDR_IPV6:
			if (TLVP_LEN(tp, NM_ATT_OSMO_NS_LINK_CFG) < 22) {
				return -1;
			}
			nsvc->remote.u.sas.ss_family = AF_INET6;
			nsvc->local.u.sas.ss_family = AF_INET6;
			memcpy(&nsvc->remote.u.sin6.sin6_addr, cur, sizeof(struct in6_addr));
			break;
		default:
			return -1;
		}
	}


	osmo_signal_dispatch(SS_GLOBAL, S_NEW_NSVC_ATTR, nsvc);

	return 0;
}

static int oml_ipa_mo_set_attr(struct gsm_bts *bts, const struct gsm_abis_mo *mo,
				void *obj, const struct tlv_parsed *tp)
{
	int rc;

	switch (mo->obj_class) {
	case NM_OC_GPRS_NSE:
		rc = oml_ipa_mo_set_attr_nse(obj, tp);
		break;
	case NM_OC_GPRS_CELL:
		rc = oml_ipa_mo_set_attr_cell(obj, tp);
		break;
	case NM_OC_GPRS_NSVC:
		rc = oml_ipa_mo_set_attr_nsvc(obj, tp);
		break;
	default:
		rc = NM_NACK_OBJINST_UNKN;
	}

	return rc;
}

static int oml_ipa_set_attr(struct gsm_bts *bts, struct msgb *msg)
{
	struct abis_om_fom_hdr *foh = msgb_l3(msg);
	struct gsm_abis_mo *mo;
	struct tlv_parsed tp, *tp_merged;
	void *obj;
	int rc;

	DEBUGPFOH(DOML, foh, "Rx IPA SET ATTR\n");

	rc = oml_tlv_parse(&tp, foh->data, msgb_l3len(msg) - sizeof(*foh));
	if (rc < 0) {
		mo = gsm_objclass2mo(bts, foh->obj_class, &foh->obj_inst);
		if (!mo)
			return oml_fom_ack_nack(msg, NM_NACK_OBJINST_UNKN);
		oml_tx_failure_event_rep(mo, NM_SEVER_MAJOR, OSMO_EVT_MAJ_UNSUP_ATTR,
					 "New value for IPAC Set Attribute not supported\n");
		return oml_fom_ack_nack(msg, NM_NACK_INCORR_STRUCT);
	}

	/* Resolve MO by obj_class/obj_inst */
	mo = gsm_objclass2mo(bts, foh->obj_class, &foh->obj_inst);
	obj = gsm_objclass2obj(bts, foh->obj_class, &foh->obj_inst);
	if (!mo || !obj)
		return oml_fom_ack_nack(msg, NM_NACK_OBJINST_UNKN);

	rc = oml_ipa_mo_set_attr(bts, mo, obj, &tp);
	if (rc == 0) {
		/* Success: replace old MO attributes with new */
		/* merge existing MO attributes with new attributes */
		tp_merged = osmo_tlvp_copy(mo->nm_attr, bts);
		osmo_tlvp_merge(tp_merged, &tp);
		talloc_free(mo->nm_attr);
		mo->nm_attr = tp_merged;
	}

	return oml_fom_ack_nack(msg, rc);
}

static int rx_oml_ipa_rsl_connect(struct gsm_bts_trx *trx, struct msgb *msg,
				  const struct tlv_parsed *tp)
{
	struct e1inp_sign_link *oml_link = trx->bts->oml_link;
	uint16_t port = IPA_TCP_PORT_RSL;
	const char *trx_name = gsm_trx_name(trx);
	struct in_addr in;
	int rc;

	uint8_t stream_id = 0;

	if (TLVP_PRESENT(tp, NM_ATT_IPACC_DST_IP))
		in.s_addr = tlvp_val32_unal(tp, NM_ATT_IPACC_DST_IP);
	else
		in.s_addr = htonl(get_signlink_remote_ip(oml_link));

	if (TLVP_PRESENT(tp, NM_ATT_IPACC_DST_IP_PORT))
		port = ntohs(tlvp_val16_unal(tp, NM_ATT_IPACC_DST_IP_PORT));
	if (TLVP_PRESENT(tp, NM_ATT_IPACC_STREAM_ID))
		stream_id = *TLVP_VAL(tp, NM_ATT_IPACC_STREAM_ID);

	LOGP(DOML, LOGL_INFO, "%s: Rx IPA RSL CONNECT IP=%s PORT=%u STREAM=0x%02x\n",
	     trx_name, inet_ntoa(in), port, stream_id);

	if (trx->bts->variant == BTS_OSMO_OMLDUMMY) {
		rc = 0;
		LOGP(DOML, LOGL_NOTICE, "%s: Not connecting RSL in OML-DUMMY!\n", trx_name);
	} else {
		trx->rsl_tei = stream_id;
		rc = e1inp_ipa_bts_rsl_connect_n(oml_link->ts->line, inet_ntoa(in), port, trx->nr);
	}

	/* The ACK/NACK is expected to contain all IEs */
	if (!TLVP_PRESENT(tp, NM_ATT_IPACC_DST_IP)) /* TV32 */
		msgb_tv_fixed_put(msg, NM_ATT_IPACC_DST_IP, sizeof(in),
				  (const uint8_t *) &in);
	if (!TLVP_PRESENT(tp, NM_ATT_IPACC_DST_IP_PORT)) /* TV16 */
		msgb_tv16_put(msg, NM_ATT_IPACC_DST_IP_PORT, port);
	if (!TLVP_PRESENT(tp, NM_ATT_IPACC_STREAM_ID)) /* TV */
		msgb_tv_put(msg, NM_ATT_IPACC_STREAM_ID, stream_id);

	if (rc < 0) {
		LOGP(DOML, LOGL_ERROR, "%s: Error in abis_open(RSL): %d\n", trx_name, rc);
		return oml_fom_ack_nack(msg, NM_NACK_CANT_PERFORM);
	}

	return oml_fom_ack_nack(msg, 0);
}

static int down_mom(struct gsm_bts *bts, struct msgb *msg)
{
	struct abis_om_hdr *oh = msgb_l2(msg);
	const struct gsm_abis_mo *mo = &bts->mo;
	struct abis_om_fom_hdr *foh;
	struct gsm_bts_trx *trx;
	uint8_t idstrlen = oh->data[0];
	struct tlv_parsed tp;
	int ret;

	if (msgb_l2len(msg) < sizeof(*foh)) {
		oml_tx_failure_event_rep(&bts->mo, NM_SEVER_MAJOR, OSMO_EVT_MAJ_UKWN_MSG,
					 "Manufacturer O&M message too short\n");
		return -EIO;
	}

	if (strncmp((char *)&oh->data[1], abis_nm_ipa_magic, idstrlen)) {
		oml_tx_failure_event_rep(&bts->mo, NM_SEVER_MAJOR, OSMO_EVT_MAJ_UKWN_MSG,
					 "Manufacturer OML message != ipaccess not supported\n");
		return -EINVAL;
	}

	if (msgb_l3len(msg) > oh->length + 1 + oh->data[0]) {
		LOGP(DOML, LOGL_NOTICE, "OML message with %u extraneous bytes at end: %s\n",
			msgb_l3len(msg) - oh->length, msgb_hexdump(msg));
		/* remove extra bytes at end */
		msgb_l3trim(msg, oh->length);
	}

	msg->l3h = oh->data + 1 + idstrlen;
	foh = (struct abis_om_fom_hdr *) msg->l3h;

	if (report_bts_number_incorrect(bts, foh, false))
		return oml_fom_ack_nack(msg, NM_NACK_BTSNR_UNKN);

	ret = oml_tlv_parse(&tp, foh->data, oh->length - sizeof(*foh));
	if (ret < 0) {
		LOGPFOH(DOML, LOGL_ERROR, foh, "TLV parse error %d\n", ret);
		return oml_fom_ack_nack(msg, NM_NACK_BTSNR_UNKN);
	}

	DEBUGPFOH(DOML, foh, "Rx IPACCESS(0x%02x): %s\n", foh->msg_type,
		  osmo_hexdump(foh->data, oh->length - sizeof(*foh)));

	switch (foh->msg_type) {
	case NM_MT_IPACC_RSL_CONNECT:
		trx = gsm_bts_trx_num(bts, foh->obj_inst.trx_nr);
		ret = rx_oml_ipa_rsl_connect(trx, msg, &tp);
		break;
	case NM_MT_IPACC_SET_ATTR:
		ret = oml_ipa_set_attr(bts, msg);
		break;
	default:
		trx = gsm_bts_trx_num(bts, foh->obj_inst.trx_nr);
		if (trx)
			mo = &trx->mo;
		oml_tx_failure_event_rep(mo, NM_SEVER_MINOR, OSMO_EVT_MAJ_UKWN_MSG,
					 "unknown Manufacturer O&M msg_type 0x%02x", foh->msg_type);
		ret = oml_fom_ack_nack(msg, NM_NACK_MSGTYPE_INVAL);
	}

	return ret;
}

/* incoming OML message from BSC */
int down_oml(struct gsm_bts *bts, struct msgb *msg)
{
	struct abis_om_hdr *oh = msgb_l2(msg);
	int ret = 0;

	if (msgb_l2len(msg) < sizeof(*oh)) {
		oml_tx_failure_event_rep(&bts->mo, NM_SEVER_MAJOR, OSMO_EVT_MAJ_UKWN_MSG,
					 "OML message too short\n");
		msgb_free(msg);
		return -EIO;
	}
	msg->l3h = (unsigned char *)oh + sizeof(*oh);

	/* We don't implement de-segmentation of segmented OML messages */
	if (oh->placement != ABIS_OM_PLACEMENT_ONLY || oh->sequence != 0) {
		oml_tx_failure_event_rep(&bts->mo, NM_SEVER_MAJOR, OSMO_EVT_MAJ_UKWN_MSG,
					 "Unsupported segmented O&M message\n");
		msgb_free(msg);
		return -EIO;
	}

	if (msgb_l3len(msg) < oh->length) {
		oml_tx_failure_event_rep(&bts->mo, NM_SEVER_MAJOR, OSMO_EVT_MAJ_UKWN_MSG,
					 "Short OML message: %u < %u\n",
					 msgb_l3len(msg), oh->length);
		msgb_free(msg);
		return -EIO;
	}

	switch (oh->mdisc) {
	case ABIS_OM_MDISC_FOM:
		if (msgb_l2len(msg) < sizeof(*oh)) {
			oml_tx_failure_event_rep(&bts->mo, NM_SEVER_MAJOR, OSMO_EVT_MAJ_UKWN_MSG,
						"Formatted O&M message too short\n");
			ret = -EIO;
			break;
		}
		ret = down_fom(bts, msg);
		break;
	case ABIS_OM_MDISC_MANUF:
		if (msgb_l2len(msg) < sizeof(*oh)) {
			oml_tx_failure_event_rep(&bts->mo, NM_SEVER_MAJOR, OSMO_EVT_MAJ_UKWN_MSG,
						"Manufacturer O&M message too short\n");
			ret = -EIO;
			break;
		}
		ret = down_mom(bts, msg);
		break;
	default:
		oml_tx_failure_event_rep(&bts->mo, NM_SEVER_MINOR, OSMO_EVT_MAJ_UKWN_MSG,
					 "unknown O&M msg_disc 0x%02x\n", oh->mdisc);
		ret = -EINVAL;
	}

	/* msgb was reused, do not free() */
	if (ret == 1)
		return 0;

	msgb_free(msg);

	return ret;
}

int oml_init()
{
	DEBUGP(DOML, "Initializing OML attribute definitions\n");
	tlv_def_patch(&abis_nm_att_tlvdef_ipa_local, &abis_nm_att_tlvdef_ipa);
	tlv_def_patch(&abis_nm_att_tlvdef_ipa_local, &abis_nm_att_tlvdef);
	tlv_def_patch(&abis_nm_att_tlvdef_ipa_local, &abis_nm_osmo_att_tlvdef);

	return 0;
}

void gsm_mo_init(struct gsm_abis_mo *mo, struct gsm_bts *bts,
		 uint8_t obj_class, uint8_t p1, uint8_t p2, uint8_t p3)
{
	mo->bts = bts;
	mo->obj_class = obj_class;
	mo->obj_inst.bts_nr = p1;
	mo->obj_inst.trx_nr = p2;
	mo->obj_inst.ts_nr = p3;
	mo->nm_state.operational = NM_OPSTATE_DISABLED;
	mo->nm_state.availability = NM_AVSTATE_POWER_OFF;
	mo->nm_state.administrative = NM_STATE_LOCKED;
}

/* obtain the MO structure for a given object instance */
struct gsm_abis_mo *
gsm_objclass2mo(struct gsm_bts *bts, uint8_t obj_class,
	    const struct abis_om_obj_inst *obj_inst)
{
	struct gsm_bts_trx *trx;
	struct gsm_abis_mo *mo = NULL;

	switch (obj_class) {
	case NM_OC_BTS:
		mo = &bts->mo;
		break;
	case NM_OC_RADIO_CARRIER:
		if (obj_inst->trx_nr >= bts->num_trx) {
			return NULL;
		}
		trx = gsm_bts_trx_num(bts, obj_inst->trx_nr);
		mo = &trx->mo;
		break;
	case NM_OC_BASEB_TRANSC:
		if (obj_inst->trx_nr >= bts->num_trx) {
			return NULL;
		}
		trx = gsm_bts_trx_num(bts, obj_inst->trx_nr);
		mo = &trx->bb_transc.mo;
		break;
	case NM_OC_CHANNEL:
		if (obj_inst->trx_nr >= bts->num_trx) {
			return NULL;
		}
		trx = gsm_bts_trx_num(bts, obj_inst->trx_nr);
		if (obj_inst->ts_nr >= TRX_NR_TS)
			return NULL;
		mo = &trx->ts[obj_inst->ts_nr].mo;
		break;
	case NM_OC_SITE_MANAGER:
		mo = &bts->site_mgr.mo;
		break;
	case NM_OC_GPRS_NSE:
		mo = &bts->gprs.nse.mo;
		break;
	case NM_OC_GPRS_CELL:
		mo = &bts->gprs.cell.mo;
		break;
	case NM_OC_GPRS_NSVC:
		if (obj_inst->trx_nr >= ARRAY_SIZE(bts->gprs.nsvc))
			return NULL;
		mo = &bts->gprs.nsvc[obj_inst->trx_nr].mo;
		break;
	}
	return mo;
}

/* obtain the gsm_nm_state data structure for a given object instance */
struct gsm_nm_state *
gsm_objclass2nmstate(struct gsm_bts *bts, uint8_t obj_class,
		 const struct abis_om_obj_inst *obj_inst)
{
	struct gsm_abis_mo *mo;

	mo = gsm_objclass2mo(bts, obj_class, obj_inst);
	if (!mo)
		return NULL;

	return &mo->nm_state;
}

/* obtain the in-memory data structure of a given object instance */
void *
gsm_objclass2obj(struct gsm_bts *bts, uint8_t obj_class,
	     const struct abis_om_obj_inst *obj_inst)
{
	struct gsm_bts_trx *trx;
	void *obj = NULL;

	switch (obj_class) {
	case NM_OC_BTS:
		obj = bts;
		break;
	case NM_OC_RADIO_CARRIER:
		if (obj_inst->trx_nr >= bts->num_trx) {
			return NULL;
		}
		trx = gsm_bts_trx_num(bts, obj_inst->trx_nr);
		obj = trx;
		break;
	case NM_OC_BASEB_TRANSC:
		if (obj_inst->trx_nr >= bts->num_trx) {
			return NULL;
		}
		trx = gsm_bts_trx_num(bts, obj_inst->trx_nr);
		obj = &trx->bb_transc;
		break;
	case NM_OC_CHANNEL:
		if (obj_inst->trx_nr >= bts->num_trx) {
			return NULL;
		}
		trx = gsm_bts_trx_num(bts, obj_inst->trx_nr);
		if (obj_inst->ts_nr >= TRX_NR_TS)
			return NULL;
		obj = &trx->ts[obj_inst->ts_nr];
		break;
	case NM_OC_SITE_MANAGER:
		obj = &bts->site_mgr;
		break;
	case NM_OC_GPRS_NSE:
		obj = &bts->gprs.nse;
		break;
	case NM_OC_GPRS_CELL:
		obj = &bts->gprs.cell;
		break;
	case NM_OC_GPRS_NSVC:
		if (obj_inst->trx_nr >= ARRAY_SIZE(bts->gprs.nsvc))
			return NULL;
		obj = &bts->gprs.nsvc[obj_inst->trx_nr];
		break;
	}
	return obj;
}
