/* TRX provision FSM */

/* (C) 2020 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
 * Author: Pau Espin Pedrol <pespin@sysmocom.de>
 *
 * 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 Affero 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/>.
 *
 */

#include <errno.h>
#include <unistd.h>
#include <inttypes.h>

#include <osmocom/core/fsm.h>
#include <osmocom/core/tdef.h>
#include <osmocom/gsm/protocol/gsm_12_21.h>

#include <osmo-bts/logging.h>
#include <osmo-bts/gsm_data.h>
#include <osmo-bts/bts_model.h>
#include <osmo-bts/bts.h>
#include <osmo-bts/rsl.h>
#include <osmo-bts/nm_common_fsm.h>
#include <osmo-bts/signal.h>
#include <osmo-bts/tx_power.h>

#include "l1_if.h"
#include "trx_provision_fsm.h"

#define X(s) (1 << (s))

#define trx_prov_fsm_state_chg(fi, NEXT_STATE) \
	osmo_fsm_inst_state_chg(fi, NEXT_STATE, 0, 0)

static void l1if_poweronoff_cb(struct trx_l1h *l1h, bool poweronoff, int rc)
{
	struct phy_instance *pinst = l1h->phy_inst;
	struct phy_link *plink = pinst->phy_link;

	plink->u.osmotrx.powered = poweronoff;

	if (poweronoff) {
		plink->u.osmotrx.poweron_sent = false;
		osmo_fsm_inst_dispatch(l1h->provision_fi, TRX_PROV_EV_POWERON_CNF, (void*)(intptr_t)rc);
	} else {
		plink->u.osmotrx.poweroff_sent = false;
		osmo_fsm_inst_dispatch(l1h->provision_fi, TRX_PROV_EV_POWEROFF_CNF, (void*)(intptr_t)rc);
	}
}


static void l1if_rxtune_cb(struct trx_l1h *l1h, int rc)
{
	if (rc != 0)
		return;
	osmo_fsm_inst_dispatch(l1h->provision_fi, TRX_PROV_EV_RXTUNE_CNF, NULL);
}

static void l1if_txtune_cb(struct trx_l1h *l1h, int rc)
{
	if (rc != 0)
		return;
	osmo_fsm_inst_dispatch(l1h->provision_fi, TRX_PROV_EV_TXTUNE_CNF, NULL);
}

static void l1if_settsc_cb(struct trx_l1h *l1h, int rc)
{
	if (rc != 0)
		return;
	osmo_fsm_inst_dispatch(l1h->provision_fi, TRX_PROV_EV_SETTSC_CNF, NULL);
}

static void l1if_setbsic_cb(struct trx_l1h *l1h, int rc)
{
	if (rc != 0)
		return;
	osmo_fsm_inst_dispatch(l1h->provision_fi, TRX_PROV_EV_SETBSIC_CNF, NULL);
}

static void l1if_getnompower_cb(struct trx_l1h *l1h, int nominal_power, int rc)
{
	struct phy_instance *pinst = l1h->phy_inst;
	LOGPPHI(pinst, DL1C, LOGL_DEBUG, "l1if_getnompower_cb(nominal_power=%d, rc=%d)\n", nominal_power, rc);
	osmo_fsm_inst_dispatch(l1h->provision_fi, TRX_PROV_EV_NOMTXPOWER_CNF, (void*)(intptr_t)nominal_power);
}

static void l1if_setformat_cb(struct trx_l1h *l1h, int rc)
{
	osmo_fsm_inst_dispatch(l1h->provision_fi, TRX_PROV_EV_SETFORMAT_CNF, (void*)(intptr_t)rc);
}

static void l1if_setpower_att_cb(struct trx_l1h *l1h, int power_att_db, int rc)
{
	if (rc != 0)
		return;
	osmo_fsm_inst_dispatch(l1h->provision_fi, TRX_PROV_EV_SETPOWER_CNF, NULL);
}

/*
 * transceiver provisioning
 */

static void trx_provision_reset(struct trx_l1h *l1h)
{
	struct phy_instance *pinst = l1h->phy_inst;
	uint8_t tn;

	l1h->config.trxd_pdu_ver_req = pinst->phy_link->u.osmotrx.trxd_pdu_ver_max;
	l1h->config.trxd_pdu_ver_use = 0;
	l1h->config.setformat_sent = false;
	l1h->config.setformat_acked = false;

	l1h->config.enabled = false;
	l1h->config.arfcn_valid = false;
	l1h->config.arfcn = 0;
	l1h->config.rxtune_sent = false;
	l1h->config.rxtune_acked = false;
	l1h->config.txtune_sent = false;
	l1h->config.txtune_acked = false;

	l1h->config.tsc_valid = false;
	l1h->config.tsc = 0;
	l1h->config.tsc_sent = false;
	l1h->config.tsc_acked = false;

	l1h->config.bsic_valid = false;
	l1h->config.bsic = 0;
	l1h->config.bsic_sent = false;
	l1h->config.bsic_acked = false;

	l1h->config.rxgain_sent = false;

	l1h->config.nomtxpower_sent = false;
	l1h->config.nomtxpower_acked = false;

	l1h->config.setpower_sent = false;
	l1h->config.setpower_acked = false;

	l1h->config.maxdly_sent = false;

	l1h->config.maxdlynb_sent = false;

	for (tn = 0; tn < TRX_NR_TS; tn++) {
		l1h->config.setslot_valid[tn] = false;
		l1h->config.setslot_sent[tn] = false;
		l1h->config.setslot[tn].slottype = 0;
		l1h->config.setslot[tn].tsc_set = 0;
		l1h->config.setslot[tn].tsc_val = 0;
		l1h->config.setslot[tn].tsc_valid = 0;
	}
}
int l1if_provision_transceiver_trx(struct trx_l1h *l1h)
{
	struct phy_instance *pinst = l1h->phy_inst;
	struct phy_link *plink = pinst->phy_link;

	/* During setup, pinst may still not be associated to a TRX nr */
	if (!pinst->trx) {
		LOGPPHI(pinst, DL1C, LOGL_INFO,
			"Delaying provision, TRX not yet assigned to phy instance\n");
		return -EIO;
	}

	if (phy_link_state_get(plink) == PHY_LINK_SHUTDOWN) {
		LOGPPHI(pinst, DL1C, LOGL_INFO,
			"Delaying provision, TRX not yet available\n");
		return -EIO;
	}

	/* before power on */
	if (l1h->config.arfcn_valid) {
		if (!l1h->config.rxtune_sent) {
			trx_if_cmd_rxtune(l1h, l1h->config.arfcn, l1if_rxtune_cb);
			l1h->config.rxtune_sent = true;
			l1h->config.rxtune_acked = false;
		}
		if (!l1h->config.txtune_sent) {
			trx_if_cmd_txtune(l1h, l1h->config.arfcn, l1if_txtune_cb);
			l1h->config.txtune_sent = true;
			l1h->config.txtune_acked = false;
		}
		if (l1h->config.txtune_acked) {
			/* After TXTUNE is sent to TRX, get the tx nominal power
			 * (which may vary precisly on band/arfcn. Avoid sending
			 * it if we are forced by VTY to use a specific nominal
			 * power (because TRX may not support the command or
			 * provide broken values) */
			if (!l1h->config.nominal_power_set_by_vty && !l1h->config.nomtxpower_sent) {
				trx_if_cmd_getnompower(l1h, l1if_getnompower_cb);
				l1h->config.nomtxpower_sent = true;
				l1h->config.nomtxpower_acked = false;
			}
		}
	}
	if (!pinst->phy_link->u.osmotrx.use_legacy_setbsic &&
	    l1h->config.tsc_valid && !l1h->config.tsc_sent) {
		trx_if_cmd_settsc(l1h, l1h->config.tsc, l1if_settsc_cb);
		l1h->config.tsc_sent = true;
		l1h->config.tsc_acked = false;
	}
	if (pinst->phy_link->u.osmotrx.use_legacy_setbsic &&
	    l1h->config.bsic_valid && !l1h->config.bsic_sent) {
		trx_if_cmd_setbsic(l1h, l1h->config.bsic, l1if_setbsic_cb);
		l1h->config.bsic_sent = true;
		l1h->config.bsic_acked = false;
	}
	/* Once the nominal Tx power is known, apply the initial Tx power attenuation
	 * before the transceiver is powered on.  Otherwise it will be transmitting
	 * at full power for a certain period of time before the power ramping begins. */
	if (!l1h->config.setpower_sent &&
	    (l1h->config.nomtxpower_acked || l1h->config.nominal_power_set_by_vty)) {
		int initial_mdBm = power_ramp_initial_power_mdBm(pinst->trx);
		int power_att_dB = (get_p_max_out_mdBm(pinst->trx) - initial_mdBm) / 1000;

		trx_if_cmd_setpower_att(l1h, power_att_dB, l1if_setpower_att_cb);
		l1h->config.setpower_sent = true;
		l1h->config.setpower_acked = false;
	}

	/* Ask transceiver to use the newest TRXD PDU version if not using it yet */
	if (!l1h->config.setformat_sent) {
		l1h->config.setformat_sent = true;
		if (plink->u.osmotrx.trxd_pdu_ver_max == 0) {
			LOGPPHI(pinst, DL1C, LOGL_INFO,
				"No need to negotiate max TRXD version 0\n");
			l1h->config.trxd_pdu_ver_use = 0;
			l1h->config.setformat_acked = true;
		} else {
			trx_if_cmd_setformat(l1h, l1h->config.trxd_pdu_ver_req, l1if_setformat_cb);
			l1h->config.setformat_acked = false;
		}
	}
	return 0;
}

static void l1if_setslot_cb(struct trx_l1h *l1h, uint8_t tn, uint8_t type, int rc)
{
	struct phy_instance *pinst = l1h->phy_inst;
	struct gsm_bts_trx *trx = pinst->trx;
	struct gsm_bts_trx_ts *ts;
	enum gsm_phys_chan_config pchan;

	if (tn >= TRX_NR_TS) {
		LOGPPHI(pinst, DL1C, LOGL_ERROR, "transceiver SETSLOT invalid param TN (%" PRIu8 ")\n",
			tn);
		return;
	}

	pchan = transceiver_chan_type_2_pchan(type);
	if (pchan == GSM_PCHAN_UNKNOWN) {
		LOGPPHI(pinst, DL1C, LOGL_ERROR, "transceiver SETSLOT invalid param TS_TYPE (%" PRIu8 ")\n",
			type);
		return;
	}

	ts = &trx->ts[tn];
	LOGPPHI(pinst, DL1C, LOGL_DEBUG, "%s l1if_setslot_cb(as_pchan=%s),"
	     " calling cb_ts_connected(rc=%d)\n",
	     gsm_ts_name(ts), gsm_pchan_name(pchan), rc);
	cb_ts_connected(ts, rc);
}

static void update_ts_data(struct trx_l1h *l1h, struct trx_prov_ev_cfg_ts_data *data)
{
	l1h->config.setslot[data->tn].slottype = data->slottype;
	l1h->config.setslot[data->tn].tsc_set = data->tsc_set;
	l1h->config.setslot[data->tn].tsc_val = data->tsc_val;
	l1h->config.setslot[data->tn].tsc_valid = data->tsc_valid;

	l1h->config.setslot_valid[data->tn] = true;
	l1h->config.setslot_sent[data->tn] = false;
}

/* Whether a given TRX is fully configured */
static bool trx_is_provisioned(struct trx_l1h *l1h)
{
	struct phy_instance *pinst = l1h->phy_inst;
	if (l1h->config.rxtune_acked && l1h->config.txtune_acked &&
	    (l1h->config.bsic_acked || !pinst->phy_link->u.osmotrx.use_legacy_setbsic) &&
	    (l1h->config.tsc_acked || pinst->phy_link->u.osmotrx.use_legacy_setbsic) &&
	    (l1h->config.nomtxpower_acked || l1h->config.nominal_power_set_by_vty) &&
	    (l1h->config.setpower_acked) &&
	    (l1h->config.setformat_acked)) {
		    return true;
	}
	return false;
}

/* Whether a given TRX is fully configured and can be powered on */
static bool trx_is_provisioned_and_enabled(struct trx_l1h *l1h)
{
	return l1h->config.enabled && trx_is_provisioned(l1h);
}

static void trx_signal_ready_trx0(struct trx_l1h *l1h)
{
	struct phy_instance *pinst = l1h->phy_inst;
	struct phy_instance *pinst_it;

	llist_for_each_entry(pinst_it, &pinst->phy_link->instances, list) {
		struct trx_l1h *l1h_it = pinst_it->u.osmotrx.hdl;
		if (l1h_it->phy_inst->num != 0)
			continue;
		osmo_fsm_inst_dispatch(l1h_it->provision_fi, TRX_PROV_EV_OTHER_TRX_READY, NULL);
		return;
	}
}

/* Called from TRX0 to check if other TRX are already configured so POWERON can be sent */
static bool trx_other_trx0_ready(struct trx_l1h *l1h)
{
	struct phy_instance *pinst = l1h->phy_inst;
	struct phy_instance *pinst_it;

	/* Don't POWERON until all trx are ready */
	llist_for_each_entry(pinst_it, &pinst->phy_link->instances, list) {
		struct trx_l1h *l1h_it = pinst_it->u.osmotrx.hdl;
		if (l1h_it->phy_inst->num == 0)
			continue;
		if (!trx_is_provisioned(l1h_it))
			return false;
	}
	return true;
}

/* Closes a phy_link and all its associated TRX */
static void trx_prov_fsm_apply_close(struct phy_link *plink, int rc)
{
	struct trx_l1h *l1h;
	struct phy_instance *pinst;

	if (plink->state == PHY_LINK_SHUTDOWN)
		return;

	bts_model_phy_link_close(plink);
	/* Notify TRX close on all TRX associated with this phy */
	llist_for_each_entry(pinst, &plink->instances, list) {
		l1h = pinst->u.osmotrx.hdl;
		trx_prov_fsm_state_chg(l1h->provision_fi, TRX_PROV_ST_CLOSED);
		bts_model_trx_close_cb(pinst->trx, rc);
	}
}

static int trx_prov_fsm_signal_cb(unsigned int subsys, unsigned int signal,
				  void *hdlr_data, void *signal_data)
{
	struct nm_statechg_signal_data *nsd;
	struct gsm_bts_trx *trx;

	if (subsys != SS_GLOBAL)
		return -EINVAL;

	if (signal != S_NEW_OP_STATE)
		return 0;

	nsd = (struct nm_statechg_signal_data *)signal_data;

	if (nsd->mo->obj_class != NM_OC_RADIO_CARRIER)
		return 0;

	if (nsd->old_state != NM_OPSTATE_ENABLED && nsd->new_state == NM_OPSTATE_ENABLED) {
		trx = gsm_objclass2obj(nsd->mo->bts, nsd->mo->obj_class, &nsd->mo->obj_inst, NULL);
		l1if_trx_start_power_ramp(trx, NULL);
	}
	return 0;
}

//////////////////////////
// FSM STATE ACTIONS
//////////////////////////

static void st_closed(struct osmo_fsm_inst *fi, uint32_t event, void *data)
{
	struct trx_l1h *l1h = (struct trx_l1h *)fi->priv;

	switch (event) {
	case TRX_PROV_EV_OPEN:
		/* enable all slots */
		l1h->config.slotmask = 0xff;
		if (l1h->phy_inst->num == 0)
			trx_if_cmd_poweroff(l1h, NULL); /* TODO: jump to poweroff upon cb received */
		trx_prov_fsm_state_chg(fi, TRX_PROV_ST_OPEN_POWEROFF);
		break;
	default:
		OSMO_ASSERT(0);
	}
}

static void st_open_poweroff_on_enter(struct osmo_fsm_inst *fi, uint32_t prev_state)
{
	struct trx_l1h *l1h = (struct trx_l1h *)fi->priv;
	struct phy_instance *pinst = l1h->phy_inst;

	trx_provision_reset(l1h);

	if (pinst->trx == NULL) {
		trx_if_cmd_rfmute(l1h, true);
		return;
	}

	/* Apply initial RFMUTE state */
	trx_if_cmd_rfmute(l1h, pinst->trx->mo.nm_state.administrative != NM_STATE_UNLOCKED);

	osmo_fsm_inst_dispatch(pinst->trx->mo.fi, NM_EV_SW_ACT, NULL);
	osmo_fsm_inst_dispatch(pinst->trx->bb_transc.mo.fi, NM_EV_SW_ACT, NULL);
}

static void st_open_poweroff(struct osmo_fsm_inst *fi, uint32_t event, void *data)
{
	struct trx_l1h *l1h = (struct trx_l1h *)fi->priv;
	struct phy_instance *pinst = l1h->phy_inst;
	struct gsm_bts_trx *trx = pinst->trx;
	uint16_t arfcn;
	int nominal_power;
	int status;
	bool waiting_other_trx;
	bool was_ready = trx_is_provisioned(l1h);

	switch (event) {
	case TRX_PROV_EV_CLOSE:
		/* In this state, we didn't for sure send a POWERON yet, hence we
		   are safe directly applying the close as if we received a
		   POWEROFF RSP: */
		if (pinst->num == 0)
			trx_prov_fsm_apply_close(pinst->phy_link, 0);
		return;
	case TRX_PROV_EV_CFG_ENABLE:
		l1h->config.enabled =(bool)data;
		break;
	case TRX_PROV_EV_CFG_BSIC:
		/* We always get BSIC from the BSC, TSC can be derived from the BCC */
		if (!pinst->phy_link->u.osmotrx.use_legacy_setbsic) {
			const uint8_t tsc = BSIC2BCC((uint8_t)(intptr_t)data);
			if (l1h->config.tsc != tsc || !l1h->config.tsc_valid) {
				l1h->config.tsc = tsc;
				l1h->config.tsc_valid = true;
				l1h->config.tsc_sent = false;
			}
		} else {
			const uint8_t bsic = (uint8_t)(intptr_t)data;
			if (l1h->config.bsic != bsic || !l1h->config.bsic_valid) {
				l1h->config.bsic = bsic;
				l1h->config.bsic_valid = true;
				l1h->config.bsic_sent = false;
			}
		}
		break;
	case TRX_PROV_EV_CFG_ARFCN:
		arfcn = (uint16_t)(intptr_t)data;
		if (l1h->config.arfcn != arfcn || !l1h->config.arfcn_valid) {
			l1h->config.arfcn = arfcn;
			l1h->config.arfcn_valid = true;
			l1h->config.txtune_sent = false;
			l1h->config.rxtune_sent = false;
			l1h->config.nomtxpower_sent = false;
		}
		break;
	case TRX_PROV_EV_CFG_TS:
		update_ts_data(l1h, (struct trx_prov_ev_cfg_ts_data*)data);
		break;

	/* CONFIRMATIONS FROM TRXC */
	case TRX_PROV_EV_RXTUNE_CNF:
		if (l1h->config.rxtune_sent)
			l1h->config.rxtune_acked = true;
		break;
	case TRX_PROV_EV_TXTUNE_CNF:
		if (l1h->config.txtune_sent)
			l1h->config.txtune_acked = true;
		break;
	case TRX_PROV_EV_NOMTXPOWER_CNF:
		nominal_power = (int)(intptr_t)data;
		if (l1h->config.nomtxpower_sent)
			l1h->config.nomtxpower_acked = true;
		l1if_trx_set_nominal_power(trx, nominal_power);
		break;
	case TRX_PROV_EV_SETPOWER_CNF:
		if (l1h->config.setpower_sent)
			l1h->config.setpower_acked = true;
		break;
	case TRX_PROV_EV_SETBSIC_CNF:
		if (l1h->config.bsic_sent)
			l1h->config.bsic_acked = true;
		break;
	case TRX_PROV_EV_SETTSC_CNF:
		if (l1h->config.tsc_sent)
			l1h->config.tsc_acked = true;
		break;
	case TRX_PROV_EV_SETFORMAT_CNF:
		status = (int)(intptr_t)data;
		/* Transceiver may suggest a lower version (than requested) */
		if (status == l1h->config.trxd_pdu_ver_req) {
			l1h->config.trxd_pdu_ver_use = status;
			l1h->config.setformat_acked = true;
			LOGPPHI(l1h->phy_inst, DTRX, LOGL_INFO,
				"Using TRXD PDU version %u\n",
				l1h->config.trxd_pdu_ver_use);
		} else {
			LOGPPHI(l1h->phy_inst, DTRX, LOGL_DEBUG,
				"Transceiver suggests TRXD PDU version %u (requested %u)\n",
				status, l1h->config.trxd_pdu_ver_req);
			/* Send another SETFORMAT with suggested version */
			l1h->config.trxd_pdu_ver_req = status;
			l1h->config.setformat_sent = false;
		}
		break;
	case TRX_PROV_EV_OTHER_TRX_READY:
		OSMO_ASSERT(pinst->num == 0);
		/* Do nothing here, we were triggered to see if we can finally poweron TRX0 below */
		break;
	default:
		OSMO_ASSERT(0);
	}

	l1if_provision_transceiver_trx(l1h);

	if (l1h->phy_inst->num == 0) {
		waiting_other_trx = !trx_other_trx0_ready(l1h);
	} else {
		waiting_other_trx = false; /* we don't care about others in TRX!=0 */
		/* If we just became ready for TRX0 POWERON (aka this TRX becomes provisioned), signal it to TRX0: */
		if (l1h->phy_inst->num != 0 && (!was_ready && trx_is_provisioned(l1h)))
			trx_signal_ready_trx0(l1h);
	}

	/* if we gathered all data and could go forward. For TRX0, only after
	 * all other TRX are prepared, since it will send POWERON commad */
	if (trx_is_provisioned_and_enabled(l1h) && !waiting_other_trx) {
		if (l1h->phy_inst->num != 0)
			trx_prov_fsm_state_chg(fi, TRX_PROV_ST_OPEN_POWERON);
		else
			trx_prov_fsm_state_chg(fi, TRX_PROV_ST_OPEN_WAIT_POWERON_CNF);
	} else {
		LOGPFSML(fi, LOGL_INFO, "Delay poweron, wait for:%s%s%s%s%s%s%s%s%s\n",
			l1h->config.enabled ? "" :" enable",
			pinst->phy_link->u.osmotrx.use_legacy_setbsic ?
				(l1h->config.bsic_valid ? (l1h->config.bsic_acked ? "" : " bsic-ack") : " bsic") :
				(l1h->config.tsc_valid ? (l1h->config.tsc_acked ? "" : " tsc-ack") : " tsc"),
			l1h->config.arfcn_valid ? "" : " arfcn",
			l1h->config.rxtune_acked ? "" : " rxtune-ack",
			l1h->config.txtune_acked ? "" : " txtune-ack",
			l1h->config.nominal_power_set_by_vty ? "" : (l1h->config.nomtxpower_acked ? "" : " nomtxpower-ack"),
			l1h->config.setpower_acked ? "" : " setpower-ack",
			l1h->config.setformat_acked ? "" : " setformat-ack",
			waiting_other_trx ? "" : " other-trx"
			);
	}
}


static void st_open_wait_power_cnf_on_enter(struct osmo_fsm_inst *fi, uint32_t prev_state)
{
	struct trx_l1h *l1h = (struct trx_l1h *)fi->priv;
	struct phy_instance *pinst = l1h->phy_inst;

	trx_if_cmd_poweron(l1h, l1if_poweronoff_cb);
	pinst->phy_link->u.osmotrx.poweron_sent = true;
}

static void st_open_wait_power_cnf(struct osmo_fsm_inst *fi, uint32_t event, void *data)
{
	struct trx_l1h *l1h = (struct trx_l1h *)fi->priv;
	struct phy_instance *pinst = l1h->phy_inst;
	struct phy_link *plink = pinst->phy_link;
	int rc;

	switch (event) {
	case TRX_PROV_EV_POWERON_CNF:
		rc = (uint16_t)(intptr_t)data;
		if (rc == 0 && plink->state != PHY_LINK_CONNECTED) {
			trx_sched_clock_started(pinst->trx->bts);
			phy_link_state_set(plink, PHY_LINK_CONNECTED);
			trx_prov_fsm_state_chg(fi, TRX_PROV_ST_OPEN_POWERON);
		} else if (rc != 0 && plink->state != PHY_LINK_SHUTDOWN) {
			trx_sched_clock_stopped(pinst->trx->bts);
			phy_link_state_set(plink, PHY_LINK_SHUTDOWN);
		}
		break;
	case TRX_PROV_EV_CFG_TS:
		update_ts_data(l1h, (struct trx_prov_ev_cfg_ts_data*)data);
		break;
	case TRX_PROV_EV_CLOSE:
		/* power off transceiver, if not already */
		if (pinst->num == 0 && !plink->u.osmotrx.poweroff_sent) {
			trx_if_cmd_poweroff(l1h, l1if_poweronoff_cb);
			plink->u.osmotrx.poweroff_sent = true;
		}
		trx_prov_fsm_state_chg(fi, TRX_PROV_ST_OPEN_WAIT_POWEROFF_CNF);
		break;
	default:
		OSMO_ASSERT(0);
	}
}

static void st_open_poweron_on_enter(struct osmo_fsm_inst *fi, uint32_t prev_state)
{
	struct trx_l1h *l1h = (struct trx_l1h *)fi->priv;
	uint8_t tn;

	/* after power on */
	if (l1h->config.rxgain_valid && !l1h->config.rxgain_sent) {
		trx_if_cmd_setrxgain(l1h, l1h->config.rxgain);
		l1h->config.rxgain_sent = true;
	}
	if (l1h->config.maxdly_valid && !l1h->config.maxdly_sent) {
		trx_if_cmd_setmaxdly(l1h, l1h->config.maxdly);
		l1h->config.maxdly_sent = true;
	}
	if (l1h->config.maxdlynb_valid && !l1h->config.maxdlynb_sent) {
		trx_if_cmd_setmaxdlynb(l1h, l1h->config.maxdlynb);
		l1h->config.maxdlynb_sent = true;
	}

	for (tn = 0; tn < TRX_NR_TS; tn++) {
		if (l1h->config.setslot_valid[tn]
		 && !l1h->config.setslot_sent[tn]) {
			trx_if_cmd_setslot(l1h, tn, l1if_setslot_cb);
			l1h->config.setslot_sent[tn] = true;
		}
	}
}

static void st_open_poweron(struct osmo_fsm_inst *fi, uint32_t event, void *data)
{
	struct trx_l1h *l1h = (struct trx_l1h *)fi->priv;
	struct phy_instance *pinst = l1h->phy_inst;
	struct phy_link *plink = pinst->phy_link;
	struct trx_prov_ev_cfg_ts_data* ts_data;

	switch (event) {
	case TRX_PROV_EV_CLOSE:
		/* power off transceiver, if not already */
		if (pinst->num == 0 && plink->u.osmotrx.powered && !plink->u.osmotrx.poweroff_sent) {
			trx_if_cmd_poweroff(l1h, l1if_poweronoff_cb);
			plink->u.osmotrx.poweroff_sent = true;
		}
		trx_prov_fsm_state_chg(fi, TRX_PROV_ST_OPEN_WAIT_POWEROFF_CNF);
		break;
	case TRX_PROV_EV_CFG_TS:
		ts_data = (struct trx_prov_ev_cfg_ts_data*)data;
		update_ts_data(l1h, ts_data);
		/* While in this state we can send SETSLOT immediately */
		trx_if_cmd_setslot(l1h, ts_data->tn, l1if_setslot_cb);
		l1h->config.setslot_sent[ts_data->tn] = true;
		break;
	default:
		OSMO_ASSERT(0);
	}
}

static void st_open_wait_poweroff_cnf(struct osmo_fsm_inst *fi, uint32_t event, void *data)
{
	struct trx_l1h *l1h = (struct trx_l1h *)fi->priv;
	struct phy_instance *pinst = l1h->phy_inst;
	struct phy_link *plink = pinst->phy_link;
	int rc;

	switch (event) {
	case TRX_PROV_EV_POWEROFF_CNF:
		rc = (uint16_t)(intptr_t)data;
		trx_prov_fsm_apply_close(plink, rc);
		break;
	default:
		OSMO_ASSERT(0);
	}
}

static struct osmo_fsm_state trx_prov_fsm_states[] = {
	[TRX_PROV_ST_CLOSED] = {
		.in_event_mask =
			X(TRX_PROV_EV_OPEN),
		.out_state_mask =
			X(TRX_PROV_ST_OPEN_POWEROFF),
		.name = "CLOSED",
		.action = st_closed,
	},
	[TRX_PROV_ST_OPEN_POWEROFF] = {
		.in_event_mask =
			X(TRX_PROV_EV_CLOSE) |
			X(TRX_PROV_EV_OTHER_TRX_READY) |
			X(TRX_PROV_EV_CFG_ENABLE) |
			X(TRX_PROV_EV_CFG_BSIC) |
			X(TRX_PROV_EV_CFG_ARFCN) |
			X(TRX_PROV_EV_CFG_TS) |
			X(TRX_PROV_EV_RXTUNE_CNF) |
			X(TRX_PROV_EV_TXTUNE_CNF) |
			X(TRX_PROV_EV_NOMTXPOWER_CNF) |
			X(TRX_PROV_EV_SETPOWER_CNF) |
			X(TRX_PROV_EV_SETBSIC_CNF) |
			X(TRX_PROV_EV_SETTSC_CNF) |
			X(TRX_PROV_EV_SETFORMAT_CNF),
		.out_state_mask =
			X(TRX_PROV_ST_CLOSED) |
			X(TRX_PROV_ST_OPEN_WAIT_POWERON_CNF) |
			X(TRX_PROV_ST_OPEN_POWERON),
		.name = "OPEN_POWEROFF",
		.onenter = st_open_poweroff_on_enter,
		.action = st_open_poweroff,
	},
	[TRX_PROV_ST_OPEN_WAIT_POWERON_CNF] = {
		.in_event_mask =
			X(TRX_PROV_EV_CLOSE) |
			X(TRX_PROV_EV_POWERON_CNF) |
			X(TRX_PROV_EV_CFG_TS),
		.out_state_mask =
			X(TRX_PROV_ST_OPEN_POWERON) |
			X(TRX_PROV_ST_OPEN_WAIT_POWEROFF_CNF),
		.name = "OPEN_WAIT_POWERON_CNF",
		.onenter = st_open_wait_power_cnf_on_enter,
		.action = st_open_wait_power_cnf,
	},
	[TRX_PROV_ST_OPEN_POWERON] = {
		.in_event_mask =
			X(TRX_PROV_EV_CLOSE) |
			X(TRX_PROV_EV_CFG_TS),
		.out_state_mask =
			X(TRX_PROV_ST_OPEN_WAIT_POWEROFF_CNF),
		.name = "OPEN_POWERON",
		.onenter = st_open_poweron_on_enter,
		.action = st_open_poweron,
	},
	[TRX_PROV_ST_OPEN_WAIT_POWEROFF_CNF] = {
		.in_event_mask =
			X(TRX_PROV_EV_POWEROFF_CNF),
		.out_state_mask =
			X(TRX_PROV_ST_CLOSED),
		.name = "OPEN_WAIT_POWEROFF_CNF",
		.action = st_open_wait_poweroff_cnf,
	},
};

const struct value_string trx_prov_fsm_event_names[] = {
	OSMO_VALUE_STRING(TRX_PROV_EV_OTHER_TRX_READY),
	OSMO_VALUE_STRING(TRX_PROV_EV_OPEN),
	OSMO_VALUE_STRING(TRX_PROV_EV_CFG_ENABLE),
	OSMO_VALUE_STRING(TRX_PROV_EV_CFG_BSIC),
	OSMO_VALUE_STRING(TRX_PROV_EV_CFG_ARFCN),
	OSMO_VALUE_STRING(TRX_PROV_EV_CFG_TS),
	OSMO_VALUE_STRING(TRX_PROV_EV_CFG_RXGAIN),
	OSMO_VALUE_STRING(TRX_PROV_EV_CFG_SETMAXDLY),
	OSMO_VALUE_STRING(TRX_PROV_EV_RXTUNE_CNF),
	OSMO_VALUE_STRING(TRX_PROV_EV_TXTUNE_CNF),
	OSMO_VALUE_STRING(TRX_PROV_EV_NOMTXPOWER_CNF),
	OSMO_VALUE_STRING(TRX_PROV_EV_SETPOWER_CNF),
	OSMO_VALUE_STRING(TRX_PROV_EV_SETBSIC_CNF),
	OSMO_VALUE_STRING(TRX_PROV_EV_SETTSC_CNF),
	OSMO_VALUE_STRING(TRX_PROV_EV_SETFORMAT_CNF),
	OSMO_VALUE_STRING(TRX_PROV_EV_POWERON_CNF),
	OSMO_VALUE_STRING(TRX_PROV_EV_POWEROFF_CNF),
	OSMO_VALUE_STRING(TRX_PROV_EV_CLOSE),
	{ 0, NULL }
};

struct osmo_fsm trx_prov_fsm = {
	.name = "TRX_PROV",
	.states = trx_prov_fsm_states,
	.num_states = ARRAY_SIZE(trx_prov_fsm_states),
	.event_names = trx_prov_fsm_event_names,
	.log_subsys = DL1C,
};

static __attribute__((constructor)) void trx_prov_fsm_init(void)
{
	OSMO_ASSERT(osmo_fsm_register(&trx_prov_fsm) == 0);
	OSMO_ASSERT(osmo_signal_register_handler(SS_GLOBAL, trx_prov_fsm_signal_cb, NULL) == 0);
}
