442 lines
14 KiB
C
442 lines
14 KiB
C
/* Handle LCS BSSMAP-LE Perform Location Request */
|
|
/*
|
|
* (C) 2020 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
|
|
* All Rights Reserved
|
|
*
|
|
* Author: Neels Hofmeyr <neels@hofmeyr.de>
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0+
|
|
*
|
|
* 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 <osmocom/smlc/smlc_data.h>
|
|
#include <osmocom/smlc/smlc_loc_req.h>
|
|
#include <osmocom/smlc/smlc_subscr.h>
|
|
#include <osmocom/smlc/lb_conn.h>
|
|
#include <osmocom/smlc/cell_locations.h>
|
|
|
|
#include <osmocom/core/fsm.h>
|
|
#include <osmocom/core/tdef.h>
|
|
#include <osmocom/core/utils.h>
|
|
#include <osmocom/gsm/bsslap.h>
|
|
#include <osmocom/gsm/bssmap_le.h>
|
|
#include <osmocom/gsm/gad.h>
|
|
|
|
enum smlc_loc_req_fsm_state {
|
|
SMLC_LOC_REQ_ST_INIT,
|
|
SMLC_LOC_REQ_ST_WAIT_TA,
|
|
SMLC_LOC_REQ_ST_GOT_TA,
|
|
SMLC_LOC_REQ_ST_FAILED,
|
|
};
|
|
|
|
static const struct value_string smlc_loc_req_fsm_event_names[] = {
|
|
OSMO_VALUE_STRING(SMLC_LOC_REQ_EV_RX_TA_RESPONSE),
|
|
OSMO_VALUE_STRING(SMLC_LOC_REQ_EV_RX_BSSLAP_RESET),
|
|
OSMO_VALUE_STRING(SMLC_LOC_REQ_EV_RX_LE_PERFORM_LOCATION_ABORT),
|
|
{}
|
|
};
|
|
|
|
static struct osmo_fsm smlc_loc_req_fsm;
|
|
|
|
static const struct osmo_tdef_state_timeout smlc_loc_req_fsm_timeouts[32] = {
|
|
[SMLC_LOC_REQ_ST_WAIT_TA] = { .T = -12 },
|
|
};
|
|
|
|
/* Transition to a state, using the T timer defined in smlc_loc_req_fsm_timeouts.
|
|
* The actual timeout value is in turn obtained from network->T_defs.
|
|
* Assumes local variable fi exists. */
|
|
#define smlc_loc_req_fsm_state_chg(FI, STATE) \
|
|
osmo_tdef_fsm_inst_state_chg(FI, STATE, \
|
|
smlc_loc_req_fsm_timeouts, \
|
|
g_smlc_tdefs, \
|
|
5)
|
|
|
|
#define smlc_loc_req_fail(cause, fmt, args...) do { \
|
|
LOG_SMLC_LOC_REQ(smlc_loc_req, LOGL_ERROR, "Perform Location Request failed in state %s: " fmt "\n", \
|
|
smlc_loc_req ? osmo_fsm_inst_state_name(smlc_loc_req->fi) : "NULL", ## args); \
|
|
smlc_loc_req->lcs_cause = (struct lcs_cause_ie){ \
|
|
.present = true, \
|
|
.cause_val = cause, \
|
|
}; \
|
|
smlc_loc_req_fsm_state_chg(smlc_loc_req->fi, SMLC_LOC_REQ_ST_FAILED); \
|
|
} while(0)
|
|
|
|
static struct smlc_loc_req *smlc_loc_req_alloc(void *ctx)
|
|
{
|
|
struct smlc_loc_req *smlc_loc_req;
|
|
|
|
struct osmo_fsm_inst *fi = osmo_fsm_inst_alloc(&smlc_loc_req_fsm, ctx, NULL, LOGL_DEBUG, "no-id");
|
|
OSMO_ASSERT(fi);
|
|
|
|
smlc_loc_req = talloc(fi, struct smlc_loc_req);
|
|
OSMO_ASSERT(smlc_loc_req);
|
|
fi->priv = smlc_loc_req;
|
|
*smlc_loc_req = (struct smlc_loc_req){
|
|
.fi = fi,
|
|
};
|
|
|
|
return smlc_loc_req;
|
|
}
|
|
|
|
static int smlc_loc_req_start(struct lb_conn *lb_conn, const struct bssmap_le_perform_loc_req *loc_req_pdu)
|
|
{
|
|
struct smlc_loc_req *smlc_loc_req;
|
|
|
|
rate_ctr_inc(rate_ctr_group_get_ctr(g_smlc->ctrs, SMLC_CTR_BSSMAP_LE_RX_DT1_PERFORM_LOCATION_REQUEST));
|
|
|
|
if (lb_conn->smlc_loc_req) {
|
|
/* Another request is already pending. If we send Perform Location Abort, the peer doesn't know which
|
|
* request we would mean. Just drop this on the floor. */
|
|
LOG_SMLC_LOC_REQ(lb_conn->smlc_loc_req, LOGL_ERROR,
|
|
"Ignoring Perform Location Request, another request is still pending\n");
|
|
return -EAGAIN;
|
|
}
|
|
|
|
if (loc_req_pdu->imsi.type == GSM_MI_TYPE_IMSI
|
|
&& (!lb_conn->smlc_subscr
|
|
|| osmo_mobile_identity_cmp(&loc_req_pdu->imsi, &lb_conn->smlc_subscr->imsi))) {
|
|
|
|
struct smlc_subscr *smlc_subscr;
|
|
struct lb_conn *other_conn;
|
|
smlc_subscr = smlc_subscr_find_or_create(&loc_req_pdu->imsi, __func__);
|
|
OSMO_ASSERT(smlc_subscr);
|
|
|
|
if (lb_conn->smlc_subscr && lb_conn->smlc_subscr != smlc_subscr) {
|
|
LOG_LB_CONN(lb_conn, LOGL_ERROR,
|
|
"IMSI mismatch: lb_conn has %s, Rx Perform Location Request has %s\n",
|
|
smlc_subscr_to_str_c(OTC_SELECT, lb_conn->smlc_subscr),
|
|
smlc_subscr_to_str_c(OTC_SELECT, smlc_subscr));
|
|
smlc_subscr_put(smlc_subscr, __func__);
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* Find another conn before setting this conn's subscriber */
|
|
other_conn = lb_conn_find_by_smlc_subscr(lb_conn->smlc_subscr, __func__);
|
|
|
|
/* Set the subscriber before logging about it, so that it shows as log context */
|
|
if (!lb_conn->smlc_subscr) {
|
|
lb_conn->smlc_subscr = smlc_subscr;
|
|
smlc_subscr_get(lb_conn->smlc_subscr, SMLC_SUBSCR_USE_LB_CONN);
|
|
}
|
|
|
|
if (other_conn && other_conn != lb_conn) {
|
|
LOG_LB_CONN(lb_conn, LOGL_ERROR, "Another conn already active for this subscriber\n");
|
|
LOG_LB_CONN(other_conn, LOGL_ERROR, "Another conn opened for this subscriber, discarding\n");
|
|
lb_conn_close(other_conn);
|
|
}
|
|
|
|
smlc_subscr_put(smlc_subscr, __func__);
|
|
if (other_conn)
|
|
lb_conn_put(other_conn, __func__);
|
|
}
|
|
|
|
/* smlc_loc_req has a use count on lb_conn, so its talloc ctx must not be a child of lb_conn. (Otherwise an
|
|
* lb_conn_put() from smlc_loc_req could cause a free of smlc_loc_req's parent ctx, causing a use after free on
|
|
* FSM termination.) */
|
|
smlc_loc_req = smlc_loc_req_alloc(lb_conn->lb_peer);
|
|
|
|
*smlc_loc_req = (struct smlc_loc_req){
|
|
.fi = smlc_loc_req->fi,
|
|
.lb_conn = lb_conn,
|
|
.req = *loc_req_pdu,
|
|
};
|
|
smlc_loc_req->latest_cell_id = loc_req_pdu->cell_id;
|
|
lb_conn->smlc_loc_req = smlc_loc_req;
|
|
lb_conn_get(smlc_loc_req->lb_conn, LB_CONN_USE_SMLC_LOC_REQ);
|
|
|
|
LOG_LB_CONN(lb_conn, LOGL_INFO, "Rx Perform Location Request (BSSLAP APDU %s), cell id is %s\n",
|
|
loc_req_pdu->apdu_present ?
|
|
osmo_bsslap_msgt_name(loc_req_pdu->apdu.msg_type) : "omitted",
|
|
gsm0808_cell_id_name_c(OTC_SELECT, &smlc_loc_req->latest_cell_id));
|
|
|
|
/* state change to start the timeout */
|
|
smlc_loc_req_fsm_state_chg(smlc_loc_req->fi, SMLC_LOC_REQ_ST_WAIT_TA);
|
|
return 0;
|
|
}
|
|
|
|
static int handle_bssmap_le_conn_oriented_info(struct smlc_loc_req *smlc_loc_req,
|
|
const struct bssmap_le_conn_oriented_info *coi)
|
|
{
|
|
switch (coi->apdu.msg_type) {
|
|
|
|
case BSSLAP_MSGT_TA_RESPONSE:
|
|
return osmo_fsm_inst_dispatch(smlc_loc_req->fi, SMLC_LOC_REQ_EV_RX_TA_RESPONSE,
|
|
(void*)&coi->apdu.ta_response);
|
|
|
|
case BSSLAP_MSGT_RESET:
|
|
return osmo_fsm_inst_dispatch(smlc_loc_req->fi, SMLC_LOC_REQ_EV_RX_BSSLAP_RESET,
|
|
(void*)&coi->apdu.reset);
|
|
|
|
case BSSLAP_MSGT_ABORT:
|
|
smlc_loc_req_fail(LCS_CAUSE_REQUEST_ABORTED, "Aborting Location Request due to BSSLAP Abort");
|
|
return 0;
|
|
|
|
case BSSLAP_MSGT_REJECT:
|
|
smlc_loc_req_fail(LCS_CAUSE_REQUEST_ABORTED, "Aborting Location Request due to BSSLAP Reject");
|
|
return 0;
|
|
|
|
default:
|
|
LOG_SMLC_LOC_REQ(smlc_loc_req, LOGL_ERROR, "rx BSSLAP APDU with unsupported message type %s\n",
|
|
osmo_bsslap_msgt_name(coi->apdu.msg_type));
|
|
return -ENOTSUP;
|
|
};
|
|
}
|
|
|
|
int smlc_loc_req_rx_bssap_le(struct lb_conn *lb_conn, const struct bssap_le_pdu *bssap_le)
|
|
{
|
|
struct smlc_loc_req *smlc_loc_req = lb_conn->smlc_loc_req;
|
|
const struct bssmap_le_pdu *bssmap_le = &bssap_le->bssmap_le;
|
|
|
|
LOG_LB_CONN(lb_conn, LOGL_DEBUG, "Rx %s\n", osmo_bssap_le_pdu_to_str_c(OTC_SELECT, bssap_le));
|
|
|
|
if (bssap_le->discr != BSSAP_LE_MSG_DISCR_BSSMAP_LE) {
|
|
LOG_LB_CONN(lb_conn, LOGL_ERROR, "BSSAP-LE discr %d not implemented\n", bssap_le->discr);
|
|
return -ENOTSUP;
|
|
}
|
|
|
|
switch (bssmap_le->msg_type) {
|
|
|
|
case BSSMAP_LE_MSGT_PERFORM_LOC_REQ:
|
|
return smlc_loc_req_start(lb_conn, &bssmap_le->perform_loc_req);
|
|
|
|
case BSSMAP_LE_MSGT_PERFORM_LOC_ABORT:
|
|
return osmo_fsm_inst_dispatch(smlc_loc_req->fi, SMLC_LOC_REQ_EV_RX_LE_PERFORM_LOCATION_ABORT,
|
|
(void*)&bssmap_le->perform_loc_abort);
|
|
|
|
case BSSMAP_LE_MSGT_CONN_ORIENTED_INFO:
|
|
return handle_bssmap_le_conn_oriented_info(smlc_loc_req, &bssmap_le->conn_oriented_info);
|
|
|
|
default:
|
|
LOG_SMLC_LOC_REQ(smlc_loc_req, LOGL_ERROR, "Rx BSSMAP-LE from SMLC with unsupported message type: %s\n",
|
|
osmo_bssap_le_pdu_to_str_c(OTC_SELECT, bssap_le));
|
|
return -ENOTSUP;
|
|
}
|
|
}
|
|
|
|
void smlc_loc_req_reset(struct lb_conn *lb_conn)
|
|
{
|
|
struct smlc_loc_req *smlc_loc_req = lb_conn->smlc_loc_req;
|
|
if (!smlc_loc_req)
|
|
return;
|
|
smlc_loc_req_fail(LCS_CAUSE_SYSTEM_FAILURE, "Aborting Location Request due to RESET on Lb");
|
|
}
|
|
|
|
static int smlc_loc_req_fsm_timer_cb(struct osmo_fsm_inst *fi)
|
|
{
|
|
struct smlc_loc_req *smlc_loc_req = fi->priv;
|
|
smlc_loc_req_fail(LCS_CAUSE_SYSTEM_FAILURE, "Timeout");
|
|
return 1;
|
|
}
|
|
|
|
static void smlc_loc_req_wait_ta_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state)
|
|
{
|
|
struct smlc_loc_req *smlc_loc_req = fi->priv;
|
|
struct bssmap_le_pdu bssmap_le;
|
|
|
|
/* Did the original request contain a TA already? */
|
|
if (smlc_loc_req->req.apdu_present && smlc_loc_req->req.apdu.msg_type == BSSLAP_MSGT_TA_LAYER3) {
|
|
smlc_loc_req->ta_present = true;
|
|
smlc_loc_req->ta = smlc_loc_req->req.apdu.ta_layer3.ta;
|
|
LOG_SMLC_LOC_REQ(smlc_loc_req, LOGL_INFO, "TA = %u\n", smlc_loc_req->ta);
|
|
smlc_loc_req_fsm_state_chg(smlc_loc_req->fi, SMLC_LOC_REQ_ST_GOT_TA);
|
|
return;
|
|
}
|
|
|
|
/* No TA known yet, ask via BSSLAP */
|
|
bssmap_le = (struct bssmap_le_pdu){
|
|
.msg_type = BSSMAP_LE_MSGT_CONN_ORIENTED_INFO,
|
|
.conn_oriented_info = {
|
|
.apdu = {
|
|
.msg_type = BSSLAP_MSGT_TA_REQUEST,
|
|
},
|
|
},
|
|
};
|
|
|
|
lb_conn_send_bssmap_le(smlc_loc_req->lb_conn, &bssmap_le);
|
|
}
|
|
|
|
static void update_ci(struct gsm0808_cell_id *cell_id, int16_t new_ci)
|
|
{
|
|
struct osmo_cell_global_id cgi = {};
|
|
struct gsm0808_cell_id ci = {
|
|
.id_discr = CELL_IDENT_CI,
|
|
.id.ci = new_ci,
|
|
};
|
|
/* Set all values from the cell_id to the cgi */
|
|
gsm0808_cell_id_to_cgi(&cgi, cell_id);
|
|
/* Overwrite the CI part */
|
|
gsm0808_cell_id_to_cgi(&cgi, &ci);
|
|
/* write back to cell_id, without changing its type */
|
|
gsm0808_cell_id_from_cgi(cell_id, cell_id->id_discr, &cgi);
|
|
}
|
|
|
|
static void smlc_loc_req_wait_ta_action(struct osmo_fsm_inst *fi, uint32_t event, void *data)
|
|
{
|
|
struct smlc_loc_req *smlc_loc_req = fi->priv;
|
|
const struct bsslap_ta_response *ta_response;
|
|
const struct bsslap_reset *reset;
|
|
|
|
switch (event) {
|
|
|
|
case SMLC_LOC_REQ_EV_RX_TA_RESPONSE:
|
|
ta_response = data;
|
|
smlc_loc_req->ta_present = true;
|
|
smlc_loc_req->ta = ta_response->ta;
|
|
update_ci(&smlc_loc_req->latest_cell_id, ta_response->cell_id);
|
|
LOG_SMLC_LOC_REQ(smlc_loc_req, LOGL_INFO, "Rx BSSLAP TA Response: cell id is now %s\n",
|
|
gsm0808_cell_id_name_c(OTC_SELECT, &smlc_loc_req->latest_cell_id));
|
|
smlc_loc_req_fsm_state_chg(smlc_loc_req->fi, SMLC_LOC_REQ_ST_GOT_TA);
|
|
return;
|
|
|
|
case SMLC_LOC_REQ_EV_RX_BSSLAP_RESET:
|
|
reset = data;
|
|
smlc_loc_req->ta_present = true;
|
|
smlc_loc_req->ta = reset->ta;
|
|
update_ci(&smlc_loc_req->latest_cell_id, reset->cell_id);
|
|
LOG_SMLC_LOC_REQ(smlc_loc_req, LOGL_INFO, "Rx BSSLAP Reset: cell id is now %s\n",
|
|
gsm0808_cell_id_name_c(OTC_SELECT, &smlc_loc_req->latest_cell_id));
|
|
smlc_loc_req_fsm_state_chg(smlc_loc_req->fi, SMLC_LOC_REQ_ST_GOT_TA);
|
|
return;
|
|
|
|
case SMLC_LOC_REQ_EV_RX_LE_PERFORM_LOCATION_ABORT:
|
|
LOG_SMLC_LOC_REQ(smlc_loc_req, LOGL_INFO, "Rx Perform Location Abort, stopping this request dead\n");
|
|
osmo_fsm_inst_term(fi, OSMO_FSM_TERM_REQUEST, NULL);
|
|
return;
|
|
|
|
default:
|
|
OSMO_ASSERT(false);
|
|
}
|
|
}
|
|
|
|
static void smlc_loc_req_got_ta_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state)
|
|
{
|
|
struct smlc_loc_req *smlc_loc_req = fi->priv;
|
|
struct bssmap_le_pdu bssmap_le;
|
|
struct osmo_gad location;
|
|
int rc;
|
|
|
|
if (!smlc_loc_req->ta_present) {
|
|
smlc_loc_req_fail(LCS_CAUSE_SYSTEM_FAILURE,
|
|
"Internal error: GOT_TA event, but no TA present");
|
|
return;
|
|
}
|
|
|
|
bssmap_le = (struct bssmap_le_pdu){
|
|
.msg_type = BSSMAP_LE_MSGT_PERFORM_LOC_RESP,
|
|
.perform_loc_resp = {
|
|
.location_estimate_present = true,
|
|
},
|
|
};
|
|
|
|
rc = cell_location_from_ta(&location, &smlc_loc_req->latest_cell_id, smlc_loc_req->ta);
|
|
if (rc) {
|
|
smlc_loc_req_fail(LCS_CAUSE_FACILITY_NOTSUPP, "Unable to compose Location Estimate for %s: %s",
|
|
gsm0808_cell_id_name_c(OTC_SELECT, &smlc_loc_req->latest_cell_id),
|
|
rc == -ENOENT ? "No location information for this cell" : "unknown error");
|
|
return;
|
|
}
|
|
|
|
rc = osmo_gad_enc(&bssmap_le.perform_loc_resp.location_estimate, &location);
|
|
if (rc <= 0) {
|
|
smlc_loc_req_fail(LCS_CAUSE_FACILITY_NOTSUPP, "Unable to encode Location Estimate for %s (rc=%d)",
|
|
gsm0808_cell_id_name_c(OTC_SELECT, &smlc_loc_req->latest_cell_id), rc);
|
|
return;
|
|
}
|
|
|
|
LOG_SMLC_LOC_REQ(smlc_loc_req, LOGL_INFO, "Returning location estimate to BSC: %s TA=%u --> %s\n",
|
|
gsm0808_cell_id_name_c(OTC_SELECT, &smlc_loc_req->latest_cell_id),
|
|
smlc_loc_req->ta, osmo_gad_to_str_c(OTC_SELECT, &location));
|
|
|
|
if (lb_conn_send_bssmap_le(smlc_loc_req->lb_conn, &bssmap_le)) {
|
|
smlc_loc_req_fail(LCS_CAUSE_SYSTEM_FAILURE,
|
|
"Unable to encode/send BSSMAP-LE Perform Location Response");
|
|
return;
|
|
}
|
|
osmo_fsm_inst_term(fi, OSMO_FSM_TERM_REGULAR, NULL);
|
|
}
|
|
|
|
static void smlc_loc_req_failed_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state)
|
|
{
|
|
struct smlc_loc_req *smlc_loc_req = fi->priv;
|
|
struct bssmap_le_pdu bssmap_le = {
|
|
.msg_type = BSSMAP_LE_MSGT_PERFORM_LOC_RESP,
|
|
.perform_loc_resp = {
|
|
.lcs_cause = smlc_loc_req->lcs_cause,
|
|
},
|
|
};
|
|
int rc;
|
|
rc = lb_conn_send_bssmap_le(smlc_loc_req->lb_conn, &bssmap_le);
|
|
osmo_fsm_inst_term(fi, rc ? OSMO_FSM_TERM_ERROR : OSMO_FSM_TERM_REGULAR, NULL);
|
|
}
|
|
|
|
void smlc_loc_req_fsm_cleanup(struct osmo_fsm_inst *fi, enum osmo_fsm_term_cause cause)
|
|
{
|
|
struct smlc_loc_req *smlc_loc_req = fi->priv;
|
|
if (smlc_loc_req->lb_conn && smlc_loc_req->lb_conn->smlc_loc_req == smlc_loc_req) {
|
|
smlc_loc_req->lb_conn->smlc_loc_req = NULL;
|
|
lb_conn_put(smlc_loc_req->lb_conn, LB_CONN_USE_SMLC_LOC_REQ);
|
|
}
|
|
}
|
|
|
|
#define S(x) (1 << (x))
|
|
|
|
static const struct osmo_fsm_state smlc_loc_req_fsm_states[] = {
|
|
[SMLC_LOC_REQ_ST_INIT] = {
|
|
.name = "INIT",
|
|
.out_state_mask = 0
|
|
| S(SMLC_LOC_REQ_ST_WAIT_TA)
|
|
| S(SMLC_LOC_REQ_ST_FAILED)
|
|
,
|
|
},
|
|
[SMLC_LOC_REQ_ST_WAIT_TA] = {
|
|
.name = "WAIT_TA",
|
|
.in_event_mask = 0
|
|
| S(SMLC_LOC_REQ_EV_RX_TA_RESPONSE)
|
|
| S(SMLC_LOC_REQ_EV_RX_BSSLAP_RESET)
|
|
| S(SMLC_LOC_REQ_EV_RX_LE_PERFORM_LOCATION_ABORT)
|
|
,
|
|
.out_state_mask = 0
|
|
| S(SMLC_LOC_REQ_ST_GOT_TA)
|
|
| S(SMLC_LOC_REQ_ST_FAILED)
|
|
,
|
|
.onenter = smlc_loc_req_wait_ta_onenter,
|
|
.action = smlc_loc_req_wait_ta_action,
|
|
},
|
|
[SMLC_LOC_REQ_ST_GOT_TA] = {
|
|
.name = "GOT_TA",
|
|
.out_state_mask = 0
|
|
| S(SMLC_LOC_REQ_ST_FAILED)
|
|
,
|
|
.onenter = smlc_loc_req_got_ta_onenter,
|
|
},
|
|
[SMLC_LOC_REQ_ST_FAILED] = {
|
|
.name = "FAILED",
|
|
.onenter = smlc_loc_req_failed_onenter,
|
|
},
|
|
};
|
|
|
|
static struct osmo_fsm smlc_loc_req_fsm = {
|
|
.name = "smlc_loc_req",
|
|
.states = smlc_loc_req_fsm_states,
|
|
.num_states = ARRAY_SIZE(smlc_loc_req_fsm_states),
|
|
.log_subsys = DLCS,
|
|
.event_names = smlc_loc_req_fsm_event_names,
|
|
.timer_cb = smlc_loc_req_fsm_timer_cb,
|
|
.cleanup = smlc_loc_req_fsm_cleanup,
|
|
};
|
|
|
|
static __attribute__((constructor)) void smlc_loc_req_fsm_register(void)
|
|
{
|
|
OSMO_ASSERT(osmo_fsm_register(&smlc_loc_req_fsm) == 0);
|
|
}
|