Hello Jenkins Builder, I'd like you to reexamine a change. Please visit
https://gerrit.osmocom.org/5921 to look at the new patch set (#3). HO: Implement load based handover, as handover_decision_2.c Change-Id: Ice2d3ef5668564a9d3bc4d5118d59dfaa9af6978 --- M configure.ac M include/osmocom/bsc/Makefile.am M include/osmocom/bsc/gsm_data.h M include/osmocom/bsc/gsm_data_shared.h M include/osmocom/bsc/handover.h M include/osmocom/bsc/handover_cfg.h A include/osmocom/bsc/handover_decision_2.h M include/osmocom/bsc/handover_vty.h M include/osmocom/bsc/signal.h M src/libbsc/Makefile.am M src/libbsc/abis_rsl.c M src/libbsc/bsc_api.c M src/libbsc/bsc_vty.c M src/libbsc/chan_alloc.c A src/libbsc/handover_decision_2.c M src/libbsc/handover_logic.c M src/libbsc/handover_vty.c M src/libbsc/net_init.c M src/libcommon/gsm_data.c M src/libcommon/handover_cfg.c M src/osmo-bsc/osmo_bsc_bssap.c M src/osmo-bsc/osmo_bsc_main.c M tests/Makefile.am A tests/handover/Makefile.am A tests/handover/handover_test.c A tests/handover/handover_test.ok M tests/testsuite.at 27 files changed, 3,858 insertions(+), 73 deletions(-) git pull ssh://gerrit.osmocom.org:29418/osmo-bsc refs/changes/21/5921/3 diff --git a/configure.ac b/configure.ac index d756970..fc4678f 100644 --- a/configure.ac +++ b/configure.ac @@ -166,6 +166,7 @@ tests/subscr/Makefile tests/nanobts_omlattr/Makefile tests/bssap/Makefile + tests/handover/Makefile doc/Makefile doc/examples/Makefile Makefile) diff --git a/include/osmocom/bsc/Makefile.am b/include/osmocom/bsc/Makefile.am index 699aeb3..a3d9adf 100644 --- a/include/osmocom/bsc/Makefile.am +++ b/include/osmocom/bsc/Makefile.am @@ -27,6 +27,7 @@ handover.h \ handover_cfg.h \ handover_decision.h \ + handover_decision_2.h \ handover_vty.h \ ipaccess.h \ meas_feed.h \ diff --git a/include/osmocom/bsc/gsm_data.h b/include/osmocom/bsc/gsm_data.h index d9dd2d4..c24e22a 100644 --- a/include/osmocom/bsc/gsm_data.h +++ b/include/osmocom/bsc/gsm_data.h @@ -10,6 +10,7 @@ #include <osmocom/core/rate_ctr.h> #include <osmocom/core/select.h> #include <osmocom/core/stats.h> +#include <osmocom/gsm/protocol/gsm_08_08.h> #include <osmocom/crypt/auth.h> @@ -74,8 +75,8 @@ /* penalty timers for handover */ struct ho_penalty_timer { struct llist_head entry; - uint8_t bts; - time_t timeout; + uint8_t bts_nr; + unsigned int timeout; }; /* active radio connection of a mobile subscriber */ @@ -112,8 +113,18 @@ struct llist_head ho_dtap_cache; unsigned int ho_dtap_cache_len; - /* penalty timers for handover */ + /* failure count and penalty timers for handover */ + int ho_failure; struct llist_head ho_penalty_timers; + + /* "Codec List (MSC Preferred)" as received by the BSSAP Assignment Request. 3GPP 48.008 + * 3.2.2.103 says: + * The "Codec List (MSC Preferred)" shall not include codecs + * that are not supported by the MS. + * i.e. by heeding the "Codec list (MSC Preferred)", we inherently heed the MS bearer + * capabilities, which the MSC is required to translate into the codec list. */ + struct gsm0808_speech_codec_list codec_list; + bool codec_list_present; }; static inline struct gsm_bts *conn_get_bts(struct gsm_subscriber_connection *conn) { @@ -243,7 +254,12 @@ uint16_t network_code; int a5_encryption; int neci; + struct handover_cfg *ho; + struct { + unsigned int congestion_check_interval_s; + struct osmo_timer_list congestion_check_timer; + } ho2; struct rate_ctr_group *bsc_ctrs; @@ -443,4 +459,11 @@ bool classmark_is_r99(struct gsm_classmark *cm); +void conn_penalty_timer_add(struct gsm_subscriber_connection *conn, + struct gsm_bts *bts, int timeout); +unsigned int conn_penalty_timer_remaining(struct gsm_subscriber_connection *conn, + struct gsm_bts *bts); +void conn_penalty_timer_clear(struct gsm_subscriber_connection *conn, + struct gsm_bts *bts); + #endif /* _GSM_DATA_H */ diff --git a/include/osmocom/bsc/gsm_data_shared.h b/include/osmocom/bsc/gsm_data_shared.h index 86c5ca9..776e047 100644 --- a/include/osmocom/bsc/gsm_data_shared.h +++ b/include/osmocom/bsc/gsm_data_shared.h @@ -269,6 +269,7 @@ struct gsm_meas_rep meas_rep[MAX_MEAS_REP]; int meas_rep_idx; int meas_rep_count; + uint8_t meas_rep_last_seen_nr; /* GSM Random Access data */ struct gsm48_req_ref *rqd_ref; diff --git a/include/osmocom/bsc/handover.h b/include/osmocom/bsc/handover.h index a9349ee..f764456 100644 --- a/include/osmocom/bsc/handover.h +++ b/include/osmocom/bsc/handover.h @@ -5,6 +5,8 @@ struct gsm_subscriber_connection; int bsc_handover_start(struct gsm_lchan *old_lchan, struct gsm_bts *bts); +int bsc_handover_start_lchan_change(struct gsm_lchan *old_lchan, struct gsm_bts *bts, + enum gsm_chan_t new_lchan_type); void bsc_clear_handover(struct gsm_subscriber_connection *conn, int free_lchan); struct gsm_lchan *bsc_handover_pending(struct gsm_lchan *new_lchan); diff --git a/include/osmocom/bsc/handover_cfg.h b/include/osmocom/bsc/handover_cfg.h index 1b81235..7ef27ee 100644 --- a/include/osmocom/bsc/handover_cfg.h +++ b/include/osmocom/bsc/handover_cfg.h @@ -10,6 +10,8 @@ * the defaults from 'network' level are used implicitly, and changes take effect immediately. */ struct handover_cfg; +#define HO_CFG_CONGESTION_CHECK_DEFAULT 10 + struct handover_cfg *ho_cfg_init(void *ctx, struct handover_cfg *higher_level_cfg); #define HO_CFG_STR_HANDOVER "Handover options\n" @@ -46,22 +48,6 @@ static inline const char *tdma2a(bool val) { return val? "full" : "subset"; -} - -static inline const int a2congestion_check_interval(const char *arg) -{ - if (!strcmp(arg, "disabled")) - return 0; - return atoi(arg); -} - -static inline const char *congestion_check_interval2a(int val) -{ - static char str[9]; - if (val < 1 - || snprintf(str, sizeof(str), "%d", val) >= sizeof(str)) - return "disabled"; - return str; } /* The HO_CFG_ONE_MEMBER macro gets redefined, depending on whether to define struct members, @@ -146,14 +132,6 @@ "Enable or disable in-call channel re-assignment" HO_CFG_STR_2 \ "Disable in-call assignment\n" \ "Enable in-call assignment\n") \ - \ - HO_CFG_ONE_MEMBER(int, congestion_check_interval, 10, \ - "handover congestion-check", "disabled|<1-60>", \ - a2congestion_check_interval, "%s", congestion_check_interval2a, \ - HO_CFG_STR_HANDOVER \ - "Configure congestion check interval" HO_CFG_STR_2 \ - "Disable congestion checking, do not handover based on cell overload\n" \ - "Congestion check interval in seconds\n") \ \ HO_CFG_ONE_MEMBER(bool, full_tdma, subset, \ "handover tdma-measurement", "full|subset", a2tdma, "%s", tdma2a, \ diff --git a/include/osmocom/bsc/handover_decision_2.h b/include/osmocom/bsc/handover_decision_2.h new file mode 100644 index 0000000..f245b07 --- /dev/null +++ b/include/osmocom/bsc/handover_decision_2.h @@ -0,0 +1,9 @@ +/* Handover Decision Algorithm 2 for intra-BSC (inter-BTS) handover, public API for OsmoBSC */ + +#pragma once +struct gsm_bts; + +void hodec2_init(struct gsm_network *net); + +void hodec2_on_change_congestion_check_interval(struct gsm_network *net, unsigned int new_interval); +void hodec2_congestion_check(struct gsm_network *net); diff --git a/include/osmocom/bsc/handover_vty.h b/include/osmocom/bsc/handover_vty.h index 48af136..6ad5276 100644 --- a/include/osmocom/bsc/handover_vty.h +++ b/include/osmocom/bsc/handover_vty.h @@ -4,4 +4,5 @@ #include <osmocom/bsc/handover_cfg.h> void ho_vty_init(); -void ho_vty_write(struct vty *vty, const char *indent, struct handover_cfg *ho); +void ho_vty_write_net(struct vty *vty, struct gsm_network *net); +void ho_vty_write_bts(struct vty *vty, struct gsm_bts *bts); diff --git a/include/osmocom/bsc/signal.h b/include/osmocom/bsc/signal.h index 9c0d5a3..1c8b51e 100644 --- a/include/osmocom/bsc/signal.h +++ b/include/osmocom/bsc/signal.h @@ -104,6 +104,7 @@ enum signal_global { S_GLOBAL_BTS_CLOSE_OM, + S_GLOBAL_BTS_NEW, }; /* SS_RF signals */ diff --git a/src/libbsc/Makefile.am b/src/libbsc/Makefile.am index d118f44..7e088e5 100644 --- a/src/libbsc/Makefile.am +++ b/src/libbsc/Makefile.am @@ -58,5 +58,6 @@ bsc_dyn_ts.c \ bts_ipaccess_nanobts_omlattr.c \ handover_vty.c \ + handover_decision_2.c \ $(NULL) diff --git a/src/libbsc/abis_rsl.c b/src/libbsc/abis_rsl.c index bdd86a9..82b21b9 100644 --- a/src/libbsc/abis_rsl.c +++ b/src/libbsc/abis_rsl.c @@ -1523,8 +1523,9 @@ } mr->lchan->meas_rep_count++; - LOGP(DRSL, LOGL_DEBUG, "%s: meas_rep_cnt++=%d\n", - gsm_lchan_name(mr->lchan), mr->lchan->meas_rep_count); + mr->lchan->meas_rep_last_seen_nr = mr->nr; + LOGP(DRSL, LOGL_DEBUG, "%s: meas_rep_count++=%d meas_rep_last_seen_nr=%u\n", + gsm_lchan_name(mr->lchan), mr->lchan->meas_rep_count, mr->lchan->meas_rep_last_seen_nr); print_meas_rep(msg->lchan, mr); diff --git a/src/libbsc/bsc_api.c b/src/libbsc/bsc_api.c index a501db2..73bdee5 100644 --- a/src/libbsc/bsc_api.c +++ b/src/libbsc/bsc_api.c @@ -323,8 +323,6 @@ void bsc_subscr_con_free(struct gsm_subscriber_connection *conn) { - struct ho_penalty_timer *penalty; - if (!conn) return; @@ -349,12 +347,7 @@ /* drop pending messages */ ho_dtap_cache_flush(conn, 0); - /* flush handover penalty timers */ - while ((penalty = llist_first_entry_or_null(&conn->ho_penalty_timers, - struct ho_penalty_timer, entry))) { - llist_del(&penalty->entry); - talloc_free(penalty); - } + conn_penalty_timer_clear(conn, NULL); llist_del(&conn->entry); talloc_free(conn); @@ -672,8 +665,8 @@ struct lchan_signal_data sig; struct gsm48_hdr *gh = msgb_l3(msg); - DEBUGP(DRR, "HANDOVER FAILED cause = %s\n", - rr_cause_name(gh->data[0])); + DEBUGP(DRR, "HANDOVER FAILED cause = %s\n", rr_cause_name(gh->data[0])); + DEBUGP(DHO, "HANDOVER FAILED cause = %s\n", rr_cause_name(gh->data[0])); sig.lchan = msg->lchan; sig.mr = NULL; diff --git a/src/libbsc/bsc_vty.c b/src/libbsc/bsc_vty.c index a304e86..d4591b7 100644 --- a/src/libbsc/bsc_vty.c +++ b/src/libbsc/bsc_vty.c @@ -795,7 +795,7 @@ if (bts->pcu_sock_path) vty_out(vty, " pcu-socket %s%s", bts->pcu_sock_path, VTY_NEWLINE); - ho_vty_write(vty, " ", bts->ho); + ho_vty_write_bts(vty, bts); config_write_bts_model(vty, bts); } @@ -827,7 +827,7 @@ vty_out(vty, " neci %u%s", gsmnet->neci, VTY_NEWLINE); vty_out(vty, " paging any use tch %d%s", gsmnet->pag_any_tch, VTY_NEWLINE); - ho_vty_write(vty, " ", gsmnet->ho); + ho_vty_write_net(vty, gsmnet); VTY_OUT_TIMER(3101); VTY_OUT_TIMER(3103); diff --git a/src/libbsc/chan_alloc.c b/src/libbsc/chan_alloc.c index b3056da..9e83f27 100644 --- a/src/libbsc/chan_alloc.c +++ b/src/libbsc/chan_alloc.c @@ -414,6 +414,7 @@ /* reset measurement report counter and index */ lchan->meas_rep_count = 0; lchan->meas_rep_idx = 0; + lchan->meas_rep_last_seen_nr = 255; /* clear sapis */ memset(lchan->sapis, 0, ARRAY_SIZE(lchan->sapis)); diff --git a/src/libbsc/handover_decision_2.c b/src/libbsc/handover_decision_2.c new file mode 100644 index 0000000..2286147 --- /dev/null +++ b/src/libbsc/handover_decision_2.c @@ -0,0 +1,1761 @@ +/* Handover Decision Algorithm 2 for intra-BSC (inter-BTS) handover, public API for OsmoBSC. */ + +/* (C) 2017 by sysmocom - s.f.m.c. GmbH <i...@sysmocom.de> + * + * All Rights Reserved + * + * Author: Andreas Eversberg <jo...@eversberg.eu> + * Neels Hofmeyr <nhofm...@sysmocom.de> + * + * 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 <stdbool.h> +#include <errno.h> + +#include <osmocom/bsc/debug.h> +#include <osmocom/bsc/gsm_data.h> +#include <osmocom/bsc/handover.h> +#include <osmocom/bsc/handover_decision.h> +#include <osmocom/bsc/handover_decision_2.h> +#include <osmocom/bsc/handover_cfg.h> +#include <osmocom/bsc/bsc_subscriber.h> +#include <osmocom/bsc/chan_alloc.h> +#include <osmocom/bsc/signal.h> + +#define LOGPHOBTS(bts, level, fmt, args...) \ + LOGP(DHODEC, level, "(BTS %u) " fmt, bts->nr, ## args) + +#define LOGPHOLCHAN(lchan, level, fmt, args...) \ + LOGP(DHODEC, level, "(lchan %u.%u%u%u %s) (subscr %s) " fmt, \ + lchan->ts->trx->bts->nr, \ + lchan->ts->trx->nr, \ + lchan->ts->nr, \ + lchan->nr, \ + gsm_pchan_name(lchan->ts->pchan), \ + bsc_subscr_name(lchan->conn->bsub), \ + ## args) + +#define LOGPHOLCHANTOBTS(lchan, new_bts, level, fmt, args...) \ + LOGP(DHODEC, level, "(lchan %u.%u%u%u %s)->(BTS %u) (subscr %s) " fmt, \ + lchan->ts->trx->bts->nr, \ + lchan->ts->trx->nr, \ + lchan->ts->nr, \ + lchan->nr, \ + gsm_pchan_name(lchan->ts->pchan), \ + new_bts->nr, \ + bsc_subscr_name(lchan->conn->bsub), \ + ## args) + +#define REQUIREMENT_A_TCHF 0x01 +#define REQUIREMENT_B_TCHF 0x02 +#define REQUIREMENT_C_TCHF 0x04 +#define REQUIREMENT_A_TCHH 0x10 +#define REQUIREMENT_B_TCHH 0x20 +#define REQUIREMENT_C_TCHH 0x40 +#define REQUIREMENT_TCHF_MASK (REQUIREMENT_A_TCHF | REQUIREMENT_B_TCHF | REQUIREMENT_C_TCHF) +#define REQUIREMENT_TCHH_MASK (REQUIREMENT_A_TCHH | REQUIREMENT_B_TCHH | REQUIREMENT_C_TCHH) +#define REQUIREMENT_A_MASK (REQUIREMENT_A_TCHF | REQUIREMENT_A_TCHH) +#define REQUIREMENT_B_MASK (REQUIREMENT_B_TCHF | REQUIREMENT_B_TCHH) +#define REQUIREMENT_C_MASK (REQUIREMENT_C_TCHF | REQUIREMENT_C_TCHH) + +struct ho_candidate { + struct gsm_lchan *lchan; /* candidate for whom */ + struct gsm_bts *bts; /* target BTS */ + uint8_t requirements; /* what is fulfilled */ + int avg; /* average RX level */ +}; + +enum ho_reason { + HO_REASON_INTERFERENCE, + HO_REASON_BAD_QUALITY, + HO_REASON_LOW_RXLEVEL, + HO_REASON_MAX_DISTANCE, + HO_REASON_BETTER_CELL, + HO_REASON_CONGESTION, +}; + +static const struct value_string ho_reason_names[] = { + { HO_REASON_INTERFERENCE, "interference (bad quality)" }, + { HO_REASON_BAD_QUALITY, "bad quality" }, + { HO_REASON_LOW_RXLEVEL, "low rxlevel" }, + { HO_REASON_MAX_DISTANCE, "maximum allowed distance" }, + { HO_REASON_BETTER_CELL, "better cell" }, + { HO_REASON_CONGESTION, "congestion" }, + {0, NULL} +}; + +static const char *ho_reason_name(int value) +{ + return get_value_string(ho_reason_names, value); +} + + +static bool ho2_initialized = false; +static enum ho_reason global_ho_reason; + +static void congestion_check_cb(void *arg); + +/* This function gets called on ho2 init, whenever the congestion check interval is changed, and also + * when the timer has fired to trigger again after the next congestion check timeout. */ +static void reinit_congestion_timer(struct gsm_network *net) +{ + int congestion_check_interval_s; + bool was_active; + + /* Don't setup timers from VTY config parsing before the main program has actually initialized + * the data structures. */ + if (!ho2_initialized) + return; + + was_active = net->ho2.congestion_check_timer.active; + if (was_active) + osmo_timer_del(&net->ho2.congestion_check_timer); + + congestion_check_interval_s = net->ho2.congestion_check_interval_s; + if (congestion_check_interval_s < 1) { + if (was_active) + LOGP(DHODEC, LOGL_NOTICE, "HO algorithm 2: Disabling congestion check\n"); + return; + } + + LOGP(DHODEC, LOGL_DEBUG, "HO algorithm 2: next periodical congestion check in %u seconds\n", + congestion_check_interval_s); + + osmo_timer_setup(&net->ho2.congestion_check_timer, + congestion_check_cb, net); + osmo_timer_schedule(&net->ho2.congestion_check_timer, + congestion_check_interval_s, 0); +} + +void hodec2_on_change_congestion_check_interval(struct gsm_network *net, unsigned int new_interval) +{ + net->ho2.congestion_check_interval_s = new_interval; + reinit_congestion_timer(net); +} + +/* did we get a RXLEV for a given cell in the given report? Mark matches as MRC_F_PROCESSED. */ +static struct gsm_meas_rep_cell *cell_in_rep(struct gsm_meas_rep *mr, uint16_t arfcn, uint8_t bsic) +{ + int i; + + for (i = 0; i < mr->num_cell; i++) { + struct gsm_meas_rep_cell *mrc = &mr->cell[i]; + + if (mrc->arfcn != arfcn) + continue; + if (mrc->bsic != bsic) + continue; + + return mrc; + } + return NULL; +} + +/* obtain averaged rxlev for given neighbor */ +static int neigh_meas_avg(struct neigh_meas_proc *nmp, int window) +{ + unsigned int i, idx; + int avg = 0; + + /* reduce window to the actual number of existing measurements */ + if (window > nmp->rxlev_cnt) + window = nmp->rxlev_cnt; + /* this should never happen */ + if (window <= 0) + return 0; + + idx = calc_initial_idx(ARRAY_SIZE(nmp->rxlev), + nmp->rxlev_cnt % ARRAY_SIZE(nmp->rxlev), + window); + + for (i = 0; i < window; i++) { + int j = (idx+i) % ARRAY_SIZE(nmp->rxlev); + + avg += nmp->rxlev[j]; + } + + return avg / window; +} + +/* Find empty slot or the worst neighbor. */ +static struct neigh_meas_proc *find_unused_or_worst_neigh(struct gsm_lchan *lchan) +{ + struct neigh_meas_proc *nmp_worst = NULL; + int worst; + int j; + + /* First try to find an empty/unused slot. */ + for (j = 0; j < ARRAY_SIZE(lchan->neigh_meas); j++) { + struct neigh_meas_proc *nmp = &lchan->neigh_meas[j]; + if (!nmp->arfcn) + return nmp; + } + + /* No empty slot found. Return worst neighbor to be evicted. */ + worst = 0; /* (overwritten on first loop, but avoid compiler warning) */ + for (j = 0; j < ARRAY_SIZE(lchan->neigh_meas); j++) { + struct neigh_meas_proc *nmp = &lchan->neigh_meas[j]; + int avg = neigh_meas_avg(nmp, MAX_WIN_NEIGH_AVG); + if (nmp_worst && avg >= worst) + continue; + worst = avg; + nmp_worst = nmp; + } + + return nmp_worst; +} + +/* process neighbor cell measurement reports */ +static void process_meas_neigh(struct gsm_meas_rep *mr) +{ + int i, j, idx; + + /* for each reported cell, try to update global state */ + for (j = 0; j < ARRAY_SIZE(mr->lchan->neigh_meas); j++) { + struct neigh_meas_proc *nmp = &mr->lchan->neigh_meas[j]; + unsigned int idx; + struct gsm_meas_rep_cell *mrc; + + /* skip unused entries */ + if (!nmp->arfcn) + continue; + + mrc = cell_in_rep(mr, nmp->arfcn, nmp->bsic); + idx = nmp->rxlev_cnt % ARRAY_SIZE(nmp->rxlev); + if (mrc) { + nmp->rxlev[idx] = mrc->rxlev; + nmp->last_seen_nr = mr->nr; + LOGPHOLCHAN(mr->lchan, LOGL_DEBUG, "neigh %u rxlev=%d last_seen_nr=%u\n", + nmp->arfcn, mrc->rxlev, nmp->last_seen_nr); + mrc->flags |= MRC_F_PROCESSED; + } else { + nmp->rxlev[idx] = 0; + LOGPHOLCHAN(mr->lchan, LOGL_DEBUG, "neigh %u not in report (last_seen_nr=%u)\n", + nmp->arfcn, nmp->last_seen_nr); + } + nmp->rxlev_cnt++; + } + + /* iterate over list of reported cells, check if we did not + * process all of them */ + for (i = 0; i < mr->num_cell; i++) { + struct gsm_meas_rep_cell *mrc = &mr->cell[i]; + struct neigh_meas_proc *nmp; + + if (mrc->flags & MRC_F_PROCESSED) + continue; + + nmp = find_unused_or_worst_neigh(mr->lchan); + + nmp->arfcn = mrc->arfcn; + nmp->bsic = mrc->bsic; + + nmp->rxlev_cnt = 0; + idx = nmp->rxlev_cnt % ARRAY_SIZE(nmp->rxlev); + nmp->rxlev[idx] = mrc->rxlev; + nmp->rxlev_cnt++; + nmp->last_seen_nr = mr->nr; + LOGPHOLCHAN(mr->lchan, LOGL_DEBUG, "neigh %u new in report rxlev=%d last_seen_nr=%u\n", + nmp->arfcn, mrc->rxlev, nmp->last_seen_nr); + + mrc->flags |= MRC_F_PROCESSED; + } +} + +static bool codec_type_is_supported(struct gsm_subscriber_connection *conn, + enum gsm0808_speech_codec_type type) +{ + int i; + struct gsm0808_speech_codec_list *clist = &conn->codec_list; + + if (!conn->codec_list_present) { + /* We don't have a list of supported codecs. This should never happen. */ + LOGPHOLCHAN(conn->lchan, LOGL_ERROR, + "No Speech Codec List present, accepting all codecs\n"); + return true; + } + + for (i = 0; i < clist->len; i++) { + if (clist->codec[i].type == type) { + LOGPHOLCHAN(conn->lchan, LOGL_DEBUG, "%s supported\n", + gsm0808_speech_codec_type_name(type)); + return true; + } + } + LOGPHOLCHAN(conn->lchan, LOGL_DEBUG, "Codec not supported by MS or not allowed by MSC: %s\n", + gsm0808_speech_codec_type_name(type)); + return false; +} + +/* + * Check what requirements the given cell fulfills. + * A bit mask of fulfilled requirements is returned. + * + * Target cell requirement A -- ability to service the call + * + * In order to successfully handover/assign to a better cell, the target cell + * must be able to continue the current call. Therefore the cell must fulfill + * the following criteria: + * + * * The handover must be enabled for the target cell, if it differs from the + * originating cell. + * * The assignment must be enabled for the cell, if it equals the current + * cell. + * * The handover penalty timer must not run for the cell. + * * If FR, EFR or HR codec is used, the cell must support this codec. + * * If FR or EFR codec is used, the cell must have a TCH/F slot type + * available. + * * If HR codec is used, the cell must have a TCH/H slot type available. + * * If AMR codec is used, the cell must have a TCH/F slot available, if AFS + * is supported by mobile and BTS. + * * If AMR codec is used, the cell must have a TCH/H slot available, if AHS + * is supported by mobile and BTS. + * * osmo-nitb with built-in MNCC application: + * o If AMR codec is used, the cell must support AMR codec with equal codec + * rate or rates. (not meaning TCH types) + * * If defined, the number of maximum unsynchronized handovers to this cell + * may not be exceeded. (This limits processing load for random access + * bursts.) + * + * + * Target cell requirement B -- avoid congestion + * + * In order to prevent congestion of a target cell, the cell must fulfill the + * requirement A, but also: + * + * * The minimum free channels, that are defined for that cell must be + * maintained after handover/assignment. + * * The minimum free channels are defined for TCH/F and TCH/H slot types + * individually. + * + * + * Target cell requirement C -- balance congestion + * + * In order to balance congested cells, the target cell must fulfill the + * requirement A, but also: + * + * * The target cell (which is congested also) must have more or equal free + * slots after handover/assignment. + * * The number of free slots are checked for TCH/F and TCH/H slot types + * individually. + */ +static uint8_t check_requirements(struct gsm_lchan *lchan, struct gsm_bts *bts, int tchf_count, int tchh_count) +{ + int count; + uint8_t requirement = 0; + unsigned int penalty_time; + struct gsm_bts *current_bts = lchan->ts->trx->bts; + + /* Requirement A */ + + /* the handover/assignment must not be disabled */ + if (current_bts == bts) { + if (!ho_get_as_active(bts->ho)) { + LOGPHOLCHAN(lchan, LOGL_DEBUG, "Assignment disabled\n"); + return 0; + } + } else { + if (!ho_get_ho_active(bts->ho)) { + LOGPHOLCHANTOBTS(lchan, bts, LOGL_DEBUG, + "not a candidate, handover is disabled in target BTS\n"); + return 0; + } + } + + /* the handover penalty timer must not run for this bts */ + penalty_time = conn_penalty_timer_remaining(lchan->conn, bts); + if (penalty_time) { + LOGPHOLCHANTOBTS(lchan, bts, LOGL_DEBUG, "not a candidate, target BTS still in penalty time" + " (%u seconds left)\n", penalty_time); + return 0; + } + + LOGPHOLCHANTOBTS(lchan, bts, LOGL_DEBUG, "tch_mode='%s' type='%s'\n", + get_value_string(gsm48_chan_mode_names, lchan->tch_mode), + gsm_lchant_name(lchan->type)); + + /* compatibility check for codecs. + * if so, the candidates for full rate and half rate are selected */ + switch (lchan->tch_mode) { + case GSM48_CMODE_SPEECH_V1: + switch (lchan->type) { + case GSM_LCHAN_TCH_F: /* mandatory */ + requirement |= REQUIREMENT_A_TCHF; + LOGPHOLCHANTOBTS(lchan, bts, LOGL_DEBUG, "tch_mode='%s' type='%s' supported\n", + get_value_string(gsm48_chan_mode_names, lchan->tch_mode), + gsm_lchant_name(lchan->type)); + break; + case GSM_LCHAN_TCH_H: + if (!bts->codec.hr) { + LOGPHOLCHANTOBTS(lchan, bts, LOGL_DEBUG, + "tch_mode='%s' type='%s' not supported\n", + get_value_string(gsm48_chan_mode_names, + lchan->tch_mode), + gsm_lchant_name(lchan->type)); + break; + } + if (codec_type_is_supported(lchan->conn, GSM0808_SCT_HR1)) + requirement |= REQUIREMENT_A_TCHH; + break; + default: + LOGPHOLCHAN(lchan, LOGL_ERROR, "Unexpected channel type: neither TCH/F nor TCH/H for %s\n", + get_value_string(gsm48_chan_mode_names, lchan->tch_mode)); + return 0; + } + break; + case GSM48_CMODE_SPEECH_EFR: + if (!bts->codec.efr) { + LOGPHOBTS(bts, LOGL_DEBUG, "EFR not supported\n"); + break; + } + if (codec_type_is_supported(lchan->conn, GSM0808_SCT_FR2)) + requirement |= REQUIREMENT_A_TCHF; + break; + case GSM48_CMODE_SPEECH_AMR: + if (!bts->codec.amr) { + LOGPHOBTS(bts, LOGL_DEBUG, "AMR not supported\n"); + break; + } + if (codec_type_is_supported(lchan->conn, GSM0808_SCT_FR3)) + requirement |= REQUIREMENT_A_TCHF; + if (codec_type_is_supported(lchan->conn, GSM0808_SCT_HR3)) + requirement |= REQUIREMENT_A_TCHH; + break; + default: + LOGPHOLCHANTOBTS(lchan, bts, LOGL_DEBUG, "Not even considering: src is not a SPEECH mode lchan\n"); + return 0; + } + + /* no candidate, because new cell is incompatible */ + if (!requirement) { + LOGPHOLCHANTOBTS(lchan, bts, LOGL_DEBUG, "not a candidate, because codec of MS and BTS are incompatible\n"); + return 0; + } + + /* remove slot types that are not available */ + if (!tchf_count && requirement & REQUIREMENT_A_TCHF) { + LOGPHOLCHANTOBTS(lchan, bts, LOGL_DEBUG, + "removing TCH/F, since all TCH/F lchans are in use\n"); + requirement &= ~(REQUIREMENT_A_TCHF); + } + if (!tchh_count && requirement & REQUIREMENT_A_TCHH) { + LOGPHOLCHANTOBTS(lchan, bts, LOGL_DEBUG, + "removing TCH/H, since all TCH/H lchans are in use\n"); + requirement &= ~(REQUIREMENT_A_TCHH); + } + + if (!requirement) { + LOGPHOLCHANTOBTS(lchan, bts, LOGL_DEBUG, "not a candidate, because no suitable slots available\n"); + return 0; + } + + /* omit same channel type on same BTS (will not change anything) */ + if (bts == current_bts) { + switch (lchan->type) { + case GSM_LCHAN_TCH_F: + LOGPHOLCHANTOBTS(lchan, bts, LOGL_DEBUG, + "removing TCH/F, already on TCH/F in this cell\n"); + requirement &= ~(REQUIREMENT_A_TCHF); + break; + case GSM_LCHAN_TCH_H: + LOGPHOLCHANTOBTS(lchan, bts, LOGL_DEBUG, + "removing TCH/H, already on TCH/H in this cell\n"); + requirement &= ~(REQUIREMENT_A_TCHH); + break; + default: + break; + } + + if (!requirement) { + LOGPHOLCHAN(lchan, LOGL_DEBUG, + "Reassignment within cell not an option, no differing channel types available\n"); + return 0; + } + } + +#ifdef LEGACY + // This was useful in osmo-nitb. We're in osmo-bsc now and have no idea whether the osmo-msc does + // internal or external call control. Maybe a future config switch wants to add this behavior? + /* Built-in call control requires equal codec rates. Remove rates that are not equal. */ + if (lchan->tch_mode == GSM48_CMODE_SPEECH_AMR + && current_bts->network->mncc_recv != mncc_sock_from_cc) { + switch (lchan->type) { + case GSM_LCHAN_TCH_F: + if ((requirement & REQUIREMENT_A_TCHF) + && !!memcmp(¤t_bts->mr_full, &bts->mr_full, + sizeof(struct amr_multirate_conf))) + requirement &= ~(REQUIREMENT_A_TCHF); + if ((requirement & REQUIREMENT_A_TCHH) + && !!memcmp(¤t_bts->mr_full, &bts->mr_half, + sizeof(struct amr_multirate_conf))) + requirement &= ~(REQUIREMENT_A_TCHH); + break; + case GSM_LCHAN_TCH_H: + if ((requirement & REQUIREMENT_A_TCHF) + && !!memcmp(¤t_bts->mr_half, &bts->mr_full, + sizeof(struct amr_multirate_conf))) + requirement &= ~(REQUIREMENT_A_TCHF); + if ((requirement & REQUIREMENT_A_TCHH) + && !!memcmp(¤t_bts->mr_half, &bts->mr_half, + sizeof(struct amr_multirate_conf))) + requirement &= ~(REQUIREMENT_A_TCHH); + break; + default: + break; + } + + if (!requirement) { + LOGPHOLCHANTOBTS(lchan, bts, LOGL_DEBUG, + "not a candidate, cannot provide identical codec rate\n"); + return 0; + } + } +#endif + + /* the maximum number of unsynchonized handovers must no be exceeded */ + if (current_bts != bts + && bsc_ho_count(bts, true) >= ho_get_ho_max(bts->ho)) { + LOGPHOLCHANTOBTS(lchan, bts, LOGL_DEBUG, + "not a candidate, number of allowed handovers (%d) would be exceeded\n", + ho_get_ho_max(bts->ho)); + return 0; + } + + /* Requirement B */ + + /* the minimum free timeslots that are defined for this cell must + * be maintained _after_ handover/assignment */ + if (requirement & REQUIREMENT_A_TCHF) { + if (tchf_count - 1 >= ho_get_tchf_min_slots(bts->ho)) { + LOGPHOLCHANTOBTS(lchan, bts, LOGL_DEBUG, + "TCH/F would not be congested after HO\n"); + requirement |= REQUIREMENT_B_TCHF; + } else { + LOGPHOLCHANTOBTS(lchan, bts, LOGL_DEBUG, + "TCH/F would be congested after HO\n"); + } + } + if (requirement & REQUIREMENT_A_TCHH) { + if (tchh_count - 1 >= ho_get_tchh_min_slots(bts->ho)) { + LOGPHOLCHANTOBTS(lchan, bts, LOGL_DEBUG, + "TCH/H would not be congested after HO\n"); + requirement |= REQUIREMENT_B_TCHH; + } else { + LOGPHOLCHANTOBTS(lchan, bts, LOGL_DEBUG, + "TCH/H would be congested after HO\n"); + } + } + + /* Requirement C */ + + /* the nr of free timeslots of the target cell must be >= the + * free slots of the current cell _after_ handover/assignment */ + count = bts_count_free_ts(current_bts, + (lchan->type == GSM_LCHAN_TCH_H) ? + GSM_PCHAN_TCH_H : GSM_PCHAN_TCH_F); + if (requirement & REQUIREMENT_A_TCHF) { + if (tchf_count - 1 >= count + 1) { + LOGPHOLCHANTOBTS(lchan, bts, LOGL_DEBUG, + "TCH/F would be less congested in target than source cell after HO\n"); + requirement |= REQUIREMENT_C_TCHF; + } else { + LOGPHOLCHANTOBTS(lchan, bts, LOGL_DEBUG, + "TCH/F would not be less congested in target than source cell after HO\n"); + } + } + if (requirement & REQUIREMENT_A_TCHH) { + if (tchh_count - 1 >= count + 1) { + LOGPHOLCHANTOBTS(lchan, bts, LOGL_DEBUG, + "TCH/H would be less congested in target than source cell after HO\n"); + requirement |= REQUIREMENT_C_TCHH; + } else { + LOGPHOLCHANTOBTS(lchan, bts, LOGL_DEBUG, + "TCH/H would not be less congested in target than source cell after HO\n"); + } + } + + LOGPHOLCHANTOBTS(lchan, bts, LOGL_DEBUG, "requirements=0x%x\n", requirement); + + /* return mask of fulfilled requirements */ + return requirement; +} + +/* Trigger handover or assignment depending on the target BTS */ +static int trigger_handover_or_assignment(struct gsm_lchan *lchan, struct gsm_bts *new_bts, uint8_t requirements) +{ + struct gsm_bts *current_bts = lchan->ts->trx->bts; + int afs_bias = 0; + bool full_rate = false; + + if (current_bts == new_bts) + LOGPHOLCHAN(lchan, LOGL_NOTICE, "Triggering Assignment\n"); + else + LOGPHOLCHANTOBTS(lchan, new_bts, LOGL_NOTICE, "Triggering Handover\n"); + + /* afs_bias becomes > 0, if AFS is used and is improved */ + if (lchan->tch_mode == GSM48_CMODE_SPEECH_AMR) + afs_bias = ho_get_afs_bias_rxlev(new_bts->ho); + + /* select TCH rate, prefer TCH/F if AFS is improved */ + switch (lchan->type) { + case GSM_LCHAN_TCH_F: + /* keep on full rate, if TCH/F is a candidate */ + if ((requirements & REQUIREMENT_TCHF_MASK)) { + if (current_bts == new_bts) { + LOGPHOLCHAN(lchan, LOGL_INFO, "Not performing assignment: Already on target type\n"); + return 0; + } + full_rate = true; + break; + } + /* change to half rate */ + if (!(requirements & REQUIREMENT_TCHH_MASK)) { + LOGPHOLCHANTOBTS(lchan, new_bts, LOGL_ERROR, + "neither TCH/F nor TCH/H requested, aborting ho/as\n"); + return -EINVAL; + } + break; + case GSM_LCHAN_TCH_H: + /* change to full rate if AFS is improved and a candidate */ + if (afs_bias > 0 && (requirements & REQUIREMENT_TCHF_MASK)) { + full_rate = true; + LOGPHOLCHAN(lchan, LOGL_DEBUG, "[Improve AHS->AFS]\n"); + break; + } + /* change to full rate if the only candidate */ + if ((requirements & REQUIREMENT_TCHF_MASK) + && !(requirements & REQUIREMENT_TCHH_MASK)) { + full_rate = true; + break; + } + /* keep on half rate */ + if (!(requirements & REQUIREMENT_TCHH_MASK)) { + LOGPHOLCHANTOBTS(lchan, new_bts, LOGL_ERROR, + "neither TCH/F nor TCH/H requested, aborting ho/as\n"); + return -EINVAL; + } + if (current_bts == new_bts) { + LOGPHOLCHAN(lchan, LOGL_INFO, "Not performing assignment: Already on target type\n"); + return 0; + } + break; + default: + LOGPHOLCHANTOBTS(lchan, new_bts, LOGL_ERROR, "lchan is neither TCH/F nor TCH/H, aborting ho/as\n"); + return -EINVAL; + } + + /* trigger handover or assignment */ + if (current_bts == new_bts) + LOGPHOLCHAN(lchan, LOGL_NOTICE, "Triggering assignment to %s, due to %s\n", + full_rate ? "TCH/F" : "TCH/H", + ho_reason_name(global_ho_reason)); + else + LOGPHOLCHANTOBTS(lchan, new_bts, LOGL_NOTICE, + "Triggering handover to %s, due to %s\n", + full_rate ? "TCH/F" : "TCH/H", + ho_reason_name(global_ho_reason)); + + return bsc_handover_start_lchan_change(lchan, current_bts == new_bts? NULL : new_bts, + full_rate? GSM_LCHAN_TCH_F : GSM_LCHAN_TCH_H); +} + +/* debug collected candidates */ +static inline void debug_candidate(struct ho_candidate *candidate, + int neighbor, int8_t rxlev, int tchf_count, int tchh_count) +{ + if (neighbor) + LOGP(DHODEC, LOGL_DEBUG, " - neighbor BTS %d, RX level " + "%d -> %d\n", candidate->bts->nr, rxlev2dbm(rxlev), + rxlev2dbm(candidate->avg)); + else + LOGP(DHODEC, LOGL_DEBUG, " - current BTS %d, RX level %d\n", + candidate->bts->nr, rxlev2dbm(candidate->avg)); + + LOGP(DHODEC, LOGL_DEBUG, " o free TCH/F slots %d, minimum required " + "%d\n", tchf_count, ho_get_tchf_min_slots(candidate->bts->ho)); + LOGP(DHODEC, LOGL_DEBUG, " o free TCH/H slots %d, minimum required " + "%d\n", tchh_count, ho_get_tchh_min_slots(candidate->bts->ho)); + + if ((candidate->requirements & REQUIREMENT_TCHF_MASK)) + LOGP(DHODEC, LOGL_DEBUG, " o requirement "); + else + LOGP(DHODEC, LOGL_DEBUG, " o no requirement "); + if ((candidate->requirements & REQUIREMENT_A_TCHF)) + LOGPC(DHODEC, LOGL_DEBUG, "A "); + if ((candidate->requirements & REQUIREMENT_B_TCHF)) + LOGPC(DHODEC, LOGL_DEBUG, "B "); + if ((candidate->requirements & REQUIREMENT_C_TCHF)) + LOGPC(DHODEC, LOGL_DEBUG, "C "); + LOGPC(DHODEC, LOGL_DEBUG, "fulfilled for TCHF"); + if (!(candidate->requirements & REQUIREMENT_TCHF_MASK)) /* nothing */ + LOGPC(DHODEC, LOGL_DEBUG, " (no %s possible)\n", + (neighbor) ? "handover" : "assignment"); + else if ((candidate->requirements & REQUIREMENT_TCHF_MASK) + == REQUIREMENT_A_TCHF) /* only A */ + LOGPC(DHODEC, LOGL_DEBUG, " (more congestion after %s)\n", + (neighbor) ? "handover" : "assignment"); + else if ((candidate->requirements & REQUIREMENT_B_TCHF)) /* B incl. */ + LOGPC(DHODEC, LOGL_DEBUG, " (not congested after %s)\n", + (neighbor) ? "handover" : "assignment"); + else /* so it must include C */ + LOGPC(DHODEC, LOGL_DEBUG, " (less or equally congested after " + "%s)\n", (neighbor) ? "handover" : "assignment"); + + if ((candidate->requirements & REQUIREMENT_TCHH_MASK)) + LOGP(DHODEC, LOGL_DEBUG, " o requirement "); + else + LOGP(DHODEC, LOGL_DEBUG, " o no requirement "); + if ((candidate->requirements & REQUIREMENT_A_TCHH)) + LOGPC(DHODEC, LOGL_DEBUG, "A "); + if ((candidate->requirements & REQUIREMENT_B_TCHH)) + LOGPC(DHODEC, LOGL_DEBUG, "B "); + if ((candidate->requirements & REQUIREMENT_C_TCHH)) + LOGPC(DHODEC, LOGL_DEBUG, "C "); + LOGPC(DHODEC, LOGL_DEBUG, "fulfilled for TCHH"); + if (!(candidate->requirements & REQUIREMENT_TCHH_MASK)) /* nothing */ + LOGPC(DHODEC, LOGL_DEBUG, " (no %s possible)\n", + (neighbor) ? "handover" : "assignment"); + else if ((candidate->requirements & REQUIREMENT_TCHH_MASK) + == REQUIREMENT_A_TCHH) /* only A */ + LOGPC(DHODEC, LOGL_DEBUG, " (more congestion after %s)\n", + (neighbor) ? "handover" : "assignment"); + else if ((candidate->requirements & REQUIREMENT_B_TCHH)) /* B incl. */ + LOGPC(DHODEC, LOGL_DEBUG, " (not congested after %s)\n", + (neighbor) ? "handover" : "assignment"); + else /* so it must include C */ + LOGPC(DHODEC, LOGL_DEBUG, " (less or equally congested after " + "%s)\n", (neighbor) ? "handover" : "assignment"); +} + +/* add candidate for re-assignment within the current cell */ +static void collect_assignment_candidate(struct gsm_lchan *lchan, struct ho_candidate *clist, + unsigned int *candidates, int av_rxlev) +{ + struct gsm_bts *bts = lchan->ts->trx->bts; + int tchf_count, tchh_count; + struct ho_candidate *c; + + tchf_count = bts_count_free_ts(bts, GSM_PCHAN_TCH_F); + tchh_count = bts_count_free_ts(bts, GSM_PCHAN_TCH_H); + + c = &clist[*candidates]; + c->lchan = lchan; + c->bts = bts; + c->requirements = check_requirements(lchan, bts, tchf_count, tchh_count); + c->avg = av_rxlev; + debug_candidate(c, 0, 0, tchf_count, tchh_count); + (*candidates)++; +} + +/* add candidates for handover to all neighbor cells */ +static void collect_handover_candidate(struct gsm_lchan *lchan, struct neigh_meas_proc *nmp, + struct ho_candidate *clist, unsigned int *candidates, + bool include_weaker_rxlev, int av_rxlev, + int *neighbors_count) +{ + struct gsm_bts *bts = lchan->ts->trx->bts; + int tchf_count, tchh_count; + struct gsm_bts *neighbor_bts; + int avg; + struct ho_candidate *c; + int min_rxlev; + + /* skip empty slots */ + if (nmp->arfcn == 0) + return; + + if (neighbors_count) + (*neighbors_count)++; + + /* skip if measurement report is old */ + if (nmp->last_seen_nr != lchan->meas_rep_last_seen_nr) { + LOGPHOLCHAN(lchan, LOGL_DEBUG, "neighbor ARFCN %u measurement report is old" + " (nmp->last_seen_nr=%u lchan->meas_rep_last_seen_nr=%u)\n", + nmp->arfcn, nmp->last_seen_nr, lchan->meas_rep_last_seen_nr); + return; + } + + neighbor_bts = bts_by_arfcn_bsic(bts->network, nmp->arfcn, nmp->bsic); + if (!neighbor_bts) { + LOGPHOBTS(bts, LOGL_DEBUG, "neighbor ARFCN %u does not belong to this network\n", + nmp->arfcn); + return; + } + + /* in case we have measurements of our bts, due to misconfiguration */ + if (neighbor_bts == bts) { + LOGPHOBTS(bts, LOGL_ERROR, "Configuration error: this BTS appears as its own neighbor\n"); + return; + } + + /* caculate average rxlev for this cell over the window */ + avg = neigh_meas_avg(nmp, ho_get_rxlev_neigh_avg_win(bts->ho)); + + /* Heed rxlev hysteresis only if the RXLEV/RXQUAL/TA levels of the MS aren't critically bad and + * we're just looking for an improvement. If levels are critical, we desperately need a handover + * and thus skip the hysteresis check. */ + if (!include_weaker_rxlev) { + unsigned int pwr_hyst = ho_get_pwr_hysteresis(bts->ho); + if (avg <= (av_rxlev + pwr_hyst)) { + LOGPHOLCHAN(lchan, LOGL_DEBUG, + "BTS %d is not a candidate, because RX level (%d) is lower" + " or equal than current RX level (%d) + hysteresis (%d)\n", + neighbor_bts->nr, rxlev2dbm(avg), rxlev2dbm(av_rxlev), pwr_hyst); + return; + } + } + + /* if the minimum level is not reached */ + min_rxlev = ho_get_min_rxlev(neighbor_bts->ho); + if (rxlev2dbm(avg) < min_rxlev) { + LOGPHOLCHAN(lchan, LOGL_DEBUG, + "BTS %d is not a candidate, because RX level (%d) is lower" + " than its minimum required RX level (%d)\n", + neighbor_bts->nr, rxlev2dbm(avg), min_rxlev); + return; + } + + tchf_count = bts_count_free_ts(neighbor_bts, GSM_PCHAN_TCH_F); + tchh_count = bts_count_free_ts(neighbor_bts, GSM_PCHAN_TCH_H); + c = &clist[*candidates]; + c->lchan = lchan; + c->bts = neighbor_bts; + c->requirements = check_requirements(lchan, neighbor_bts, tchf_count, + tchh_count); + c->avg = avg; + debug_candidate(c, 1, av_rxlev, tchf_count, tchh_count); + (*candidates)++; +} + +static void collect_candidates_for_lchan(struct gsm_lchan *lchan, + struct ho_candidate *clist, unsigned int *candidates, + int *_av_rxlev, bool include_weaker_rxlev) +{ + struct gsm_bts *bts = lchan->ts->trx->bts; + int av_rxlev; + unsigned int candidates_was; + bool assignment; + bool handover; + int neighbors_count = 0; + unsigned int rxlev_avg_win = ho_get_rxlev_avg_win(bts->ho); + + OSMO_ASSERT(candidates); + candidates_was = *candidates; + + /* caculate average rxlev for this cell over the window */ + av_rxlev = get_meas_rep_avg(lchan, + ho_get_full_tdma(bts->ho) ? + MEAS_REP_DL_RXLEV_FULL : MEAS_REP_DL_RXLEV_SUB, + rxlev_avg_win); + if (_av_rxlev) + *_av_rxlev = av_rxlev; + + /* in case there is no measurment report (yet) */ + if (av_rxlev < 0) { + LOGPHOLCHAN(lchan, LOGL_DEBUG, "Not collecting candidates, not enough measurements" + " (got %d, want %u)\n", + lchan->meas_rep_count, rxlev_avg_win); + return; + } + + assignment = ho_get_as_active(bts->ho); + handover = ho_get_ho_active(bts->ho); + + LOGPHOLCHAN(lchan, LOGL_DEBUG, "Collecting candidates for%s%s%s\n", + assignment ? " Assignment" : "", + assignment && handover ? " and" : "", + handover ? " Handover" : ""); + + if (assignment) + collect_assignment_candidate(lchan, clist, candidates, av_rxlev); + + if (handover) { + int i; + for (i = 0; i < ARRAY_SIZE(lchan->neigh_meas); i++) { + collect_handover_candidate(lchan, &lchan->neigh_meas[i], + clist, candidates, + include_weaker_rxlev, av_rxlev, &neighbors_count); + } + } + + LOGPHOLCHAN(lchan, LOGL_DEBUG, "adding %u candidates from %u neighbors, total %u\n", + *candidates - candidates_was, neighbors_count, *candidates); +} + +/* + * Search for a alternative / better cell. + * + * Do not trigger handover/assignment on slots which have already ongoing + * handover/assignment processes. If no AFS improvement offset is given, try to + * maintain the same TCH rate, if available. + * Do not perform this process, if handover and assignment are disabled for + * the current cell. + * Do not perform handover, if the minimum acceptable RX level + * is not reched for this cell. + * + * If one or more 'better cells' are available, check the current and neighbor + * cell measurements in descending order of their RX levels (down-link): + * + * * Select the best candidate that fulfills requirement B (no congestion + * after handover/assignment) and trigger handover or assignment. + * * If no candidate fulfills requirement B, select the best candidate that + * fulfills requirement C (less or equally congested cells after handover) + * and trigger handover or assignment. + * * If no candidate fulfills requirement C, do not perform handover nor + * assignment. + * + * If the RX level (down-link) or RX quality (down-link) of the current cell is + * below minimum acceptable level, or if the maximum allowed timing advance is + * reached or exceeded, check the RX levels (down-link) of the current and + * neighbor cells in descending order of their levels: (bad BTS case) + * + * * Select the best candidate that fulfills requirement B (no congestion after + * handover/assignment) and trigger handover or assignment. + * * If no candidate fulfills requirement B, select the best candidate that + * fulfills requirement C (less or equally congested cells after handover) + * and trigger handover or assignment. + * * If no candidate fulfills requirement C, select the best candidate that + * fulfills requirement A (ignore congestion after handover or assignment) + * and trigger handover or assignment. + * * If no candidate fulfills requirement A, do not perform handover nor + * assignment. + * + * RX levels (down-link) of current and neighbor cells: + * + * * The RX levels of the current cell and neighbor cells are improved by a + * given offset, if AFS (AMR on TCH/F) is used or is a candidate for + * handover/assignment. + * * If AMR is used, the requirement for handover is checked for TCH/F and + * TCH/H. Both results (if any) are used as a candidate. + * * If AMR is used, the requirement for assignment to a different TCH slot + * rate is checked. The result (if available) is used as a candidate. + * + * If minimum RXLEV, minimum RXQUAL or maximum TA are exceeded, the caller should pass + * include_weaker_rxlev=true so that handover is performed despite congestion. + */ +static int find_alternative_lchan(struct gsm_lchan *lchan, bool include_weaker_rxlev) +{ + struct gsm_bts *bts = lchan->ts->trx->bts; + int ahs = (lchan->tch_mode == GSM48_CMODE_SPEECH_AMR + && lchan->type == GSM_LCHAN_TCH_H); + int av_rxlev; + struct ho_candidate clist[1 + ARRAY_SIZE(lchan->neigh_meas)]; + unsigned int candidates = 0; + int i; + struct ho_candidate *best_cand = NULL; + unsigned int best_better_db; + bool best_applied_afs_bias = false; + int better; + + /* check for disabled handover/assignment at the current cell */ + if (!ho_get_as_active(bts->ho) + && !ho_get_ho_active(bts->ho)) { + LOGP(DHODEC, LOGL_INFO, "Skipping, Handover and Assignment both disabled in this cell\n"); + return 0; + } + + collect_candidates_for_lchan(lchan, clist, &candidates, &av_rxlev, include_weaker_rxlev); + + /* If assignment is disabled and no neighbor cell report exists, or no neighbor cell qualifies, + * we may not even have any candidates. */ + if (!candidates) + goto no_candidates; + + /* select best candidate that fulfills requirement B: no congestion after HO */ + best_better_db = 0; + for (i = 0; i < candidates; i++) { + int afs_bias; + if (!(clist[i].requirements & REQUIREMENT_B_MASK)) + continue; + + better = clist[i].avg - av_rxlev; + /* Apply AFS bias? */ + afs_bias = 0; + if (ahs && (clist[i].requirements & REQUIREMENT_B_TCHF)) + afs_bias = ho_get_afs_bias_rxlev(clist[i].bts->ho); + better += afs_bias; + if (better > best_better_db) { + best_cand = &clist[i]; + best_better_db = better; + best_applied_afs_bias = afs_bias? true : false; + } + } + + /* perform handover, if there is a candidate */ + if (best_cand) { + LOGPHOLCHANTOBTS(lchan, best_cand->bts, LOGL_INFO, "Best candidate, RX level %d%s\n", + rxlev2dbm(best_cand->avg), + best_applied_afs_bias ? " (applied AHS -> AFS rxlev bias)" : ""); + return trigger_handover_or_assignment(lchan, best_cand->bts, + best_cand->requirements & REQUIREMENT_B_MASK); + } + + /* select best candidate that fulfills requirement C: less or equal congestion after HO */ + best_better_db = 0; + for (i = 0; i < candidates; i++) { + int afs_bias; + if (!(clist[i].requirements & REQUIREMENT_C_MASK)) + continue; + + better = clist[i].avg - av_rxlev; + /* Apply AFS bias? */ + afs_bias = 0; + if (ahs && (clist[i].requirements & REQUIREMENT_C_TCHF)) + afs_bias = ho_get_afs_bias_rxlev(clist[i].bts->ho); + better += afs_bias; + if (better > best_better_db) { + best_cand = &clist[i]; + best_better_db = better; + best_applied_afs_bias = afs_bias? true : false; + } + } + + /* perform handover, if there is a candidate */ + if (best_cand) { + LOGPHOLCHANTOBTS(lchan, best_cand->bts, LOGL_INFO, "Best candidate, RX level %d%s\n", + rxlev2dbm(best_cand->avg), + best_applied_afs_bias? " (applied AHS -> AFS rxlev bias)" : ""); + return trigger_handover_or_assignment(lchan, best_cand->bts, + best_cand->requirements & REQUIREMENT_C_MASK); + } + + /* we are done in case the MS RXLEV/RXQUAL/TA aren't critical and we're avoiding congestion. */ + if (!include_weaker_rxlev) + goto no_candidates; + + /* Select best candidate that fulfills requirement A: can service the call. + * From above we know that there are no options that avoid congestion. Here we're trying to find + * *any* free lchan that has no critically low RXLEV and is able to handle the MS. */ + best_better_db = 0; + for (i = 0; i < candidates; i++) { + int afs_bias; + if (!(clist[i].requirements & REQUIREMENT_A_MASK)) + continue; + + better = clist[i].avg - av_rxlev; + /* Apply AFS bias? */ + afs_bias = 0; + if (ahs && (clist[i].requirements & REQUIREMENT_A_TCHF)) + afs_bias = ho_get_afs_bias_rxlev(clist[i].bts->ho); + better += afs_bias; + if (better > best_better_db) { + best_cand = &clist[i]; + best_better_db = better; + best_applied_afs_bias = afs_bias? true : false; + } + } + + /* perform handover, if there is a candidate */ + if (best_cand) { + LOGPHOLCHANTOBTS(lchan, best_cand->bts, LOGL_INFO, "Best candidate, RX level %d" + " with greater congestion found%s\n", + rxlev2dbm(best_cand->avg), + best_applied_afs_bias ? " (applied AHS -> AFS rxlev bias)" : ""); + return trigger_handover_or_assignment(lchan, best_cand->bts, + best_cand->requirements & REQUIREMENT_A_MASK); + } + + /* Damn, all is congested, has too low RXLEV or cannot service the voice call due to codec + * restrictions or because all lchans are taken. */ + +no_candidates: + if (include_weaker_rxlev) + LOGPHOLCHAN(lchan, LOGL_INFO, "No alternative lchan found\n"); + else + LOGPHOLCHAN(lchan, LOGL_INFO, "No better/less congested neighbor cell found\n"); + + return 0; +} + +/* + * Handover/assignment check, if measurement report is received + * + * Do not trigger handover/assignment on slots which have already ongoing + * handover/assignment processes. + * + * In case of handover triggered because maximum allowed timing advance is + * exceeded, the handover penalty timer is started for the originating cell. + * + */ +static int attempt_handover_after_mr(struct gsm_meas_rep *mr) +{ + struct gsm_lchan *lchan = mr->lchan; + struct gsm_bts *bts = lchan->ts->trx->bts; + int av_rxlev = -EINVAL, av_rxqual = -EINVAL; + int rc; + + /* we currently only do handover for TCH channels */ + switch (mr->lchan->type) { + case GSM_LCHAN_TCH_F: + case GSM_LCHAN_TCH_H: + break; + default: + return 0; + } + + /* parse actual neighbor cell info */ + if (mr->num_cell > 0 && mr->num_cell < 7) + process_meas_neigh(mr); + + /* check for ongoing handover/assignment */ + if (!lchan->conn) { + LOGPHOLCHAN(lchan, LOGL_ERROR, "Skipping, No subscriber connection???\n"); + return 0; + } + if (lchan->conn->secondary_lchan) { + LOGPHOLCHAN(lchan, LOGL_INFO, "Skipping, Initial Assignment is still ongoing\n"); + return 0; + } + if (lchan->conn->ho_lchan) { + LOGPHOLCHAN(lchan, LOGL_INFO, "Skipping, Handover already triggered\n"); + return 0; + } + + /* get average levels. if not enought measurements yet, value is < 0 */ + av_rxlev = get_meas_rep_avg(lchan, + ho_get_full_tdma(bts->ho) ? + MEAS_REP_DL_RXLEV_FULL : MEAS_REP_DL_RXLEV_SUB, + ho_get_rxlev_avg_win(bts->ho)); + av_rxqual = get_meas_rep_avg(lchan, + ho_get_full_tdma(bts->ho) ? + MEAS_REP_DL_RXQUAL_FULL : MEAS_REP_DL_RXQUAL_SUB, + ho_get_rxqual_avg_win(bts->ho)); + if (av_rxlev < 0 && av_rxqual < 0) { + LOGPHOLCHAN(lchan, LOGL_INFO, "Skipping, Not enough recent measuements\n"); + return 0; + } + if (av_rxlev >= 0) { + LOGPHOLCHAN(lchan, LOGL_DEBUG, "Measurement report: average RX level = %d\n", + rxlev2dbm(av_rxlev)); + } + if (av_rxqual >= 0) { + LOGPHOLCHAN(lchan, LOGL_DEBUG, "Measurement report: average RX quality = %d\n", + av_rxqual); + } + + /* improve levels in case of AFS, if defined */ + if (lchan->type == GSM_LCHAN_TCH_F + && lchan->tch_mode == GSM48_CMODE_SPEECH_AMR) { + int rxlev_bias = ho_get_afs_bias_rxlev(bts->ho); + int rxqual_bias = ho_get_afs_bias_rxqual(bts->ho); + if (av_rxlev >= 0 && rxlev_bias) { + int imp = av_rxlev + rxlev_bias; + LOGPHOLCHAN(lchan, LOGL_INFO, "Virtually improving RX level from %d to %d," + " due to AFS bias\n", rxlev2dbm(av_rxlev), rxlev2dbm(imp)); + av_rxlev = imp; + } + if (av_rxqual >= 0 && rxqual_bias) { + int imp = av_rxqual - rxqual_bias; + if (imp < 0) + imp = 0; + LOGPHOLCHAN(lchan, LOGL_INFO, "Virtually improving RX quality from %d to %d," + " due to AFS bias\n", rxlev2dbm(av_rxqual), rxlev2dbm(imp)); + av_rxqual = imp; + } + } + + /* Bad Quality */ + if (av_rxqual >= 0 && av_rxqual > ho_get_min_rxqual(bts->ho)) { + if (rxlev2dbm(av_rxlev) > -85) { + global_ho_reason = HO_REASON_INTERFERENCE; + LOGPHOLCHAN(lchan, LOGL_INFO, "Trying handover/assignment" + " due to interference (bad quality)\n"); + } else { + global_ho_reason = HO_REASON_BAD_QUALITY; + LOGPHOLCHAN(lchan, LOGL_INFO, "Trying handover/assignment due to bad quality\n"); + } + rc = find_alternative_lchan(lchan, true); + if (lchan->conn->ho_lchan || lchan->conn->secondary_lchan) + return rc; + return 0; + } + + /* Low Level */ + if (av_rxlev >= 0 && rxlev2dbm(av_rxlev) < ho_get_min_rxlev(bts->ho)) { + global_ho_reason = HO_REASON_LOW_RXLEVEL; + LOGPHOLCHAN(lchan, LOGL_INFO, "Attempting handover/assignment due to low rxlev\n"); + rc = find_alternative_lchan(lchan, true); + if (lchan->conn->ho_lchan || lchan->conn->secondary_lchan) + return rc; + return 0; + } + + /* Max Distance */ + if (lchan->meas_rep_count > 0 + && lchan->rqd_ta > ho_get_max_distance(bts->ho)) { + global_ho_reason = HO_REASON_MAX_DISTANCE; + LOGPHOLCHAN(lchan, LOGL_INFO, "Attempting handover due to high TA\n"); + /* start penalty timer to prevent comming back too + * early. it must be started before selecting a better cell, + * so there is no assignment selected, due to running + * penalty timer. */ + conn_penalty_timer_add(lchan->conn, bts, ho_get_penalty_max_dist(bts->ho)); + rc = find_alternative_lchan(lchan, true); + if (lchan->conn->ho_lchan || lchan->conn->secondary_lchan) + return rc; + return 0; + } + + /* try handover to a better cell */ + if (av_rxlev >= 0 && (mr->nr % ho_get_pwr_interval(bts->ho)) == 0) { + LOGPHOLCHAN(lchan, LOGL_INFO, "Looking whether a cell has better RXLEV\n"); + global_ho_reason = HO_REASON_BETTER_CELL; + rc = find_alternative_lchan(lchan, false); + if (lchan->conn->ho_lchan || lchan->conn->secondary_lchan) + return rc; + return 0; + } + + return 0; +} + +/* + * Handover/assignment check after timer timeout: + * + * Even if handover process tries to prevent a congestion, a cell might get + * congested due to new call setups or handovers to prevent loss of radio link. + * A cell is congested, if not the minimum number of free slots are available. + * The minimum number can be defined for TCH/F and TCH/H individually. + * + * Do not perform congestion check, if no minimum free slots are defined for + * a cell. + * Do not trigger handover/assignment on slots which have already ongoing + * handover/assignment processes. If no AFS improvement offset is given, try to + * maintain the same TCH rate, if available. + * Do not perform this process, if handover and assignment are disabled for + * the current cell. + * Do not perform handover, if the minimum acceptable RX level + * is not reched for this cell. + * Only check candidates that will solve/reduce congestion. + * + * If a cell is congested, all slots are checked for all their RX levels + * (down-link) of the current and neighbor cell measurements in descending + * order of their RX levels: + * + * * Select the best candidate that fulfills requirement B (no congestion after + * handover/assignment), trigger handover or assignment. Candidates that will + * cause an assignment from AHS (AMR on TCH/H) to AFS (AMR on TCH/F) are + * omitted. + * o This process repeated until the minimum required number of free slots + * are restored or if all cell measurements are checked. The process ends + * then, otherwise: + * * Select the worst candidate that fulfills requirement B, trigger + * assignment. Note that only assignment candidates for changing from AHS to + * AFS are left. + * o This process repeated until the minimum required number of free slots + * are restored or if all cell measurements are checked. The process ends + * then, otherwise: + * * Select the best candidates that fulfill requirement C (less or equally + * congested cells after handover/assignment), trigger handover or + * assignment. Candidates that will cause an assignment from AHS (AMR on + * TCH/H) to AFS (AMR on TCH/F) are omitted. + * o This process repeated until the minimum required number of free slots + * are restored or if all cell measurements are checked. The process ends + * then, otherwise: + * * Select the worst candidate that fulfills requirement C, trigger + * assignment. Note that only assignment candidates for changing from AHS to + * AFS are left. + * o This process repeated until the minimum required number of free slots + * are restored or if all cell measurements are checked. + */ +static int bts_resolve_congestion(struct gsm_bts *bts, int tchf_congestion, int tchh_congestion) +{ + struct gsm_lchan *lc; + struct gsm_bts_trx *trx; + struct gsm_bts_trx_ts *ts; + int i, j; + struct ho_candidate *clist; + unsigned int candidates; + struct ho_candidate *best_cand = NULL, *worst_cand = NULL; + struct gsm_lchan *delete_lchan = NULL; + unsigned int best_avg_db, worst_avg_db; + int avg; + int rc = 0; + int any_ho = 0; + int is_improved = 0; + + if (tchf_congestion < 0) + tchf_congestion = 0; + if (tchh_congestion < 0) + tchh_congestion = 0; + + LOGPHOBTS(bts, LOGL_INFO, "congested: %d TCH/F and %d TCH/H should be moved\n", + tchf_congestion, tchh_congestion); + + /* allocate array of all bts */ + clist = talloc_zero_array(tall_bsc_ctx, struct ho_candidate, + bts->num_trx * 8 * 2 * (1 + ARRAY_SIZE(lc->neigh_meas))); + if (!clist) + return 0; + + candidates = 0; + + /* loop through all active lchan and collect candidates */ + llist_for_each_entry(trx, &bts->trx_list, list) { + if (!trx_is_usable(trx)) + continue; + + for (i = 0; i < 8; i++) { + ts = &trx->ts[i]; + if (!ts_is_usable(ts)) + continue; + + /* (Do not consider dynamic TS that are in PDCH mode) */ + switch (ts_pchan(ts)) { + case GSM_PCHAN_TCH_F: + lc = &ts->lchan[0]; + /* omit if channel not active */ + if (lc->type != GSM_LCHAN_TCH_F + || lc->state != LCHAN_S_ACTIVE) + break; + /* omit if there is an ongoing ho/as */ + if (!lc->conn || lc->conn->secondary_lchan + || lc->conn->ho_lchan) + break; + /* We desperately want to resolve congestion, ignore rxlev when + * collecting candidates by passing include_weaker_rxlev=true. */ + collect_candidates_for_lchan(lc, clist, &candidates, NULL, true); + break; + case GSM_PCHAN_TCH_H: + for (j = 0; j < 2; j++) { + lc = &ts->lchan[j]; + /* omit if channel not active */ + if (lc->type != GSM_LCHAN_TCH_H + || lc->state != LCHAN_S_ACTIVE) + continue; + /* omit of there is an ongoing ho/as */ + if (!lc->conn + || lc->conn->secondary_lchan + || lc->conn->ho_lchan) + continue; + /* We desperately want to resolve congestion, ignore rxlev when + * collecting candidates by passing include_weaker_rxlev=true. */ + collect_candidates_for_lchan(lc, clist, &candidates, NULL, true); + } + break; + default: + break; + } + } + } + + if (!candidates) { + LOGPHOBTS(bts, LOGL_DEBUG, "No neighbor cells qualify to solve congestion\n"); + goto exit; + } + LOGPHOBTS(bts, LOGL_DEBUG, "Considering %u candidates to solve congestion\n", candidates); + +#if 0 +next_b1: +#endif + /* select best candidate that fulfills requirement B, + * omit change from AHS to AFS */ + best_avg_db = 0; + for (i = 0; i < candidates; i++) { + /* delete subscriber that just have handovered */ + if (clist[i].lchan == delete_lchan) + clist[i].lchan = NULL; + /* omit all subscribers that are handovered */ + if (!clist[i].lchan) + continue; + + if (!(clist[i].requirements & REQUIREMENT_B_MASK)) + continue; + /* omit assignment from AHS to AFS */ + if (clist[i].lchan->ts->trx->bts == clist[i].bts + && clist[i].lchan->type == GSM_LCHAN_TCH_H + && (clist[i].requirements & REQUIREMENT_B_TCHF)) + continue; + /* omit candidates that will not solve/reduce congestion */ + if (clist[i].lchan->type == GSM_LCHAN_TCH_F + && tchf_congestion <= 0) + continue; + if (clist[i].lchan->type == GSM_LCHAN_TCH_H + && tchh_congestion <= 0) + continue; + + avg = clist[i].avg; + /* improve AHS */ + if (clist[i].lchan->tch_mode == GSM48_CMODE_SPEECH_AMR + && clist[i].lchan->type == GSM_LCHAN_TCH_H + && (clist[i].requirements & REQUIREMENT_B_TCHF)) { + avg += ho_get_afs_bias_rxlev(clist[i].bts->ho); + is_improved = 1; + } else + is_improved = 0; + if (avg > best_avg_db) { + best_cand = &clist[i]; + best_avg_db = avg; + } + } + + /* perform handover, if there is a candidate */ + if (best_cand) { + any_ho = 1; + LOGPHOLCHAN(best_cand->lchan, LOGL_INFO, + "Best candidate BTS %u (RX level %d) without congestion found\n", + best_cand->bts->nr, rxlev2dbm(best_cand->avg)); + if (is_improved) + LOGP(DHODEC, LOGL_INFO, "(is improved due to " + "AHS -> AFS)\n"); + trigger_handover_or_assignment(best_cand->lchan, best_cand->bts, + best_cand->requirements & REQUIREMENT_B_MASK); +#if 0 + /* if there is still congestion, mark lchan as deleted + * and redo this process */ + if (best_cand->lchan->type == GSM_LCHAN_TCH_H) + tchh_congestion--; + else + tchf_congestion--; + if (tchf_congestion > 0 || tchh_congestion > 0) { + delete_lchan = best_cand->lchan; + best_cand = NULL; + goto next_b1; + } +#else + /* must exit here, because triggering handover/assignment + * will cause change in requirements. more check for this + * bts is performed in the next iteration. + */ +#endif + goto exit; + } + +#if 0 +next_b2: +#endif + /* select worst candidate that fulfills requirement B, + * select candidates that change from AHS to AFS only */ + if (tchh_congestion > 0) { + /* since this will only check half rate channels, it will + * only need to be checked, if tchh is congested */ + worst_avg_db = 999; + for (i = 0; i < candidates; i++) { + /* delete subscriber that just have handovered */ + if (clist[i].lchan == delete_lchan) + clist[i].lchan = NULL; + /* omit all subscribers that are handovered */ + if (!clist[i].lchan) + continue; + + if (!(clist[i].requirements & REQUIREMENT_B_MASK)) + continue; + /* omit all but assignment from AHS to AFS */ + if (clist[i].lchan->ts->trx->bts != clist[i].bts + || clist[i].lchan->type != GSM_LCHAN_TCH_H + || !(clist[i].requirements & REQUIREMENT_B_TCHF)) + continue; + + avg = clist[i].avg; + /* improve AHS */ + if (clist[i].lchan->tch_mode == GSM48_CMODE_SPEECH_AMR + && clist[i].lchan->type == GSM_LCHAN_TCH_H) { + avg += ho_get_afs_bias_rxlev(clist[i].bts->ho); + is_improved = 1; + } else + is_improved = 0; + if (avg < worst_avg_db) { + worst_cand = &clist[i]; + worst_avg_db = avg; + } + } + } + + /* perform handover, if there is a candidate */ + if (worst_cand) { + any_ho = 1; + LOGP(DHODEC, LOGL_INFO, "Worst candidate for assignment " + "(RX level %d) from TCH/H -> TCH/F without congestion " + "found\n", rxlev2dbm(worst_cand->avg)); + if (is_improved) + LOGP(DHODEC, LOGL_INFO, "(is improved due to " + "AHS -> AFS)\n"); + trigger_handover_or_assignment(worst_cand->lchan, + worst_cand->bts, + worst_cand->requirements & REQUIREMENT_B_MASK); +#if 0 + /* if there is still congestion, mark lchan as deleted + * and redo this process */ + tchh_congestion--; + if (tchh_congestion > 0) { + delete_lchan = worst_cand->lchan; + best_cand = NULL; + goto next_b2; + } +#else + /* must exit here, because triggering handover/assignment + * will cause change in requirements. more check for this + * bts is performed in the next iteration. + */ +#endif + goto exit; + } + +#if 0 +next_c1: +#endif + /* select best candidate that fulfills requirement C, + * omit change from AHS to AFS */ + best_avg_db = 0; + for (i = 0; i < candidates; i++) { + /* delete subscriber that just have handovered */ + if (clist[i].lchan == delete_lchan) + clist[i].lchan = NULL; + /* omit all subscribers that are handovered */ + if (!clist[i].lchan) + continue; + + if (!(clist[i].requirements & REQUIREMENT_C_MASK)) + continue; + /* omit assignment from AHS to AFS */ + if (clist[i].lchan->ts->trx->bts == clist[i].bts + && clist[i].lchan->type == GSM_LCHAN_TCH_H + && (clist[i].requirements & REQUIREMENT_C_TCHF)) + continue; + /* omit candidates that will not solve/reduce congestion */ + if (clist[i].lchan->type == GSM_LCHAN_TCH_F + && tchf_congestion <= 0) + continue; + if (clist[i].lchan->type == GSM_LCHAN_TCH_H + && tchh_congestion <= 0) + continue; + + avg = clist[i].avg; + /* improve AHS */ + if (clist[i].lchan->tch_mode == GSM48_CMODE_SPEECH_AMR + && clist[i].lchan->type == GSM_LCHAN_TCH_H + && (clist[i].requirements & REQUIREMENT_C_TCHF)) { + avg += ho_get_afs_bias_rxlev(clist[i].bts->ho); + is_improved = 1; + } else + is_improved = 0; + if (avg > best_avg_db) { + best_cand = &clist[i]; + best_avg_db = avg; + } + } + + /* perform handover, if there is a candidate */ + if (best_cand) { + any_ho = 1; + LOGP(DHODEC, LOGL_INFO, "Best candidate BTS %d (RX level %d) " + "with less or equal congestion found\n", + best_cand->bts->nr, rxlev2dbm(best_cand->avg)); + if (is_improved) + LOGP(DHODEC, LOGL_INFO, "(is improved due to " + "AHS -> AFS)\n"); + trigger_handover_or_assignment(best_cand->lchan, best_cand->bts, + best_cand->requirements & REQUIREMENT_C_MASK); +#if 0 + /* if there is still congestion, mark lchan as deleted + * and redo this process */ + if (best_cand->lchan->type == GSM_LCHAN_TCH_H) + tchh_congestion--; + else + tchf_congestion--; + if (tchf_congestion > 0 || tchh_congestion > 0) { + delete_lchan = best_cand->lchan; + best_cand = NULL; + goto next_c1; + } +#else + /* must exit here, because triggering handover/assignment + * will cause change in requirements. more check for this + * bts is performed in the next iteration. + */ +#endif + goto exit; + } + +#if 0 +next_c2: +#endif + /* select worst candidate that fulfills requirement C, + * select candidates that change from AHS to AFS only */ + if (tchh_congestion > 0) { + /* since this will only check half rate channels, it will + * only need to be checked, if tchh is congested */ + worst_avg_db = 999; + for (i = 0; i < candidates; i++) { + /* delete subscriber that just have handovered */ + if (clist[i].lchan == delete_lchan) + clist[i].lchan = NULL; + /* omit all subscribers that are handovered */ + if (!clist[i].lchan) + continue; + + if (!(clist[i].requirements & REQUIREMENT_C_MASK)) + continue; + /* omit all but assignment from AHS to AFS */ + if (clist[i].lchan->ts->trx->bts != clist[i].bts + || clist[i].lchan->type != GSM_LCHAN_TCH_H + || !(clist[i].requirements & REQUIREMENT_C_TCHF)) + continue; + + avg = clist[i].avg; + /* improve AHS */ + if (clist[i].lchan->tch_mode == GSM48_CMODE_SPEECH_AMR + && clist[i].lchan->type == GSM_LCHAN_TCH_H) { + avg += ho_get_afs_bias_rxlev(clist[i].bts->ho); + is_improved = 1; + } else + is_improved = 0; + if (avg < worst_avg_db) { + worst_cand = &clist[i]; + worst_avg_db = avg; + } + } + } + + /* perform handover, if there is a candidate */ + if (worst_cand) { + any_ho = 1; + LOGP(DHODEC, LOGL_INFO, "Worst candidate for assignment " + "(RX level %d) from TCH/H -> TCH/F with less or equal " + "congestion found\n", rxlev2dbm(worst_cand->avg)); + if (is_improved) + LOGP(DHODEC, LOGL_INFO, "(is improved due to " + "AHS -> AFS)\n"); + trigger_handover_or_assignment(worst_cand->lchan, + worst_cand->bts, + worst_cand->requirements & REQUIREMENT_C_MASK); +#if 0 + /* if there is still congestion, mark lchan as deleted + * and redo this process */ + tchh_congestion--; + if (tchh_congestion > 0) { + delete_lchan = worst_cand->lchan; + worst_cand = NULL; + goto next_c2; + } +#else + /* must exit here, because triggering handover/assignment + * will cause change in requirements. more check for this + * bts is performed in the next iteration. + */ +#endif + goto exit; + } + +exit: + /* free array */ + talloc_free(clist); + + if (tchf_congestion <= 0 && tchh_congestion <= 0) + LOGP(DHODEC, LOGL_INFO, "Congestion at BTS %d solved!\n", + bts->nr); + else if (any_ho) + LOGP(DHODEC, LOGL_INFO, "Congestion at BTS %d reduced!\n", + bts->nr); + else + LOGP(DHODEC, LOGL_INFO, "Congestion at BTS %d can't be reduced/solved!\n", bts->nr); + + return rc; +} + +static void bts_congestion_check(struct gsm_bts *bts) +{ + int min_free_tchf, min_free_tchh; + int tchf_count, tchh_count; + + global_ho_reason = HO_REASON_CONGESTION; + + /* only check BTS if TRX 0 is usable */ + if (!trx_is_usable(bts->c0)) { + LOGPHOBTS(bts, LOGL_DEBUG, "No congestion check: TRX 0 not usable\n"); + return; + } + + /* only check BTS if handover or assignment is enabled */ + if (!ho_get_as_active(bts->ho) + && !ho_get_ho_active(bts->ho)) { + LOGPHOBTS(bts, LOGL_DEBUG, "No congestion check: Assignment and Handover both disabled\n"); + return; + } + + min_free_tchf = ho_get_tchf_min_slots(bts->ho); + min_free_tchh = ho_get_tchh_min_slots(bts->ho); + + /* only check BTS with congestion level set */ + if (!min_free_tchf && !min_free_tchh) { + LOGPHOBTS(bts, LOGL_DEBUG, "No congestion check: no minimum for free TCH/F nor TCH/H set\n"); + return; + } + + tchf_count = bts_count_free_ts(bts, GSM_PCHAN_TCH_F); + tchh_count = bts_count_free_ts(bts, GSM_PCHAN_TCH_H); + LOGPHOBTS(bts, LOGL_INFO, "Congestion check: (free/want-free) TCH/F=%d/%d TCH/H=%d/%d\n", + tchf_count, min_free_tchf, tchh_count, min_free_tchh); + + /* only check BTS if congested */ + if (tchf_count >= min_free_tchf && tchh_count >= min_free_tchh) { + LOGPHOBTS(bts, LOGL_DEBUG, "Not congested\n"); + return; + } + + LOGPHOBTS(bts, LOGL_DEBUG, "Attempting to resolve congestion...\n"); + bts_resolve_congestion(bts, min_free_tchf - tchf_count, min_free_tchh - tchh_count); +} + +void hodec2_congestion_check(struct gsm_network *net) +{ + struct gsm_bts *bts; + + llist_for_each_entry(bts, &net->bts_list, list) + bts_congestion_check(bts); +} + +static void congestion_check_cb(void *arg) +{ + struct gsm_network *net = arg; + hodec2_congestion_check(net); + reinit_congestion_timer(net); +} + +static int ho_dec_2_sig_lchan(unsigned int signal, void *handler_data, void *signal_data) +{ + struct lchan_signal_data *lchan_data = signal_data; + + switch (signal) { + case S_LCHAN_MEAS_REP: + /* This is Handover Algorithm 2. If we're not responsible, drop it. */ + if (ho_get_algorithm(lchan_data->lchan->ts->trx->bts->ho) != 2) + return 0; + + attempt_handover_after_mr(lchan_data->mr); + break; + } + + return 0; +} + +static int ho_dec_2_sig_cb(unsigned int subsys, unsigned int signal, void *handler_data, + void *signal_data) +{ + switch (subsys) { + case SS_LCHAN: + return ho_dec_2_sig_lchan(signal, handler_data, signal_data); + default: + return 0; + } +} + + +void hodec2_init(struct gsm_network *net) +{ + osmo_signal_register_handler(SS_LCHAN, ho_dec_2_sig_cb, NULL); + ho2_initialized = true; + reinit_congestion_timer(net); +} diff --git a/src/libbsc/handover_logic.c b/src/libbsc/handover_logic.c index 4b86de7..0cce1d9 100644 --- a/src/libbsc/handover_logic.c +++ b/src/libbsc/handover_logic.c @@ -38,6 +38,29 @@ #include <osmocom/core/talloc.h> #include <osmocom/bsc/bsc_subscriber.h> #include <osmocom/bsc/gsm_04_08_utils.h> +#include <osmocom/bsc/handover.h> +#include <osmocom/bsc/handover_cfg.h> + +#define LOGPHOLCHANTOLCHAN(lchan, new_lchan, level, fmt, args...) \ + LOGP(DHODEC, level, "(BTS %u trx %u arfcn %u ts %u lchan %u %s)->(BTS %u trx %u arfcn %u ts %u lchan %u %s) (subscr %s) " fmt, \ + lchan->ts->trx->bts->nr, \ + lchan->ts->trx->nr, \ + lchan->ts->trx->arfcn, \ + lchan->ts->nr, \ + lchan->nr, \ + gsm_pchan_name(lchan->ts->pchan), \ + new_lchan->ts->trx->bts->nr, \ + new_lchan->ts->trx->nr, \ + new_lchan->ts->trx->arfcn, \ + new_lchan->ts->nr, \ + new_lchan->nr, \ + gsm_pchan_name(new_lchan->ts->pchan), \ + bsc_subscr_name(lchan->conn->bsub), \ + ## args) + +#define LOGPHO(struct_bsc_handover, level, fmt, args ...) \ + LOGPHOLCHANTOLCHAN(struct_bsc_handover->old_lchan, struct_bsc_handover->new_lchan, level, fmt, ## args) + struct bsc_handover { struct llist_head list; @@ -86,36 +109,56 @@ return NULL; } -/*! \brief Hand over the specified logical channel to the specified new BTS. - * This is the main entry point for the actual handover algorithm, after the - * decision whether to initiate HO to a specific BTS. */ +/*! Hand over the specified logical channel to the specified new BTS. */ int bsc_handover_start(struct gsm_lchan *old_lchan, struct gsm_bts *bts) { + return bsc_handover_start_lchan_change(old_lchan, bts, old_lchan->type); +} + +/*! Hand over the specified logical channel to the specified new BTS and possibly change the lchan type. + * This is the main entry point for the actual handover algorithm, after the decision whether to initiate + * HO to a specific BTS. */ +int bsc_handover_start_lchan_change(struct gsm_lchan *old_lchan, struct gsm_bts *new_bts, + enum gsm_chan_t new_lchan_type) +{ + struct gsm_network *network; struct gsm_lchan *new_lchan; struct bsc_handover *ho; static uint8_t ho_ref = 0; int rc; + bool do_assignment = false; /* don't attempt multiple handovers for the same lchan at * the same time */ if (bsc_ho_by_old_lchan(old_lchan)) return -EBUSY; - DEBUGP(DHO, "Beginning with handover operation" - "(old_lchan on BTS %u, new BTS %u) ...\n", - old_lchan->ts->trx->bts->nr, bts->nr); + if (!new_bts) + new_bts = old_lchan->ts->trx->bts; + do_assignment = (new_bts == old_lchan->ts->trx->bts); - rate_ctr_inc(&bts->network->bsc_ctrs->ctr[BSC_CTR_HANDOVER_ATTEMPTED]); + network = new_bts->network; + + rate_ctr_inc(&network->bsc_ctrs->ctr[BSC_CTR_HANDOVER_ATTEMPTED]); if (!old_lchan->conn) { LOGP(DHO, LOGL_ERROR, "Old lchan lacks connection data.\n"); return -ENOSPC; } - new_lchan = lchan_alloc(bts, old_lchan->type, 0); + DEBUGP(DHO, "(BTS %u trx %u ts %u lchan %u %s)->(BTS %u lchan %s) Beginning with handover operation...\n", + old_lchan->ts->trx->bts->nr, + old_lchan->ts->trx->nr, + old_lchan->ts->nr, + old_lchan->nr, + gsm_pchan_name(old_lchan->ts->pchan), + new_bts->nr, + gsm_lchant_name(new_lchan_type)); + + new_lchan = lchan_alloc(new_bts, new_lchan_type, 0); if (!new_lchan) { - LOGP(DHO, LOGL_NOTICE, "No free channel\n"); - rate_ctr_inc(&bts->network->bsc_ctrs->ctr[BSC_CTR_HANDOVER_NO_CHANNEL]); + LOGP(DHO, LOGL_NOTICE, "No free channel for %s\n", gsm_lchant_name(new_lchan_type)); + rate_ctr_inc(&network->bsc_ctrs->ctr[BSC_CTR_HANDOVER_NO_CHANNEL]); return -ENOSPC; } @@ -128,31 +171,41 @@ ho->old_lchan = old_lchan; ho->new_lchan = new_lchan; ho->ho_ref = ho_ref++; - if (old_lchan->ts->trx->bts != bts) { + if (!do_assignment) { ho->inter_cell = true; ho->async = true; } + LOGPHO(ho, LOGL_INFO, "Triggering %s\n", do_assignment? "Assignment" : "Handover"); + /* copy some parameters from old lchan */ memcpy(&new_lchan->encr, &old_lchan->encr, sizeof(new_lchan->encr)); - new_lchan->ms_power = old_lchan->ms_power; + if (do_assignment) { + new_lchan->ms_power = old_lchan->ms_power; + new_lchan->rqd_ta = old_lchan->rqd_ta; + } else { + new_lchan->ms_power = + ms_pwr_ctl_lvl(new_bts->band, new_bts->ms_max_power); + /* FIXME: do we have a better idea of the timing advance? */ + //new_lchan->rqd_ta = old_lchan->rqd_ta; + } new_lchan->bs_power = old_lchan->bs_power; new_lchan->rsl_cmode = old_lchan->rsl_cmode; new_lchan->tch_mode = old_lchan->tch_mode; - memcpy(&new_lchan->mr_ms_lv, &old_lchan->mr_ms_lv, ARRAY_SIZE(new_lchan->mr_ms_lv)); - memcpy(&new_lchan->mr_bts_lv, &old_lchan->mr_bts_lv, ARRAY_SIZE(new_lchan->mr_bts_lv)); + memcpy(&new_lchan->mr_ms_lv, &old_lchan->mr_ms_lv, sizeof(new_lchan->mr_ms_lv)); + memcpy(&new_lchan->mr_bts_lv, &old_lchan->mr_bts_lv, sizeof(new_lchan->mr_bts_lv)); new_lchan->conn = old_lchan->conn; new_lchan->conn->ho_lchan = new_lchan; - /* FIXME: do we have a better idea of the timing advance? */ rc = rsl_chan_activate_lchan(new_lchan, ho->inter_cell ? (ho->async ? RSL_ACT_INTER_ASYNC : RSL_ACT_INTER_SYNC) : RSL_ACT_INTRA_IMM_ASS, ho->ho_ref); if (rc < 0) { - LOGP(DHO, LOGL_ERROR, "could not activate channel\n"); + LOGPHO(ho, LOGL_INFO, "%s Failure: activate lchan rc = %d\n", + do_assignment? "Assignment" : "Handover", rc); new_lchan->conn->ho_lchan = NULL; new_lchan->conn = NULL; talloc_free(ho); @@ -218,12 +271,16 @@ if (!ho) return -ENODEV; - DEBUGP(DHO, "handover activate ack, send HO Command\n"); + LOGPHO(ho, LOGL_INFO, "Channel Activate Ack, send %s COMMAND\n", ho->inter_cell? "HANDOVER" : "ASSIGNMENT"); /* we can now send the 04.08 HANDOVER COMMAND to the MS * using the old lchan */ - gsm48_send_ho_cmd(ho->old_lchan, new_lchan, 0, ho->ho_ref); + if (ho->inter_cell) { + gsm48_send_rr_ass_cmd(ho->old_lchan, new_lchan, new_lchan->ms_power); + } else { + gsm48_send_ho_cmd(ho->old_lchan, new_lchan, new_lchan->ms_power, ho->ho_ref); + } /* start T3103. We can continue either with T3103 expiration, * 04.08 HANDOVER COMPLETE or 04.08 HANDOVER FAIL */ @@ -241,12 +298,18 @@ static int ho_chan_activ_nack(struct gsm_lchan *new_lchan) { struct bsc_handover *ho; + struct gsm_bts *new_bts = new_lchan->ts->trx->bts; ho = bsc_ho_by_new_lchan(new_lchan); if (!ho) { LOGP(DHO, LOGL_INFO, "ACT NACK: unable to find HO record\n"); return -ENODEV; } + + LOGPHO(ho, LOGL_ERROR, "Channel Activate Nack for %s, starting penalty timer\n", ho->inter_cell? "Handover" : "Assignment"); + + /* if channel failed, wait 10 seconds befor allowing to retry handover */ + conn_penalty_timer_add(ho->old_lchan->conn, new_bts, 10); /* FIXME configurable */ new_lchan->conn->ho_lchan = NULL; new_lchan->conn = NULL; @@ -269,22 +332,19 @@ return -ENODEV; } - net = new_lchan->ts->trx->bts->network; - LOGP(DHO, LOGL_INFO, "Subscriber %s HO from BTS %u->%u on ARFCN " - "%u->%u\n", bsc_subscr_name(ho->old_lchan->conn->bsub), - ho->old_lchan->ts->trx->bts->nr, new_lchan->ts->trx->bts->nr, - ho->old_lchan->ts->trx->arfcn, new_lchan->ts->trx->arfcn); + LOGPHO(ho, LOGL_INFO, "%s Complete\n", ho->inter_cell ? "Handover" : "Assignment"); + net = new_lchan->ts->trx->bts->network; rate_ctr_inc(&net->bsc_ctrs->ctr[BSC_CTR_HANDOVER_COMPLETED]); osmo_timer_del(&ho->T3103); /* Replace the ho lchan with the primary one */ if (ho->old_lchan != new_lchan->conn->lchan) - LOGP(DHO, LOGL_ERROR, "Primary lchan changed during handover.\n"); + LOGPHO(ho, LOGL_ERROR, "Primary lchan changed during handover.\n"); if (new_lchan != new_lchan->conn->ho_lchan) - LOGP(DHO, LOGL_ERROR, "Handover channel changed during this handover.\n"); + LOGPHO(ho, LOGL_ERROR, "Handover channel changed during this handover.\n"); new_lchan->conn->ho_lchan = NULL; new_lchan->conn->lchan = new_lchan; @@ -301,6 +361,8 @@ static int ho_gsm48_ho_fail(struct gsm_lchan *old_lchan) { struct gsm_network *net = old_lchan->ts->trx->bts->network; + struct gsm_bts *old_bts; + struct gsm_bts *new_bts; struct bsc_handover *ho; struct gsm_lchan *new_lchan; @@ -308,6 +370,25 @@ if (!ho) { LOGP(DHO, LOGL_ERROR, "unable to find HO record\n"); return -ENODEV; + } + + old_bts = old_lchan->ts->trx->bts; + new_bts = ho->new_lchan->ts->trx->bts; + + if (old_lchan->conn->ho_failure >= ho_get_retries(old_bts->ho)) { + int penalty = ho->inter_cell + ? ho_get_penalty_failed_ho(old_bts->ho) + : ho_get_penalty_failed_as(old_bts->ho); + LOGPHO(ho, LOGL_NOTICE, "%s failed, starting penalty timer (%d)\n", + ho->inter_cell ? "Handover" : "Assignment", + penalty); + old_lchan->conn->ho_failure = 0; + conn_penalty_timer_add(old_lchan->conn, new_bts, penalty); + } else { + old_lchan->conn->ho_failure++; + LOGPHO(ho, LOGL_NOTICE, "%s failed, trying again (%d/%d)\n", + ho->inter_cell ? "Handover" : "Assignment", + old_lchan->conn->ho_failure, ho_get_retries(old_bts->ho)); } rate_ctr_inc(&net->bsc_ctrs->ctr[BSC_CTR_HANDOVER_FAILED]); @@ -320,7 +401,6 @@ handover_free(ho); lchan_release(new_lchan, 0, RSL_REL_LOCAL_END); - return 0; } @@ -336,7 +416,7 @@ return -ENODEV; } - LOGP(DHO, LOGL_DEBUG, "%s Handover RACH detected\n", gsm_lchan_name(new_lchan)); + LOGPHO(ho, LOGL_DEBUG, "Handover RACH detected\n"); /* This is just for logging on the DHO category. The actual MGCP switchover happens in * osmo_bsc_mgcp.c by receiving the same S_LCHAN_HANDOVER_DETECT signal. diff --git a/src/libbsc/handover_vty.c b/src/libbsc/handover_vty.c index 225e9a9..5d66e4c 100644 --- a/src/libbsc/handover_vty.c +++ b/src/libbsc/handover_vty.c @@ -23,6 +23,7 @@ #include <osmocom/bsc/gsm_data.h> #include <osmocom/bsc/vty.h> #include <osmocom/bsc/handover_cfg.h> +#include <osmocom/bsc/handover_decision_2.h> static struct handover_cfg *ho_cfg_from_vty(struct vty *vty) { @@ -70,7 +71,42 @@ #undef HO_CFG_ONE_MEMBER -void ho_vty_write(struct vty *vty, const char *indent, struct handover_cfg *ho) +static inline const int a2congestion_check_interval(const char *arg) +{ + if (!strcmp(arg, "disabled")) + return 0; + return atoi(arg); +} + +static inline const char *congestion_check_interval2a(int val) +{ + static char str[9]; + if (val < 1 + || snprintf(str, sizeof(str), "%d", val) >= sizeof(str)) + return "disabled"; + return str; +} + +DEFUN(cfg_net_ho_congestion_check_interval, cfg_net_ho_congestion_check_interval_cmd, + "handover congestion-check (disabled|<1-999>|now)", + HO_CFG_STR_HANDOVER + "Configure congestion check interval" HO_CFG_STR_2 + "Disable congestion checking, do not handover based on cell overload\n" + "Congestion check interval in seconds (default " + OSMO_STRINGIFY_VAL(HO_CFG_CONGESTION_CHECK_DEFAULT) ")\n" + "Manually trigger a congestion check to run right now\n") +{ + if (!strcmp(argv[0], "now")) { + hodec2_congestion_check(gsmnet_from_vty(vty)); + return CMD_SUCCESS; + } + + hodec2_on_change_congestion_check_interval(gsmnet_from_vty(vty), + a2congestion_check_interval(argv[0])); + return CMD_SUCCESS; +} + +static void ho_vty_write(struct vty *vty, const char *indent, struct handover_cfg *ho) { #define HO_CFG_ONE_MEMBER(TYPE, NAME, DEFAULT_VAL, \ VTY_CMD, VTY_CMD_ARG, VTY_ARG_EVAL, \ @@ -82,6 +118,21 @@ HO_CFG_ALL_MEMBERS #undef HO_CFG_ONE_MEMBER +} + +void ho_vty_write_bts(struct vty *vty, struct gsm_bts *bts) +{ + ho_vty_write(vty, " ", bts->ho); +} + +void ho_vty_write_net(struct vty *vty, struct gsm_network *net) +{ + ho_vty_write(vty, " ", net->ho); + + if (net->ho2.congestion_check_interval_s != HO_CFG_CONGESTION_CHECK_DEFAULT) + vty_out(vty, " handover congestion-check %s%s", + congestion_check_interval2a(net->ho2.congestion_check_interval_s), + VTY_NEWLINE); } static void ho_vty_init_cmds(int parent_node) @@ -96,6 +147,8 @@ void ho_vty_init() { ho_vty_init_cmds(GSMNET_NODE); + install_element(GSMNET_NODE, &cfg_net_ho_congestion_check_interval_cmd); + ho_vty_init_cmds(BTS_NODE); } diff --git a/src/libbsc/net_init.c b/src/libbsc/net_init.c index 57d8241..4368598 100644 --- a/src/libbsc/net_init.c +++ b/src/libbsc/net_init.c @@ -57,6 +57,7 @@ net->T3141 = GSM_T3141_DEFAULT; net->ho = ho_cfg_init(net, NULL); + net->ho2.congestion_check_interval_s = HO_CFG_CONGESTION_CHECK_DEFAULT; INIT_LLIST_HEAD(&net->bts_list); diff --git a/src/libcommon/gsm_data.c b/src/libcommon/gsm_data.c index 92ebbfe..17858f6 100644 --- a/src/libcommon/gsm_data.c +++ b/src/libcommon/gsm_data.c @@ -25,9 +25,11 @@ #include <ctype.h> #include <stdbool.h> #include <netinet/in.h> +#include <time.h> #include <osmocom/core/linuxlist.h> #include <osmocom/core/talloc.h> +#include <osmocom/core/signal.h> #include <osmocom/gsm/gsm_utils.h> #include <osmocom/gsm/abis_nm.h> #include <osmocom/core/statistics.h> @@ -38,6 +40,7 @@ #include <osmocom/bsc/bsc_msc_data.h> #include <osmocom/bsc/abis_nm.h> #include <osmocom/bsc/handover_cfg.h> +#include <osmocom/bsc/signal.h> void *tall_bsc_ctx; @@ -266,6 +269,8 @@ INIT_LLIST_HEAD(&bts->loc_list); + osmo_signal_dispatch(SS_L_GLOBAL, S_GLOBAL_BTS_NEW, bts); + return bts; } @@ -416,3 +421,73 @@ rev_lev = (cm->classmark2[0] >> 5) & 0x3; return rev_lev >= 2; } + +static unsigned int time_now(void) +{ + time_t now; + time(&now); + return (unsigned int)now; +} + +void conn_penalty_timer_add(struct gsm_subscriber_connection *conn, + struct gsm_bts *bts, int timeout) +{ + struct ho_penalty_timer *timer; + unsigned int now; + unsigned int then; + now = time_now(); + + /* no not add timer, if there is no timeout set */ + if (!timeout) + return; + + then = now + timeout; + + /* timer already running for that BTS? */ + llist_for_each_entry(timer, &conn->ho_penalty_timers, entry) { + if (timer->bts_nr != bts->nr) + continue; + /* raise, if running timer will timeout earlier or has timed + * out already, otherwise keep later timeout */ + if (timer->timeout < then) + timer->timeout = then; + return; + } + + /* add new timer */ + timer = talloc_zero(tall_bsc_ctx, struct ho_penalty_timer); + if (!timer) + return; + + timer->bts_nr = bts->nr; + timer->timeout = then; + + llist_add_tail(&timer->entry, &conn->ho_penalty_timers); +} + +unsigned int conn_penalty_timer_remaining(struct gsm_subscriber_connection *conn, + struct gsm_bts *bts) +{ + struct ho_penalty_timer *timer; + unsigned int now = time_now(); + llist_for_each_entry(timer, &conn->ho_penalty_timers, entry) { + if (timer->bts_nr != bts->nr) + continue; + if (now > timer->timeout) + continue; + return timer->timeout - now; + } + return 0; +} + +void conn_penalty_timer_clear(struct gsm_subscriber_connection *conn, + struct gsm_bts *bts) +{ + struct ho_penalty_timer *timer, *timer2; + llist_for_each_entry_safe(timer, timer2, &conn->ho_penalty_timers, entry) { + if (bts && timer->bts_nr != bts->nr) + continue; + llist_del(&timer->entry); + talloc_free(timer); + } +} diff --git a/src/libcommon/handover_cfg.c b/src/libcommon/handover_cfg.c index 8c208f6..204e5a2 100644 --- a/src/libcommon/handover_cfg.c +++ b/src/libcommon/handover_cfg.c @@ -23,7 +23,10 @@ #include <stdbool.h> #include <talloc.h> +#include <osmocom/bsc/debug.h> + #include <osmocom/bsc/vty.h> +#include <osmocom/bsc/handover_decision_2.h> #include <osmocom/bsc/handover_cfg.h> #include <osmocom/bsc/gsm_data.h> diff --git a/src/osmo-bsc/osmo_bsc_bssap.c b/src/osmo-bsc/osmo_bsc_bssap.c index 799cb46..ab796b2 100644 --- a/src/osmo-bsc/osmo_bsc_bssap.c +++ b/src/osmo-bsc/osmo_bsc_bssap.c @@ -741,7 +741,6 @@ bool aoip = false; struct sockaddr_storage rtp_addr; struct gsm0808_channel_type ct; - struct gsm0808_speech_codec_list scl; struct gsm0808_speech_codec_list *scl_ptr = NULL; int rc; const uint8_t *data; @@ -787,6 +786,7 @@ } /* Decode speech codec list (AoIP) */ + conn->conn->codec_list_present = false; if (aoip) { /* Check for speech codec list element */ if (!TLVP_PRESENT(&tp, GSM0808_IE_SPEECH_CODEC_LIST)) { @@ -798,13 +798,14 @@ /* Decode Speech Codec list */ data = TLVP_VAL(&tp, GSM0808_IE_SPEECH_CODEC_LIST); len = TLVP_LEN(&tp, GSM0808_IE_SPEECH_CODEC_LIST); - rc = gsm0808_dec_speech_codec_list(&scl, data, len); + rc = gsm0808_dec_speech_codec_list(&conn->conn->codec_list, data, len); if (rc < 0) { LOGP(DMSC, LOGL_ERROR, "Unable to decode speech codec list\n"); goto reject; } - scl_ptr = &scl; + conn->conn->codec_list_present = true; + scl_ptr = &conn->conn->codec_list; } /* Decode Channel Type element */ diff --git a/src/osmo-bsc/osmo_bsc_main.c b/src/osmo-bsc/osmo_bsc_main.c index 1aff4c5..bac3a5e 100644 --- a/src/osmo-bsc/osmo_bsc_main.c +++ b/src/osmo-bsc/osmo_bsc_main.c @@ -31,6 +31,7 @@ #include <osmocom/bsc/osmo_bsc_sigtran.h> #include <osmocom/bsc/osmo_bsc_mgcp.h> #include <osmocom/bsc/handover_decision.h> +#include <osmocom/bsc/handover_decision_2.h> #include <osmocom/ctrl/control_cmd.h> #include <osmocom/ctrl/control_if.h> @@ -298,6 +299,7 @@ mgcp_init(bsc_gsmnet); handover_decision_1_init(); + hodec2_init(bsc_gsmnet); signal(SIGINT, &signal_handler); signal(SIGTERM, &signal_handler); diff --git a/tests/Makefile.am b/tests/Makefile.am index ba8a5e1..652dfe1 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -8,6 +8,7 @@ bsc-nat \ bsc-nat-trie \ bssap \ + handover \ $(NULL) # The `:;' works around a Bash 3.2 bug when the output is not writeable. diff --git a/tests/handover/Makefile.am b/tests/handover/Makefile.am new file mode 100644 index 0000000..69fda8a --- /dev/null +++ b/tests/handover/Makefile.am @@ -0,0 +1,37 @@ +AM_CPPFLAGS = \ + $(all_includes) \ + -I$(top_srcdir)/include \ + $(NULL) + +AM_CFLAGS = \ + -Wall \ + -ggdb3 \ + $(LIBOSMOCORE_CFLAGS) \ + $(LIBOSMOGSM_CFLAGS) \ + $(LIBOSMOABIS_CFLAGS) \ + $(NULL) + +AM_LDFLAGS = \ + $(COVERAGE_LDFLAGS) \ + $(NULL) + +EXTRA_DIST = \ + handover_test.ok \ + $(NULL) + +noinst_PROGRAMS = \ + handover_test \ + $(NULL) + +handover_test_SOURCES = \ + handover_test.c \ + $(NULL) + +handover_test_LDADD = \ + $(LIBOSMOCORE_LIBS) \ + $(LIBOSMOGSM_LIBS) \ + $(LIBOSMOABIS_LIBS) \ + $(top_builddir)/src/libcommon/libcommon.a \ + $(top_builddir)/src/libbsc/libbsc.a \ + $(top_builddir)/src/libcommon-cs/libcommon-cs.a \ + $(NULL) diff --git a/tests/handover/handover_test.c b/tests/handover/handover_test.c new file mode 100644 index 0000000..039a52f --- /dev/null +++ b/tests/handover/handover_test.c @@ -0,0 +1,1583 @@ +/* + * (C) 2013 by Andreas Eversberg <jo...@eversberg.eu> + * 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/>. + * + */ + +#include <stdio.h> +#include <stdlib.h> +#include <errno.h> + +#include <assert.h> + +#include <osmocom/core/application.h> +#include <osmocom/core/select.h> +#include <osmocom/core/talloc.h> + +#include <osmocom/bsc/abis_rsl.h> +#include <osmocom/bsc/debug.h> +#include <osmocom/bsc/bsc_subscriber.h> +#include <osmocom/bsc/chan_alloc.h> +#include <osmocom/bsc/handover_decision.h> +#include <osmocom/bsc/system_information.h> +#include <osmocom/bsc/handover_cfg.h> +#include <osmocom/bsc/handover_decision_2.h> +#include <osmocom/bsc/common_bsc.h> +#include <osmocom/bsc/bss.h> +#include <osmocom/bsc/bsc_api.h> +#include <osmocom/bsc/osmo_bsc.h> + +struct gsm_network *bsc_gsmnet; + +/* measurement report */ + +uint8_t meas_rep_ba = 0, meas_rep_valid = 1, meas_valid = 1, meas_multi_rep = 0; +uint8_t meas_dl_rxlev = 0, meas_dl_rxqual = 0; +uint8_t meas_ul_rxlev = 0, meas_ul_rxqual = 0; +uint8_t meas_tx_power_ms = 0, meas_tx_power_bs = 0, meas_ta_ms = 0; +uint8_t meas_dtx_ms = 0, meas_dtx_bs = 0, meas_nr = 0; +uint8_t meas_num_nc = 0, meas_rxlev_nc[6], meas_bsic_nc[6], meas_bcch_f_nc[6]; + +static void gen_meas_rep(struct gsm_lchan *lchan) +{ + struct msgb *msg = msgb_alloc_headroom(256, 64, "RSL"); + struct abis_rsl_dchan_hdr *dh; + uint8_t chan_nr = gsm_lchan2chan_nr(lchan); + uint8_t ulm[3], l1i[2], *buf; + struct gsm48_hdr *gh; + struct gsm48_meas_res *mr; + + dh = (struct abis_rsl_dchan_hdr *) msgb_put(msg, sizeof(*dh)); + dh->c.msg_discr = ABIS_RSL_MDISC_DED_CHAN; + dh->c.msg_type = RSL_MT_MEAS_RES; + dh->ie_chan = RSL_IE_CHAN_NR; + dh->chan_nr = chan_nr; + + msgb_tv_put(msg, RSL_IE_MEAS_RES_NR, meas_nr++); + + ulm[0] = meas_ul_rxlev | (meas_dtx_bs << 7); + ulm[1] = meas_ul_rxlev; + ulm[2] = (meas_ul_rxqual << 3) | meas_ul_rxqual; + msgb_tlv_put(msg, RSL_IE_UPLINK_MEAS, sizeof(ulm), ulm); + + msgb_tv_put(msg, RSL_IE_BS_POWER, meas_tx_power_bs); + + l1i[0] = 0; + l1i[1] = meas_ta_ms; + msgb_tv_fixed_put(msg, RSL_IE_L1_INFO, sizeof(l1i), l1i); + + buf = msgb_put(msg, 3); + buf[0] = RSL_IE_L3_INFO; + buf[1] = (sizeof(*gh) + sizeof(*mr)) >> 8; + buf[2] = (sizeof(*gh) + sizeof(*mr)) & 0xff; + + gh = (struct gsm48_hdr *) msgb_put(msg, sizeof(*gh)); + mr = (struct gsm48_meas_res *) msgb_put(msg, sizeof(*mr)); + + gh->proto_discr = GSM48_PDISC_RR; + gh->msg_type = GSM48_MT_RR_MEAS_REP; + + /* measurement results */ + mr->rxlev_full = meas_dl_rxlev; + mr->rxlev_sub = meas_dl_rxlev; + mr->rxqual_full = meas_dl_rxqual; + mr->rxqual_sub = meas_dl_rxqual; + mr->dtx_used = meas_dtx_ms; + mr->ba_used = meas_rep_ba; + mr->meas_valid = !meas_valid; /* 0 = valid */ + if (meas_rep_valid) { + mr->no_nc_n_hi = meas_num_nc >> 2; + mr->no_nc_n_lo = meas_num_nc & 3; + } else { + /* no results for serving cells */ + mr->no_nc_n_hi = 1; + mr->no_nc_n_lo = 3; + } + mr->rxlev_nc1 = meas_rxlev_nc[0]; + mr->rxlev_nc2_hi = meas_rxlev_nc[1] >> 1; + mr->rxlev_nc2_lo = meas_rxlev_nc[1] & 1; + mr->rxlev_nc3_hi = meas_rxlev_nc[2] >> 2; + mr->rxlev_nc3_lo = meas_rxlev_nc[2] & 3; + mr->rxlev_nc4_hi = meas_rxlev_nc[3] >> 3; + mr->rxlev_nc4_lo = meas_rxlev_nc[3] & 7; + mr->rxlev_nc5_hi = meas_rxlev_nc[4] >> 4; + mr->rxlev_nc5_lo = meas_rxlev_nc[4] & 15; + mr->rxlev_nc6_hi = meas_rxlev_nc[5] >> 5; + mr->rxlev_nc6_lo = meas_rxlev_nc[5] & 31; + mr->bsic_nc1_hi = meas_bsic_nc[0] >> 3; + mr->bsic_nc1_lo = meas_bsic_nc[0] & 7; + mr->bsic_nc2_hi = meas_bsic_nc[1] >> 4; + mr->bsic_nc2_lo = meas_bsic_nc[1] & 15; + mr->bsic_nc3_hi = meas_bsic_nc[2] >> 5; + mr->bsic_nc3_lo = meas_bsic_nc[2] & 31; + mr->bsic_nc4 = meas_bsic_nc[3]; + mr->bsic_nc5 = meas_bsic_nc[4]; + mr->bsic_nc6 = meas_bsic_nc[5]; + mr->bcch_f_nc1 = meas_bcch_f_nc[0]; + mr->bcch_f_nc2 = meas_bcch_f_nc[1]; + mr->bcch_f_nc3 = meas_bcch_f_nc[2]; + mr->bcch_f_nc4 = meas_bcch_f_nc[3]; + mr->bcch_f_nc5_hi = meas_bcch_f_nc[4] >> 1; + mr->bcch_f_nc5_lo = meas_bcch_f_nc[4] & 1; + mr->bcch_f_nc6_hi = meas_bcch_f_nc[5] >> 2; + mr->bcch_f_nc6_lo = meas_bcch_f_nc[5] & 3; + + msg->dst = lchan->ts->trx->bts->c0->rsl_link; + msg->l2h = (unsigned char *)dh; + msg->l3h = (unsigned char *)gh; + + abis_rsl_rcvmsg(msg); +} + +static struct gsm_bts *create_bts(int arfcn) +{ + struct gsm_bts *bts; + struct e1inp_sign_link *rsl_link; + int i; + + bts = gsm_bts_alloc_register(bsc_gsmnet, GSM_BTS_TYPE_OSMOBTS, 0x3f); + if (!bts) { + printf("No resource for bts1\n"); + return NULL; + } + + bts->location_area_code = 23; + bts->c0->arfcn = arfcn; + + bts->codec.efr = 1; + bts->codec.hr = 1; + bts->codec.amr = 1; + + rsl_link = talloc_zero(0, struct e1inp_sign_link); + rsl_link->trx = bts->c0; + bts->c0->rsl_link = rsl_link; + + bts->c0->mo.nm_state.operational = NM_OPSTATE_ENABLED; + bts->c0->mo.nm_state.availability = NM_AVSTATE_OK; + bts->c0->bb_transc.mo.nm_state.operational = NM_OPSTATE_ENABLED; + bts->c0->bb_transc.mo.nm_state.availability = NM_AVSTATE_OK; + + /* 4 full rate and 4 half rate channels */ + for (i = 1; i <= 6; i++) { + bts->c0->ts[i].pchan = + (i < 5) ? GSM_PCHAN_TCH_F : GSM_PCHAN_TCH_H; + bts->c0->ts[i].mo.nm_state.operational = NM_OPSTATE_ENABLED; + bts->c0->ts[i].mo.nm_state.availability = NM_AVSTATE_OK; + bts->c0->ts[i].lchan[0].type = GSM_LCHAN_NONE; + bts->c0->ts[i].lchan[0].state = LCHAN_S_NONE; + bts->c0->ts[i].lchan[1].type = GSM_LCHAN_NONE; + bts->c0->ts[i].lchan[1].state = LCHAN_S_NONE; + } + return bts; +} + +void create_conn(struct gsm_lchan *lchan) +{ + lchan->conn = bsc_subscr_con_allocate(lchan); +} + +/* create lchan */ +struct gsm_lchan *create_lchan(struct gsm_bts *bts, int full_rate, char *codec) +{ + struct gsm_lchan *lchan; + + lchan = lchan_alloc(bts, + (full_rate) ? GSM_LCHAN_TCH_F : GSM_LCHAN_TCH_H, 0); + if (!lchan) { + printf("No resource for lchan\n"); + exit(EXIT_FAILURE); + } + lchan->state = LCHAN_S_ACTIVE; + create_conn(lchan); + if (!strcasecmp(codec, "FR") && full_rate) + lchan->tch_mode = GSM48_CMODE_SPEECH_V1; + else if (!strcasecmp(codec, "HR") && !full_rate) + lchan->tch_mode = GSM48_CMODE_SPEECH_V1; + else if (!strcasecmp(codec, "EFR") && full_rate) + lchan->tch_mode = GSM48_CMODE_SPEECH_EFR; + else if (!strcasecmp(codec, "AMR")) + lchan->tch_mode = GSM48_CMODE_SPEECH_AMR; + else { + printf("Given codec unknown\n"); + exit(EXIT_FAILURE); + } + + lchan->conn->codec_list = (struct gsm0808_speech_codec_list){ + .codec = { + { .fi=true, .type=GSM0808_SCT_FR1, }, + { .fi=true, .type=GSM0808_SCT_FR2, }, + { .fi=true, .type=GSM0808_SCT_FR3, }, + { .fi=true, .type=GSM0808_SCT_HR1, }, + { .fi=true, .type=GSM0808_SCT_HR3, }, + }, + .len = 5, + }; + lchan->conn->codec_list_present = true; + + return lchan; +} + +/* parse channel request */ + +static int got_chan_req = 0; +static struct gsm_lchan *chan_req_lchan = NULL; + +static int parse_chan_act(struct gsm_lchan *lchan, uint8_t *data) +{ + chan_req_lchan = lchan; + return 0; +} + +static int parse_chan_rel(struct gsm_lchan *lchan, uint8_t *data) +{ + chan_req_lchan = lchan; + return 0; +} + +/* parse handover request */ + +static int got_ho_req = 0; +static struct gsm_lchan *ho_req_lchan = NULL; + +static int parse_ho_command(struct gsm_lchan *lchan, uint8_t *data, int len) +{ + struct gsm48_hdr *gh = (struct gsm48_hdr *) data; + struct gsm48_ho_cmd *ho = (struct gsm48_ho_cmd *) gh->data; + int arfcn; + struct gsm_bts *neigh; + + switch (gh->msg_type) { + case GSM48_MT_RR_HANDO_CMD: + arfcn = (ho->cell_desc.arfcn_hi << 8) | ho->cell_desc.arfcn_lo; + + /* look up trx. since every dummy bts uses different arfcn and + * only one trx, it is simple */ + llist_for_each_entry(neigh, &bsc_gsmnet->bts_list, list) { + if (neigh->c0->arfcn != arfcn) + continue; + ho_req_lchan = lchan; + return 0; + } + break; + case GSM48_MT_RR_ASS_CMD: + ho_req_lchan = lchan; + return 0; + break; + default: + fprintf(stderr, "Error, expecting HO or AS command\n"); + return -EINVAL; + } + + return -1; +} + +/* send channel activation ack */ +static void send_chan_act_ack(struct gsm_lchan *lchan, int act) +{ + struct msgb *msg = msgb_alloc_headroom(256, 64, "RSL"); + struct abis_rsl_dchan_hdr *dh; + + dh = (struct abis_rsl_dchan_hdr *) msgb_put(msg, sizeof(*dh)); + dh->c.msg_discr = ABIS_RSL_MDISC_DED_CHAN; + dh->c.msg_type = (act) ? RSL_MT_CHAN_ACTIV_ACK : RSL_MT_RF_CHAN_REL_ACK; + dh->ie_chan = RSL_IE_CHAN_NR; + dh->chan_nr = gsm_lchan2chan_nr(lchan); + + msg->dst = lchan->ts->trx->bts->c0->rsl_link; + msg->l2h = (unsigned char *)dh; + + abis_rsl_rcvmsg(msg); +} + +/* send handover complete */ +static void send_ho_complete(struct gsm_lchan *lchan, bool success) +{ + struct msgb *msg = msgb_alloc_headroom(256, 64, "RSL"); + struct abis_rsl_rll_hdr *rh; + uint8_t chan_nr = gsm_lchan2chan_nr(lchan); + uint8_t *buf; + struct gsm48_hdr *gh; + struct gsm48_ho_cpl *hc; + + rh = (struct abis_rsl_rll_hdr *) msgb_put(msg, sizeof(*rh)); + rh->c.msg_discr = ABIS_RSL_MDISC_RLL; + rh->c.msg_type = RSL_MT_DATA_IND; + rh->ie_chan = RSL_IE_CHAN_NR; + rh->chan_nr = chan_nr; + rh->ie_link_id = RSL_IE_LINK_IDENT; + rh->link_id = 0x00; + + buf = msgb_put(msg, 3); + buf[0] = RSL_IE_L3_INFO; + buf[1] = (sizeof(*gh) + sizeof(*hc)) >> 8; + buf[2] = (sizeof(*gh) + sizeof(*hc)) & 0xff; + + gh = (struct gsm48_hdr *) msgb_put(msg, sizeof(*gh)); + hc = (struct gsm48_ho_cpl *) msgb_put(msg, sizeof(*hc)); + + gh->proto_discr = GSM48_PDISC_RR; + gh->msg_type = + success ? GSM48_MT_RR_HANDO_COMPL : GSM48_MT_RR_HANDO_FAIL; + + msg->dst = lchan->ts->trx->bts->c0->rsl_link; + msg->l2h = (unsigned char *)rh; + msg->l3h = (unsigned char *)gh; + + abis_rsl_rcvmsg(msg); +} + +/* RSL messages from BSC */ +int abis_rsl_sendmsg(struct msgb *msg) +{ + struct abis_rsl_dchan_hdr *dh = (struct abis_rsl_dchan_hdr *) msg->data; + struct e1inp_sign_link *sign_link = msg->dst; + int rc; + struct gsm_lchan *lchan = rsl_lchan_lookup(sign_link->trx, dh->chan_nr, &rc); + + if (rc) { + printf("rsl_lchan_lookup() failed\n"); + exit(1); + } + + switch (dh->c.msg_type) { + case RSL_MT_CHAN_ACTIV: + rc = parse_chan_act(lchan, dh->data); + if (rc == 0) + got_chan_req = 1; + break; + case RSL_MT_RF_CHAN_REL: + rc = parse_chan_rel(lchan, dh->data); + if (rc == 0) + send_chan_act_ack(chan_req_lchan, 0); + break; + case RSL_MT_DATA_REQ: + rc = parse_ho_command(lchan, msg->l3h, msgb_l3len(msg)); + if (rc == 0) + got_ho_req = 1; + break; + case RSL_MT_IPAC_CRCX: + break; + default: + printf("unknown rsl message=0x%x\n", dh->c.msg_type); + } + return 0; +} + +/* test cases */ + +static char *test_case_0[] = { + "2", + + "Stay in better cell\n\n" + "There are many neighbor cells, but only the current cell is the best\n" + "cell, so no handover is performed\n", + + "create-bts", "7", + "create-ms", "0", "TCH/F", "AMR", + "meas-rep", "0", "30","0", + "6","0","20","1","21","2","18","3","20","4","23","5","19", + "expect-no-chan", + NULL +}; + +static char *test_case_1[] = { + "2", + + "Handover to best better cell\n\n" + "The best neighbor cell is selected\n", + + "create-bts", "7", + "create-ms", "0", "TCH/F", "AMR", + "meas-rep", "0", "10","0", + "6","0","20","1","21","2","18","3","20","4","23","5","19", + "expect-chan", "5", "1", + "ack-chan", + "expect-ho", "0", "1", + "ho-complete", + NULL +}; + +static char *test_case_2[] = { + "2", + + "Handover and Assignment must be enabled\n\n" + "This test will start with disabled assignment and handover. A\n" + "better neighbor cell (assignment enabled) will not be selected and \n" + "also no assignment from TCH/H to TCH/F to improve quality. There\n" + "will be no handover nor assignment. After enabling assignment on the\n" + "current cell, the MS will assign to TCH/F. After enabling handover\n" + "in the current cell, but disabling in the neighbor cell, handover\n" + "will not be performed, until it is enabled in the neighbor cell too.\n", + + "create-bts", "2", + "afs-rxlev-improve", "0", "5", + "create-ms", "0", "TCH/H", "AMR", + "as-enable", "0", "0", + "ho-enable", "0", "0", + "meas-rep", "0", "0","0", "1","0","30", + "expect-no-chan", + "as-enable", "0", "1", + "meas-rep", "0", "0","0", "1","0","30", + "expect-chan", "0", "1", + "ack-chan", + "expect-ho", "0", "5", + "ho-complete", + "ho-enable", "0", "1", + "ho-enable", "1", "0", + "meas-rep", "0", "0","0", "1","0","30", + "expect-no-chan", + "ho-enable", "1", "1", + "meas-rep", "0", "0","0", "1","0","30", + "expect-chan", "1", "1", + "ack-chan", + "expect-ho", "0", "1", + "ho-complete", + NULL +}; + +static char *test_case_3[] = { + "2", + + "Penalty timer must not run\n\n" + "The MS will try to handover to a better cell, but this will fail.\n" + "Even though the cell is still better, handover will not be performed\n" + "due to penalty timer after handover failure\n", + + "create-bts", "2", + "create-ms", "0", "TCH/F", "AMR", + "meas-rep", "0", "20","0", "1","0","30", + "expect-chan", "1", "1", + "ack-chan", + "expect-ho", "0", "1", + "ho-failed", + "meas-rep", "0", "20","0", "1","0","30", + "expect-no-chan", + NULL +}; + +static char *test_case_4[] = { + "2", + + "TCH/H keeping with HR codec\n\n" + "The MS is using half rate V1 codec, but the better cell is congested\n" + "at TCH/H slots. As the congestion is removed, the handover takes\n" + "place.\n", + + "create-bts", "2", + "set-min-free", "1", "TCH/H", "4", + "create-ms", "0", "TCH/H", "HR", + "meas-rep", "0", "20","0", "1","0","30", + "expect-no-chan", + "set-min-free", "1", "TCH/H", "3", + "meas-rep", "0", "20","0", "1","0","30", + "expect-chan", "1", "5", + "ack-chan", + "expect-ho", "0", "5", + "ho-complete", + NULL +}; + +static char *test_case_5[] = { + "2", + + "TCH/F keeping with FR codec\n\n" + "The MS is using full rate V1 codec, but the better cell is congested\n" + "at TCH/F slots. As the congestion is removed, the handover takes\n" + "place.\n", + + "create-bts", "2", + "set-min-free", "1", "TCH/F", "4", + "create-ms", "0", "TCH/F", "FR", + "meas-rep", "0", "20","0", "1","0","30", + "expect-no-chan", + "set-min-free", "1", "TCH/F", "3", + "meas-rep", "0", "20","0", "1","0","30", + "expect-chan", "1", "1", + "ack-chan", + "expect-ho", "0", "1", + "ho-complete", + NULL +}; + +static char *test_case_6[] = { + "2", + + "TCH/F keeping with EFR codec\n\n" + "The MS is using full rate V2 codec, but the better cell is congested\n" + "at TCH/F slots. As the congestion is removed, the handover takes\n" + "place.\n", + + "create-bts", "2", + "set-min-free", "1", "TCH/F", "4", + "create-ms", "0", "TCH/F", "EFR", + "meas-rep", "0", "20","0", "1","0","30", + "expect-no-chan", + "set-min-free", "1", "TCH/F", "3", + "meas-rep", "0", "20","0", "1","0","30", + "expect-chan", "1", "1", + "ack-chan", + "expect-ho", "0", "1", + "ho-complete", + NULL +}; + +static char *test_case_7[] = { + "2", + + "TCH/F to TCH/H changing with AMR codec\n\n" + "The MS is using AMR V3 codec, the better cell is congested at TCH/F\n" + "slots. The handover is performed to non-congested TCH/H slots.\n", + + "create-bts", "2", + "set-min-free", "1", "TCH/F", "4", + "create-ms", "0", "TCH/F", "AMR", + "meas-rep", "0", "20","0", "1","0","30", + "expect-chan", "1", "5", + "ack-chan", + "expect-ho", "0", "1", + "ho-complete", + NULL +}; + +static char *test_case_8[] = { + "2", + + "No handover to a cell with no slots available\n\n" + "If no slot is available, no handover is performed\n", + + "create-bts", "2", + "create-ms", "0", "TCH/F", "AMR", + "create-ms", "1", "TCH/F", "AMR", + "create-ms", "1", "TCH/F", "AMR", + "create-ms", "1", "TCH/F", "AMR", + "create-ms", "1", "TCH/F", "AMR", + "create-ms", "1", "TCH/H", "AMR", + "create-ms", "1", "TCH/H", "AMR", + "create-ms", "1", "TCH/H", "AMR", + "create-ms", "1", "TCH/H", "AMR", + "meas-rep", "0", "0","0", "1","0","30", + "expect-no-chan", + NULL +}; + +static char *test_case_9[] = { + "2", + + "No more parallel handovers, if max_unsync_ho is defined\n\n" + "There are tree mobiles that want to handover, but only two can do\n" + "it at a time, because the maximum number is limited to two.\n", + + "create-bts", "2", + "set-max-ho", "1", "2", + "create-ms", "0", "TCH/F", "AMR", + "create-ms", "0", "TCH/F", "AMR", + "create-ms", "0", "TCH/F", "AMR", + "meas-rep", "0", "0","0", "1","0","30", + "expect-chan", "1", "1", + "meas-rep", "1", "0","0", "1","0","30", + "expect-chan", "1", "2", + "meas-rep", "2", "0","0", "1","0","30", + "expect-no-chan", + NULL +}; + +static char *test_case_10[] = { + "2", + + "Hysteresis\n\n" + "If neighbor cell is better, handover is only performed if the\n" + "ammount of improvement is greater or equal hyteresis\n", + + "create-bts", "2", + "create-ms", "0", "TCH/F", "AMR", + "meas-rep", "0", "27","0", "1","0","30", + "expect-no-chan", + "meas-rep", "0", "26","0", "1","0","30", + "expect-chan", "1", "1", + "ack-chan", + "expect-ho", "0", "1", + "ho-complete", + NULL +}; + +static char *test_case_11[] = { + "2", + + "No Hysteresis and minimum RX level\n\n" + "If current cell's RX level is below mimium level, handover must be\n" + "performed, no matter of the hysteresis. First do not perform\n" + "handover to better neighbor cell, because the hysteresis is not\n" + "met. Second do not perform handover because better neighbor cell is\n" + "below minimum RX level. Third perform handover because current cell\n" + "is below minimum RX level, even if the better neighbor cell (minimum\n" + "RX level reached) does not meet the hysteresis.\n", + + "create-bts", "2", + "create-ms", "0", "TCH/F", "AMR", + "meas-rep", "0", "10","0", "1","0","11", + "expect-no-chan", + "meas-rep", "0", "8","0", "1","0","9", + "expect-no-chan", + "meas-rep", "0", "9","0", "1","0","10", + "expect-chan", "1", "1", + "ack-chan", + "expect-ho", "0", "1", + "ho-complete", + NULL +}; + +static char *test_case_12[] = { + "2", + + "No handover to congested cell\n\n" + "The better neighbor cell is congested, so no handover is performed.\n" + "After the congestion is over, handover will be performed.\n", + + "create-bts", "2", + "create-ms", "0", "TCH/F", "AMR", + "set-min-free", "1", "TCH/F", "4", + "set-min-free", "1", "TCH/H", "4", + "meas-rep", "0", "20","0", "1","0","30", + "expect-no-chan", + "set-min-free", "1", "TCH/F", "3", + "set-min-free", "1", "TCH/H", "3", + "meas-rep", "0", "20","0", "1","0","30", + "expect-chan", "1", "1", + "ack-chan", + "expect-ho", "0", "1", + "ho-complete", + NULL +}; + +static char *test_case_13[] = { + "2", + + "Handover to balance congestion\n\n" + "The current and the better cell are congested, so no handover is\n" + "performed. This is because handover would congest the neighbor cell\n" + "more. After congestion raises in the current cell, the handover is\n" + "performed to balance congestion\n", + + "create-bts", "2", + "create-ms", "0", "TCH/F", "AMR", + "set-min-free", "0", "TCH/F", "4", + "set-min-free", "0", "TCH/H", "4", + "set-min-free", "1", "TCH/F", "4", + "set-min-free", "1", "TCH/H", "4", + "meas-rep", "0", "20","0", "1","0","30", + "expect-no-chan", + "create-ms", "0", "TCH/F", "AMR", + "meas-rep", "0", "20","0", "1","0","30", + "expect-chan", "1", "1", + "ack-chan", + "expect-ho", "0", "1", + "ho-complete", + NULL +}; + +static char *test_case_14[] = { + "2", + + "Handover to congested cell, if RX level is below minimum\n\n" + "The better neighbor cell is congested, so no handover is performed.\n" + "If the RX level of the current cell drops below minimum acceptable\n" + "level, the handover is performed.\n", + + "create-bts", "2", + "create-ms", "0", "TCH/F", "AMR", + "set-min-free", "1", "TCH/F", "4", + "set-min-free", "1", "TCH/H", "4", + "meas-rep", "0", "10","0", "1","0","30", + "expect-no-chan", + "meas-rep", "0", "9","0", "1","0","30", + "expect-chan", "1", "1", + "ack-chan", + "expect-ho", "0", "1", + "ho-complete", + NULL +}; + +static char *test_case_15[] = { + "2", + + "Handover to cell with worse RXLEV, if RXQUAL is below minimum\n\n" + "The neighbor cell has worse RXLEV, so no handover is performed.\n" + "If the RXQUAL of the current cell drops below minimum acceptable\n" + "level, the handover is performed. It is also required that 10\n" + "reports are received, before RXQUAL is checked.\n", + /* (See also test 28, which tests for RXQUAL triggering HO to congested cell.) */ + /* TODO: bad RXQUAL may want to prefer assignment within the same cell to avoid interference. + * See Performence Enhancements in a Frequency Hopping GSM Network (Nielsen Wigard 2002), Chapter + * 2.1.1, "Interference" in the list of triggers on p.157. */ + + "create-bts", "2", + "create-ms", "0", "TCH/F", "AMR", + "meas-rep", "0", "40","6", "1","0","30", + "expect-no-chan", + "meas-rep", "0", "40","6", "1","0","30", + "expect-no-chan", + "meas-rep", "0", "40","6", "1","0","30", + "expect-no-chan", + "meas-rep", "0", "40","6", "1","0","30", + "expect-no-chan", + "meas-rep", "0", "40","6", "1","0","30", + "expect-no-chan", + "meas-rep", "0", "40","6", "1","0","30", + "expect-no-chan", + "meas-rep", "0", "40","6", "1","0","30", + "expect-no-chan", + "meas-rep", "0", "40","6", "1","0","30", + "expect-no-chan", + "meas-rep", "0", "40","6", "1","0","30", + "expect-no-chan", + "meas-rep", "0", "40","6", "1","0","30", + "expect-chan", "1", "1", + "ack-chan", + "expect-ho", "0", "1", + "ho-complete", + NULL +}; + +static char *test_case_16[] = { + "2", + + "Handover due to maximum TA exceeded\n\n" + "The MS in the current (best) cell has reached maximum allowed timing\n" + "advance. No handover is performed until the timing advance exceeds\n" + "it. The originating cell is still the best, but no handover is\n" + "performed back to that cell, because the penalty timer (due to\n" + "maximum allowed timing advance) is running.\n", + + "create-bts", "2", + "create-ms", "0", "TCH/F", "AMR", + "set-max-ta", "0", "5", /* of cell */ + "set-ta", "0", "5", /* of ms */ + "meas-rep", "0", "30","0", "1","0","20", + "expect-no-chan", + "set-ta", "0", "6", /* of ms */ + "meas-rep", "0", "30","0", "1","0","20", + "expect-chan", "1", "1", + "ack-chan", + "expect-ho", "0", "1", + "ho-complete", + "meas-rep", "0", "20","0", "1","0","30", + "expect-no-chan", + NULL +}; + +static char *test_case_17[] = { + "2", + + "Congestion check: No congestion\n\n" + "Three cells have different number of used slots, but there is no\n" + "congestion in any of these cells. No handover is performed.\n", + + "create-bts", "3", + "set-min-free", "0", "TCH/F", "2", + "set-min-free", "0", "TCH/H", "2", + "set-min-free", "1", "TCH/F", "2", + "set-min-free", "1", "TCH/H", "2", + "set-min-free", "2", "TCH/F", "2", + "set-min-free", "2", "TCH/H", "2", + "create-ms", "0", "TCH/F", "AMR", + "create-ms", "0", "TCH/F", "AMR", + "create-ms", "0", "TCH/H", "AMR", + "create-ms", "0", "TCH/H", "AMR", + "create-ms", "1", "TCH/F", "AMR", + "create-ms", "1", "TCH/H", "AMR", + "meas-rep", "0", "30","0", "2","0","20","1","20", + "expect-no-chan", + "meas-rep", "1", "30","0", "2","0","20","1","20", + "expect-no-chan", + "meas-rep", "2", "30","0", "2","0","20","1","20", + "expect-no-chan", + "meas-rep", "3", "30","0", "2","0","20","1","20", + "expect-no-chan", + "meas-rep", "4", "30","0", "2","0","20","1","20", + "expect-no-chan", + "meas-rep", "5", "30","0", "2","0","20","1","20", + "expect-no-chan", + "congestion-check", + "expect-no-chan", + NULL +}; + +static char *test_case_18[] = { + "2", + + "Congestion check: One out of three cells is congested\n\n" + "Three cells have different number of used slots, but there is\n" + "congestion at TCH/F in the first cell. Handover is performed with\n" + "the best candidate.\n", + + "create-bts", "3", + "set-min-free", "0", "TCH/F", "2", + "set-min-free", "0", "TCH/H", "2", + "set-min-free", "1", "TCH/F", "2", + "set-min-free", "1", "TCH/H", "2", + "set-min-free", "2", "TCH/F", "2", + "set-min-free", "2", "TCH/H", "2", + "create-ms", "0", "TCH/F", "AMR", + "create-ms", "0", "TCH/F", "AMR", + "create-ms", "0", "TCH/F", "AMR", + "create-ms", "0", "TCH/H", "AMR", + "create-ms", "0", "TCH/H", "AMR", + "create-ms", "1", "TCH/F", "AMR", + "create-ms", "1", "TCH/H", "AMR", + "meas-rep", "0", "30","0", "2","0","20","1","20", + "expect-no-chan", + "meas-rep", "1", "30","0", "2","0","20","1","20", + "expect-no-chan", + "meas-rep", "2", "30","0", "2","0","21","1","20", + "expect-no-chan", + "meas-rep", "3", "30","0", "2","0","20","1","20", + "expect-no-chan", + "meas-rep", "4", "30","0", "2","0","20","1","20", + "expect-no-chan", + "meas-rep", "5", "30","0", "2","0","20","1","20", + "expect-no-chan", + "meas-rep", "6", "30","0", "2","0","20","1","20", + "expect-no-chan", + "congestion-check", + "expect-chan", "1", "2", + "ack-chan", + "expect-ho", "0", "3", /* best candidate is MS 2 at BTS 1, TS 3 */ + "ho-complete", + NULL +}; + +static char *test_case_19[] = { + "2", + + "Congestion check: Balancing over congested cells\n\n" + "Two cells are congested, but the second cell is more congested.\n" + "Handover is performed to solve the congestion.\n", + + "create-bts", "2", + "set-min-free", "0", "TCH/F", "4", + "set-min-free", "1", "TCH/F", "4", + "create-ms", "0", "TCH/F", "FR", + "create-ms", "0", "TCH/F", "FR", + "create-ms", "0", "TCH/F", "FR", + "create-ms", "1", "TCH/F", "FR", + "meas-rep", "0", "30","0", "1","0","20", + "expect-no-chan", + "meas-rep", "1", "30","0", "1","0","21", + "expect-no-chan", + "meas-rep", "2", "30","0", "1","0","20", + "expect-no-chan", + "meas-rep", "3", "30","0", "1","0","20", + "expect-no-chan", + "congestion-check", + "expect-chan", "1", "2", + "ack-chan", + "expect-ho", "0", "2", /* best candidate is MS 1 at BTS 0, TS 2 */ + "ho-complete", + NULL +}; + +static char *test_case_20[] = { + "2", + + "Congestion check: Solving congestion by handover TCH/F -> TCH/H\n\n" + "Two BTS, one MS in the first congested BTS must handover to\n" + "non-congested TCH/H of second BTS, in order to solve congestion\n", + "create-bts", "2", + "set-min-free", "0", "TCH/F", "4", + "set-min-free", "0", "TCH/H", "4", + "set-min-free", "1", "TCH/F", "4", + "create-ms", "0", "TCH/F", "AMR", + "meas-rep", "0", "30","0", "1","0","30", + "expect-no-chan", + "congestion-check", + "expect-chan", "1", "5", + "ack-chan", + "expect-ho", "0", "1", + "ho-complete", + NULL +}; + +static char *test_case_21[] = { + "2", + + "Congestion check: Balancing congestion by handover TCH/F -> TCH/H\n\n" + "Two BTS, one MS in the first congested BTS must handover to\n" + "less-congested TCH/H of second BTS, in order to balance congestion\n", + "create-bts", "2", + "set-min-free", "0", "TCH/F", "4", + "set-min-free", "0", "TCH/H", "4", + "set-min-free", "1", "TCH/F", "4", + "set-min-free", "1", "TCH/H", "4", + "create-ms", "0", "TCH/F", "AMR", + "create-ms", "0", "TCH/F", "AMR", + "create-ms", "0", "TCH/H", "AMR", + "meas-rep", "0", "30","0", "1","0","30", + "expect-no-chan", + "congestion-check", + "expect-chan", "1", "1", + "ack-chan", + "expect-ho", "0", "1", + "ho-complete", + NULL +}; + +static char *test_case_22[] = { + "2", + + "Congestion check: Upgrading worst candidate from TCH/H -> TCH/F\n\n" + "There is only one BTS. The TCH/H slots are congested. Since\n" + "assignment is performed to less-congested TCH/F, the candidate with\n" + "the worst RX level is chosen.\n", + + "create-bts", "1", + "set-min-free", "0", "TCH/F", "4", + "set-min-free", "0", "TCH/H", "4", + "create-ms", "0", "TCH/H", "AMR", + "create-ms", "0", "TCH/H", "AMR", + "create-ms", "0", "TCH/H", "AMR", + "meas-rep", "0", "30","0", "0", + "meas-rep", "1", "34","0", "0", + "meas-rep", "2", "20","0", "0", + "expect-no-chan", + "congestion-check", + "expect-chan", "0", "1", + "ack-chan", + "expect-ho", "0", "6", + "ho-complete", + NULL +}; + +static char *test_case_23[] = { + "2", + + "Story: 'A neighbor is your friend'\n", + + "create-bts", "3", + + "print", + "Andreas is driving along the coast, on a sunny june afternoon.\n" + "Suddenly he is getting a call from his friend and neighbor Axel.\n" + "\n" + "What happens: Two MS are created, #0 for Axel, #1 for Andreas.", + /* Axel */ + "create-ms", "2", "TCH/F", "AMR", + /* andreas */ + "create-ms", "0", "TCH/F", "AMR", + "meas-rep", "1", "40","0", "1","0","30", + "expect-no-chan", + + "print", + "Axel asks Andreas if he would like to join them for a barbecue.\n" + "Axel's house is right in the neighborhood and the weather is fine.\n" + "Andreas agrees, so he drives to a close store to buy some barbecue\n" + "skewers.\n" + "\n" + "What happens: While driving, a different cell (mounted atop the\n" + "store) becomes better.", + /* drive to bts 1 */ + "meas-rep", "1", "20","0", "1","0","35", + "expect-chan", "1", "1", + "ack-chan", + "expect-ho", "0", "1", + "ho-complete", + + "print", + "While Andreas is walking into the store, Axel asks, if he could also\n" + "bring some beer. Andreas has problems understanding him: \"I have a\n" + "bad reception here. The cell tower is right atop the store, but poor\n" + "coverage inside. Can you repeat please?\"\n" + "\n" + "What happens: Inside the store the close cell is so bad, that\n" + "handover back to the previous cell is required.", + /* bts 1 becomes bad, so bts 0 helps out */ + "meas-rep", "1", "5","0", "1","0","20", + "expect-chan", "0", "1", + "ack-chan", + "expect-ho", "1", "1", + "ho-complete", + + "print", + "After Andreas bought skewers and beer, he leaves the store.\n" + "\n" + "What happens: Outside the store the close cell is better again, so\n" + "handover back to the that cell is performed.", + /* bts 1 becomes better again */ + "meas-rep", "1", "20","0", "1","0","35", + "expect-chan", "1", "1", + "ack-chan", + "expect-ho", "0", "1", + "ho-complete", + + "print", + /* bts 2 becomes better */ + "Andreas drives down to the lake where Axel's house is.\n" + "\n" + "What happens: There is a small cell at Axel's house, which becomes\n" + "better, because the current cell has no good comverage at the lake.", + "meas-rep", "1", "14","0", "2","0","2","1","63", + "expect-chan", "2", "2", + "ack-chan", + "expect-ho", "1", "1", + "ho-complete", + + "print", + "Andreas wonders why he still has good radio coverage: \"Last time it\n" + "was so bad\". Axel says: \"I installed a pico cell in my house,\n" + "now we can use our mobile phones down here at the lake.\"", + + NULL +}; + +static char *test_case_24[] = { + "2", + "No (or not enough) measurements for handover\n\n" + "Do not solve congestion in cell, because there is no measurement.\n" + "As soon as enough measurments available (1 in our case), perform\n" + "handover. Afterwards the old cell becomes congested and the new\n" + "cell is not. Do not perform handover until new measurements are\n" + "received.\n", + + /* two cells, first in congested, but no handover */ + "create-bts", "2", + "set-min-free", "0", "TCH/F", "4", + "set-min-free", "0", "TCH/H", "4", + "create-ms", "0", "TCH/F", "AMR", + "congestion-check", + "expect-no-chan", + + /* send measurement and trigger congestion check */ + "meas-rep", "0", "20","0", "1","0","20", + "expect-no-chan", + "congestion-check", + "expect-chan", "1", "1", + "ack-chan", + "expect-ho", "0", "1", + "ho-complete", + + /* congest the first cell and remove congestion from second cell */ + "set-min-free", "0", "TCH/F", "0", + "set-min-free", "0", "TCH/H", "0", + "set-min-free", "1", "TCH/F", "4", + "set-min-free", "1", "TCH/H", "4", + + /* no handover until measurements applied */ + "congestion-check", + "expect-no-chan", + "meas-rep", "0", "20","0", "1","0","20", + "expect-no-chan", + "congestion-check", + "expect-chan", "0", "1", + "ack-chan", + "expect-ho", "1", "1", + "ho-complete", + NULL +}; + +static char *test_case_25[] = { + "1", + + "Stay in better cell\n\n" + "There are many neighbor cells, but only the current cell is the best\n" + "cell, so no handover is performed\n", + + "create-bts", "7", + "create-ms", "0", "TCH/F", "AMR", + "meas-rep", "0", "30","0", + "6","0","20","1","21","2","18","3","20","4","23","5","19", + "expect-no-chan", + NULL +}; + +static char *test_case_26[] = { + "1", + + "Handover to best better cell\n\n" + "The best neighbor cell is selected\n", + + "create-bts", "7", + "create-ms", "0", "TCH/F", "AMR", + "meas-rep", "0", "10","0", + "6","0","20","1","21","2","18","3","20","4","23","5","19", + "expect-chan", "5", "1", + "ack-chan", + "expect-ho", "0", "1", + "ho-complete", + NULL +}; + +static char *test_case_27[] = { + "2", + + "Congestion check: Upgrading worst candidate from TCH/H -> TCH/F\n\n" + "There is only one BTS. The TCH/H slots are congested. Since\n" + "assignment is performed to less-congested TCH/F, the candidate with\n" + "the worst RX level is chosen. (So far like test 22.)\n" + "After that, trigger more congestion checks to ensure stability.\n", + + "create-bts", "1", + "set-min-free", "0", "TCH/F", "2", + "set-min-free", "0", "TCH/H", "4", + "create-ms", "0", "TCH/H", "AMR", + "create-ms", "0", "TCH/H", "AMR", + "create-ms", "0", "TCH/H", "AMR", + "meas-rep", "0", "30","0", "0", + "meas-rep", "1", "34","0", "0", + "meas-rep", "2", "20","0", "0", + "expect-no-chan", + "congestion-check", + "expect-chan", "0", "1", + "ack-chan", + "expect-ho", "0", "6", + "ho-complete", + "congestion-check", + "expect-chan", "0", "2", + "ack-chan", + "expect-ho", "0", "5", + "ho-complete", + "congestion-check", + "expect-no-chan", + "congestion-check", + "expect-no-chan", + NULL +}; + +static char *test_case_28[] = { + "2", + + "Handover to congested cell, if RX quality is below minimum\n\n" + "The better neighbor cell is congested, so no handover is performed.\n" + "If the RX quality of the current cell drops below minimum acceptable\n" + "level, the handover is performed. It is also required that 10\n" + "resports are received, before RX quality is checked.\n", + + "create-bts", "2", + "create-ms", "0", "TCH/F", "AMR", + "set-min-free", "1", "TCH/F", "4", + "set-min-free", "1", "TCH/H", "4", + "meas-rep", "0", "30","6", "1","0","40", + "expect-no-chan", + "meas-rep", "0", "30","6", "1","0","40", + "expect-no-chan", + "meas-rep", "0", "30","6", "1","0","40", + "expect-no-chan", + "meas-rep", "0", "30","6", "1","0","40", + "expect-no-chan", + "meas-rep", "0", "30","6", "1","0","40", + "expect-no-chan", + "meas-rep", "0", "30","6", "1","0","40", + "expect-no-chan", + "meas-rep", "0", "30","6", "1","0","40", + "expect-no-chan", + "meas-rep", "0", "30","6", "1","0","40", + "expect-no-chan", + "meas-rep", "0", "30","6", "1","0","40", + "expect-no-chan", + "meas-rep", "0", "30","6", "1","0","40", + "expect-chan", "1", "1", + "ack-chan", + "expect-ho", "0", "1", + "ho-complete", + NULL +}; + +static char **test_cases[] = { + test_case_0, + test_case_1, + test_case_2, + test_case_3, + test_case_4, + test_case_5, + test_case_6, + test_case_7, + test_case_8, + test_case_9, + test_case_10, + test_case_11, + test_case_12, + test_case_13, + test_case_14, + test_case_15, + test_case_16, + test_case_17, + test_case_18, + test_case_19, + test_case_20, + test_case_21, + test_case_22, + test_case_23, + test_case_24, + test_case_25, + test_case_26, + test_case_27, + test_case_28, + NULL +}; + +int main(int argc, char **argv) +{ + char **test_case; + struct gsm_bts *bts[256]; + int bts_num = 0; + struct gsm_lchan *lchan[256]; + int lchan_num = 0; + int test_count = 0; + int i; + int algorithm; + struct bsc_api bsc_api = {}; + + for (i = 0; test_cases[i]; i++) + test_count++; + + if (argc <= 1 || atoi(argv[1]) >= test_count) { + for (i = 0; test_cases[i]; i++) { + printf("Test #%d (algorithm %s):\n%s\n", i, + test_cases[i][0], test_cases[i][1]); + } + printf("\nPlease specify test case number 0..%d\n", + test_count - 1); + return EXIT_FAILURE; + } + + osmo_init_logging(&log_info); + + log_set_print_category(osmo_stderr_target, 1); + + log_set_category_filter(osmo_stderr_target, DHO, 1, LOGL_DEBUG); + log_set_category_filter(osmo_stderr_target, DHODEC, 1, LOGL_DEBUG); + log_set_category_filter(osmo_stderr_target, DMEAS, 1, LOGL_DEBUG); + log_set_category_filter(osmo_stderr_target, DREF, 1, LOGL_DEBUG); + log_set_category_filter(osmo_stderr_target, DRSL, 1, LOGL_DEBUG); + + /* Create a dummy network */ + bsc_gsmnet = bsc_network_init(NULL, 1, 1); + if (!bsc_gsmnet) + exit(1); + + bsc_api_init(bsc_gsmnet, &bsc_api); + + ho_set_algorithm(bsc_gsmnet->ho, 2); + ho_set_ho_active(bsc_gsmnet->ho, true); + ho_set_as_active(bsc_gsmnet->ho, true); + ho_set_min_rxlev(bsc_gsmnet->ho, -100); + ho_set_rxlev_avg_win(bsc_gsmnet->ho, 1); + ho_set_rxlev_neigh_avg_win(bsc_gsmnet->ho, 1); + ho_set_rxqual_avg_win(bsc_gsmnet->ho, 10); + ho_set_pwr_hysteresis(bsc_gsmnet->ho, 3); + ho_set_pwr_interval(bsc_gsmnet->ho, 1); + ho_set_afs_bias_rxlev(bsc_gsmnet->ho, 0); + ho_set_min_rxqual(bsc_gsmnet->ho, 5); + ho_set_afs_bias_rxqual(bsc_gsmnet->ho, 0); + ho_set_max_distance(bsc_gsmnet->ho, 9999); + ho_set_ho_max(bsc_gsmnet->ho, 9999); + ho_set_penalty_max_dist(bsc_gsmnet->ho, 300); + ho_set_penalty_failed_ho(bsc_gsmnet->ho, 60); + ho_set_penalty_failed_as(bsc_gsmnet->ho, 60); + + bts_model_sysmobts_init(); + + test_case = test_cases[atoi(argv[1])]; + + fprintf(stderr, "--------------------\n"); + fprintf(stderr, "Performing the following test %d (algorithm %s):\n%s", + atoi(argv[1]), test_case[0], test_case[1]); + algorithm = atoi(test_case[0]); + test_case += 2; + fprintf(stderr, "--------------------\n"); + + /* Disable the congestion check timer, we will trigger manually. */ + bsc_gsmnet->ho2.congestion_check_interval_s = 0; + + handover_decision_1_init(); + hodec2_init(bsc_gsmnet); + + while (*test_case) { + if (!strcmp(*test_case, "create-bts")) { + static int arfcn = 870; + int n = atoi(test_case[1]); + fprintf(stderr, "- Creating %d BTS (one TRX each, " + "TS(1-4) are TCH/F, TS(5-6) are TCH/H)\n", n); + for (i = 0; i < n; i++) + bts[bts_num + i] = create_bts(arfcn++); + for (i = 0; i < n; i++) + gsm_generate_si(bts[bts_num + i], + SYSINFO_TYPE_2); + bts_num += n; + test_case += 2; + } else + if (!strcmp(*test_case, "as-enable")) { + fprintf(stderr, "- Set assignment enable state at " + "BTS %s to %s\n", test_case[1], test_case[2]); + ho_set_as_active(bts[atoi(test_case[1])]->ho, atoi(test_case[2])); + test_case += 3; + } else + if (!strcmp(*test_case, "ho-enable")) { + fprintf(stderr, "- Set handover enable state at " + "BTS %s to %s\n", test_case[1], test_case[2]); + ho_set_ho_active(bts[atoi(test_case[1])]->ho, atoi(test_case[2])); + test_case += 3; + } else + if (!strcmp(*test_case, "afs-rxlev-improve")) { + fprintf(stderr, "- Set afs RX level improvement at " + "BTS %s to %s\n", test_case[1], test_case[2]); + ho_set_afs_bias_rxlev(bts[atoi(test_case[1])]->ho, atoi(test_case[2])); + test_case += 3; + } else + if (!strcmp(*test_case, "afs-rxqual-improve")) { + fprintf(stderr, "- Set afs RX quality improvement at " + "BTS %s to %s\n", test_case[1], test_case[2]); + ho_set_afs_bias_rxqual(bts[atoi(test_case[1])]->ho, atoi(test_case[2])); + test_case += 3; + } else + if (!strcmp(*test_case, "set-min-free")) { + fprintf(stderr, "- Setting minimum required free %s " + "slots at BTS %s to %s\n", test_case[2], + test_case[1], test_case[3]); + if (!strcmp(test_case[2], "TCH/F")) + ho_set_tchf_min_slots(bts[atoi(test_case[1])]->ho, atoi(test_case[3])); + else + ho_set_tchh_min_slots(bts[atoi(test_case[1])]->ho, atoi(test_case[3])); + test_case += 4; + } else + if (!strcmp(*test_case, "set-max-ho")) { + fprintf(stderr, "- Setting maximum parallel handovers " + "at BTS %s to %s\n", test_case[1], + test_case[2]); + ho_set_ho_max( bts[atoi(test_case[1])]->ho, atoi(test_case[2])); + test_case += 3; + } else + if (!strcmp(*test_case, "set-max-ta")) { + fprintf(stderr, "- Setting maximum timing advance " + "at BTS %s to %s\n", test_case[1], + test_case[2]); + ho_set_max_distance(bts[atoi(test_case[1])]->ho, atoi(test_case[2])); + test_case += 3; + } else + if (!strcmp(*test_case, "create-ms")) { + fprintf(stderr, "- Creating mobile #%d at BTS %s on " + "%s with %s codec\n", lchan_num, test_case[1], + test_case[2], test_case[3]); + lchan[lchan_num] = create_lchan(bts[atoi(test_case[1])], + !strcmp(test_case[2], "TCH/F"), test_case[3]); + if (!lchan[lchan_num]) { + printf("Failed to create lchan!\n"); + return EXIT_FAILURE; + } + fprintf(stderr, " * New MS is at BTS %d TS %d\n", + lchan[lchan_num]->ts->trx->bts->nr, + lchan[lchan_num]->ts->nr); + lchan_num++; + test_case += 4; + } else + if (!strcmp(*test_case, "set-ta")) { + fprintf(stderr, "- Setting maximum timing advance " + "at MS %s to %s\n", test_case[1], + test_case[2]); + meas_ta_ms = atoi(test_case[2]); + test_case += 3; + } else + if (!strcmp(*test_case, "meas-rep")) { + /* meas-rep <lchan-nr> <rxlev> <rxqual> <nr-of-neighbors> [<cell-idx> <rxlev> [...]] */ + int n = atoi(test_case[4]); + struct gsm_lchan *lc = lchan[atoi(test_case[1])]; + fprintf(stderr, "- Sending measurement report from " + "mobile #%s (rxlev=%s, rxqual=%s)\n", + test_case[1], test_case[2], test_case[3]); + meas_dl_rxlev = atoi(test_case[2]); + meas_dl_rxqual = atoi(test_case[3]); + meas_num_nc = n; + test_case += 5; + for (i = 0; i < n; i++) { + int nr = atoi(test_case[0]); + /* since our bts is not in the list of neighbor + * cells, we need to shift */ + if (nr >= lc->ts->trx->bts->nr) + nr++; + fprintf(stderr, " * Neighbor cell #%s, actual " + "BTS %d (rxlev=%s)\n", test_case[0], nr, + test_case[1]); + meas_bcch_f_nc[i] = atoi(test_case[0]); + /* bts number, not counting our own */ + meas_rxlev_nc[i] = atoi(test_case[1]); + meas_bsic_nc[i] = 0x3f; + test_case += 2; + } + got_chan_req = 0; + gen_meas_rep(lc); + } else + if (!strcmp(*test_case, "congestion-check")) { + fprintf(stderr, "- Triggering congestion check\n"); + got_chan_req = 0; + if (algorithm == 2) + hodec2_congestion_check(bsc_gsmnet); + test_case += 1; + } else + if (!strcmp(*test_case, "expect-chan")) { + fprintf(stderr, "- Expecting channel request at BTS %s " + "TS %s\n", test_case[1], test_case[2]); + if (!got_chan_req) { + printf("Test failed, because no channel was " + "requested\n"); + return EXIT_FAILURE; + } + fprintf(stderr, " * Got channel request at BTS %d " + "TS %d\n", chan_req_lchan->ts->trx->bts->nr, + chan_req_lchan->ts->nr); + if (chan_req_lchan->ts->trx->bts->nr + != atoi(test_case[1])) { + printf("Test failed, because channel was not " + "requested on expected BTS\n"); + return EXIT_FAILURE; + } + if (chan_req_lchan->ts->nr != atoi(test_case[2])) { + printf("Test failed, because channel was not " + "requested on expected TS\n"); + return EXIT_FAILURE; + } + test_case += 3; + } else + if (!strcmp(*test_case, "expect-no-chan")) { + fprintf(stderr, "- Expecting no channel request\n"); + if (got_chan_req) { + fprintf(stderr, " * Got channel request at " + "BTS %d TS %d\n", + chan_req_lchan->ts->trx->bts->nr, + chan_req_lchan->ts->nr); + printf("Test failed, because channel was " + "requested\n"); + return EXIT_FAILURE; + } + fprintf(stderr, " * Got no channel request\n"); + test_case += 1; + } else + if (!strcmp(*test_case, "expect-ho")) { + fprintf(stderr, "- Expecting handover/assignment " + "request at BTS %s TS %s\n", test_case[1], + test_case[2]); + if (!got_ho_req) { + printf("Test failed, because no handover was " + "requested\n"); + return EXIT_FAILURE; + } + fprintf(stderr, " * Got handover/assignment request at " + "BTS %d TS %d\n", + ho_req_lchan->ts->trx->bts->nr, + ho_req_lchan->ts->nr); + if (ho_req_lchan->ts->trx->bts->nr + != atoi(test_case[1])) { + printf("Test failed, because " + "handover/assignment was not commanded " + "at the expected BTS\n"); + return EXIT_FAILURE; + } + if (ho_req_lchan->ts->nr != atoi(test_case[2])) { + printf("Test failed, because " + "handover/assignment was not commanded " + "at the expected TS\n"); + return EXIT_FAILURE; + } + test_case += 3; + } else + if (!strcmp(*test_case, "ack-chan")) { + fprintf(stderr, "- Acknowledging channel request\n"); + if (!got_chan_req) { + printf("Cannot ack channel, because no " + "request\n"); + return EXIT_FAILURE; + } + test_case += 1; + got_ho_req = 0; + send_chan_act_ack(chan_req_lchan, 1); + } else + if (!strcmp(*test_case, "ho-complete")) { + fprintf(stderr, "- Acknowledging handover/assignment " + "request\n"); + if (!got_chan_req) { + printf("Cannot ack handover/assignment, " + "because no chan request\n"); + return EXIT_FAILURE; + } + if (!got_ho_req) { + printf("Cannot ack handover/assignment, " + "because no ho request\n"); + return EXIT_FAILURE; + } + test_case += 1; + got_chan_req = 0; + got_ho_req = 0; + /* switch lchan */ + for (i = 0; i < lchan_num; i++) { + if (lchan[i] == ho_req_lchan) { + fprintf(stderr, " * MS %d changes from " + "BTS=%d TS=%d to BTS=%d " + "TS=%d\n", i, + lchan[i]->ts->trx->bts->nr, + lchan[i]->ts->nr, + chan_req_lchan->ts->trx->bts->nr, + chan_req_lchan->ts->nr); + lchan[i] = chan_req_lchan; + } + } + send_ho_complete(chan_req_lchan, true); + } else + if (!strcmp(*test_case, "ho-failed")) { + fprintf(stderr, "- Making handover fail\n"); + if (!got_chan_req) { + printf("Cannot fail handover, because no chan " + "request\n"); + return EXIT_FAILURE; + } + test_case += 1; + got_chan_req = 0; + got_ho_req = 0; + send_ho_complete(ho_req_lchan, false); + } else + if (!strcmp(*test_case, "print")) { + fprintf(stderr, "\n%s\n\n", test_case[1]); + test_case += 2; + } else { + printf("Unknown test command '%s', please fix!\n", + *test_case); + return EXIT_FAILURE; + } + } + + for (i = 0; i < lchan_num; i++) { + struct gsm_subscriber_connection *conn = lchan[i]->conn; + lchan[i]->conn = NULL; + conn->lchan = NULL; + bsc_subscr_con_free(conn); + lchan_free(lchan[i]); + } + + fprintf(stderr, "--------------------\n"); + + printf("Test OK\n"); + + fprintf(stderr, "--------------------\n"); + + return EXIT_SUCCESS; +} + +void rtp_socket_free() {} +void rtp_send_frame() {} +void rtp_socket_upstream() {} +void rtp_socket_create() {} +void rtp_socket_connect() {} +void rtp_socket_proxy() {} +void trau_mux_unmap() {} +void trau_mux_map_lchan() {} +void trau_recv_lchan() {} +void trau_send_frame() {} diff --git a/tests/handover/handover_test.ok b/tests/handover/handover_test.ok new file mode 100644 index 0000000..678f9a3 --- /dev/null +++ b/tests/handover/handover_test.ok @@ -0,0 +1 @@ +Test OK diff --git a/tests/testsuite.at b/tests/testsuite.at index 6ef3f29..f0f6fd1 100644 --- a/tests/testsuite.at +++ b/tests/testsuite.at @@ -51,3 +51,177 @@ cat $abs_srcdir/bssap/bssap_test.err > experr AT_CHECK([$abs_top_builddir/tests/bssap/bssap_test], [], [expout], [experr]) AT_CLEANUP + +AT_SETUP([handover test 0]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 0], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 1]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 1], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 2]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 2], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 3]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 3], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 4]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 4], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 5]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 5], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 6]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 6], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 7]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 7], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 8]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 8], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 9]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 9], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 10]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 10], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 11]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 11], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 12]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 12], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 13]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 13], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 14]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 14], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 15]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 15], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 16]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 16], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 17]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 17], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 18]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 18], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 19]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 19], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 20]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 20], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 21]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 21], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 22]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 22], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 23]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 23], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 24]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 24], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 25]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 25], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 26]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 26], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 27]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 27], [], [expout], [ignore]) +AT_CLEANUP + +AT_SETUP([handover test 28]) +AT_KEYWORDS([handover]) +cat $abs_srcdir/handover/handover_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/handover/handover_test 28], [], [expout], [ignore]) +AT_CLEANUP -- To view, visit https://gerrit.osmocom.org/5921 To unsubscribe, visit https://gerrit.osmocom.org/settings Gerrit-MessageType: newpatchset Gerrit-Change-Id: Ice2d3ef5668564a9d3bc4d5118d59dfaa9af6978 Gerrit-PatchSet: 3 Gerrit-Project: osmo-bsc Gerrit-Branch: master Gerrit-Owner: Neels Hofmeyr <nhofm...@sysmocom.de> Gerrit-Reviewer: Jenkins Builder