Change in ...osmo-bsc[master]: Cell Broadcast: CBSP and CBCH scheduling support

This is merely a historical archive of years 2008-2021, before the migration to mailman3.

A maintained and still updated list archive can be found at https://lists.osmocom.org/hyperkitty/list/gerrit-log@lists.osmocom.org/.

laforge gerrit-no-reply at lists.osmocom.org
Sun Sep 1 21:30:08 UTC 2019


laforge has uploaded this change for review. ( https://gerrit.osmocom.org/c/osmo-bsc/+/15373


Change subject: Cell Broadcast: CBSP and CBCH scheduling support
......................................................................

Cell Broadcast: CBSP and CBCH scheduling support

This adds code to handle CBSP (Cell Broadcast Service Protocol)
from the CBC (Cell Broadcast Centre), as well as BSC-internal data
structures for scheduling the various SMSCB on the CBCH of each BTS.

There are currently one known shortcoming in the code: We don't yet
verify if keepalives are received within repetition period.

Change-Id: Ia0a0de862a104d0f447a5d6e56c7c83981b825c7
---
A doc/manuals/chapters/smscb.adoc
M doc/manuals/osmobsc-usermanual.adoc
M include/osmocom/bsc/Makefile.am
M include/osmocom/bsc/bsc_msc_data.h
M include/osmocom/bsc/debug.h
M include/osmocom/bsc/gsm_data.h
A include/osmocom/bsc/smscb.h
M include/osmocom/bsc/vty.h
M src/osmo-bsc/Makefile.am
M src/osmo-bsc/abis_rsl.c
M src/osmo-bsc/bsc_init.c
M src/osmo-bsc/bsc_vty.c
A src/osmo-bsc/cbch_scheduler.c
A src/osmo-bsc/cbsp_link.c
M src/osmo-bsc/gsm_data.c
M src/osmo-bsc/osmo_bsc_main.c
A src/osmo-bsc/smscb.c
M tests/handover/Makefile.am
18 files changed, 1,841 insertions(+), 4 deletions(-)



  git pull ssh://gerrit.osmocom.org:29418/osmo-bsc refs/changes/73/15373/1

diff --git a/doc/manuals/chapters/smscb.adoc b/doc/manuals/chapters/smscb.adoc
new file mode 100644
index 0000000..0372dbd
--- /dev/null
+++ b/doc/manuals/chapters/smscb.adoc
@@ -0,0 +1,82 @@
+[[smscb]]
+== SMSCB (Cell Broadcast)
+
+OsmoBSC supports SMS Cell Broadcast (SMSCB) services (CBS).  This
+includes the CBSP protocol to interact with a CBC (Cell Broadcast
+Centre) such as OsmoCBC, as well as the scheduling of SMSCB messages on
+both the BASIC and EXTENDED CBCH and transmission of related RSL
+messages to the attached BTS.
+
+More high-level information can be found at
+https://en.wikipedia.org/wiki/Cell_Broadcast and the related
+specification is <<3gpp-ts-23-041>>.
+
+In order to use SMSCB with OsmoBSC, you will need to
+
+* Configure the CBSP server and/or client
+* Use a channel combination including a CBCH on the BTSs
+
+=== Enabling a CBCH channel combination
+
+On the Um interface, SMSCB are transmitted via the CBCH (Cell Broadcast
+Channel).  The CBCH is a separate downlink-only logical channel which
+must be activated on any of the BTSs requiring CBSP support.
+
+The channel combination is configured in the `timeslot` node of each TRX.
+
+The two `phys_chan_config` supporting CBCH are `CCCH+SDCCH4+CBCH` and
+`SDCCH/8+CBCH`.  Please note that the CBCH steals one of the SDCCH, so
+a SDCCH/4 will only have three remaining SDCCH, and a SDCCH/8 will
+have only seven remaining SDCCH.
+
+=== Configuring the CBSP connection
+
+CBSP is the protocol between BSC and CBC.  It operates over TCP.
+
+According to 3GPP TS 48.049, a BSC typically operates as a TCP server,
+and the CBC connects as TCP client.  This would require the CBC to have
+out-of-band knowledge of all the BSCs in the network (and their IP
+addresses).
+
+In order to comply with the specifications, OsmoBSC supports this mode
+of operation as CBSP TCP server.  However, to make network operation and
+configuration more simple, ti also can operate in TCP client mode,
+connecting to the CBC.  This way the BSCs all need to know the CBC IP
+address, but not vice-versa
+
+The BSC can operate both CBSP TCP server and CBSP TCP client mode in
+parallel.
+
+The CBC related configuration of OsmoBSC can be found in the `cbc` configuration
+node of the VTY interface.
+
+.Example: Configure CBSP TCP client to connect to CBC at 1.2.3.4:48049
+----
+OsmoBSC> enable
+OsmoBSC# configure terminal
+OsmoBSC(config)# cbc
+OsmoBSC(config-cbc)# remote-ip 1.2.3.4
+OsmoBSC(config-cbc)# remote-port 48049
+OsmoBSC(config-cbc)# end
+----
+
+.Example: Disable CBSP TCP client
+----
+OsmoBSC> enable
+OsmoBSC# configure terminal
+OsmoBSC(config)# cbc
+OsmoBSC(config-cbc)# no remote-ip
+OsmoBSC(config-cbc)# end
+----
+
+.Example: Configure CBSP TCP server to listen for CBC at 127.0.0.2:9999
+----
+OsmoBSC> enable
+OsmoBSC# configure terminal
+OsmoBSC(config)# cbc
+OsmoBSC(config-cbc)# listen-ip 127.0.0.2
+OsmoBSC(config-cbc)# listen-port 9999
+OsmoBSC(config-cbc)# end
+----
+
+For more details on the available configuration commands, please check the OsmoBSC VTY Reference.
diff --git a/doc/manuals/osmobsc-usermanual.adoc b/doc/manuals/osmobsc-usermanual.adoc
index 766a11f..26f49da 100644
--- a/doc/manuals/osmobsc-usermanual.adoc
+++ b/doc/manuals/osmobsc-usermanual.adoc
@@ -24,6 +24,8 @@
 
 include::{srcdir}/chapters/handover.adoc[]
 
+include::{srcdir}/chapters/smscb.adoc[]
+
 include::./common/chapters/counters-overview.adoc[]
 
 include::{srcdir}/chapters/counters.adoc[]
@@ -34,8 +36,6 @@
 
 include::{srcdir}/chapters/control.adoc[]
 
-include::./common/chapters/cell-broadcast.adoc[]
-
 include::{srcdir}/chapters/osmux_bsc.adoc[]
 
 include::./common/chapters/port_numbers.adoc[]
diff --git a/include/osmocom/bsc/Makefile.am b/include/osmocom/bsc/Makefile.am
index f44e7fc..396604e 100644
--- a/include/osmocom/bsc/Makefile.am
+++ b/include/osmocom/bsc/Makefile.am
@@ -55,4 +55,5 @@
 	gsm_08_08.h \
 	penalty_timers.h \
 	osmo_bsc_lcls.h \
+	smscb.h \
 	$(NULL)
diff --git a/include/osmocom/bsc/bsc_msc_data.h b/include/osmocom/bsc/bsc_msc_data.h
index 5612483..b9df4ba 100644
--- a/include/osmocom/bsc/bsc_msc_data.h
+++ b/include/osmocom/bsc/bsc_msc_data.h
@@ -150,6 +150,7 @@
 /*
  * Per BSC data.
  */
+struct bsc_cbc_link;
 struct osmo_bsc_data {
 	struct gsm_network *network;
 
@@ -167,6 +168,8 @@
 	char *ussd_no_msc_txt;
 
 	char *acc_lst_name;
+
+	struct bsc_cbc_link *cbc;
 };
 
 
diff --git a/include/osmocom/bsc/debug.h b/include/osmocom/bsc/debug.h
index 3260121..adc6abb 100644
--- a/include/osmocom/bsc/debug.h
+++ b/include/osmocom/bsc/debug.h
@@ -27,6 +27,7 @@
 	DCHAN,
 	DTS,
 	DAS,
+	DCBS,
 	Debug_LastEntry,
 };
 
diff --git a/include/osmocom/bsc/gsm_data.h b/include/osmocom/bsc/gsm_data.h
index d82d1ba..8dfbc64 100644
--- a/include/osmocom/bsc/gsm_data.h
+++ b/include/osmocom/bsc/gsm_data.h
@@ -14,6 +14,7 @@
 #include <osmocom/core/stat_item.h>
 #include <osmocom/gsm/bts_features.h>
 #include <osmocom/gsm/protocol/gsm_08_08.h>
+#include <osmocom/gsm/protocol/gsm_48_049.h>
 #include <osmocom/gsm/gsm0808.h>
 #include <osmocom/gsm/gsm48.h>
 #include <osmocom/core/fsm.h>
@@ -957,6 +958,53 @@
 	struct gsm_bts *bts;
 };
 
+/* A single Page of a SMSCB message */
+struct bts_smscb_page {
+	/* SMSCB message we're part of */
+	struct bts_smscb_message *msg;
+	/* Page Number within message (1 to 15) */
+	uint8_t nr;
+	/* number of valid blocks in data (up to 4) */
+	uint8_t num_blocks;
+	/* up to four blocks of 22 bytes each */
+	uint8_t data[88];
+};
+
+/* A SMSCB message (received from CBSP) */
+struct bts_smscb_message {
+	/* entry in bts_smscb_chan_state.messages */
+	struct llist_head list;
+	struct {
+		/* input data from CBSP (CBC) side */
+		uint16_t msg_id;
+		uint16_t serial_nr;
+		enum cbsp_category category;
+		uint16_t rep_period;
+		uint16_t num_bcast_req;
+		uint8_t dcs;
+	} input;
+	/* how often have all pages of this message been broadcast? */
+	uint32_t bcast_count;
+	/* actual page data of this message */
+	uint8_t num_pages; /* up to 15 */
+	struct bts_smscb_page page[15];
+};
+
+/* per-channel (basic/extended) CBCH state for a single BTS */
+struct bts_smscb_chan_state {
+	/* back-pointer to BTS */
+	struct gsm_bts *bts;
+	/* list of bts_smscb_message */
+	struct llist_head messages;
+	/* scheduling array; pointer of SMSCB pages */
+	struct bts_smscb_page **sched_arr;
+	size_t sched_arr_size;
+	/* index of the next to be transmitted page into the scheduler array */
+	size_t next_idx;
+	/* number of messages we have to pause due to overflow */
+	uint8_t overflow;
+};
+
 /* One BTS */
 struct gsm_bts {
 	/* list header in net->bts_list */
@@ -1213,6 +1261,11 @@
 	struct load_counter chan_load_samples[7];
 	int chan_load_samples_idx;
 	uint8_t chan_load_avg; /* current channel load average in percent (0 - 100). */
+
+	/* cell broadcast system */
+	struct osmo_timer_list cbch_timer;
+	struct bts_smscb_chan_state cbch_basic;
+	struct bts_smscb_chan_state cbch_extended;
 };
 
 /* One rejected BTS */
diff --git a/include/osmocom/bsc/smscb.h b/include/osmocom/bsc/smscb.h
new file mode 100644
index 0000000..3fa9899
--- /dev/null
+++ b/include/osmocom/bsc/smscb.h
@@ -0,0 +1,61 @@
+#pragma once
+#include <osmocom/bsc/gsm_data.h>
+
+#include <osmocom/core/msgb.h>
+#include <osmocom/netif/stream.h>
+#include <osmocom/gsm/cbsp.h>
+
+struct bsc_cbc_link;
+
+/* smscb.c */
+void bts_smscb_del(struct bts_smscb_message *smscb, struct bts_smscb_chan_state *cstate,
+		   const char *reason);
+const char *bts_smscb_msg2str(const struct bts_smscb_message *smscb);
+struct bts_smscb_chan_state *bts_get_smscb_chan(struct gsm_bts *bts, bool extended);
+int cbsp_rx_decoded(struct bsc_cbc_link *cbc, const struct osmo_cbsp_decoded *dec);
+int cbsp_tx_restart(struct bsc_cbc_link *cbc, bool is_emerg);
+const char *bts_smscb_chan_state_name(const struct bts_smscb_chan_state *cstate);
+unsigned int bts_smscb_chan_load_percent(const struct bts_smscb_chan_state *cstate);
+unsigned int bts_smscb_chan_page_count(const struct bts_smscb_chan_state *cstate);
+void smscb_vty_init(void);
+
+/* cbch_scheduler.c */
+int bts_smscb_gen_sched_arr(struct bts_smscb_chan_state *cstate, struct bts_smscb_page ***arr_out);
+struct bts_smscb_page *bts_smscb_pull_page(struct bts_smscb_chan_state *cstate);
+void bts_smscb_page_done(struct bts_smscb_chan_state *cstate, struct bts_smscb_page *page);
+int bts_smscb_rx_cbch_load_ind(struct gsm_bts *bts, bool cbch_extended, bool is_overflow,
+			       uint8_t slot_count);
+void bts_cbch_timer_schedule(struct gsm_bts *bts);
+
+/* cbsp_link.c */
+struct bsc_cbc_link {
+	struct gsm_network *net;
+	struct {
+		/* hostname/IP of CBC */
+		char *cbc_hostname;
+		/* TCP port (Default: 48049) of CBC */
+		int cbc_port;
+		/* local listening port (0 for disabling local server) */
+		int listen_port;
+		/* local listening hostname/IP */
+		char *listen_hostname;
+		/* use Osmocom CBSP extensions for dynamic registration */
+		bool use_osmocom_ext;
+	} config;
+	/* for handling inbound TCP connections */
+	struct {
+		struct osmo_stream_srv *srv;
+		struct osmo_stream_srv_link *link;
+		char *sock_name;
+		struct msgb *msg;
+	} server;
+	/* for handling outbound TCP connections */
+	struct {
+		struct osmo_stream_cli *cli;
+		char *sock_name;
+		struct msgb *msg;
+	} client;
+};
+void cbc_vty_init(void);
+int bsc_cbc_link_restart(void);
+int cbsp_tx_decoded(struct bsc_cbc_link *cbc, struct osmo_cbsp_decoded *decoded);
diff --git a/include/osmocom/bsc/vty.h b/include/osmocom/bsc/vty.h
index 7e3c505..10ce16b 100644
--- a/include/osmocom/bsc/vty.h
+++ b/include/osmocom/bsc/vty.h
@@ -24,6 +24,7 @@
 	OM2K_NODE,
 	OM2K_CON_GROUP_NODE,
 	BSC_NODE,
+	CBC_NODE,
 };
 
 struct log_info;
diff --git a/src/osmo-bsc/Makefile.am b/src/osmo-bsc/Makefile.am
index d50515b..51d8875 100644
--- a/src/osmo-bsc/Makefile.am
+++ b/src/osmo-bsc/Makefile.am
@@ -88,6 +88,9 @@
 	rest_octets.c \
 	system_information.c \
 	timeslot_fsm.c \
+	smscb.c \
+	cbch_scheduler.c \
+	cbsp_link.c \
 	$(NULL)
 
 osmo_bsc_LDADD = \
@@ -96,6 +99,7 @@
 	$(LIBOSMOGSM_LIBS) \
 	$(LIBOSMOVTY_LIBS) \
 	$(LIBOSMOCTRL_LIBS) \
+	$(LIBOSMONETIF_LIBS) \
 	$(COVERAGE_LDFLAGS) \
 	$(LIBOSMOABIS_LIBS) \
 	$(LIBOSMOSIGTRAN_LIBS) \
diff --git a/src/osmo-bsc/abis_rsl.c b/src/osmo-bsc/abis_rsl.c
index 0b68d7c..06d19a5 100644
--- a/src/osmo-bsc/abis_rsl.c
+++ b/src/osmo-bsc/abis_rsl.c
@@ -1,7 +1,7 @@
 /* GSM Radio Signalling Link messages on the A-bis interface
  * 3GPP TS 08.58 version 8.6.0 Release 1999 / ETSI TS 100 596 V8.6.0 */
 
-/* (C) 2008-2010 by Harald Welte <laforge at gnumonks.org>
+/* (C) 2008-2019 by Harald Welte <laforge at gnumonks.org>
  * (C) 2012 by Holger Hans Peter Freyther
  *
  * All Rights Reserved
@@ -52,6 +52,7 @@
 #include <osmocom/bsc/lchan_fsm.h>
 #include <osmocom/bsc/lchan_rtp_fsm.h>
 #include <osmocom/bsc/handover_fsm.h>
+#include <osmocom/bsc/smscb.h>
 
 #define RSL_ALLOC_SIZE		1024
 #define RSL_ALLOC_HEADROOM	128
@@ -1489,6 +1490,36 @@
 	return 0;
 }
 
+/* 8.5.9 current load on the CBCH (Cell Broadcast) */
+static int rsl_rx_cbch_load(struct msgb *msg)
+{
+	struct e1inp_sign_link *sign_link = msg->dst;
+	struct abis_rsl_dchan_hdr *rslh = msgb_l2(msg);
+	struct gsm_bts *bts = sign_link->trx->bts;
+	bool cbch_extended = false;
+	bool is_overflow = false;
+	int8_t load_info;
+	struct tlv_parsed tp;
+	uint8_t slot_count;
+
+	rsl_tlv_parse(&tp, rslh->data, msgb_l2len(msg) - sizeof(*rslh));
+	if (!TLVP_PRESENT(&tp, RSL_IE_CBCH_LOAD_INFO)) {
+		LOG_BTS(bts, DRSL, LOGL_ERROR, "CBCH LOAD IND without mandatory CBCH Load Info IE\n");
+		return -1;
+	}
+	/* 9.4.43 */
+	load_info = *TLVP_VAL(&tp, RSL_IE_CBCH_LOAD_INFO);
+	if (load_info & 0x80)
+		is_overflow = true;
+	slot_count = load_info & 0x0F;
+
+	if (TLVP_PRES_LEN(&tp, RSL_IE_SMSCB_CHAN_INDICATOR, 1) &&
+	    (*TLVP_VAL(&tp, RSL_IE_SMSCB_CHAN_INDICATOR) & 0x0F) == 0x01)
+		cbch_extended = true;
+
+	return bts_smscb_rx_cbch_load_ind(bts, cbch_extended, is_overflow, slot_count);
+}
+
 /* Ericsson specific: Immediate Assign Sent */
 static int rsl_rx_ericsson_imm_assign_sent(struct msgb *msg)
 {
@@ -1537,7 +1568,7 @@
 		break;
 	case RSL_MT_CBCH_LOAD_IND:
 		/* current load on the CBCH */
-		/* FIXME: handle this. Ignore for now */
+		rc = rsl_rx_cbch_load(msg);
 		break;
 	case RSL_MT_ERICSSON_IMM_ASS_SENT:
 		rc = rsl_rx_ericsson_imm_assign_sent(msg);
diff --git a/src/osmo-bsc/bsc_init.c b/src/osmo-bsc/bsc_init.c
index 7d29d4f..aea8786 100644
--- a/src/osmo-bsc/bsc_init.c
+++ b/src/osmo-bsc/bsc_init.c
@@ -37,6 +37,9 @@
 #include <osmocom/bsc/gsm_04_08_rr.h>
 #include <osmocom/bsc/neighbor_ident.h>
 
+#include <osmocom/bsc/smscb.h>
+#include <osmocom/gsm/protocol/gsm_48_049.h>
+
 #include <time.h>
 #include <limits.h>
 #include <stdbool.h>
@@ -244,6 +247,11 @@
 		talloc_free(net);
 		return NULL;
 	}
+	net->bsc_data->cbc = talloc_zero(net->bsc_data, struct bsc_cbc_link);
+	if (!net->bsc_data->cbc) {
+		talloc_free(net);
+		return NULL;
+	}
 
 	/* Init back pointer */
 	net->bsc_data->auto_off_timeout = -1;
@@ -272,6 +280,14 @@
 	osmo_timer_setup(&net->t3122_chan_load_timer, update_t3122_chan_load_timer, net);
 	osmo_timer_schedule(&net->t3122_chan_load_timer, T3122_CHAN_LOAD_SAMPLE_INTERVAL, 0);
 
+	net->bsc_data->cbc->net = net;
+	/* no cbc_hostname: client not started by default */
+	net->bsc_data->cbc->config.cbc_port = CBSP_TCP_PORT;
+	/* listen_port == -1: server not started by default */
+	net->bsc_data->cbc->config.listen_port = -1;
+	net->bsc_data->cbc->config.listen_hostname = talloc_strdup(net->bsc_data->cbc, "0.0.0.0");
+	net->bsc_data->cbc->config.use_osmocom_ext = true;
+
 	return net;
 }
 
diff --git a/src/osmo-bsc/bsc_vty.c b/src/osmo-bsc/bsc_vty.c
index 6de2d4b..06c06de 100644
--- a/src/osmo-bsc/bsc_vty.c
+++ b/src/osmo-bsc/bsc_vty.c
@@ -70,6 +70,7 @@
 #include <osmocom/bsc/timeslot_fsm.h>
 #include <osmocom/bsc/lchan_fsm.h>
 #include <osmocom/bsc/lchan_select.h>
+#include <osmocom/bsc/smscb.h>
 #include <osmocom/mgcp_client/mgcp_client_endpoint_fsm.h>
 
 #include <inttypes.h>
@@ -318,6 +319,14 @@
 		vty_out(vty, " (%d)", count);
 }
 
+static void bts_dump_vty_cbch(struct vty *vty, const struct bts_smscb_chan_state *cstate)
+{
+	vty_out(vty, "  CBCH %s: %u messages, %u pages, %lu-entry sched_arr, %u%% load%s",
+		bts_smscb_chan_state_name(cstate), llist_count(&cstate->messages),
+		bts_smscb_chan_page_count(cstate), cstate->sched_arr_size,
+		bts_smscb_chan_load_percent(cstate), VTY_NEWLINE);
+}
+
 static void bts_dump_vty_features(struct vty *vty, struct gsm_bts *bts)
 {
 	unsigned int i;
@@ -504,6 +513,9 @@
 	vty_out(vty, "  Current Channel Load:%s", VTY_NEWLINE);
 	dump_pchan_load_vty(vty, "    ", &pl);
 
+	bts_dump_vty_cbch(vty, &bts->cbch_basic);
+	bts_dump_vty_cbch(vty, &bts->cbch_extended);
+
 	vty_out(vty, "  Channel Requests        : %"PRIu64" total, %"PRIu64" no channel%s",
 		bts->bts_ctrs->ctr[BTS_CTR_CHREQ_TOTAL].current,
 		bts->bts_ctrs->ctr[BTS_CTR_CHREQ_NO_CHANNEL].current,
@@ -5427,6 +5439,8 @@
 	osmo_fsm_vty_add_cmds();
 
 	ho_vty_init();
+	cbc_vty_init();
+	smscb_vty_init();
 
 	bsc_vty_init_extra();
 
diff --git a/src/osmo-bsc/cbch_scheduler.c b/src/osmo-bsc/cbch_scheduler.c
new file mode 100644
index 0000000..8d9bed2
--- /dev/null
+++ b/src/osmo-bsc/cbch_scheduler.c
@@ -0,0 +1,286 @@
+/* CBCH (Cell Broadcast Channel) Scheduler for OsmoBSC */
+/*
+ * (C) 2019 by Harald Welte <laforge at gnumonks.org>
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <osmocom/core/stats.h>
+#include <osmocom/core/select.h>
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/talloc.h>
+
+#include <osmocom/bsc/debug.h>
+#include <osmocom/bsc/gsm_data.h>
+#include <osmocom/bsc/smscb.h>
+#include <osmocom/bsc/abis_rsl.h>
+
+/* add all pages of given SMSCB so they appear as soon as possible *after* (included) base_idx. */
+static int bts_smscb_sched_add_after(struct bts_smscb_page **sched_arr, int sched_arr_size,
+				     int base_idx, struct bts_smscb_message *smscb)
+{
+	int arr_idx = base_idx;
+	int i;
+
+	OSMO_ASSERT(smscb->num_pages <= ARRAY_SIZE(smscb->page));
+	for (i = 0; i < smscb->num_pages; i++) {
+		while (sched_arr[arr_idx]) {
+			arr_idx++;
+			if (arr_idx >= sched_arr_size)
+				return -ENOSPC;
+		}
+		sched_arr[arr_idx] = &smscb->page[i];
+	}
+	return arr_idx;
+}
+
+/* add all pages of given smscb so they appear *before* (included) last_idx. */
+static int bts_smscb_sched_add_before(struct bts_smscb_page **sched_arr, int sched_arr_size,
+				      int last_idx, struct bts_smscb_message *smscb)
+{
+	int arr_idx = last_idx;
+	int last_used_idx = 0;
+	int i;
+
+	OSMO_ASSERT(smscb->num_pages <= ARRAY_SIZE(smscb->page));
+	OSMO_ASSERT(smscb->num_pages >= 1);
+
+	for (i = smscb->num_pages - 1; i >= 0; i--) {
+		while (sched_arr[arr_idx]) {
+			arr_idx--;
+			if (arr_idx < 0)
+				return -ENOSPC;
+		}
+		sched_arr[arr_idx] = &smscb->page[i];
+		if (i == smscb->num_pages)
+			last_used_idx = i;
+	}
+	return last_used_idx;
+}
+
+/* obtain the least frequently scheduled SMSCB for given SMSCB channel */
+static struct bts_smscb_message *
+bts_smscb_chan_get_least_frequent_smscb(struct bts_smscb_chan_state *cstate)
+{
+	if (llist_empty(&cstate->messages))
+		return NULL;
+	/* messages are expected to be ordered with increasing period, so we're
+	 * able to return the last message in the list */
+	return llist_entry(cstate->messages.prev, struct bts_smscb_message, list);
+}
+
+/*! Generate per-BTS SMSCB scheduling array
+ *  \param[in] cstate BTS CBCH channel state
+ *  \param[out] arr_out return argument for allocated + generated scheduling array
+ *  \return size of returned scheduling array arr_out in number of entries; negative on error */
+int bts_smscb_gen_sched_arr(struct bts_smscb_chan_state *cstate, struct bts_smscb_page ***arr_out)
+{
+	struct bts_smscb_message *smscb, *least_freq;
+	struct bts_smscb_page **arr;
+	int arr_size;
+	int rc;
+
+	/* start with one instance of the least frequent message at position 0, as we
+	 * need to transmit it exactly once during the duration of the scheduling array */
+	least_freq = bts_smscb_chan_get_least_frequent_smscb(cstate);
+	if (!least_freq) {
+		LOG_BTS(cstate->bts, DCBS, LOGL_DEBUG, "No SMSCB; cannot create schedule array\n");
+		return 0;
+	}
+	arr_size = least_freq->input.rep_period;
+	arr = talloc_zero_array(cstate->bts, struct bts_smscb_page *, arr_size);
+	OSMO_ASSERT(arr);
+	rc = bts_smscb_sched_add_after(arr, arr_size, 0, least_freq);
+	if (rc < 0) {
+		LOG_BTS(cstate->bts, DCBS, LOGL_ERROR, "Unable to schedule first instance of "
+			"very first SMSCB %s ?!?\n", bts_smscb_msg2str(least_freq));
+		talloc_free(arr);
+		return rc;
+	}
+
+	/* continue filling with repetitions of the more frequent messages, starting from
+	 * the most frequent message to the least frequent one, repeating them as needed
+	 * throughout the duration of the array */
+	llist_for_each_entry(smscb, &cstate->messages, list) {
+		int last_page;
+		if (smscb == least_freq)
+			continue;
+		/* messages are expected to be ordered with increasing period, so we're
+		 * starting with the most frequent / shortest period first */
+		rc = bts_smscb_sched_add_after(arr, arr_size, 0, smscb);
+		if (rc < 0) {
+			LOG_BTS(cstate->bts, DCBS, LOGL_ERROR, "Unable to schedule first instance of "
+				"SMSCB %s\n", bts_smscb_msg2str(smscb));
+			talloc_free(arr);
+			return rc;
+		}
+		last_page = rc;
+
+		while (last_page < cstate->sched_arr_size) {
+			/* store further instances in a way that the last block of the N+1th instance
+			 * happens no later than "interval" after the last block of the Nth instance */
+			rc = bts_smscb_sched_add_before(arr, arr_size,
+							last_page + smscb->input.rep_period, smscb);
+			if (rc < 0) {
+				LOG_BTS(cstate->bts, DCBS, LOGL_ERROR, "Unable to schedule further "
+					"SMSCB %s\n", bts_smscb_msg2str(smscb));
+				talloc_free(arr);
+				return rc;
+			}
+			last_page = rc;
+		}
+	}
+	*arr_out = arr;
+	return arr_size;
+}
+
+/*! Pull the next to-be-transmitted SMSCB page out of the scheduler for the given channel */
+struct bts_smscb_page *bts_smscb_pull_page(struct bts_smscb_chan_state *cstate)
+{
+	struct bts_smscb_page *page;
+
+	/* if there are no messages to schedule, there is no array */
+	if (!cstate->sched_arr)
+		return NULL;
+
+	/* obtain the page from the scheduler array */
+	page = cstate->sched_arr[cstate->next_idx];
+
+	/* increment the index for the next call to this function */
+	cstate->next_idx = (cstate->next_idx + 1) % cstate->sched_arr_size;
+
+	/* the array can have gaps in between where there is nothing scheduled */
+	if (!page)
+		return NULL;
+
+	return page;
+}
+
+/*! To be called after bts_smscb_pull_page() in order to update transmission count and
+ *  check if SMSCB is complete.
+ *  \param[in] cstate BTS CBC channel state
+ *  \param[in] page SMSCB Page which had been returned by bts_smscb_pull_page() and which
+ *  		    is no longer needed now */
+void bts_smscb_page_done(struct bts_smscb_chan_state *cstate, struct bts_smscb_page *page)
+{
+	struct bts_smscb_message *smscb = page->msg;
+
+	/* If this is the last page of a SMSCB, increment the SMSCB number-of-xmit counter */
+	if (page->nr == smscb->num_pages) {
+		smscb->bcast_count++;
+		/* Check if the SMSCB transmission duration is now over */
+		if (smscb->bcast_count >= smscb->input.num_bcast_req)
+			bts_smscb_del(smscb, cstate, "COMPLETE");
+	}
+}
+
+
+/***********************************************************************
+ * BTS / RSL side
+ ***********************************************************************/
+
+static void bts_cbch_send_one(struct bts_smscb_chan_state *cstate)
+{
+	struct bts_smscb_page *page;
+	struct gsm_bts *bts = cstate->bts;
+	struct rsl_ie_cb_cmd_type cb_cmd;
+	bool is_extended = false;
+
+	if (cstate == &bts->cbch_extended)
+		is_extended = true;
+
+	if (cstate->overflow) {
+		LOG_BTS(bts, DCBS, LOGL_DEBUG, "Skipping SMSCB due to overflow (%u)\n",
+			cstate->overflow);
+		cstate->overflow--;
+		return;
+	}
+
+	page = bts_smscb_pull_page(cstate);
+	if (!page) {
+		LOG_BTS(bts, DCBS, LOGL_DEBUG, "Skipping SMSCB: No page available\n");
+		return;
+	}
+
+	cb_cmd.spare = 0;
+	cb_cmd.def_bcast = 0;
+	cb_cmd.command = RSL_CB_CMD_TYPE_NORMAL;
+	switch (page->num_blocks) {
+	case 1:
+		cb_cmd.last_block = RSL_CB_CMD_LASTBLOCK_1;
+		break;
+	case 2:
+		cb_cmd.last_block = RSL_CB_CMD_LASTBLOCK_2;
+		break;
+	case 3:
+		cb_cmd.last_block = RSL_CB_CMD_LASTBLOCK_3;
+		break;
+	case 4:
+		cb_cmd.last_block = RSL_CB_CMD_LASTBLOCK_4;
+		break;
+	default:
+		osmo_panic("SMSCB Page must have 1..4 blocks, not %d\n", page->num_blocks);
+	}
+	rsl_sms_cb_command(bts, RSL_CHAN_SDCCH4_ACCH, cb_cmd, is_extended,
+			   page->data, sizeof(page->data));
+
+	bts_smscb_page_done(cstate, page);
+}
+
+static void bts_cbch_timer(void *data)
+{
+	struct gsm_bts *bts = (struct gsm_bts *)data;
+
+	bts_cbch_send_one(&bts->cbch_basic);
+	bts_cbch_send_one(&bts->cbch_extended);
+
+	bts_cbch_timer_schedule(bts);
+}
+
+/* There is one SMSCB message (page) per eight 51-multiframes, i.e. 1.882 seconds */
+void bts_cbch_timer_schedule(struct gsm_bts *bts)
+{
+	osmo_timer_setup(&bts->cbch_timer, &bts_cbch_timer, bts);
+	osmo_timer_schedule(&bts->cbch_timer, 1, 882920);
+}
+
+/*! Receive a (decoded) incoming CBCH LOAD IND from given bts. See TS 48.058 8.5.9
+ *  \param[in] bts The BTS for which the load indication was received
+ *  \param[in] cbch_extended Is this report for extended (true) or basic CBCH
+ *  \param[in] is_overflow Is this report and overflow (true) or underflow report
+ *  \param[in] slot_count amount of SMSCB messages needed / delay needed */
+int bts_smscb_rx_cbch_load_ind(struct gsm_bts *bts, bool cbch_extended, bool is_overflow,
+			       uint8_t slot_count)
+{
+	struct bts_smscb_chan_state *cstate = bts_get_smscb_chan(bts, cbch_extended);
+	int i;
+
+	if (!gsm_bts_get_cbch(bts))
+		return -ENODEV;
+
+	if (is_overflow) {
+		/* halt/delay transmission of further CBCH messages */
+		cstate->overflow = slot_count;
+	} else {
+		for (i = 0; i < slot_count; i++)
+			bts_cbch_send_one(cstate);
+		/* re-schedule the timer to count from now on */
+		bts_cbch_timer_schedule(bts);
+	}
+
+	return 0;
+}
diff --git a/src/osmo-bsc/cbsp_link.c b/src/osmo-bsc/cbsp_link.c
new file mode 100644
index 0000000..ec41bc5
--- /dev/null
+++ b/src/osmo-bsc/cbsp_link.c
@@ -0,0 +1,440 @@
+/* CBSP (Cell Broadcast Service Protocol) Handling for OsmoBSC */
+/*
+ * (C) 2019 by Harald Welte <laforge at gnumonks.org>
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+#include <osmocom/bsc/gsm_data.h>
+#include <osmocom/bsc/vty.h>
+#include <osmocom/bsc/debug.h>
+#include <osmocom/bsc/smscb.h>
+#include <osmocom/bsc/bsc_msc_data.h>
+
+#include <osmocom/core/talloc.h>
+#include <osmocom/core/socket.h>
+#include <osmocom/gsm/cbsp.h>
+
+/* if a CBC IP/port has been configured, we continuously try to re-establish the TCP
+ * connection (as a client) to the CBC.  If none has been configured, and we have a listen
+ * TCP port, we expect the CBC to connect to us.  If neither of the two is configured,
+ * CBSP is effectively disabled */
+
+/*********************************************************************************
+ * CBSP Server (inbound TCP connection from CBC)
+ *********************************************************************************/
+
+static int cbsp_srv_closed_cb(struct osmo_stream_srv *conn)
+{
+	struct bsc_cbc_link *cbc = osmo_stream_srv_get_data(conn);
+	//struct osmo_fd *ofd = osmo_stream_srv_get_ofd(conn);
+
+	LOGP(DCBS, LOGL_NOTICE, "CBSP Server lost connection from %s\n", cbc->server.sock_name);
+	talloc_free(cbc->server.sock_name);
+	cbc->server.sock_name = NULL;
+	cbc->server.srv = NULL;
+	return 0;
+}
+
+static int cbsp_srv_cb(struct osmo_stream_srv *conn)
+{
+	struct bsc_cbc_link *cbc = osmo_stream_srv_get_data(conn);
+	struct osmo_fd *ofd = osmo_stream_srv_get_ofd(conn);
+	struct osmo_cbsp_decoded *decoded;
+	struct msgb *msg;
+	int rc;
+
+	/* READ */
+	rc = osmo_cbsp_recv_buffered(cbc, ofd->fd, &msg, &cbc->server.msg);
+	if (rc <= 0) {
+		if (rc == -EAGAIN || rc == -EINTR) {
+			/* more data needs to be read */
+			return 0;
+		} else if (rc == -EPIPE || rc == -ECONNRESET) {
+			/* lost connection */
+		} else if (rc == 0) {
+			/* connection closed */
+		}
+		osmo_stream_srv_destroy(conn);
+		cbc->server.srv = NULL;
+		return -EBADF;
+	}
+	OSMO_ASSERT(msg);
+	decoded = osmo_cbsp_decode(conn, msg);
+	if (decoded) {
+		LOGP(DCBS, LOGL_DEBUG, "Received CBSP %s\n",
+			get_value_string(cbsp_msg_type_names, decoded->msg_type));
+		cbsp_rx_decoded(cbc, decoded);
+		talloc_free(decoded);
+	} else {
+		LOGP(DCBS, LOGL_ERROR, "Unable to decode CBSP %s: '%s'\n",
+			msgb_hexdump(msg), osmo_cbsp_errstr);
+	}
+	msgb_free(msg);
+	return 0;
+
+}
+
+static int cbsp_srv_link_accept_cb(struct osmo_stream_srv_link *link, int fd)
+{
+	struct bsc_cbc_link *cbc = osmo_stream_srv_link_get_data(link);
+	struct osmo_stream_srv *srv;
+
+	LOGP(DCBS, LOGL_INFO, "CBSP Server received inbound connection from CBC: %s\n",
+		osmo_sock_get_name2(fd));
+
+	if (cbc->server.srv) {
+		LOGP(DCBS, LOGL_NOTICE, "CBSP Server refusing further connection (%s) "
+		     "while we already have another connection (%s)\n",
+		     osmo_sock_get_name2(fd), cbc->server.sock_name);
+		return -1;
+	}
+
+	srv = osmo_stream_srv_create(cbc, link, fd, cbsp_srv_cb, cbsp_srv_closed_cb, cbc);
+	if (!srv) {
+		LOGP(DCBS, LOGL_ERROR, "Unable to create stream server for %s\n",
+			osmo_sock_get_name2(fd));
+		return -1;
+	}
+
+	cbc->server.srv = srv;
+	if (cbc->server.sock_name)
+		talloc_free(cbc->server.sock_name);
+	cbc->server.sock_name = osmo_sock_get_name(cbc, fd);
+	LOGP(DCBS, LOGL_NOTICE, "CBSP Server link established from CBC %s\n", cbc->server.sock_name);
+	/* FIXME: introduce ourselves to the peer */
+	cbsp_tx_restart(cbc, false);
+	return 0;
+}
+
+/*********************************************************************************
+ * CBSP Client (outbound TCP connection to CBC)
+ *********************************************************************************/
+
+static int cbsp_client_connect_cb(struct osmo_stream_cli *cli)
+{
+	struct bsc_cbc_link *cbc = osmo_stream_cli_get_data(cli);
+	struct osmo_fd *ofd = osmo_stream_cli_get_ofd(cli);
+
+	if (cbc->client.sock_name)
+		talloc_free(cbc->client.sock_name);
+	cbc->client.sock_name = osmo_sock_get_name(cbc, ofd->fd);
+
+	LOGP(DCBS, LOGL_NOTICE, "CBSP Client connected to CBC: %s\n", cbc->client.sock_name);
+
+	/* FIXME: introduce ourselves to the peer */
+	cbsp_tx_restart(cbc, false);
+
+	return 0;
+}
+
+static int cbsp_client_disconnect_cb(struct osmo_stream_cli *cli)
+{
+	struct bsc_cbc_link *cbc = osmo_stream_cli_get_data(cli);
+
+	LOGP(DCBS, LOGL_NOTICE, "CBSP Client lost connection to %s\n", cbc->client.sock_name);
+	talloc_free(cbc->client.sock_name);
+	cbc->client.sock_name = NULL;
+	return 0;
+}
+
+static int cbsp_client_read_cb(struct osmo_stream_cli *cli)
+{
+	struct bsc_cbc_link *cbc = osmo_stream_cli_get_data(cli);
+	struct osmo_fd *ofd = osmo_stream_cli_get_ofd(cli);
+	struct osmo_cbsp_decoded *decoded;
+	struct msgb *msg = NULL;
+	int rc;
+
+	/* READ */
+	rc = osmo_cbsp_recv_buffered(cbc, ofd->fd, &msg, &cbc->client.msg);
+	if (rc <= 0) {
+		if (rc == -EAGAIN || rc == -EINTR) {
+			/* more data needs to be read */
+			return 0;
+		} else if (rc == -EPIPE || rc == -ECONNRESET) {
+			/* lost connection */
+		} else if (rc == 0) {
+			/* connection closed */
+		}
+		osmo_stream_cli_reconnect(cli);
+		return -EBADF;
+	}
+	OSMO_ASSERT(msg);
+	decoded = osmo_cbsp_decode(cli, msg);
+	if (decoded) {
+		LOGP(DCBS, LOGL_DEBUG, "Received CBSP %s\n",
+			get_value_string(cbsp_msg_type_names, decoded->msg_type));
+		cbsp_rx_decoded(cbc, decoded);
+		talloc_free(decoded);
+	} else {
+		LOGP(DCBS, LOGL_ERROR, "Unable to decode CBSP %s: '%s'\n",
+			msgb_hexdump(msg), osmo_cbsp_errstr);
+	}
+	msgb_free(msg);
+	return 0;
+}
+
+int bsc_cbc_link_restart(void)
+{
+	struct bsc_cbc_link *cbc = bsc_gsmnet->bsc_data->cbc;
+
+	/* shut down client, if no longer configured */
+	if (cbc->client.cli && !cbc->config.cbc_hostname) {
+		LOGP(DCBS, LOGL_NOTICE, "Stopping CBSP client\n");
+		osmo_stream_cli_close(cbc->client.cli);
+		osmo_stream_cli_destroy(cbc->client.cli);
+		cbc->client.cli = NULL;
+	}
+
+	/* shut down server, if no longer configured */
+	if (cbc->config.listen_port == -1) {
+		if (cbc->server.srv || cbc->server.link)
+			LOGP(DCBS, LOGL_NOTICE, "Stopping CBSP server\n");
+		if (cbc->server.srv) {
+			osmo_stream_srv_destroy(cbc->server.srv);
+			cbc->server.srv = NULL;
+		}
+		if (cbc->server.link) {
+			osmo_stream_srv_link_close(cbc->server.link);
+			osmo_stream_srv_link_destroy(cbc->server.link);
+			cbc->server.link = NULL;
+		}
+	}
+
+	/* start client, if configured */
+	if (cbc->config.cbc_hostname) {
+		LOGP(DCBS, LOGL_NOTICE, "Starting CBSP Client (to CBC at %s:%u)\n",
+			cbc->config.cbc_hostname, cbc->config.cbc_port);
+		if (!cbc->client.cli) {
+			cbc->client.cli = osmo_stream_cli_create(cbc);
+			osmo_stream_cli_set_data(cbc->client.cli, cbc);
+			osmo_stream_cli_set_connect_cb(cbc->client.cli, cbsp_client_connect_cb);
+			osmo_stream_cli_set_disconnect_cb(cbc->client.cli, cbsp_client_disconnect_cb);
+			osmo_stream_cli_set_read_cb(cbc->client.cli, cbsp_client_read_cb);
+		}
+		/* CBC side */
+		osmo_stream_cli_set_addr(cbc->client.cli, cbc->config.cbc_hostname);
+		osmo_stream_cli_set_port(cbc->client.cli, cbc->config.cbc_port);
+		/* Close/Reconnect? */
+		osmo_stream_cli_open(cbc->client.cli);
+	}
+
+	/* start server, if configured */
+	if (cbc->config.listen_port != -1) {
+		LOGP(DCBS, LOGL_NOTICE, "Starting CBSP Server (bound to %s:%u)\n",
+			cbc->config.listen_hostname, cbc->config.listen_port);
+		if (!cbc->server.srv) {
+			cbc->server.link = osmo_stream_srv_link_create(cbc);
+			osmo_stream_srv_link_set_data(cbc->server.link, cbc);
+			osmo_stream_srv_link_set_accept_cb(cbc->server.link, cbsp_srv_link_accept_cb);
+		}
+		osmo_stream_srv_link_set_addr(cbc->server.link, cbc->config.listen_hostname);
+		osmo_stream_srv_link_set_port(cbc->server.link, cbc->config.listen_port);
+	}
+	return 0;
+}
+
+/*! Encode + Transmit a 'decoded' CBSP message over given CBC link
+ *  \param[in] cbc Data structure representing the BSCs link to the CBC
+ *  \param[in] cbsp Decoded CBSP message to be transmitted. Ownership is transferred.
+ *  \return 0 on success, negative otherwise */
+int cbsp_tx_decoded(struct bsc_cbc_link *cbc, struct osmo_cbsp_decoded *cbsp)
+{
+	struct msgb *msg;
+
+	msg = osmo_cbsp_encode(cbc, cbsp);
+	if (!msg) {
+		LOGP(DCBS, LOGL_ERROR, "Unable to encode CBSP Message Type %s: %s\n",
+			get_value_string(cbsp_msg_type_names, cbsp->msg_type), osmo_cbsp_errstr);
+		talloc_free(cbsp);
+		return -1;
+	}
+	if (cbc->client.cli)
+		osmo_stream_cli_send(cbc->client.cli, msg);
+	else if (cbc->server.srv)
+		osmo_stream_srv_send(cbc->server.srv, msg);
+	else {
+		LOGP(DCBS, LOGL_ERROR, "Discarding CBSP Message, link is down: %s\n", msgb_hexdump(msg));
+		msgb_free(msg);
+	}
+
+	talloc_free(cbsp);
+	return 0;
+}
+
+static struct bsc_cbc_link *vty_cbc_data(struct vty *vty)
+{
+	return bsc_gsmnet->bsc_data->cbc;
+}
+
+/*********************************************************************************
+ * VTY Interface (Configuration + Introspection)
+ *********************************************************************************/
+
+DEFUN(cfg_cbc, cfg_cbc_cmd,
+	"cbc", "Configure CBSP Link to Cell Broadcast Centre\n")
+{
+	vty->node = CBC_NODE;
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_cbc_remote_ip, cfg_cbc_remote_ip_cmd,
+	"remote-ip A.B.C.D",
+	"IP Address of the Cell Broadcast Centre\n"
+	"IP Address of the Cell Broadcast Centre\n")
+{
+	struct bsc_cbc_link *cbc = vty_cbc_data(vty);
+	osmo_talloc_replace_string(cbc, &cbc->config.cbc_hostname, argv[0]);
+	return CMD_SUCCESS;
+}
+DEFUN(cfg_cbc_no_remote_ip, cfg_cbc_no_remote_ip_cmd,
+	"no remote-ip",
+	NO_STR "Remove IP address of CBC; disables outbound CBSP connections\n")
+{
+	struct bsc_cbc_link *cbc = vty_cbc_data(vty);
+	talloc_free(cbc->config.cbc_hostname);
+	cbc->config.cbc_hostname = NULL;
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_cbc_remote_port, cfg_cbc_remote_port_cmd,
+	"remote-port <1-65535>",
+	"TCP Port number of the Cell Broadcast Centre (Default: 48049)\n"
+	"TCP Port number of the Cell Broadcast Centre (Default: 48049)\n")
+{
+	struct bsc_cbc_link *cbc = vty_cbc_data(vty);
+	cbc->config.cbc_port = atoi(argv[0]);
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_cbc_listen_port, cfg_cbc_listen_port_cmd,
+	"listen-port <1-65535>",
+	"Local TCP port at which BSC listens for incoming CBSP connections from CBC\n"
+	"Local TCP port at which BSC listens for incoming CBSP connections from CBC\n")
+{
+	struct bsc_cbc_link *cbc = vty_cbc_data(vty);
+	cbc->config.listen_port = atoi(argv[0]);
+	return CMD_SUCCESS;
+}
+DEFUN(cfg_cbc_no_listen_port, cfg_cbc_no_listen_port_cmd,
+	"no listen-port",
+	NO_STR "Remove CBSP Listen Port; disables inbound CBSP connections\n")
+{
+	struct bsc_cbc_link *cbc = vty_cbc_data(vty);
+	cbc->config.listen_port = -1;
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_cbc_listen_ip, cfg_cbc_listen_ip_cmd,
+	"listen-ip A.B.C.D",
+	"Local IP Address where BSC listens for incoming CBC connections (Default: 0.0.0.0)\n"
+	"Local IP Address where BSC listens for incoming CBC connections\n")
+{
+	struct bsc_cbc_link *cbc = vty_cbc_data(vty);
+	osmo_talloc_replace_string(cbc, &cbc->config.listen_hostname, argv[0]);
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_cbc_osmo_ext, cfg_cbc_osmo_ext_cmd,
+	"osmocom-extensions",
+	"Enable Osmocom CBSP extensions (Default: Enabled)\n")
+{
+	struct bsc_cbc_link *cbc = vty_cbc_data(vty);
+	cbc->config.use_osmocom_ext = true;
+	return CMD_SUCCESS;
+}
+DEFUN(cfg_cbc_no_osmo_ext, cfg_cbc_no_osmo_ext_cmd,
+	"no osmocom-extensions",
+	NO_STR "Disable Osmocom CBSP extensions (Default: Enabled)\n")
+{
+	struct bsc_cbc_link *cbc = vty_cbc_data(vty);
+	cbc->config.use_osmocom_ext = false;
+	return CMD_SUCCESS;
+}
+
+static struct cmd_node cbc_node = {
+	CBC_NODE,
+	"%s(config-cbc)# ",
+	1,
+};
+
+static int config_write_cbc(struct vty *vty)
+{
+	struct bsc_cbc_link *cbc = vty_cbc_data(vty);
+
+	vty_out(vty, "cbc%s", VTY_NEWLINE);
+
+	if (cbc->config.cbc_hostname)
+		vty_out(vty, " remote-ip %s%s", cbc->config.cbc_hostname, VTY_NEWLINE);
+	else
+		vty_out(vty, " no remote-ip%s", VTY_NEWLINE);
+	vty_out(vty, " remote-port %u%s", cbc->config.cbc_port, VTY_NEWLINE);
+
+	if (cbc->config.listen_port >= 0)
+		vty_out(vty, " listen-port %u%s", cbc->config.listen_port, VTY_NEWLINE);
+	else
+		vty_out(vty, " no listen-port%s", VTY_NEWLINE);
+	vty_out(vty, " listen-ip %s%s", cbc->config.listen_hostname, VTY_NEWLINE);
+	if (cbc->config.use_osmocom_ext)
+		vty_out(vty, " osmocom-extensions%s", VTY_NEWLINE);
+	else
+		vty_out(vty, " no osmocom-extensions%s", VTY_NEWLINE);
+
+	return 0;
+}
+
+DEFUN(show_cbc, show_cbc_cmd,
+	"show cbc",
+	"Display state of CBC / CBSP\n")
+{
+	struct bsc_cbc_link *cbc = vty_cbc_data(vty);
+
+	if (!cbc->config.cbc_hostname)
+		vty_out(vty, "CBSP Client Config: Disabled%s", VTY_NEWLINE);
+	else {
+		vty_out(vty, "CBSP Client Config: CBC IP=%s, CBC Port=%u%s",
+			cbc->config.cbc_hostname, cbc->config.cbc_port, VTY_NEWLINE);
+		vty_out(vty, "CBSP Client Connection: %s%s",
+			cbc->client.sock_name ? cbc->client.sock_name : "Disconnected", VTY_NEWLINE);
+	}
+	if (cbc->config.listen_port < 0)
+		vty_out(vty, "CBSP Server Config: Disabled%s\n", VTY_NEWLINE);
+	else {
+		vty_out(vty, "CBSP Server Config: Listen IP=%s, Port=%u%s\n",
+			cbc->config.listen_hostname, cbc->config.listen_port, VTY_NEWLINE);
+		vty_out(vty, "CBSP Server Connection: %s%s",
+			cbc->server.sock_name ? cbc->server.sock_name : "Disconnected", VTY_NEWLINE);
+	}
+	return CMD_SUCCESS;
+}
+
+void cbc_vty_init(void)
+{
+	install_element(VIEW_NODE, &show_cbc_cmd);
+	install_element(CONFIG_NODE, &cfg_cbc_cmd);
+	install_node(&cbc_node, config_write_cbc);
+	install_element(CBC_NODE, &cfg_cbc_remote_ip_cmd);
+	install_element(CBC_NODE, &cfg_cbc_no_remote_ip_cmd);
+	install_element(CBC_NODE, &cfg_cbc_remote_port_cmd);
+	install_element(CBC_NODE, &cfg_cbc_listen_port_cmd);
+	install_element(CBC_NODE, &cfg_cbc_no_listen_port_cmd);
+	install_element(CBC_NODE, &cfg_cbc_listen_ip_cmd);
+	install_element(CBC_NODE, &cfg_cbc_osmo_ext_cmd);
+	install_element(CBC_NODE, &cfg_cbc_no_osmo_ext_cmd);
+}
diff --git a/src/osmo-bsc/gsm_data.c b/src/osmo-bsc/gsm_data.c
index ea33817..c2cfacf 100644
--- a/src/osmo-bsc/gsm_data.c
+++ b/src/osmo-bsc/gsm_data.c
@@ -767,6 +767,12 @@
 	.initial_mcs = 6,
 };
 
+static void bts_init_cbch_state(struct bts_smscb_chan_state *cstate, struct gsm_bts *bts)
+{
+	cstate->bts = bts;
+	INIT_LLIST_HEAD(&cstate->messages);
+}
+
 /* Initialize those parts that don't require osmo-bsc specific dependencies.
  * This part is shared among the thin programs in osmo-bsc/src/utils/.
  * osmo-bsc requires further initialization that pulls in more dependencies (see
@@ -945,6 +951,9 @@
 	}
 	bts->mr_half.num_modes = 3;
 
+	bts_init_cbch_state(&bts->cbch_basic, bts);
+	bts_init_cbch_state(&bts->cbch_extended, bts);
+
 	return bts;
 }
 
diff --git a/src/osmo-bsc/osmo_bsc_main.c b/src/osmo-bsc/osmo_bsc_main.c
index aba8c40..dacd61a 100644
--- a/src/osmo-bsc/osmo_bsc_main.c
+++ b/src/osmo-bsc/osmo_bsc_main.c
@@ -37,6 +37,7 @@
 #include <osmocom/bsc/bsc_subscriber.h>
 #include <osmocom/bsc/assignment_fsm.h>
 #include <osmocom/bsc/handover_fsm.h>
+#include <osmocom/bsc/smscb.h>
 
 #include <osmocom/ctrl/control_cmd.h>
 #include <osmocom/ctrl/control_if.h>
@@ -310,6 +311,10 @@
 		OSMO_ASSERT(ts->fi);
 		osmo_fsm_inst_dispatch(ts->fi, TS_EV_RSL_READY, NULL);
 	}
+
+	/* Start CBCH transmit timer if CBCH is present */
+	if (trx->nr == 0 && gsm_bts_get_cbch(trx->bts))
+		bts_cbch_timer_schedule(trx->bts);
 }
 
 static void all_ts_dispatch_event(struct gsm_bts_trx *trx, uint32_t event)
@@ -379,6 +384,8 @@
 			rate_ctr_inc(&trx->bts->bts_ctrs->ctr[BTS_CTR_BTS_RSL_FAIL]);
 			acc_ramp_abort(&trx->bts->acc_ramp);
 			all_ts_dispatch_event(trx, TS_EV_RSL_DOWN);
+			if (trx->nr == 0)
+				osmo_timer_del(&trx->bts->cbch_timer);
 		}
 
 		gsm_bts_mo_reset(trx->bts);
@@ -764,6 +771,11 @@
 		.description = "Local Call, Local Switch",
 		.enabled = 1, .loglevel = LOGL_NOTICE,
 	},
+	[DCBS] = {
+		.name = "DCBS",
+		.description = "Cell Broadcast System",
+		.enabled = 1, .loglevel = LOGL_NOTICE,
+	}
 
 };
 
@@ -912,6 +924,7 @@
 
 	handover_decision_1_init();
 	hodec2_init(bsc_gsmnet);
+	bsc_cbc_link_restart();
 
 	signal(SIGINT, &signal_handler);
 	signal(SIGTERM, &signal_handler);
diff --git a/src/osmo-bsc/smscb.c b/src/osmo-bsc/smscb.c
new file mode 100644
index 0000000..b7217fa
--- /dev/null
+++ b/src/osmo-bsc/smscb.c
@@ -0,0 +1,817 @@
+/* SMSCB (SMS Cell Broadcast) Handling for OsmoBSC */
+/*
+ * (C) 2019 by Harald Welte <laforge at gnumonks.org>
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <limits.h>
+
+#include <osmocom/core/stats.h>
+#include <osmocom/core/select.h>
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/talloc.h>
+
+#include <osmocom/gsm/cbsp.h>
+#include <osmocom/gsm/protocol/gsm_23_041.h>
+#include <osmocom/gsm/protocol/gsm_48_049.h>
+
+#include <osmocom/netif/stream.h>
+
+#include <osmocom/bsc/debug.h>
+#include <osmocom/bsc/gsm_data.h>
+#include <osmocom/bsc/smscb.h>
+#include <osmocom/bsc/vty.h>
+
+/*********************************************************************************
+ * Helper Functions
+ *********************************************************************************/
+
+/* replace the old head of an entire list with e new head; effectively moves the entire
+ * list from old to new head */
+static void llist_replace_head(struct llist_head *new, struct llist_head *old)
+{
+	if (llist_empty(old))
+		INIT_LLIST_HEAD(new);
+	else
+		__llist_add(new, old->prev, old->next);
+	INIT_LLIST_HEAD(old);
+}
+
+/*! Obtain SMSCB Channel State for given BTS (basic or extended CBCH) */
+struct bts_smscb_chan_state *bts_get_smscb_chan(struct gsm_bts *bts, bool extended)
+{
+	struct bts_smscb_chan_state *chan_state;
+
+	if (extended)
+		chan_state = &bts->cbch_extended;
+	else
+		chan_state = &bts->cbch_basic;
+
+	return chan_state;
+}
+
+/* do an ordered list insertion. we keep the list with increasing period, i.e. the most
+ * frequent message first */
+static void __bts_smscb_add(struct bts_smscb_chan_state *cstate, struct bts_smscb_message *new)
+{
+	struct bts_smscb_message *tmp, *tmp2;
+
+	if (llist_empty(&cstate->messages)) {
+		llist_add(&new->list, &cstate->messages);
+		return;
+	}
+
+	llist_for_each_entry_safe(tmp, tmp2, &cstate->messages, list) {
+		if (tmp->input.rep_period > new->input.rep_period) {
+			/* we found the first message with longer period than the new message,
+			 * we must insert ourselves before that one */
+			__llist_add(&new->list, tmp->list.prev, &tmp->list);
+			return;
+		}
+	}
+}
+
+/* stringify a SMSCB for logging */
+const char *bts_smscb_msg2str(const struct bts_smscb_message *smscb)
+{
+	static char buf[128];
+	snprintf(buf, sizeof(buf), "MsgId=0x%04x/SerialNr=0x%04x/Pages=%u/Period=%u/NumBcastReq=%u",
+		 smscb->input.msg_id, smscb->input.serial_nr, smscb->num_pages,
+		 smscb->input.rep_period, smscb->input.num_bcast_req);
+	return buf;
+}
+
+const char *bts_smscb_chan_state_name(const struct bts_smscb_chan_state *cstate)
+{
+	if (cstate == &cstate->bts->cbch_basic)
+		return "BASIC";
+	else if (cstate == &cstate->bts->cbch_extended)
+		return "EXTENDED";
+	else
+		return "UNKNOWN";
+}
+
+unsigned int bts_smscb_chan_load_percent(const struct bts_smscb_chan_state *cstate)
+{
+	unsigned int sched_arr_used = 0;
+	unsigned int i;
+
+	if (cstate->sched_arr_size == 0)
+		return 0;
+
+	/* count the number of used slots */
+	for (i = 0; i < cstate->sched_arr_size; i++) {
+		if (cstate->sched_arr[i])
+			sched_arr_used++;
+	}
+
+	OSMO_ASSERT(sched_arr_used <= UINT_MAX/100);
+	return (sched_arr_used * 100) / cstate->sched_arr_size;
+}
+
+unsigned int bts_smscb_chan_page_count(const struct bts_smscb_chan_state *cstate)
+{
+	struct bts_smscb_message *smscb;
+	unsigned int page_count = 0;
+
+	llist_for_each_entry(smscb, &cstate->messages, list)
+		page_count += smscb->num_pages;
+
+	return page_count;
+}
+
+
+/*! Obtain the Cell Global Identifier (CGI) of given BTS; returned in static buffer. */
+static struct osmo_cell_global_id *bts_get_cgi(struct gsm_bts *bts)
+{
+	static struct osmo_cell_global_id cgi;
+	cgi.lai.plmn = bts->network->plmn;
+	cgi.lai.lac = bts->location_area_code;
+	cgi.cell_identity = bts->cell_identity;
+	return &cgi;
+}
+
+/* represents the various lists that the BSC can create as part of a response */
+struct response_state {
+	struct osmo_cbsp_cell_list success;	/* osmo_cbsp_cell_ent */
+	struct llist_head fail;			/* osmo_cbsp_fail_ent */
+	struct osmo_cbsp_num_compl_list num_completed;	/* osmo_cbsp_num_compl_ent */
+	struct osmo_cbsp_loading_list loading;	/* osmo_cbsp_loading_ent */
+};
+
+/*! per-BTS callback function used by cbsp_per_bts().
+ *  \param[in] bts BTS currently being processed
+ *  \param[in] dec decoded CBSP message currently being processed
+ *  \param r_state response state accumulating cell lists (success/failure/...)
+ *  \param priv opaque private data provided by caller of cbsp_per_bts()
+ *  \returns 0 on success; negative TS 48.049 cause value on error */
+typedef int bts_cb_fn(struct gsm_bts *bts, const struct osmo_cbsp_decoded *dec,
+		      struct response_state *r_state, void *priv);
+
+/* append a success for given cell to response state */
+static void append_success(struct response_state *r_state, struct gsm_bts *bts)
+{
+	struct osmo_cbsp_cell_ent *cent = talloc_zero(r_state, struct osmo_cbsp_cell_ent);
+	struct osmo_cell_global_id *cgi = bts_get_cgi(bts);
+
+	LOG_BTS(bts, DCBS, LOGL_INFO, "Success\n");
+
+	OSMO_ASSERT(cent);
+
+	cent->cell_id.global = *cgi;
+	llist_add_tail(&cent->list, &r_state->success.list);
+}
+
+/* append a failure for given cell to response state */
+static void append_fail(struct response_state *r_state, struct gsm_bts *bts, uint8_t cause)
+{
+	struct osmo_cbsp_fail_ent *fent = talloc_zero(r_state, struct osmo_cbsp_fail_ent);
+	struct osmo_cell_global_id *cgi = bts_get_cgi(bts);
+
+	LOG_BTS(bts, DCBS, LOGL_NOTICE, "Failure Cause 0x%02x\n", cause);
+
+	OSMO_ASSERT(fent);
+
+	fent->id_discr = CELL_IDENT_WHOLE_GLOBAL;
+	fent->cell_id.global = *cgi;
+	fent->cause = cause;
+	llist_add_tail(&fent->list, &r_state->fail);
+}
+
+/* append a 'number of broadcasts completed' for given cell to response state */
+static void append_bcast_compl(struct response_state *r_state, struct gsm_bts *bts,
+				struct bts_smscb_message *smscb)
+{
+	struct osmo_cbsp_num_compl_ent *cent = talloc_zero(r_state, struct osmo_cbsp_num_compl_ent);
+	struct osmo_cell_global_id *cgi = bts_get_cgi(bts);
+
+	LOG_BTS(bts, DCBS, LOGL_DEBUG, "Number of Broadcasts Completed: %u\n", smscb->bcast_count);
+
+	OSMO_ASSERT(cent);
+
+	r_state->num_completed.id_discr = CELL_IDENT_WHOLE_GLOBAL;
+	cent->cell_id.global = *cgi;
+	if (smscb->bcast_count > INT16_MAX) {
+		cent->num_compl = INT16_MAX;
+		cent->num_bcast_info = 0x01; /* Overflow */
+	} else {
+		cent->num_compl = smscb->bcast_count;
+		cent->num_bcast_info = 0x00;
+	}
+	llist_add_tail(&cent->list, &r_state->num_completed.list);
+}
+
+/*! Iterate over all BTSs, find matching ones, execute command on BTS, add result
+ *  to succeeded/failed lists.
+ *  \param[in] net GSM network in which we operate
+ *  \param[in] caller-allocated Response state structure collecting results
+ *  \param[in] cell_list Decoded CBSP cell list describing BTSs to operate on
+ *  \param[in] cb_fn Call-back function to call for each matching BTS
+ *  \param[in] priv Opqaue private data; passed to cb_fn
+ *  */
+static int cbsp_per_bts(struct gsm_network *net, struct response_state *r_state,
+			const struct osmo_cbsp_cell_list *cell_list,
+			bts_cb_fn *cb_fn, const struct osmo_cbsp_decoded *dec, void *priv)
+{
+	struct osmo_cbsp_cell_ent *ent;
+	struct gsm_bts *bts;
+	uint8_t bts_status[net->num_bts];
+	int rc, ret = 0;
+
+	memset(bts_status, 0, sizeof(bts_status));
+	INIT_LLIST_HEAD(&r_state->success.list);
+	INIT_LLIST_HEAD(&r_state->fail);
+	INIT_LLIST_HEAD(&r_state->num_completed.list);
+	INIT_LLIST_HEAD(&r_state->loading.list);
+
+	/* special case as cell_list->list is empty in this case */
+	if (cell_list->id_discr == CELL_IDENT_BSS) {
+		llist_for_each_entry(bts, &net->bts_list, list) {
+			bts_status[bts->nr] = 1;
+			/* call function on this BTS */
+			rc = cb_fn(bts, dec, r_state, priv);
+			if (rc < 0) {
+				append_fail(r_state, bts, -rc);
+				ret = -1;
+			} else
+				append_success(r_state, bts);
+		}
+	} else {
+		/* normal case: iterate over cell list */
+		llist_for_each_entry(ent, &cell_list->list, list) {
+			bool found_at_least_one = false;
+			/* find all matching BTSs for this entry */
+			llist_for_each_entry(bts, &net->bts_list, list) {
+				struct gsm0808_cell_id cell_id = {
+					.id_discr = cell_list->id_discr,
+					.id = ent->cell_id
+				};
+				if (!gsm_bts_matches_cell_id(bts, &cell_id))
+					continue;
+				found_at_least_one = true;
+				/* skip any BTSs which we've already processed */
+				if (bts_status[bts->nr])
+					continue;
+				bts_status[bts->nr] = 1;
+				/* call function on this BTS */
+				rc = cb_fn(bts, dec, r_state, priv);
+				if (rc < 0) {
+					append_fail(r_state, bts, -rc);
+					ret = -1;
+				} else
+					append_success(r_state, bts);
+			}
+			if (!found_at_least_one) {
+				struct osmo_cbsp_fail_ent *fent;
+				LOGP(DCBS, LOGL_NOTICE, "CBSP: Couldn't find a single matching BTS\n");
+				fent = talloc_zero(r_state, struct osmo_cbsp_fail_ent);
+				OSMO_ASSERT(fent);
+				fent->id_discr = cell_list->id_discr;
+				fent->cell_id = ent->cell_id;
+				llist_add_tail(&fent->list, &r_state->fail);
+				ret = -1;
+			}
+		}
+	}
+	return ret;
+}
+
+/*! Find an existing SMSCB message within given BTS.
+ *  \param[in] chan_state BTS CBCH channel state
+ *  \param[in] msg_id Message Id of to-be-found message
+ *  \param[in] serial_nr Serial Number of to-be-found message
+ *  \returns SMSCB message if found; NULL otherwise */
+struct bts_smscb_message *bts_find_smscb(struct bts_smscb_chan_state *chan_state,
+					 uint16_t msg_id, uint16_t serial_nr)
+{
+	struct bts_smscb_message *smscb;
+
+	llist_for_each_entry(smscb, &chan_state->messages, list) {
+		if (smscb->input.msg_id == msg_id && smscb->input.serial_nr == serial_nr)
+			return smscb;
+	}
+	return NULL;
+}
+
+/*! create a new SMSCB message for specified BTS; don't link it yet.
+ *  \param[in] bts BTS for which the SMSCB is to be allocated
+ *  \param[in] wrepl CBSP write-replace message
+ *  \returns callee-allocated SMSCB message filled with data from wrepl */
+static struct bts_smscb_message *bts_smscb_msg_from_wrepl(struct gsm_bts *bts,
+						const struct osmo_cbsp_write_replace *wrepl)
+{
+	struct bts_smscb_message *smscb = talloc_zero(bts, struct bts_smscb_message);
+	struct osmo_cbsp_content *cont;
+	int i;
+
+	if (!smscb)
+		return NULL;
+
+	OSMO_ASSERT(wrepl->is_cbs);
+
+	/* initialize all pages inside the message */
+	for (i = 0; i < ARRAY_SIZE(smscb->page); i++) {
+		struct bts_smscb_page *page = &smscb->page[i];
+		page->nr = i+1; /* page numbers are 1-based */
+		page->msg = smscb;
+	}
+
+	/* initialize "header" part */
+	smscb->input.msg_id = wrepl->msg_id;
+	smscb->input.serial_nr = wrepl->new_serial_nr;
+	smscb->input.category = wrepl->u.cbs.category;
+	smscb->input.rep_period = wrepl->u.cbs.rep_period;
+	smscb->input.num_bcast_req = wrepl->u.cbs.num_bcast_req;
+	smscb->input.dcs = wrepl->u.cbs.dcs;
+	smscb->num_pages = llist_count(&wrepl->u.cbs.msg_content);
+	if (smscb->num_pages > ARRAY_SIZE(smscb->page)) {
+		LOG_BTS(bts, DCBS, LOGL_ERROR, "SMSCB with too many pages (%u > %lu)\n",
+			smscb->num_pages, ARRAY_SIZE(smscb->page));
+		talloc_free(smscb);
+		return NULL;
+	}
+
+	i = 0;
+	llist_for_each_entry(cont, &wrepl->u.cbs.msg_content, list) {
+		struct gsm23041_msg_param_gsm *msg_param;
+		struct bts_smscb_page *page;
+		size_t bytes_used;
+
+		/* we have just ensured a few lines above that this cannot overflow */
+		page = &smscb->page[i++];
+		msg_param = (struct gsm23041_msg_param_gsm *) &page->data[0];
+
+		/* build 6 byte header according to TS 23.041 9.4.1.2 */
+		osmo_store16be(wrepl->new_serial_nr, &msg_param->serial_nr);
+		osmo_store16be(wrepl->msg_id, &msg_param->message_id);
+		msg_param->dcs = wrepl->u.cbs.dcs;
+		msg_param->page_param.num_pages = smscb->num_pages;
+		msg_param->page_param.page_nr = page->nr;
+
+		OSMO_ASSERT(cont->user_len <= ARRAY_SIZE(cont->data));
+		OSMO_ASSERT(cont->user_len <= ARRAY_SIZE(page->data) - sizeof(*msg_param));
+		memcpy(&msg_param->content[0], cont->data, cont->user_len);
+		bytes_used = sizeof(*msg_param) + cont->user_len;
+		/* compute number of valid blocks in page */
+		page->num_blocks = bytes_used / 22;
+		if (bytes_used % 22)
+			page->num_blocks += 1;
+	}
+
+	return smscb;
+}
+
+/*! remove a SMSCB message */
+void bts_smscb_del(struct bts_smscb_message *smscb, struct bts_smscb_chan_state *cstate,
+		   const char *reason)
+{
+	struct bts_smscb_page **arr;
+	int rc;
+
+	LOG_BTS(cstate->bts, DCBS, LOGL_INFO, "%s Deleting %s (Reason: %s)\n",
+		bts_smscb_chan_state_name(cstate), bts_smscb_msg2str(smscb), reason);
+	llist_del(&smscb->list);
+
+	/* we must recompute the scheduler array here, as the old one will have pointers
+	 * to the pages of the just-to-be-deleted message */
+	rc = bts_smscb_gen_sched_arr(cstate, &arr);
+	if (rc < 0) {
+		LOG_BTS(cstate->bts, DCBS, LOGL_ERROR, "Cannot generate new CBCH scheduler array after "
+			"removing message %s. WTF?\n", bts_smscb_msg2str(smscb));
+		/* we cannot free the message now, to ensure the page pointers in the old
+		 * array are still valid. let's re-add it to keep things sane */
+		__bts_smscb_add(cstate, smscb);
+	} else {
+		/* success */
+		talloc_free(smscb);
+	}
+}
+
+
+/*********************************************************************************
+ * Transmit of CBSP to CBC
+ *********************************************************************************/
+
+/* transmit a CBSP RESTART message stating all message data was lost for entire BSS */
+int cbsp_tx_restart(struct bsc_cbc_link *cbc, bool is_emerg)
+{
+	struct osmo_cbsp_decoded *cbsp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_RESTART);
+
+	if (is_emerg)
+		cbsp->u.restart.bcast_msg_type = 0x01;
+	cbsp->u.restart.recovery_ind = 0x01; /* message data lost */
+	cbsp->u.restart.cell_list.id_discr = CELL_IDENT_BSS;
+
+	return cbsp_tx_decoded(cbc, cbsp);
+}
+
+/* transmit a CBSP KEEPALIVE COMPLETE to the CBC */
+static int tx_cbsp_keepalive_compl(struct bsc_cbc_link *cbc)
+{
+	struct osmo_cbsp_decoded *cbsp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_KEEP_ALIVE_COMPL);
+	return cbsp_tx_decoded(cbc, cbsp);
+}
+
+/*********************************************************************************
+ * Per-BTS Processing of CBSP from CBC, called via cbsp_per_bts()
+ *********************************************************************************/
+
+/*! Try to execute a write-replace operation; roll-back if it fails.
+ *  \param[in] chan_state BTS CBCH channel state
+ *  \param[in] extended_cbch Basic (false) or Extended (true) CBCH
+ *  \param[in] new_msg New SMSCB message which should be added
+ *  \param[in] exclude_msg Existing SMSCB message that shall be replaced (if possible). Can be NULL
+ *  \return 0 on success; negative on error */
+static int bts_try_write_replace(struct bts_smscb_chan_state *chan_state,
+				 struct bts_smscb_message *new_msg,
+				 struct bts_smscb_message *exclude_msg,
+				 struct response_state *r_state)
+{
+	struct bts_smscb_page **arr;
+	int rc;
+
+	if (exclude_msg) {
+		/* temporarily remove from list of SMSCB */
+		llist_del(&exclude_msg->list);
+	}
+	/* temporarily add new_msg to list of SMSCB */
+	__bts_smscb_add(chan_state, new_msg);
+
+	/* attempt to create scheduling array */
+	rc = bts_smscb_gen_sched_arr(chan_state, &arr);
+	if (rc < 0) {
+		/* it didn't work out; we couldn't schedule it */
+		/* remove the new message again */
+		llist_del(&new_msg->list);
+		/* up to the caller to free() it */
+		if (exclude_msg) {
+			/* re-add the temporarily removed message */
+			__bts_smscb_add(chan_state, new_msg);
+		}
+		return -1;
+	}
+
+	/* success! */
+	if (exclude_msg) {
+		LOG_BTS(chan_state->bts, DCBS, LOGL_INFO, "%s Replaced MsgId=0x%04x/Serial=0x%04x, "
+			"pages(%u -> %u), period(%u -> %u), num_bcast(%u -> %u)\n",
+			bts_smscb_chan_state_name(chan_state),
+			new_msg->input.msg_id, new_msg->input.serial_nr,
+			exclude_msg->num_pages, new_msg->num_pages,
+			exclude_msg->input.rep_period, new_msg->input.rep_period,
+			exclude_msg->input.num_bcast_req, new_msg->input.num_bcast_req);
+		append_bcast_compl(r_state, chan_state->bts, exclude_msg);
+		talloc_free(exclude_msg);
+	} else
+		LOG_BTS(chan_state->bts, DCBS, LOGL_INFO, "%s Added %s\n",
+			bts_smscb_chan_state_name(chan_state), bts_smscb_msg2str(new_msg));
+
+	/* replace array with new one */
+	talloc_free(chan_state->sched_arr);
+	chan_state->sched_arr = arr;
+	chan_state->sched_arr_size = rc;
+	chan_state->next_idx = 0;
+	return 0;
+}
+
+
+static int bts_rx_write_replace(struct gsm_bts *bts, const struct osmo_cbsp_decoded *dec,
+				struct response_state *r_state, void *priv)
+{
+	const struct osmo_cbsp_write_replace *wrepl = &dec->u.write_replace;
+	bool extended_cbch = wrepl->u.cbs.channel_ind;
+	struct bts_smscb_chan_state *chan_state = bts_get_smscb_chan(bts, extended_cbch);
+	struct bts_smscb_message *smscb;
+	int rc;
+
+	if (!wrepl->is_cbs) {
+		LOG_BTS(bts, DCBS, LOGL_ERROR, "(Primary) Emergency Message not supported\n");
+		return -CBSP_CAUSE_CB_NOT_SUPPORTED;
+	}
+
+	/* check if cell has a CBCH at all */
+	if (!gsm_bts_get_cbch(bts))
+		return -CBSP_CAUSE_CB_NOT_SUPPORTED;
+
+	/* check for duplicate */
+	if (bts_find_smscb(chan_state, wrepl->msg_id, wrepl->new_serial_nr))
+		return -CBSP_CAUSE_MSG_REF_ALREADY_USED;
+
+	if (!wrepl->old_serial_nr) { /* new message */
+		/* create new message */
+		smscb = bts_smscb_msg_from_wrepl(bts, wrepl);
+		if (!smscb)
+			return -CBSP_CAUSE_BSC_MEMORY_EXCEEDED;
+		/* check if scheduling permits this additional message */
+		rc = bts_try_write_replace(chan_state, smscb, NULL, r_state);
+		if (rc < 0) {
+			talloc_free(smscb);
+			return -CBSP_CAUSE_BSC_CAPACITY_EXCEEDED;
+		}
+	} else { /* modify / replace existing message */
+		struct bts_smscb_message *smscb_old;
+		/* find existing message */
+		smscb_old = bts_find_smscb(chan_state, wrepl->msg_id, *wrepl->old_serial_nr);
+		if (!smscb_old)
+			return -CBSP_CAUSE_MSG_REF_NOT_IDENTIFIED;
+		/* create new message */
+		smscb = bts_smscb_msg_from_wrepl(bts, wrepl);
+		if (!smscb)
+			return -CBSP_CAUSE_BSC_MEMORY_EXCEEDED;
+		/* check if scheduling permits this modified message */
+		rc = bts_try_write_replace(chan_state, smscb, smscb_old, r_state);
+		if (rc < 0) {
+			talloc_free(smscb);
+			return -CBSP_CAUSE_BSC_CAPACITY_EXCEEDED;
+		}
+	}
+	return 0;
+}
+
+static int bts_rx_kill(struct gsm_bts *bts, const struct osmo_cbsp_decoded *dec,
+			struct response_state *r_state, void *priv)
+{
+	const struct osmo_cbsp_kill *kill = &dec->u.kill;
+	struct bts_smscb_chan_state *chan_state;
+	struct bts_smscb_message *smscb;
+	bool extended = false;
+
+	if (kill->channel_ind && *kill->channel_ind == 0x01)
+		extended = true;
+	chan_state = bts_get_smscb_chan(bts, extended);
+
+	/* Find message by msg_id + old_serial_nr */
+	smscb = bts_find_smscb(chan_state, kill->msg_id, kill->old_serial_nr);
+	if (!smscb)
+		return -CBSP_CAUSE_MSG_REF_NOT_IDENTIFIED;
+
+	/* Remove it */
+	bts_smscb_del(smscb, chan_state, "KILL");
+	return 0;
+}
+
+static int bts_rx_reset(struct gsm_bts *bts, const struct osmo_cbsp_decoded *dec,
+			struct response_state *r_state, void *priv)
+{
+	struct bts_smscb_chan_state *chan_state;
+	struct bts_smscb_message *smscb, *smscb2;
+
+	/* remove all SMSCB from CBCH BASIC this BTS */
+	chan_state = bts_get_smscb_chan(bts, false);
+	llist_for_each_entry_safe(smscb, smscb2, &chan_state->messages, list)
+		bts_smscb_del(smscb, chan_state, "RESET");
+
+	/* remove all SMSCB from CBCH EXTENDED this BTS */
+	chan_state = bts_get_smscb_chan(bts, true);
+	llist_for_each_entry_safe(smscb, smscb2, &chan_state->messages, list)
+		bts_smscb_del(smscb, chan_state, "RESET");
+
+	return 0;
+}
+
+/*********************************************************************************
+ * Receive of CBSP from CBC
+ *********************************************************************************/
+
+static int cbsp_rx_write_replace(struct bsc_cbc_link *cbc, const struct osmo_cbsp_decoded *dec)
+{
+	const struct osmo_cbsp_write_replace *wrepl = &dec->u.write_replace;
+	struct gsm_network *net = cbc->net;
+	struct response_state *r_state = talloc_zero(cbc, struct response_state);
+	struct osmo_cbsp_decoded *resp;
+	enum cbsp_channel_ind channel_ind;
+	int rc;
+
+	LOGP(DCBS, LOGL_INFO, "CBSP Rx WRITE_REPLACE (%s)\n", wrepl->is_cbs ? "CBS" : "EMERGENCY");
+
+	rc = cbsp_per_bts(net, r_state, &dec->u.write_replace.cell_list,
+			  bts_rx_write_replace, dec, NULL);
+	/* generate response */
+	if (rc < 0) {
+		resp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_WRITE_REPLACE_FAIL);
+		struct osmo_cbsp_write_replace_failure *fail = &resp->u.write_replace_fail;
+		fail->msg_id = wrepl->msg_id;
+		fail->new_serial_nr = wrepl->new_serial_nr;
+		fail->old_serial_nr = wrepl->old_serial_nr;
+		llist_replace_head(&fail->fail_list, &r_state->fail);
+		fail->cell_list.id_discr = r_state->success.id_discr;
+		llist_replace_head(&fail->cell_list.list, &r_state->success.list);
+		if (wrepl->is_cbs) {
+			channel_ind = wrepl->u.cbs.channel_ind;
+			fail->channel_ind = &channel_ind;
+		}
+		if (wrepl->old_serial_nr) {
+			fail->num_compl_list.id_discr = r_state->num_completed.id_discr;
+			llist_replace_head(&fail->num_compl_list.list, &r_state->num_completed.list);
+		}
+	} else {
+		resp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_WRITE_REPLACE_COMPL);
+		struct osmo_cbsp_write_replace_complete *compl = &resp->u.write_replace_compl;
+		compl->msg_id = wrepl->msg_id;
+		compl->new_serial_nr = wrepl->new_serial_nr;
+		compl->old_serial_nr = wrepl->old_serial_nr;
+		compl->cell_list.id_discr = r_state->success.id_discr;
+		llist_replace_head(&compl->cell_list.list, &r_state->success.list);
+		if (wrepl->is_cbs) {
+			channel_ind = wrepl->u.cbs.channel_ind;
+			compl->channel_ind = &channel_ind;
+		}
+		if (wrepl->old_serial_nr) {
+			compl->num_compl_list.id_discr = r_state->num_completed.id_discr;
+			llist_replace_head(&compl->num_compl_list.list, &r_state->num_completed.list);
+		}
+	}
+
+	cbsp_tx_decoded(cbc, resp);
+	talloc_free(r_state);
+	return rc;
+}
+
+static int cbsp_rx_keep_alive(struct bsc_cbc_link *cbc, const struct osmo_cbsp_decoded *dec)
+{
+	LOGP(DCBS, LOGL_DEBUG, "CBSP Rx KEEP_ALIVE\n");
+
+	/* FIXME: repetition period */
+	return tx_cbsp_keepalive_compl(cbc);
+}
+
+static int cbsp_rx_kill(struct bsc_cbc_link *cbc, const struct osmo_cbsp_decoded *dec)
+{
+	const struct osmo_cbsp_kill *kill = &dec->u.kill;
+	struct gsm_network *net = cbc->net;
+	struct response_state *r_state = talloc_zero(cbc, struct response_state);
+	struct osmo_cbsp_decoded *resp;
+	int rc;
+
+	LOGP(DCBS, LOGL_DEBUG, "CBSP Rx KILL\n");
+
+	rc = cbsp_per_bts(net, r_state, &dec->u.kill.cell_list, bts_rx_kill, dec, NULL);
+	if (rc < 0) {
+		resp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_KILL_FAIL);
+		struct osmo_cbsp_kill_failure *fail = &resp->u.kill_fail;
+		fail->msg_id = kill->msg_id;
+		fail->old_serial_nr = kill->old_serial_nr;
+		fail->channel_ind = kill->channel_ind;
+		llist_replace_head(&fail->fail_list, &r_state->fail);
+
+		fail->cell_list.id_discr = r_state->success.id_discr;
+		llist_replace_head(&fail->cell_list.list, &r_state->success.list);
+
+		fail->num_compl_list.id_discr = r_state->num_completed.id_discr;
+		llist_replace_head(&fail->num_compl_list.list, &r_state->num_completed.list);
+	} else {
+		resp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_KILL_COMPL);
+		struct osmo_cbsp_kill_complete *compl = &resp->u.kill_compl;
+		compl->msg_id = kill->msg_id;
+		compl->old_serial_nr = kill->old_serial_nr;
+		compl->channel_ind = kill->channel_ind;
+
+		compl->cell_list.id_discr = r_state->success.id_discr;
+		llist_replace_head(&compl->cell_list.list, &r_state->success.list);
+
+		compl->num_compl_list.id_discr = r_state->num_completed.id_discr;
+		llist_replace_head(&compl->num_compl_list.list, &r_state->num_completed.list);
+	}
+
+	cbsp_tx_decoded(cbc, resp);
+	talloc_free(r_state);
+	return rc;
+}
+
+static int cbsp_rx_reset(struct bsc_cbc_link *cbc, const struct osmo_cbsp_decoded *dec)
+{
+	struct gsm_network *net = cbc->net;
+	struct response_state *r_state = talloc_zero(cbc, struct response_state);
+	struct osmo_cbsp_decoded *resp;
+	int rc;
+
+	LOGP(DCBS, LOGL_DEBUG, "CBSP Rx RESET\n");
+
+	rc = cbsp_per_bts(net, r_state, &dec->u.reset.cell_list, bts_rx_reset, dec, NULL);
+	if (rc < 0) {
+		resp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_RESET_FAIL);
+		struct osmo_cbsp_reset_failure *fail = &resp->u.reset_fail;
+		llist_replace_head(&fail->fail_list, &r_state->fail);
+
+		fail->cell_list.id_discr = r_state->success.id_discr;
+		llist_replace_head(&fail->cell_list.list, &r_state->success.list);
+	} else {
+		resp = osmo_cbsp_decoded_alloc(cbc, CBSP_MSGT_RESET_COMPL);
+		struct osmo_cbsp_reset_complete *compl = &resp->u.reset_compl;
+		if (dec->u.reset.cell_list.id_discr == CELL_IDENT_BSS) {
+			/* replace the list of individual cell identities with CELL_IDENT_BSS */
+			compl->cell_list.id_discr = CELL_IDENT_BSS;
+			/* no need to free success_list entries, hierarchical talloc works */
+		} else {
+			compl->cell_list.id_discr = r_state->success.id_discr;
+			llist_replace_head(&compl->cell_list.list, &r_state->success.list);
+		}
+	}
+	cbsp_tx_decoded(cbc, resp);
+	talloc_free(r_state);
+	return rc;
+}
+
+
+/*! process an incoming, already decoded CBSP message from the CBC.
+ *  \param[in] cbc link to the CBC
+ *  \param[in] dec decoded CBSP message structure. Ownership not transferred.
+ *  \returns 0 on success; negative on error. */
+int cbsp_rx_decoded(struct bsc_cbc_link *cbc, const struct osmo_cbsp_decoded *dec)
+{
+	int rc = -1;
+
+	switch (dec->msg_type) {
+	case CBSP_MSGT_WRITE_REPLACE: 	/* create or modify message */
+		rc = cbsp_rx_write_replace(cbc, dec);
+		break;
+	case CBSP_MSGT_KEEP_ALIVE:	/* solicit an acknowledgement */
+		rc = cbsp_rx_keep_alive(cbc, dec);
+		break;
+	case CBSP_MSGT_KILL:		/* remove message */
+		rc = cbsp_rx_kill(cbc, dec);
+		break;
+	case CBSP_MSGT_RESET:		/* stop broadcasting of all messages */
+		rc = cbsp_rx_reset(cbc, dec);
+		break;
+	case CBSP_MSGT_LOAD_QUERY:
+	case CBSP_MSGT_MSG_STATUS_QUERY:
+	case CBSP_MSGT_SET_DRX:
+		LOGP(DCBS, LOGL_ERROR, "Received Unimplemented CBSP Message Type %s",
+			get_value_string(cbsp_msg_type_names, dec->msg_type));
+		/* we should implement those eventually */
+		break;
+	default:
+		LOGP(DCBS, LOGL_ERROR, "Received Unknown/Unexpected CBSP Message Type %s",
+			get_value_string(cbsp_msg_type_names, dec->msg_type));
+		break;
+	}
+	return rc;
+}
+
+/*********************************************************************************
+ * VTY Interface (Introspection)
+ *********************************************************************************/
+
+static void vty_dump_smscb_chan_state(struct vty *vty, const struct bts_smscb_chan_state *cs)
+{
+	const struct bts_smscb_message *sm;
+
+	vty_out(vty, "%s CBCH:%s", cs == &cs->bts->cbch_basic ? "BASIC" : "EXTENDED", VTY_NEWLINE);
+
+	vty_out(vty, " MsgId | SerNo | Pg |      Category | Perd | #Tx  | #Req | DCS%s", VTY_NEWLINE);
+	vty_out(vty, "-------|-------|----|---------------|------|------|------|----%s", VTY_NEWLINE);
+	llist_for_each_entry(sm, &cs->messages, list) {
+		vty_out(vty, "  %04x |  %04x | %2u | %13s | %4u | %4u | %4u | %02x%s",
+			sm->input.msg_id, sm->input.serial_nr, sm->num_pages,
+			get_value_string(cbsp_category_names, sm->input.category),
+			sm->input.rep_period, sm->bcast_count, sm->input.num_bcast_req,
+			sm->input.dcs, VTY_NEWLINE);
+	}
+	vty_out(vty, "%s", VTY_NEWLINE);
+}
+
+DEFUN(bts_show_cbs, bts_show_cbs_cmd,
+	"show bts <0-255> smscb [(basic|extended)]",
+	SHOW_STR "Display information about a BTS\n" "BTS number\n"
+	"SMS Cell Broadcast State\n"
+	"Show only information related to CBCH BASIC\n"
+	"Show only information related to CBCH EXTENDED\n")
+{
+	struct gsm_network *net = gsmnet_from_vty(vty);
+	int bts_nr = atoi(argv[0]);
+	struct gsm_bts *bts;
+
+	if (bts_nr >= net->num_bts) {
+		vty_out(vty, "%% can't find BTS '%s'%s", argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	bts = gsm_bts_num(net, bts_nr);
+
+	if (argc < 2 || !strcmp(argv[1], "basic"))
+		vty_dump_smscb_chan_state(vty, &bts->cbch_basic);
+	if (argc < 2 || !strcmp(argv[1], "extended"))
+		vty_dump_smscb_chan_state(vty, &bts->cbch_extended);
+
+	return CMD_SUCCESS;
+}
+
+void smscb_vty_init(void)
+{
+	install_element_ve(&bts_show_cbs_cmd);
+}
diff --git a/tests/handover/Makefile.am b/tests/handover/Makefile.am
index 84c341e..8bd0012 100644
--- a/tests/handover/Makefile.am
+++ b/tests/handover/Makefile.am
@@ -11,6 +11,7 @@
 	$(LIBOSMOCTRL_CFLAGS) \
 	$(LIBOSMOVTY_CFLAGS) \
 	$(LIBOSMOABIS_CFLAGS) \
+	$(LIBOSMONETIF_CFLAGS) \
 	$(LIBOSMOSIGTRAN_CFLAGS) \
 	$(LIBOSMOMGCPCLIENT_CFLAGS) \
 	$(NULL)
@@ -91,11 +92,15 @@
 	$(top_builddir)/src/osmo-bsc/rest_octets.o \
 	$(top_builddir)/src/osmo-bsc/system_information.o \
 	$(top_builddir)/src/osmo-bsc/timeslot_fsm.o \
+	$(top_builddir)/src/osmo-bsc/smscb.o \
+	$(top_builddir)/src/osmo-bsc/cbch_scheduler.o \
+	$(top_builddir)/src/osmo-bsc/cbsp_link.o \
 	$(LIBOSMOCORE_LIBS) \
 	$(LIBOSMOGSM_LIBS) \
 	$(LIBOSMOCTRL_LIBS) \
 	$(LIBOSMOVTY_LIBS) \
 	$(LIBOSMOABIS_LIBS) \
+	$(LIBOSMONETIF_LIBS) \
 	$(LIBOSMOSIGTRAN_LIBS) \
 	$(LIBOSMOMGCPCLIENT_LIBS) \
 	$(NULL)

-- 
To view, visit https://gerrit.osmocom.org/c/osmo-bsc/+/15373
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings

Gerrit-Project: osmo-bsc
Gerrit-Branch: master
Gerrit-Change-Id: Ia0a0de862a104d0f447a5d6e56c7c83981b825c7
Gerrit-Change-Number: 15373
Gerrit-PatchSet: 1
Gerrit-Owner: laforge <laforge at gnumonks.org>
Gerrit-MessageType: newchange
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.osmocom.org/pipermail/gerrit-log/attachments/20190901/adca7d4a/attachment.htm>


More information about the gerrit-log mailing list