Hello Harald Welte, Jenkins Builder,

I'd like you to reexamine a change.  Please visit

    https://gerrit.osmocom.org/6499

to look at the new patch set (#4).

HO: Implement load based handover, as handover_decision_2.c

Change-Id: Ie597eae82722baf32546331e443dd9d94f1f25e6
---
M configure.ac
M doc/examples/osmo-bsc/osmo-bsc.cfg
M doc/examples/osmo-bsc/osmo-bsc_custom-sccp.cfg
M include/osmocom/bsc/Makefile.am
M include/osmocom/bsc/gsm_data.h
M include/osmocom/bsc/handover_cfg.h
A include/osmocom/bsc/handover_decision_2.h
M src/libbsc/Makefile.am
A src/libbsc/handover_decision_2.c
M src/libbsc/handover_vty.c
M src/libbsc/net_init.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/handover_cfg.vty
M tests/testsuite.at
18 files changed, 3,725 insertions(+), 2 deletions(-)


  git pull ssh://gerrit.osmocom.org:29418/osmo-bsc refs/changes/99/6499/4

diff --git a/configure.ac b/configure.ac
index dac222f..e95a16d 100644
--- a/configure.ac
+++ b/configure.ac
@@ -164,6 +164,7 @@
     tests/subscr/Makefile
     tests/nanobts_omlattr/Makefile
     tests/bssap/Makefile
+    tests/handover/Makefile
     doc/Makefile
     doc/examples/Makefile
     Makefile)
diff --git a/doc/examples/osmo-bsc/osmo-bsc.cfg 
b/doc/examples/osmo-bsc/osmo-bsc.cfg
index 17412f7..59732af 100644
--- a/doc/examples/osmo-bsc/osmo-bsc.cfg
+++ b/doc/examples/osmo-bsc/osmo-bsc.cfg
@@ -10,6 +10,7 @@
  neci 0
  paging any use tch 0
  handover 0
+ handover algorithm 1
  handover window rxlev averaging 10
  handover window rxqual averaging 1
  handover window rxlev neighbor averaging 10
diff --git a/doc/examples/osmo-bsc/osmo-bsc_custom-sccp.cfg 
b/doc/examples/osmo-bsc/osmo-bsc_custom-sccp.cfg
index 581d541..aa2c99f 100644
--- a/doc/examples/osmo-bsc/osmo-bsc_custom-sccp.cfg
+++ b/doc/examples/osmo-bsc/osmo-bsc_custom-sccp.cfg
@@ -9,6 +9,7 @@
  neci 0
  paging any use tch 0
  handover 0
+ handover algorithm 1
  handover window rxlev averaging 10
  handover window rxqual averaging 1
  handover window rxlev neighbor averaging 10
diff --git a/include/osmocom/bsc/Makefile.am b/include/osmocom/bsc/Makefile.am
index 9247119..a5d7d18 100644
--- a/include/osmocom/bsc/Makefile.am
+++ b/include/osmocom/bsc/Makefile.am
@@ -24,6 +24,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 bf87595..07e5478 100644
--- a/include/osmocom/bsc/gsm_data.h
+++ b/include/osmocom/bsc/gsm_data.h
@@ -116,6 +116,7 @@
        unsigned int ho_dtap_cache_len;
 
        struct {
+               int failures;
                struct penalty_timers *penalty_timers;
        } hodec2;
 
@@ -1197,7 +1198,12 @@
        /* bit-mask of permitted encryption algorithms. LSB=A5/0, MSB=A5/7 */
        uint8_t a5_encryption_mask;
        int neci;
+
        struct handover_cfg *ho;
+       struct {
+               unsigned int congestion_check_interval_s;
+               struct osmo_timer_list congestion_check_timer;
+       } hodec2;
 
        struct rate_ctr_group *bsc_ctrs;
 
diff --git a/include/osmocom/bsc/handover_cfg.h 
b/include/osmocom/bsc/handover_cfg.h
index 55b9dbc..024bc97 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_HANDOVER1 "Handover options for handover decision algorithm 
1\n"
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/src/libbsc/Makefile.am b/src/libbsc/Makefile.am
index 81b7a66..bb14227 100644
--- a/src/libbsc/Makefile.am
+++ b/src/libbsc/Makefile.am
@@ -60,5 +60,6 @@
        handover_vty.c \
        handover_cfg.c \
        penalty_timers.c \
+       handover_decision_2.c \
        $(NULL)
 
diff --git a/src/libbsc/handover_decision_2.c b/src/libbsc/handover_decision_2.c
new file mode 100644
index 0000000..752b0d5
--- /dev/null
+++ b/src/libbsc/handover_decision_2.c
@@ -0,0 +1,1824 @@
+/* Handover Decision Algorithm 2 for intra-BSC (inter-BTS) handover, public 
API for OsmoBSC. */
+
+/* (C) 2009 by Andreas Eversberg <jo...@eversberg.eu>
+ * (C) 2017-2018 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>
+#include <osmocom/bsc/penalty_timers.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 hodec2_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 (!hodec2_initialized)
+               return;
+
+       was_active = net->hodec2.congestion_check_timer.active;
+       if (was_active)
+               osmo_timer_del(&net->hodec2.congestion_check_timer);
+
+       congestion_check_interval_s = net->hodec2.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->hodec2.congestion_check_timer,
+                        congestion_check_cb, net);
+       osmo_timer_schedule(&net->hodec2.congestion_check_timer,
+                           congestion_check_interval_s, 0);
+}
+
+void hodec2_on_change_congestion_check_interval(struct gsm_network *net, 
unsigned int new_interval)
+{
+       net->hodec2.congestion_check_interval_s = new_interval;
+       reinit_congestion_timer(net);
+}
+
+static void conn_penalty_time_add(struct gsm_subscriber_connection *conn, 
struct gsm_bts *bts,
+                                  int penalty_time)
+{
+       if (!conn->hodec2.penalty_timers) {
+               conn->hodec2.penalty_timers = penalty_timers_init(conn);
+               OSMO_ASSERT(conn->hodec2.penalty_timers);
+       }
+       penalty_timers_add(conn->hodec2.penalty_timers, bts, penalty_time);
+}
+
+static unsigned int conn_penalty_time_remaining(struct 
gsm_subscriber_connection *conn,
+                                               struct gsm_bts *bts)
+{
+       if (!conn->hodec2.penalty_timers)
+               return 0;
+       return penalty_timers_remaining(conn->hodec2.penalty_timers, bts);
+}
+
+/* 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 measurements we already have 
from previous reports. */
+       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++;
+       }
+
+       /* Add cells that we don't know about yet, if necessary overwriting 
previous records that reflect
+        * cells with worse receive levels */
+       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_hodec2_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_time_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(&current_bts->mr_full, &bts->mr_full,
+                                       sizeof(struct amr_multirate_conf)))
+                               requirement &= ~(REQUIREMENT_A_TCHF);
+                       if ((requirement & REQUIREMENT_A_TCHH)
+                           && !!memcmp(&current_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(&current_bts->mr_half, &bts->mr_full,
+                                       sizeof(struct amr_multirate_conf)))
+                               requirement &= ~(REQUIREMENT_A_TCHF);
+                       if ((requirement & REQUIREMENT_A_TCHH)
+                           && !!memcmp(&current_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_hodec2_ho_max(bts->ho)) {
+               LOGPHOLCHANTOBTS(lchan, bts, LOGL_DEBUG,
+                                "not a candidate, number of allowed handovers 
(%d) would be exceeded\n",
+                                ho_get_hodec2_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_hodec2_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_hodec2_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_hodec2_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(HODEC2, 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_hodec2_tchf_min_slots(candidate->bts->ho));
+       LOGP(DHODEC, LOGL_DEBUG, "   o free TCH/H slots %d, minimum required "
+               "%d\n", tchh_count, 
ho_get_hodec2_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_hodec2_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_hodec2_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_hodec2_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_hodec2_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_hodec2_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_hodec2_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_hodec2_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_hodec2_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_hodec2_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_hodec2_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 void on_measurement_report(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;
+
+       /* 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;
+       }
+
+       if (log_check_level(DHODEC, LOGL_DEBUG)) {
+               int i;
+               LOGPHOLCHAN(lchan, LOGL_DEBUG, "MEASUREMENT REPORT\n");
+               for (i = 0; i < mr->num_cell; i++) {
+                       struct gsm_meas_rep_cell *mrc = &mr->cell[i];
+                       LOGPHOLCHAN(lchan, LOGL_DEBUG,
+                                   "  %d: arfcn=%u bsic=%u neigh_idx=%u 
rxlev=%u flags=%x\n",
+                                   i, mrc->arfcn, mrc->bsic, mrc->neigh_idx, 
mrc->rxlev, mrc->flags);
+               }
+       }
+
+       /* 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;
+       }
+       if (lchan->conn->secondary_lchan) {
+               LOGPHOLCHAN(lchan, LOGL_INFO, "Skipping, Initial Assignment is 
still ongoing\n");
+               return;
+       }
+       if (lchan->conn->ho_lchan) {
+               LOGPHOLCHAN(lchan, LOGL_INFO, "Skipping, Handover already 
triggered\n");
+               return;
+       }
+
+       LOGPHOLCHAN(lchan, LOGL_DEBUG, "HODEC2: evaluating measurement 
report\n");
+
+       /* get average levels. if not enought measurements yet, value is < 0 */
+       av_rxlev = get_meas_rep_avg(lchan,
+                                   ho_get_hodec2_full_tdma(bts->ho) ?
+                                   MEAS_REP_DL_RXLEV_FULL : 
MEAS_REP_DL_RXLEV_SUB,
+                                   ho_get_hodec2_rxlev_avg_win(bts->ho));
+       av_rxqual = get_meas_rep_avg(lchan,
+                                    ho_get_hodec2_full_tdma(bts->ho) ?
+                                    MEAS_REP_DL_RXQUAL_FULL : 
MEAS_REP_DL_RXQUAL_SUB,
+                                    ho_get_hodec2_rxqual_avg_win(bts->ho));
+       if (av_rxlev < 0 && av_rxqual < 0) {
+               LOGPHOLCHAN(lchan, LOGL_INFO, "Skipping, Not enough recent 
measuements\n");
+               return;
+       }
+       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_hodec2_afs_bias_rxlev(bts->ho);
+               int rxqual_bias = ho_get_hodec2_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_hodec2_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");
+               }
+               find_alternative_lchan(lchan, true);
+               return;
+       }
+
+       /* Low Level */
+       if (av_rxlev >= 0 && rxlev2dbm(av_rxlev) < 
ho_get_hodec2_min_rxlev(bts->ho)) {
+               global_ho_reason = HO_REASON_LOW_RXLEVEL;
+               LOGPHOLCHAN(lchan, LOGL_INFO, "Attempting handover/assignment 
due to low rxlev\n");
+               find_alternative_lchan(lchan, true);
+               return;
+       }
+
+       /* Max Distance */
+       if (lchan->meas_rep_count > 0
+           && lchan->rqd_ta > ho_get_hodec2_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_time_add(lchan->conn, bts, 
ho_get_hodec2_penalty_max_dist(bts->ho));
+               find_alternative_lchan(lchan, true);
+               return;
+       }
+
+       /* try handover to a better cell */
+       if (av_rxlev >= 0 && (mr->nr % ho_get_hodec2_pwr_interval(bts->ho)) == 
0) {
+               LOGPHOLCHAN(lchan, LOGL_INFO, "Looking whether a cell has 
better RXLEV\n");
+               global_ho_reason = HO_REASON_BETTER_CELL;
+               find_alternative_lchan(lchan, false);
+       }
+}
+
+/*
+ * 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;
+       }
+       if (log_check_level(DHODEC, LOGL_DEBUG)) {
+               LOGPHOBTS(bts, LOGL_DEBUG, "Considering %u candidates to solve 
congestion:\n", candidates);
+               for (i = 0; i < candidates; i++) {
+                       LOGPHOLCHANTOBTS(clist[i].lchan, clist[i].bts, 
LOGL_DEBUG,
+                                        "#%d: req=0x%x avg-rxlev=%d\n",
+                                        i, clist[i].requirements, 
clist[i].avg);
+               }
+       }
+
+#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_hodec2_afs_bias_rxlev(clist[i].bts->ho);
+                       is_improved = 1;
+               } else
+                       is_improved = 0;
+               LOGP(DHODEC, LOGL_DEBUG, "candidate %d: avg=%d 
best_avg_db=%d\n", i, avg, best_avg_db);
+               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;
+       }
+
+       LOGPHOBTS(bts, LOGL_DEBUG, "Did not find a best candidate that fulfills 
requirement B"
+                 " (omitting change from AHS to AFS)\n");
+
+#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_hodec2_afs_bias_rxlev(clist[i].bts->ho);
+                               is_improved = 1;
+                       } else
+                               is_improved = 0;
+                       LOGP(DHODEC, LOGL_DEBUG, "candidate %d: avg=%d 
worst_avg_db=%d\n", i, avg,
+                            worst_avg_db);
+                       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;
+       }
+
+       LOGPHOBTS(bts, LOGL_DEBUG, "Did not find a worst candidate that 
fulfills requirement B,"
+                 " selecting candidates that change from AHS to AFS only\n");
+
+#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_hodec2_afs_bias_rxlev(clist[i].bts->ho);
+                       is_improved = 1;
+               } else
+                       is_improved = 0;
+               LOGP(DHODEC, LOGL_DEBUG, "candidate %d: avg=%d 
best_avg_db=%d\n", i, avg, best_avg_db);
+               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;
+       }
+
+       LOGPHOBTS(bts, LOGL_DEBUG, "Did not find a best candidate that fulfills 
requirement C"
+                 " (omitting change from AHS to AFS)\n");
+
+#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_hodec2_afs_bias_rxlev(clist[i].bts->ho);
+                               is_improved = 1;
+                       } else
+                               is_improved = 0;
+                       LOGP(DHODEC, LOGL_DEBUG, "candidate %d: avg=%d 
worst_avg_db=%d\n", i, avg,
+                            worst_avg_db);
+                       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;
+       }
+       LOGPHOBTS(bts, LOGL_DEBUG, "Did not find a worst candidate that 
fulfills requirement C,"
+                 " selecting candidates that change from AHS to AFS only\n");
+
+
+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_hodec2_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_hodec2_tchf_min_slots(bts->ho);
+       min_free_tchh = ho_get_hodec2_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);
+}
+
+void on_ho_chan_activ_nack(struct bsc_handover *ho)
+{
+       struct gsm_bts *new_bts = ho->new_lchan->ts->trx->bts;
+
+       LOGPHO(ho, LOGL_ERROR, "Channel Activate Nack for %s, starting penalty 
timer\n", ho->inter_cell? "Handover" : "Assignment");
+
+       /* if channel failed, wait 10 seconds before allowing to retry handover 
*/
+       conn_penalty_time_add(ho->old_lchan->conn, new_bts, 10); /* FIXME 
configurable */
+}
+
+void on_ho_failure(struct bsc_handover *ho)
+{
+       struct gsm_bts *old_bts = ho->old_lchan->ts->trx->bts;
+       struct gsm_bts *new_bts = ho->new_lchan->ts->trx->bts;
+       struct gsm_subscriber_connection *conn = ho->old_lchan->conn;
+
+       if (!conn) {
+               LOGPHO(ho, LOGL_ERROR, "HO failure, but no conn");
+               return;
+       }
+
+       if (conn->hodec2.failures >= ho_get_hodec2_retries(old_bts->ho)) {
+               int penalty = ho->inter_cell
+                       ? ho_get_hodec2_penalty_failed_ho(old_bts->ho)
+                       : ho_get_hodec2_penalty_failed_as(old_bts->ho);
+               LOGPHO(ho, LOGL_NOTICE, "%s failed, starting penalty timer (%d 
s)\n",
+                      ho->inter_cell ? "Handover" : "Assignment",
+                      penalty);
+               conn->hodec2.failures = 0;
+               conn_penalty_time_add(conn, new_bts, penalty);
+       } else {
+               conn->hodec2.failures++;
+               LOGPHO(ho, LOGL_NOTICE, "%s failed, allowing handover decision 
to try again"
+                      " (%d/%d attempts)\n",
+                      ho->inter_cell ? "Handover" : "Assignment",
+                      conn->hodec2.failures, 
ho_get_hodec2_retries(old_bts->ho));
+       }
+}
+
+struct handover_decision_callbacks hodec2_callbacks = {
+       .hodec_id = 2,
+       .on_measurement_report = on_measurement_report,
+       .on_ho_chan_activ_nack = on_ho_chan_activ_nack,
+       .on_ho_failure = on_ho_failure,
+};
+
+void hodec2_init(struct gsm_network *net)
+{
+       handover_decision_callbacks_register(&hodec2_callbacks);
+       hodec2_initialized = true;
+       reinit_congestion_timer(net);
+}
diff --git a/src/libbsc/handover_vty.c b/src/libbsc/handover_vty.c
index 3ebdc0f..9ec9bc7 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,6 +71,41 @@
 #undef HO_CFG_ONE_MEMBER
 
 
+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,
+      "handover2 congestion-check (disabled|<1-999>|now)",
+      HO_CFG_STR_HANDOVER2
+      "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, \
@@ -93,7 +129,10 @@
 {
        ho_vty_write(vty, " ", net->ho);
 
-       /* future: net specific vty commands */
+       if (net->hodec2.congestion_check_interval_s != 
HO_CFG_CONGESTION_CHECK_DEFAULT)
+               vty_out(vty, " handover congestion-check %s%s",
+                       
congestion_check_interval2a(net->hodec2.congestion_check_interval_s),
+                       VTY_NEWLINE);
 }
 
 static void ho_vty_init_cmds(int parent_node)
@@ -108,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 d560b0c..2f65a52 100644
--- a/src/libbsc/net_init.c
+++ b/src/libbsc/net_init.c
@@ -87,6 +87,7 @@
        net->T3141 = GSM_T3141_DEFAULT;
 
        net->ho = ho_cfg_init(net, NULL);
+       net->hodec2.congestion_check_interval_s = 
HO_CFG_CONGESTION_CHECK_DEFAULT;
 
        INIT_LLIST_HEAD(&net->bts_list);
 
diff --git a/src/osmo-bsc/osmo_bsc_main.c b/src/osmo-bsc/osmo_bsc_main.c
index fd52734..af8f83d 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>
@@ -513,6 +514,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..4dd5223
--- /dev/null
+++ b/tests/handover/Makefile.am
@@ -0,0 +1,35 @@
+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/libbsc/libbsc.a \
+       $(NULL)
diff --git a/tests/handover/handover_test.c b/tests/handover/handover_test.c
new file mode 100644
index 0000000..e8716f2
--- /dev/null
+++ b/tests/handover/handover_test.c
@@ -0,0 +1,1615 @@
+/*
+ * (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
+};
+
+static const struct log_info_cat log_categories[] = {
+       [DHO] = {
+               .name = "DHO",
+               .description = "Hand-Over Process",
+               .color = "\033[1;38m",
+               .enabled = 1, .loglevel = LOGL_DEBUG,
+       },
+       [DHODEC] = {
+               .name = "DHODEC",
+               .description = "Hand-Over Decision",
+               .color = "\033[1;38m",
+               .enabled = 1, .loglevel = LOGL_DEBUG,
+       },
+       [DMEAS] = {
+               .name = "DMEAS",
+               .description = "Radio Measurement Processing",
+               .enabled = 1, .loglevel = LOGL_DEBUG,
+       },
+       [DREF] = {
+               .name = "DREF",
+               .description = "Reference Counting",
+               .enabled = 1, .loglevel = LOGL_DEBUG,
+       },
+       [DRSL] = {
+               .name = "DRSL",
+               .description = "A-bis Radio Siganlling Link (RSL)",
+               .color = "\033[1;35m",
+               .enabled = 1, .loglevel = LOGL_DEBUG,
+       },
+};
+
+const struct log_info log_info = {
+       .cat = log_categories,
+       .num_cat = ARRAY_SIZE(log_categories),
+};
+
+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_print_category_hex(osmo_stderr_target, 0);
+       log_set_print_filename2(osmo_stderr_target, LOG_FILENAME_BASENAME);
+
+       /* 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_hodec2_as_active(bsc_gsmnet->ho, true);
+       ho_set_hodec2_min_rxlev(bsc_gsmnet->ho, -100);
+       ho_set_hodec2_rxlev_avg_win(bsc_gsmnet->ho, 1);
+       ho_set_hodec2_rxlev_neigh_avg_win(bsc_gsmnet->ho, 1);
+       ho_set_hodec2_rxqual_avg_win(bsc_gsmnet->ho, 10);
+       ho_set_hodec2_pwr_hysteresis(bsc_gsmnet->ho, 3);
+       ho_set_hodec2_pwr_interval(bsc_gsmnet->ho, 1);
+       ho_set_hodec2_afs_bias_rxlev(bsc_gsmnet->ho, 0);
+       ho_set_hodec2_min_rxqual(bsc_gsmnet->ho, 5);
+       ho_set_hodec2_afs_bias_rxqual(bsc_gsmnet->ho, 0);
+       ho_set_hodec2_max_distance(bsc_gsmnet->ho, 9999);
+       ho_set_hodec2_ho_max(bsc_gsmnet->ho, 9999);
+       ho_set_hodec2_penalty_max_dist(bsc_gsmnet->ho, 300);
+       ho_set_hodec2_penalty_failed_ho(bsc_gsmnet->ho, 60);
+       ho_set_hodec2_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->hodec2.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_hodec2_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_hodec2_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_hodec2_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_hodec2_tchf_min_slots(bts[atoi(test_case[1])]->ho, atoi(test_case[3]));
+                       else
+                               
ho_set_hodec2_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_hodec2_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_hodec2_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/handover_cfg.vty b/tests/handover_cfg.vty
index 5856a0a..e2e4b28 100644
--- a/tests/handover_cfg.vty
+++ b/tests/handover_cfg.vty
@@ -190,6 +190,7 @@
   handover2 penalty-time failed-ho (<0-99999>|default)
   handover2 penalty-time failed-assignment (<0-99999>|default)
   handover2 retries (<0-9>|default)
+  handover2 congestion-check (disabled|<1-999>|now)
 ...
 
 OsmoBSC(config-net)# handover?
@@ -219,6 +220,7 @@
   max-handovers     Maximum number of concurrent handovers allowed per cell 
(HO algo 2 only)
   penalty-time      Set penalty times to wait between repeated handovers (HO 
algo 2 only)
   retries           Immediately retry on handover/assignment failure (HO algo 
2 only)
+  congestion-check  Configure congestion check interval (HO algo 2 only)
 
 OsmoBSC(config-net)# handover algorithm ?
   1        Algorithm 1: trigger handover based on comparing current cell and 
neighbor RxLev and RxQual, only.
@@ -395,8 +397,13 @@
   <0-9>    Number of retries
   default  Use default (0), remove explicit setting on this node
 
+OsmoBSC(config-net)# handover2 congestion-check ?
+  disabled  Disable congestion checking, do not handover based on cell overload
+  <1-999>   Congestion check interval in seconds (default 10)
+  now       Manually trigger a congestion check to run right now
 
-OsmoBSC(config-net)# ### Same on BTS level
+
+OsmoBSC(config-net)# ### Same on BTS level, except for the congestion-check
 OsmoBSC(config-net)# bts 0
 
 OsmoBSC(config-net-bts)# handover?
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/6499
To unsubscribe, visit https://gerrit.osmocom.org/settings

Gerrit-MessageType: newpatchset
Gerrit-Change-Id: Ie597eae82722baf32546331e443dd9d94f1f25e6
Gerrit-PatchSet: 4
Gerrit-Project: osmo-bsc
Gerrit-Branch: master
Gerrit-Owner: Neels Hofmeyr <nhofm...@sysmocom.de>
Gerrit-Reviewer: Harald Welte <lafo...@gnumonks.org>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: Neels Hofmeyr <nhofm...@sysmocom.de>

Reply via email to