| /* SMSCB (SMS Cell Broadcast) Handling for OsmoBSC */ |
| /* |
| * (C) 2019 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 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 <limits.h> |
| |
| #include <osmocom/core/stats.h> |
| #include <osmocom/core/select.h> |
| #include <osmocom/core/msgb.h> |
| #include <osmocom/core/talloc.h> |
| #include <osmocom/core/byteswap.h> |
| |
| #include <osmocom/gsm/cbsp.h> |
| #include <osmocom/gsm/protocol/gsm_23_041.h> |
| #include <osmocom/gsm/protocol/gsm_48_049.h> |
| #include <osmocom/gsm/protocol/gsm_03_41.h> |
| |
| #include <osmocom/netif/stream.h> |
| |
| #include <osmocom/bsc/debug.h> |
| #include <osmocom/bsc/gsm_data.h> |
| #include <osmocom/bsc/smscb.h> |
| #include <osmocom/bsc/vty.h> |
| #include <osmocom/bsc/gsm_04_08_rr.h> |
| #include <osmocom/bsc/lchan_fsm.h> |
| #include <osmocom/bsc/abis_rsl.h> |
| |
| /********************************************************************************* |
| * Helper Functions |
| *********************************************************************************/ |
| |
| /* replace the old head of an entire list with a new head; effectively moves the entire |
| * list from old to new head */ |
| static void llist_replace_head(struct llist_head *new, struct llist_head *old) |
| { |
| if (llist_empty(old)) |
| INIT_LLIST_HEAD(new); |
| else |
| __llist_add(new, old->prev, old->next); |
| INIT_LLIST_HEAD(old); |
| } |
| |
| #define ETWS_PRIM_NOTIF_SIZE 56 |
| |
| /* Build a ETWS Primary Notification message as per TS 23.041 9.4.1.3 */ |
| static int gen_etws_primary_notification(uint8_t *out, uint16_t serial_nr, uint16_t msg_id, |
| uint16_t warn_type, const uint8_t *sec_info) |
| { |
| struct gsm341_etws_message *etws = (struct gsm341_etws_message *)out; |
| |
| memset(out, 0, ETWS_PRIM_NOTIF_SIZE); |
| |
| osmo_store16be(serial_nr, out); |
| etws->msg_id = osmo_htons(msg_id); |
| etws->warning_type = osmo_htons(warn_type); |
| |
| if (sec_info) |
| memcpy(etws->data, sec_info, ETWS_PRIM_NOTIF_SIZE - sizeof(*etws)); |
| |
| return ETWS_PRIM_NOTIF_SIZE; |
| } |
| |
| /*! Obtain SMSCB Channel State for given BTS (basic or extended CBCH) */ |
| struct bts_smscb_chan_state *bts_get_smscb_chan(struct gsm_bts *bts, bool extended) |
| { |
| struct bts_smscb_chan_state *chan_state; |
| |
| if (extended) |
| chan_state = &bts->cbch_extended; |
| else |
| chan_state = &bts->cbch_basic; |
| |
| return chan_state; |
| } |
| |
| /* do an ordered list insertion. we keep the list with increasing period, i.e. the most |
| * frequent message first */ |
| static void __bts_smscb_add(struct bts_smscb_chan_state *cstate, struct bts_smscb_message *new) |
| { |
| struct bts_smscb_message *tmp, *tmp2; |
| |
| if (llist_empty(&cstate->messages)) { |
| llist_add(&new->list, &cstate->messages); |
| return; |
| } |
| |
| llist_for_each_entry_safe(tmp, tmp2, &cstate->messages, list) { |
| if (tmp->input.rep_period > new->input.rep_period) { |
| /* we found the first message with longer period than the new message, |
| * we must insert ourselves before that one */ |
| __llist_add(&new->list, tmp->list.prev, &tmp->list); |
| return; |
| } |
| } |
| } |
| |
| /* stringify a SMSCB for logging */ |
| const char *bts_smscb_msg2str(const struct bts_smscb_message *smscb) |
| { |
| static char buf[128]; |
| snprintf(buf, sizeof(buf), "MsgId=0x%04x/SerialNr=0x%04x/Pages=%u/Period=%u/NumBcastReq=%u", |
| smscb->input.msg_id, smscb->input.serial_nr, smscb->num_pages, |
| smscb->input.rep_period, smscb->input.num_bcast_req); |
| return buf; |
| } |
| |
| const char *bts_smscb_chan_state_name(const struct bts_smscb_chan_state *cstate) |
| { |
| if (cstate == &cstate->bts->cbch_basic) |
| return "BASIC"; |
| else if (cstate == &cstate->bts->cbch_extended) |
| return "EXTENDED"; |
| else |
| return "UNKNOWN"; |
| } |
| |
| unsigned int bts_smscb_chan_load_percent(const struct bts_smscb_chan_state *cstate) |
| { |
| unsigned int sched_arr_used = 0; |
| unsigned int i; |
| |
| if (cstate->sched_arr_size == 0) |
| return 0; |
| |
| /* count the number of used slots */ |
| for (i = 0; i < cstate->sched_arr_size; i++) { |
| if (cstate->sched_arr[i]) |
| sched_arr_used++; |
| } |
| |
| OSMO_ASSERT(sched_arr_used <= UINT_MAX/100); |
| return (sched_arr_used * 100) / cstate->sched_arr_size; |
| } |
| |
| unsigned int bts_smscb_chan_page_count(const struct bts_smscb_chan_state *cstate) |
| { |
| struct bts_smscb_message *smscb; |
| unsigned int page_count = 0; |
| |
| llist_for_each_entry(smscb, &cstate->messages, list) |
| page_count += smscb->num_pages; |
| |
| return page_count; |
| } |
| |
| |
| /*! Obtain the Cell Global Identifier (CGI) of given BTS; returned in static buffer. */ |
| static struct osmo_cell_global_id *bts_get_cgi(struct gsm_bts *bts) |
| { |
| static struct osmo_cell_global_id cgi; |
| cgi.lai.plmn = bts->network->plmn; |
| cgi.lai.lac = bts->location_area_code; |
| cgi.cell_identity = bts->cell_identity; |
| return &cgi; |
| } |
| |
| /* represents the various lists that the BSC can create as part of a response */ |
| struct response_state { |
| struct osmo_cbsp_cell_list success; /* osmo_cbsp_cell_ent */ |
| struct llist_head fail; /* osmo_cbsp_fail_ent */ |
| struct osmo_cbsp_num_compl_list num_completed; /* osmo_cbsp_num_compl_ent */ |
| struct osmo_cbsp_loading_list loading; /* osmo_cbsp_loading_ent */ |
| }; |
| |
| /*! per-BTS callback function used by cbsp_per_bts(). |
| * \param[in] bts BTS currently being processed |
| * \param[in] dec decoded CBSP message currently being processed |
| * \param r_state response state accumulating cell lists (success/failure/...) |
| * \param priv opaque private data provided by caller of cbsp_per_bts() |
| * \returns 0 on success; negative TS 48.049 cause value on error */ |
| typedef int bts_cb_fn(struct gsm_bts *bts, const struct osmo_cbsp_decoded *dec, |
| struct response_state *r_state, void *priv); |
| |
| /* append a success for given cell to response state */ |
| static void append_success(struct response_state *r_state, struct gsm_bts *bts) |
| { |
| struct osmo_cbsp_cell_ent *cent = talloc_zero(r_state, struct osmo_cbsp_cell_ent); |
| struct osmo_cell_global_id *cgi = bts_get_cgi(bts); |
| |
| LOG_BTS(bts, DCBS, LOGL_INFO, "Success\n"); |
| |
| OSMO_ASSERT(cent); |
| |
| cent->cell_id.global = *cgi; |
| llist_add_tail(¢->list, &r_state->success.list); |
| } |
| |
| /* append a failure for given cell to response state */ |
| static void append_fail(struct response_state *r_state, struct gsm_bts *bts, uint8_t cause) |
| { |
| struct osmo_cbsp_fail_ent *fent = talloc_zero(r_state, struct osmo_cbsp_fail_ent); |
| struct osmo_cell_global_id *cgi = bts_get_cgi(bts); |
| |
| LOG_BTS(bts, DCBS, LOGL_NOTICE, "Failure Cause 0x%02x\n", cause); |
| |
| OSMO_ASSERT(fent); |
| |
| fent->id_discr = CELL_IDENT_WHOLE_GLOBAL; |
| fent->cell_id.global = *cgi; |
| fent->cause = cause; |
| llist_add_tail(&fent->list, &r_state->fail); |
| } |
| |
| /* append a 'number of broadcasts completed' for given cell to response state */ |
| static void append_bcast_compl(struct response_state *r_state, struct gsm_bts *bts, |
| struct bts_smscb_message *smscb) |
| { |
| struct osmo_cbsp_num_compl_ent *cent = talloc_zero(r_state, struct osmo_cbsp_num_compl_ent); |
| struct osmo_cell_global_id *cgi = bts_get_cgi(bts); |
| |
| LOG_BTS(bts, DCBS, LOGL_DEBUG, "Number of Broadcasts Completed: %u\n", smscb->bcast_count); |
| |
| OSMO_ASSERT(cent); |
| |
| r_state->num_completed.id_discr = CELL_IDENT_WHOLE_GLOBAL; |
| cent->cell_id.global = *cgi; |
| if (smscb->bcast_count > INT16_MAX) { |
| cent->num_compl = INT16_MAX; |
| cent->num_bcast_info = 0x01; /* Overflow */ |
| } else { |
| cent->num_compl = smscb->bcast_count; |
| cent->num_bcast_info = 0x00; |
| } |
| llist_add_tail(¢->list, &r_state->num_completed.list); |
| } |
| |
| /*! Iterate over all BTSs, find matching ones, execute command on BTS, add result |
| * to succeeded/failed lists. |
| * \param[in] net GSM network in which we operate |
| * \param[in] caller-allocated Response state structure collecting results |
| * \param[in] cell_list Decoded CBSP cell list describing BTSs to operate on |
| * \param[in] cb_fn Call-back function to call for each matching BTS |
| * \param[in] priv Opqaue private data; passed to cb_fn |
| * */ |
| static int cbsp_per_bts(struct gsm_network *net, struct response_state *r_state, |
| const struct osmo_cbsp_cell_list *cell_list, |
| bts_cb_fn *cb_fn, const struct osmo_cbsp_decoded *dec, void *priv) |
| { |
| struct osmo_cbsp_cell_ent *ent; |
| struct gsm_bts *bts; |
| uint8_t bts_status[net->num_bts]; |
| int rc, ret = 0; |
| |
| memset(bts_status, 0, sizeof(bts_status)); |
| INIT_LLIST_HEAD(&r_state->success.list); |
| INIT_LLIST_HEAD(&r_state->fail); |
| INIT_LLIST_HEAD(&r_state->num_completed.list); |
| INIT_LLIST_HEAD(&r_state->loading.list); |
| |
| /* special case as cell_list->list is empty in this case */ |
| if (cell_list->id_discr == CELL_IDENT_BSS) { |
| llist_for_each_entry(bts, &net->bts_list, list) { |
| bts_status[bts->nr] = 1; |
| /* call function on this BTS */ |
| rc = cb_fn(bts, dec, r_state, priv); |
| if (rc < 0) { |
| append_fail(r_state, bts, -rc); |
| ret = -1; |
| } else |
| append_success(r_state, bts); |
| } |
| } else { |
| /* normal case: iterate over cell list */ |
| llist_for_each_entry(ent, &cell_list->list, list) { |
| bool found_at_least_one = false; |
| /* find all matching BTSs for this entry */ |
| llist_for_each_entry(bts, &net->bts_list, list) { |
| struct gsm0808_cell_id cell_id = { |
| .id_discr = cell_list->id_discr, |
| .id = ent->cell_id |
| }; |
| if (!gsm_bts_matches_cell_id(bts, &cell_id)) |
| continue; |
| found_at_least_one = true; |
| /* skip any BTSs which we've already processed */ |
| if (bts_status[bts->nr]) |
| continue; |
| bts_status[bts->nr] = 1; |
| /* call function on this BTS */ |
| rc = cb_fn(bts, dec, r_state, priv); |
| if (rc < 0) { |
| append_fail(r_state, bts, -rc); |
| ret = -1; |
| } else |
| append_success(r_state, bts); |
| } |
| if (!found_at_least_one) { |
| struct osmo_cbsp_fail_ent *fent; |
| LOGP(DCBS, LOGL_NOTICE, "CBSP: Couldn't find a single matching BTS\n"); |
| fent = talloc_zero(r_state, struct osmo_cbsp_fail_ent); |
| OSMO_ASSERT(fent); |
| fent->id_discr = cell_list->id_discr; |
| fent->cell_id = ent->cell_id; |
| llist_add_tail(&fent->list, &r_state->fail); |
| ret = -1; |
| } |
| } |
| } |
| return ret; |
| } |
| |
| /*! Find an existing SMSCB message within given BTS. |
| * \param[in] chan_state BTS CBCH channel state |
| * \param[in] msg_id Message Id of to-be-found message |
| * \param[in] serial_nr Serial Number of to-be-found message |
| * \returns SMSCB message if found; NULL otherwise */ |
| struct bts_smscb_message *bts_find_smscb(struct bts_smscb_chan_state *chan_state, |
| uint16_t msg_id, uint16_t serial_nr) |
| { |
| struct bts_smscb_message *smscb; |
| |
| llist_for_each_entry(smscb, &chan_state->messages, list) { |
| if (smscb->input.msg_id == msg_id && smscb->input.serial_nr == serial_nr) |
| return smscb; |
| } |
| return NULL; |
| } |
| |
| /*! create a new SMSCB message for specified BTS; don't link it yet. |
| * \param[in] bts BTS for which the SMSCB is to be allocated |
| * \param[in] wrepl CBSP write-replace message |
| * \returns callee-allocated SMSCB message filled with data from wrepl */ |
| static struct bts_smscb_message *bts_smscb_msg_from_wrepl(struct gsm_bts *bts, |
| const struct osmo_cbsp_write_replace *wrepl) |
| { |
| struct bts_smscb_message *smscb = talloc_zero(bts, struct bts_smscb_message); |
| struct osmo_cbsp_content *cont; |
| int i; |
| |
| if (!smscb) |
| return NULL; |
| |
| OSMO_ASSERT(wrepl->is_cbs); |
| |
| /* initialize all pages inside the message */ |
| for (i = 0; i < ARRAY_SIZE(smscb->page); i++) { |
| struct bts_smscb_page *page = &smscb->page[i]; |
| page->nr = i+1; /* page numbers are 1-based */ |
| page->msg = smscb; |
| } |
| |
| /* initialize "header" part */ |
| smscb->input.msg_id = wrepl->msg_id; |
| smscb->input.serial_nr = wrepl->new_serial_nr; |
| smscb->input.category = wrepl->u.cbs.category; |
| smscb->input.rep_period = wrepl->u.cbs.rep_period; |
| smscb->input.num_bcast_req = wrepl->u.cbs.num_bcast_req; |
| smscb->input.dcs = wrepl->u.cbs.dcs; |
| smscb->num_pages = llist_count(&wrepl->u.cbs.msg_content); |
| if (smscb->num_pages > ARRAY_SIZE(smscb->page)) { |
| LOG_BTS(bts, DCBS, LOGL_ERROR, "SMSCB with too many pages (%u > %lu)\n", |
| smscb->num_pages, ARRAY_SIZE(smscb->page)); |
| talloc_free(smscb); |
| return NULL; |
| } |
| |
| i = 0; |
| llist_for_each_entry(cont, &wrepl->u.cbs.msg_content, list) { |
| struct gsm23041_msg_param_gsm *msg_param; |
| struct bts_smscb_page *page; |
| size_t bytes_used; |
| |
| /* we have just ensured a few lines above that this cannot overflow */ |
| page = &smscb->page[i++]; |
| msg_param = (struct gsm23041_msg_param_gsm *) &page->data[0]; |
| |
| /* build 6 byte header according to TS 23.041 9.4.1.2 */ |
| osmo_store16be(wrepl->new_serial_nr, &msg_param->serial_nr); |
| osmo_store16be(wrepl->msg_id, &msg_param->message_id); |
| msg_param->dcs = wrepl->u.cbs.dcs; |
| msg_param->page_param.num_pages = smscb->num_pages; |
| msg_param->page_param.page_nr = page->nr; |
| |
| OSMO_ASSERT(cont->user_len <= ARRAY_SIZE(cont->data)); |
| OSMO_ASSERT(cont->user_len <= ARRAY_SIZE(page->data) - sizeof(*msg_param)); |
| memcpy(&msg_param->content, cont->data, cont->user_len); |
| bytes_used = sizeof(*msg_param) + cont->user_len; |
| /* compute number of valid blocks in page */ |
| page->num_blocks = bytes_used / 22; |
| if (bytes_used % 22) |
| page->num_blocks += 1; |
| } |
| |
| return smscb; |
| } |
| |
| /*! remove a SMSCB message */ |
| void bts_smscb_del(struct bts_smscb_message *smscb, struct bts_smscb_chan_state *cstate, |
| const char *reason) |
| { |
| struct bts_smscb_page **arr; |
| int rc; |
| |
| LOG_BTS(cstate->bts, DCBS, LOGL_INFO, "%s Deleting %s (Reason: %s)\n", |
| bts_smscb_chan_state_name(cstate), bts_smscb_msg2str(smscb), reason); |
| llist_del(&smscb->list); |
| |
| /* we must recompute the scheduler array here, as the old one will have pointers |
| * to the pages of the just-to-be-deleted message */ |
| rc = bts_smscb_gen_sched_arr(cstate, &arr); |
| if (rc < 0) { |
| LOG_BTS(cstate->bts, DCBS, LOGL_ERROR, "Cannot generate new CBCH scheduler array after " |
| "removing message %s. WTF?\n", bts_smscb_msg2str(smscb)); |
| /* we cannot free the message now, to ensure the page pointers in the old |
| * array are still valid. let's re-add it to keep things sane */ |
| __bts_smscb_add(cstate, smscb); |
| } else { |
| /* success */ |
| talloc_free(smscb); |
| |
| /* replace array with new one */ |
| talloc_free(cstate->sched_arr); |
| cstate->sched_arr = arr; |
| cstate->sched_arr_size = rc; |
| cstate->next_idx = 0; |
| } |
| } |
| |
| |
| /********************************************************************************* |
| * Transmit of CBSP to CBC |
| *********************************************************************************/ |
| |
| /* transmit a CBSP RESTART message stating all message data was lost for entire BSS */ |
| int cbsp_tx_restart(struct bsc_cbc_link *cbc, bool is_emerg) |
| { |
| struct osmo_cbsp_decoded *cbsp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_RESTART); |
| |
| if (is_emerg) |
| cbsp->u.restart.bcast_msg_type = 0x01; |
| cbsp->u.restart.recovery_ind = 0x01; /* message data lost */ |
| cbsp->u.restart.cell_list.id_discr = CELL_IDENT_BSS; |
| |
| return cbsp_tx_decoded(cbc, cbsp); |
| } |
| |
| /* transmit a CBSP KEEPALIVE COMPLETE to the CBC */ |
| static int tx_cbsp_keepalive_compl(struct bsc_cbc_link *cbc) |
| { |
| struct osmo_cbsp_decoded *cbsp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_KEEP_ALIVE_COMPL); |
| return cbsp_tx_decoded(cbc, cbsp); |
| } |
| |
| /********************************************************************************* |
| * Per-BTS Processing of CBSP from CBC, called via cbsp_per_bts() |
| *********************************************************************************/ |
| |
| /* timer call-back once ETWS warning period has expired */ |
| static void etws_pn_cb(void *data) |
| { |
| struct gsm_bts *bts = (struct gsm_bts *)data; |
| LOG_BTS(bts, DCBS, LOGL_NOTICE, "ETWS PN Timeout; disabling broadcast via PCH\n"); |
| rsl_etws_pn_command(bts, RSL_CHAN_PCH_AGCH, NULL, 0); |
| } |
| |
| static void etws_primary_to_bts(struct gsm_bts *bts, const struct osmo_cbsp_write_replace *wrepl) |
| { |
| uint8_t etws_primary[ETWS_PRIM_NOTIF_SIZE]; |
| struct gsm_bts_trx *trx; |
| unsigned int count = 0; |
| int i, j; |
| |
| gen_etws_primary_notification(etws_primary, wrepl->new_serial_nr, wrepl->msg_id, |
| wrepl->u.emergency.warning_type, |
| wrepl->u.emergency.warning_sec_info); |
| |
| /* iterate over all lchan in each TS in each TRX of this BTS */ |
| llist_for_each_entry(trx, &bts->trx_list, list) { |
| for (i = 0; i < ARRAY_SIZE(trx->ts); i++) { |
| struct gsm_bts_trx_ts *ts = &trx->ts[i]; |
| for (j = 0; j < ARRAY_SIZE(ts->lchan); j++) { |
| struct gsm_lchan *lchan = &ts->lchan[j]; |
| if (!lchan_may_receive_data(lchan)) |
| continue; |
| gsm48_send_rr_app_info(lchan, 0x1, 0x0, etws_primary, |
| sizeof(etws_primary)); |
| count++; |
| } |
| } |
| } |
| |
| LOG_BTS(bts, DCBS, LOGL_NOTICE, "Sent ETWS Primary Notification via %u dedicated channels\n", |
| count); |
| |
| /* Notify BTS of primary ETWS notification via vendor-specific Abis message */ |
| if (osmo_bts_has_feature(&bts->features, BTS_FEAT_ETWS_PN)) { |
| rsl_etws_pn_command(bts, RSL_CHAN_PCH_AGCH, etws_primary, sizeof(etws_primary)); |
| LOG_BTS(bts, DCBS, LOGL_NOTICE, "Sent ETWS Primary Notification via common channel\n"); |
| if (wrepl->u.emergency.warning_period != 0xffffffff) { |
| osmo_timer_setup(&bts->etws_timer, etws_pn_cb, bts); |
| osmo_timer_schedule(&bts->etws_timer, wrepl->u.emergency.warning_period, 0); |
| } else |
| LOG_BTS(bts, DCBS, LOGL_NOTICE, "Unlimited ETWS PN broadcast, this breaks " |
| "normal network operation due to PCH blockage\n"); |
| } else |
| LOG_BTS(bts, DCBS, LOGL_ERROR, "BTS doesn't support RSL command for ETWS PN\n"); |
| } |
| |
| /*! Try to execute a write-replace operation; roll-back if it fails. |
| * \param[in] chan_state BTS CBCH channel state |
| * \param[in] extended_cbch Basic (false) or Extended (true) CBCH |
| * \param[in] new_msg New SMSCB message which should be added |
| * \param[in] exclude_msg Existing SMSCB message that shall be replaced (if possible). Can be NULL |
| * \return 0 on success; negative on error */ |
| static int bts_try_write_replace(struct bts_smscb_chan_state *chan_state, |
| struct bts_smscb_message *new_msg, |
| struct bts_smscb_message *exclude_msg, |
| struct response_state *r_state) |
| { |
| struct bts_smscb_page **arr; |
| int rc; |
| |
| if (exclude_msg) { |
| /* temporarily remove from list of SMSCB */ |
| llist_del(&exclude_msg->list); |
| } |
| /* temporarily add new_msg to list of SMSCB */ |
| __bts_smscb_add(chan_state, new_msg); |
| |
| /* attempt to create scheduling array */ |
| rc = bts_smscb_gen_sched_arr(chan_state, &arr); |
| if (rc < 0) { |
| /* it didn't work out; we couldn't schedule it */ |
| /* remove the new message again */ |
| llist_del(&new_msg->list); |
| /* up to the caller to free() it */ |
| if (exclude_msg) { |
| /* re-add the temporarily removed message */ |
| __bts_smscb_add(chan_state, new_msg); |
| } |
| return -1; |
| } |
| |
| /* success! */ |
| if (exclude_msg) { |
| LOG_BTS(chan_state->bts, DCBS, LOGL_INFO, "%s Replaced MsgId=0x%04x/Serial=0x%04x, " |
| "pages(%u -> %u), period(%u -> %u), num_bcast(%u -> %u)\n", |
| bts_smscb_chan_state_name(chan_state), |
| new_msg->input.msg_id, new_msg->input.serial_nr, |
| exclude_msg->num_pages, new_msg->num_pages, |
| exclude_msg->input.rep_period, new_msg->input.rep_period, |
| exclude_msg->input.num_bcast_req, new_msg->input.num_bcast_req); |
| append_bcast_compl(r_state, chan_state->bts, exclude_msg); |
| talloc_free(exclude_msg); |
| } else |
| LOG_BTS(chan_state->bts, DCBS, LOGL_INFO, "%s Added %s\n", |
| bts_smscb_chan_state_name(chan_state), bts_smscb_msg2str(new_msg)); |
| |
| /* replace array with new one */ |
| talloc_free(chan_state->sched_arr); |
| chan_state->sched_arr = arr; |
| chan_state->sched_arr_size = rc; |
| chan_state->next_idx = 0; |
| return 0; |
| } |
| |
| |
| static int bts_rx_write_replace(struct gsm_bts *bts, const struct osmo_cbsp_decoded *dec, |
| struct response_state *r_state, void *priv) |
| { |
| const struct osmo_cbsp_write_replace *wrepl = &dec->u.write_replace; |
| bool extended_cbch = wrepl->u.cbs.channel_ind; |
| struct bts_smscb_chan_state *chan_state = bts_get_smscb_chan(bts, extended_cbch); |
| struct bts_smscb_message *smscb; |
| int rc; |
| |
| if (!wrepl->is_cbs) { |
| etws_primary_to_bts(bts, wrepl); |
| return 0; |
| } |
| |
| /* check if cell has a CBCH at all */ |
| if (!gsm_bts_get_cbch(bts)) |
| return -CBSP_CAUSE_CB_NOT_SUPPORTED; |
| |
| /* check for duplicate */ |
| if (bts_find_smscb(chan_state, wrepl->msg_id, wrepl->new_serial_nr)) |
| return -CBSP_CAUSE_MSG_REF_ALREADY_USED; |
| |
| if (!wrepl->old_serial_nr) { /* new message */ |
| /* create new message */ |
| smscb = bts_smscb_msg_from_wrepl(bts, wrepl); |
| if (!smscb) |
| return -CBSP_CAUSE_BSC_MEMORY_EXCEEDED; |
| /* check if scheduling permits this additional message */ |
| rc = bts_try_write_replace(chan_state, smscb, NULL, r_state); |
| if (rc < 0) { |
| talloc_free(smscb); |
| return -CBSP_CAUSE_BSC_CAPACITY_EXCEEDED; |
| } |
| } else { /* modify / replace existing message */ |
| struct bts_smscb_message *smscb_old; |
| /* find existing message */ |
| smscb_old = bts_find_smscb(chan_state, wrepl->msg_id, *wrepl->old_serial_nr); |
| if (!smscb_old) |
| return -CBSP_CAUSE_MSG_REF_NOT_IDENTIFIED; |
| /* create new message */ |
| smscb = bts_smscb_msg_from_wrepl(bts, wrepl); |
| if (!smscb) |
| return -CBSP_CAUSE_BSC_MEMORY_EXCEEDED; |
| /* check if scheduling permits this modified message */ |
| rc = bts_try_write_replace(chan_state, smscb, smscb_old, r_state); |
| if (rc < 0) { |
| talloc_free(smscb); |
| return -CBSP_CAUSE_BSC_CAPACITY_EXCEEDED; |
| } |
| } |
| return 0; |
| } |
| |
| static int bts_rx_kill(struct gsm_bts *bts, const struct osmo_cbsp_decoded *dec, |
| struct response_state *r_state, void *priv) |
| { |
| const struct osmo_cbsp_kill *kill = &dec->u.kill; |
| struct bts_smscb_chan_state *chan_state; |
| struct bts_smscb_message *smscb; |
| bool extended = false; |
| |
| if (kill->channel_ind && *kill->channel_ind == 0x01) |
| extended = true; |
| chan_state = bts_get_smscb_chan(bts, extended); |
| |
| /* Find message by msg_id + old_serial_nr */ |
| smscb = bts_find_smscb(chan_state, kill->msg_id, kill->old_serial_nr); |
| if (!smscb) |
| return -CBSP_CAUSE_MSG_REF_NOT_IDENTIFIED; |
| |
| /* Remove it */ |
| bts_smscb_del(smscb, chan_state, "KILL"); |
| return 0; |
| } |
| |
| static int bts_rx_reset(struct gsm_bts *bts, const struct osmo_cbsp_decoded *dec, |
| struct response_state *r_state, void *priv) |
| { |
| struct bts_smscb_chan_state *chan_state; |
| struct bts_smscb_message *smscb, *smscb2; |
| |
| /* remove all SMSCB from CBCH BASIC this BTS */ |
| chan_state = bts_get_smscb_chan(bts, false); |
| llist_for_each_entry_safe(smscb, smscb2, &chan_state->messages, list) |
| bts_smscb_del(smscb, chan_state, "RESET"); |
| |
| /* remove all SMSCB from CBCH EXTENDED this BTS */ |
| chan_state = bts_get_smscb_chan(bts, true); |
| llist_for_each_entry_safe(smscb, smscb2, &chan_state->messages, list) |
| bts_smscb_del(smscb, chan_state, "RESET"); |
| |
| return 0; |
| } |
| |
| /********************************************************************************* |
| * Receive of CBSP from CBC |
| *********************************************************************************/ |
| |
| static int cbsp_rx_write_replace(struct bsc_cbc_link *cbc, const struct osmo_cbsp_decoded *dec) |
| { |
| const struct osmo_cbsp_write_replace *wrepl = &dec->u.write_replace; |
| struct gsm_network *net = cbc->net; |
| struct response_state *r_state = talloc_zero(cbc, struct response_state); |
| struct osmo_cbsp_decoded *resp; |
| enum cbsp_channel_ind channel_ind; |
| int rc; |
| |
| LOGP(DCBS, LOGL_INFO, "CBSP Rx WRITE_REPLACE (%s)\n", wrepl->is_cbs ? "CBS" : "EMERGENCY"); |
| |
| rc = cbsp_per_bts(net, r_state, &dec->u.write_replace.cell_list, |
| bts_rx_write_replace, dec, NULL); |
| /* generate response */ |
| if (rc < 0) { |
| resp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_WRITE_REPLACE_FAIL); |
| struct osmo_cbsp_write_replace_failure *fail = &resp->u.write_replace_fail; |
| fail->msg_id = wrepl->msg_id; |
| fail->new_serial_nr = wrepl->new_serial_nr; |
| fail->old_serial_nr = wrepl->old_serial_nr; |
| llist_replace_head(&fail->fail_list, &r_state->fail); |
| fail->cell_list.id_discr = r_state->success.id_discr; |
| llist_replace_head(&fail->cell_list.list, &r_state->success.list); |
| if (wrepl->is_cbs) { |
| channel_ind = wrepl->u.cbs.channel_ind; |
| fail->channel_ind = &channel_ind; |
| } |
| if (wrepl->old_serial_nr) { |
| fail->num_compl_list.id_discr = r_state->num_completed.id_discr; |
| llist_replace_head(&fail->num_compl_list.list, &r_state->num_completed.list); |
| } |
| } else { |
| resp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_WRITE_REPLACE_COMPL); |
| struct osmo_cbsp_write_replace_complete *compl = &resp->u.write_replace_compl; |
| compl->msg_id = wrepl->msg_id; |
| compl->new_serial_nr = wrepl->new_serial_nr; |
| compl->old_serial_nr = wrepl->old_serial_nr; |
| compl->cell_list.id_discr = r_state->success.id_discr; |
| llist_replace_head(&compl->cell_list.list, &r_state->success.list); |
| if (wrepl->is_cbs) { |
| channel_ind = wrepl->u.cbs.channel_ind; |
| compl->channel_ind = &channel_ind; |
| } |
| if (wrepl->old_serial_nr) { |
| compl->num_compl_list.id_discr = r_state->num_completed.id_discr; |
| llist_replace_head(&compl->num_compl_list.list, &r_state->num_completed.list); |
| } |
| } |
| |
| cbsp_tx_decoded(cbc, resp); |
| talloc_free(r_state); |
| return rc; |
| } |
| |
| static int cbsp_rx_keep_alive(struct bsc_cbc_link *cbc, const struct osmo_cbsp_decoded *dec) |
| { |
| LOGP(DCBS, LOGL_DEBUG, "CBSP Rx KEEP_ALIVE\n"); |
| |
| /* FIXME: repetition period */ |
| return tx_cbsp_keepalive_compl(cbc); |
| } |
| |
| static int cbsp_rx_kill(struct bsc_cbc_link *cbc, const struct osmo_cbsp_decoded *dec) |
| { |
| const struct osmo_cbsp_kill *kill = &dec->u.kill; |
| struct gsm_network *net = cbc->net; |
| struct response_state *r_state = talloc_zero(cbc, struct response_state); |
| struct osmo_cbsp_decoded *resp; |
| int rc; |
| |
| LOGP(DCBS, LOGL_DEBUG, "CBSP Rx KILL\n"); |
| |
| rc = cbsp_per_bts(net, r_state, &dec->u.kill.cell_list, bts_rx_kill, dec, NULL); |
| if (rc < 0) { |
| resp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_KILL_FAIL); |
| struct osmo_cbsp_kill_failure *fail = &resp->u.kill_fail; |
| fail->msg_id = kill->msg_id; |
| fail->old_serial_nr = kill->old_serial_nr; |
| fail->channel_ind = kill->channel_ind; |
| llist_replace_head(&fail->fail_list, &r_state->fail); |
| |
| fail->cell_list.id_discr = r_state->success.id_discr; |
| llist_replace_head(&fail->cell_list.list, &r_state->success.list); |
| |
| fail->num_compl_list.id_discr = r_state->num_completed.id_discr; |
| llist_replace_head(&fail->num_compl_list.list, &r_state->num_completed.list); |
| } else { |
| resp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_KILL_COMPL); |
| struct osmo_cbsp_kill_complete *compl = &resp->u.kill_compl; |
| compl->msg_id = kill->msg_id; |
| compl->old_serial_nr = kill->old_serial_nr; |
| compl->channel_ind = kill->channel_ind; |
| |
| compl->cell_list.id_discr = r_state->success.id_discr; |
| llist_replace_head(&compl->cell_list.list, &r_state->success.list); |
| |
| compl->num_compl_list.id_discr = r_state->num_completed.id_discr; |
| llist_replace_head(&compl->num_compl_list.list, &r_state->num_completed.list); |
| } |
| |
| cbsp_tx_decoded(cbc, resp); |
| talloc_free(r_state); |
| return rc; |
| } |
| |
| static int cbsp_rx_reset(struct bsc_cbc_link *cbc, const struct osmo_cbsp_decoded *dec) |
| { |
| struct gsm_network *net = cbc->net; |
| struct response_state *r_state = talloc_zero(cbc, struct response_state); |
| struct osmo_cbsp_decoded *resp; |
| int rc; |
| |
| LOGP(DCBS, LOGL_DEBUG, "CBSP Rx RESET\n"); |
| |
| rc = cbsp_per_bts(net, r_state, &dec->u.reset.cell_list, bts_rx_reset, dec, NULL); |
| if (rc < 0) { |
| resp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_RESET_FAIL); |
| struct osmo_cbsp_reset_failure *fail = &resp->u.reset_fail; |
| llist_replace_head(&fail->fail_list, &r_state->fail); |
| |
| fail->cell_list.id_discr = r_state->success.id_discr; |
| llist_replace_head(&fail->cell_list.list, &r_state->success.list); |
| } else { |
| resp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_RESET_COMPL); |
| struct osmo_cbsp_reset_complete *compl = &resp->u.reset_compl; |
| if (dec->u.reset.cell_list.id_discr == CELL_IDENT_BSS) { |
| /* replace the list of individual cell identities with CELL_IDENT_BSS */ |
| compl->cell_list.id_discr = CELL_IDENT_BSS; |
| /* no need to free success_list entries, hierarchical talloc works */ |
| } else { |
| compl->cell_list.id_discr = r_state->success.id_discr; |
| llist_replace_head(&compl->cell_list.list, &r_state->success.list); |
| } |
| } |
| cbsp_tx_decoded(cbc, resp); |
| talloc_free(r_state); |
| return rc; |
| } |
| |
| |
| /*! process an incoming, already decoded CBSP message from the CBC. |
| * \param[in] cbc link to the CBC |
| * \param[in] dec decoded CBSP message structure. Ownership not transferred. |
| * \returns 0 on success; negative on error. */ |
| int cbsp_rx_decoded(struct bsc_cbc_link *cbc, const struct osmo_cbsp_decoded *dec) |
| { |
| int rc = -1; |
| |
| switch (dec->msg_type) { |
| case CBSP_MSGT_WRITE_REPLACE: /* create or modify message */ |
| rc = cbsp_rx_write_replace(cbc, dec); |
| break; |
| case CBSP_MSGT_KEEP_ALIVE: /* solicit an acknowledgement */ |
| rc = cbsp_rx_keep_alive(cbc, dec); |
| break; |
| case CBSP_MSGT_KILL: /* remove message */ |
| rc = cbsp_rx_kill(cbc, dec); |
| break; |
| case CBSP_MSGT_RESET: /* stop broadcasting of all messages */ |
| rc = cbsp_rx_reset(cbc, dec); |
| break; |
| case CBSP_MSGT_LOAD_QUERY: |
| case CBSP_MSGT_MSG_STATUS_QUERY: |
| case CBSP_MSGT_SET_DRX: |
| LOGP(DCBS, LOGL_ERROR, "Received Unimplemented CBSP Message Type %s", |
| get_value_string(cbsp_msg_type_names, dec->msg_type)); |
| /* we should implement those eventually */ |
| break; |
| default: |
| LOGP(DCBS, LOGL_ERROR, "Received Unknown/Unexpected CBSP Message Type %s", |
| get_value_string(cbsp_msg_type_names, dec->msg_type)); |
| break; |
| } |
| return rc; |
| } |
| |
| /********************************************************************************* |
| * VTY Interface (Introspection) |
| *********************************************************************************/ |
| |
| static void vty_dump_smscb_chan_state(struct vty *vty, const struct bts_smscb_chan_state *cs) |
| { |
| const struct bts_smscb_message *sm; |
| |
| vty_out(vty, "%s CBCH:%s", cs == &cs->bts->cbch_basic ? "BASIC" : "EXTENDED", VTY_NEWLINE); |
| |
| vty_out(vty, " MsgId | SerNo | Pg | Category | Perd | #Tx | #Req | DCS%s", VTY_NEWLINE); |
| vty_out(vty, "-------|-------|----|---------------|------|------|------|----%s", VTY_NEWLINE); |
| llist_for_each_entry(sm, &cs->messages, list) { |
| vty_out(vty, " %04x | %04x | %2u | %13s | %4u | %4u | %4u | %02x%s", |
| sm->input.msg_id, sm->input.serial_nr, sm->num_pages, |
| get_value_string(cbsp_category_names, sm->input.category), |
| sm->input.rep_period, sm->bcast_count, sm->input.num_bcast_req, |
| sm->input.dcs, VTY_NEWLINE); |
| } |
| vty_out(vty, "%s", VTY_NEWLINE); |
| } |
| |
| DEFUN(bts_show_cbs, bts_show_cbs_cmd, |
| "show bts <0-255> smscb [(basic|extended)]", |
| SHOW_STR "Display information about a BTS\n" "BTS number\n" |
| "SMS Cell Broadcast State\n" |
| "Show only information related to CBCH BASIC\n" |
| "Show only information related to CBCH EXTENDED\n") |
| { |
| struct gsm_network *net = gsmnet_from_vty(vty); |
| int bts_nr = atoi(argv[0]); |
| struct gsm_bts *bts; |
| |
| if (bts_nr >= net->num_bts) { |
| vty_out(vty, "%% can't find BTS '%s'%s", argv[0], VTY_NEWLINE); |
| return CMD_WARNING; |
| } |
| bts = gsm_bts_num(net, bts_nr); |
| |
| if (argc < 2 || !strcmp(argv[1], "basic")) |
| vty_dump_smscb_chan_state(vty, &bts->cbch_basic); |
| if (argc < 2 || !strcmp(argv[1], "extended")) |
| vty_dump_smscb_chan_state(vty, &bts->cbch_extended); |
| |
| return CMD_SUCCESS; |
| } |
| |
| void smscb_vty_init(void) |
| { |
| install_element_ve(&bts_show_cbs_cmd); |
| } |