dexter has uploaded this change for review. (
https://gerrit.osmocom.org/c/osmo-pcu/+/31176 )
Change subject: WIP support for ericsson CCU
......................................................................
WIP support for ericsson CCU
Change-Id: I5c0a76667339ca984a12cbd2052f5d9e5b0f9c4d
---
M configure.ac
M src/Makefile.am
A src/ericsson-rbs/er_ccu_descr.h
A src/ericsson-rbs/er_ccu_if.c
A src/ericsson-rbs/er_ccu_if.h
A src/ericsson-rbs/er_ccu_l1_if.c
A src/ericsson-rbs/er_ccu_l1_if.h
M src/gprs_debug.c
M src/gprs_debug.h
M src/pcu_main.cpp
10 files changed, 850 insertions(+), 1 deletion(-)
git pull ssh://gerrit.osmocom.org:29418/osmo-pcu refs/changes/76/31176/1
diff --git a/configure.ac b/configure.ac
index 3f38d93..f78d2be 100644
--- a/configure.ac
+++ b/configure.ac
@@ -87,6 +87,8 @@
PKG_CHECK_MODULES(LIBOSMOCTRL, libosmoctrl >= 1.7.0)
PKG_CHECK_MODULES(LIBOSMOGSM, libosmogsm >= 1.7.0)
PKG_CHECK_MODULES(LIBOSMOGB, libosmogb >= 1.7.0)
+PKG_CHECK_MODULES(LIBOSMOABIS, libosmoabis >= 1.3.0)
+PKG_CHECK_MODULES(LIBOSMOTRAU, libosmotrau >= 1.3.0)
AC_MSG_CHECKING([whether to enable direct DSP access for PDCH of sysmocom-bts])
AC_ARG_ENABLE(sysmocom-dsp,
@@ -167,6 +169,14 @@
CPPFLAGS=$oldCPPFLAGS
fi
+AC_MSG_CHECKING([whether to enable direct E1 CCU access for PDCH of Ericsson RBS])
+AC_ARG_ENABLE(er-e1-ccu,
+ AC_HELP_STRING([--enable-er-e1-ccu],
+ [enable code for Ericsson RBS E1 CCU [default=no]]),
+
[enable_er_e1_ccu="$enableval"],[enable_er_e1_ccu="no"])
+AC_MSG_RESULT([$enable_er_e1_ccu])
+AM_CONDITIONAL(ENABLE_ER_E1_CCU, test "x$enable_er_e1_ccu" = "xyes")
+
AC_ARG_ENABLE([vty_tests],
AC_HELP_STRING([--enable-vty-tests],
[Include the VTY tests in make check [default=no]]),
diff --git a/src/Makefile.am b/src/Makefile.am
index e020ffa..7c2b995 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -19,7 +19,7 @@
#
AUTOMAKE_OPTIONS = subdir-objects
-AM_CPPFLAGS = -I$(top_srcdir)/include $(STD_DEFINES_AND_INCLUDES) $(LIBOSMOCORE_CFLAGS)
$(LIBOSMOGB_CFLAGS) $(LIBOSMOCTRL_CFLAGS) $(LIBOSMOGSM_CFLAGS)
+AM_CPPFLAGS = -I$(top_srcdir)/include $(STD_DEFINES_AND_INCLUDES) $(LIBOSMOCORE_CFLAGS)
$(LIBOSMOGB_CFLAGS) $(LIBOSMOCTRL_CFLAGS) $(LIBOSMOGSM_CFLAGS) $(LIBOSMOABIS_CFLAGS)
$(LIBOSMOTRAU_CFLAGS)
if ENABLE_SYSMODSP
AM_CPPFLAGS += -DENABLE_DIRECT_PHY
@@ -33,6 +33,10 @@
AM_CPPFLAGS += -DENABLE_DIRECT_PHY
endif
+if ENABLE_ER_E1_CCU
+AM_CPPFLAGS += -DENABLE_DIRECT_PHY
+endif
+
AM_CXXFLAGS = -Wall
AM_LDFLAGS = -lrt
@@ -207,12 +211,33 @@
osmo-bts-oc2g/oc2gbts.c
endif
+if ENABLE_ER_E1_CCU
+AM_CPPFLAGS += -I$(srcdir)/ericsson-rbs
+AM_CPPFLAGS += -DENABLE_ERICSSON_RBS # RBS but not RBS CCU specific
+AM_CPPFLAGS += -DENABLE_ER_E1_CCU # RBS CCU specific
+
+EXTRA_DIST = \
+ ericsson-rbs/er_ccu_l1_if.c \
+ ericsson-rbs/er_ccu_l1_if.h \
+ ericsson-rbs/er_ccu_if.h
+
+noinst_HEADERS += \
+ ericsson-rbs/er_ccu_l1_if.h \
+ ericsson-rbs/er_ccu_if.h
+
+osmo_pcu_SOURCES += \
+ ericsson-rbs/er_ccu_l1_if.c \
+ ericsson-rbs/er_ccu_if.c
+endif
+
osmo_pcu_LDADD = \
libgprs.la \
$(LIBOSMOGB_LIBS) \
$(LIBOSMOCORE_LIBS) \
$(LIBOSMOCTRL_LIBS) \
$(LIBOSMOGSM_LIBS) \
+ $(LIBOSMOABIS_LIBS) \
+ $(LIBOSMOTRAU_LIBS) \
$(NULL)
#MOSTLYCLEANFILES += testSource testDestination
diff --git a/src/ericsson-rbs/er_ccu_descr.h b/src/ericsson-rbs/er_ccu_descr.h
new file mode 100644
index 0000000..ae50685
--- /dev/null
+++ b/src/ericsson-rbs/er_ccu_descr.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include <stdint.h>
+#include <osmocom/abis/e1_input.h>
+#include <osmocom/trau/trau_pcu_ericsson.h>
+
+struct er_ccu_descr;
+typedef void (er_ccu_empty) (struct er_ccu_descr * ccu_descr);
+typedef void (er_ccu_rx) (struct er_ccu_descr * ccu_descr, const ubit_t *bits, unsigned
int num_bits);
+
+struct er_ccu_descr {
+
+ /* E1-line and timeslot (filled in by user) */
+ struct gsm_e1_subslot e1_link;
+
+ /* Callback functions (provided by user) */
+ er_ccu_empty *er_ccu_empty_cb;
+ er_ccu_rx *er_ccu_rx_cb;
+
+ /* I.460 Subslot */
+ struct osmo_i460_schan_desc scd;
+ struct osmo_i460_subchan *schan;
+ struct osmo_fsm_inst *trau_sync_fi;
+ bool ccu_connected;
+
+ /* Sync state */
+ uint32_t pseq_ccu;
+ uint32_t pseq_pcu;
+ uint32_t last_afn_ul;
+ uint32_t last_afn_dl;
+ bool ccu_synced;
+ enum time_adj_val tav;
+ bool ul_frame_err;
+
+ /* PCU related context */
+ uint8_t trx_no;
+ uint8_t ts;
+ bool pdch_connected;
+};
diff --git a/src/ericsson-rbs/er_ccu_if.c b/src/ericsson-rbs/er_ccu_if.c
new file mode 100644
index 0000000..08cc093
--- /dev/null
+++ b/src/ericsson-rbs/er_ccu_if.c
@@ -0,0 +1,334 @@
+/*
+ * (C) 2022 by sysmocom s.f.m.c. GmbH <info(a)sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Philipp Maier
+ *
+ * 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 "er_ccu_if.h"
+#include <string.h>
+#include <errno.h>
+
+#include <osmocom/abis/e1_input.h>
+#include <osmocom/abis/abis.h>
+#include <osmocom/trau/trau_sync.h>
+#include <osmocom/trau/trau_pcu_ericsson.h>
+#include <bts.h>
+#include <gprs_debug.h>
+
+#define E1_TS_BYTES 160
+#define DEBUG_BITS_MAX 1000
+#define DEBUG_BYTES_MAX 40
+
+#define LOGPCCU(ccu_descr, level, tag, fmt, args...) \
+ LOGP(DE1, level, "E1-%s: (line:%u, TS:%u, SS:%u) " fmt, \
+ tag, ccu_descr->e1_link.e1_nr, ccu_descr->e1_link.e1_ts,
ccu_descr->e1_link.e1_ts_ss, \
+ ## args)
+
+struct e1_ts_descr {
+ bool in_use;
+ struct osmo_i460_timeslot i460_ts;
+};
+
+struct e1_line_descr {
+ struct e1_ts_descr e1_ts[NUM_E1_TS - 1];
+};
+
+struct e1_line_descr e1_lines[32];
+void *tall_ctx = NULL;
+
+static const struct e1inp_line_ops dummy_e1_line_ops = {
+ .sign_link_up = NULL,
+ .sign_link_down = NULL,
+ .sign_link = NULL,
+};
+
+/* called by trau frame synchronizer: feed received MAC blocks into PCU */
+static void sync_frame_out_cb(void *user_data, const ubit_t *bits, unsigned int
num_bits)
+{
+ struct er_ccu_descr *ccu_descr = user_data;
+
+ if (!bits || num_bits == 0)
+ return;
+
+ LOGPCCU(ccu_descr, LOGL_DEBUG, "I.460-RX", "receiving %u TRAU frame bits
from subslot (synchronized): %s...\n",
+ num_bits, osmo_ubit_dump(bits, num_bits > DEBUG_BITS_MAX ? DEBUG_BITS_MAX :
num_bits));
+
+ ccu_descr->er_ccu_rx_cb(ccu_descr, bits, num_bits);
+}
+
+/* called by I.460 de-multeiplexer: feed output of I.460 demux into TRAU frame sync */
+static void e1_i460_demux_bits_cb(struct osmo_i460_subchan *schan, void *user_data, const
ubit_t *bits,
+ unsigned int num_bits)
+{
+ struct er_ccu_descr *ccu_descr = user_data;
+
+ LOGPCCU(ccu_descr, LOGL_DEBUG, "I.460-RX", "receiving %u TRAU frame bits
from subslot: %s...\n", num_bits,
+ osmo_ubit_dump(bits, num_bits > DEBUG_BITS_MAX ? DEBUG_BITS_MAX : num_bits));
+
+ OSMO_ASSERT(ccu_descr->trau_sync_fi);
+ osmo_trau_sync_rx_ubits(ccu_descr->trau_sync_fi, bits, num_bits);
+
+}
+
+/* called by I.460 de-multeiplexer: ensure that sync indications are sent when mux buffer
runs empty */
+static void e1_i460_mux_empty_cb(struct osmo_i460_subchan *schan2, void *user_data)
+{
+ struct er_ccu_descr *ccu_descr = user_data;
+
+ LOGPCCU(ccu_descr, LOGL_DEBUG, "I.460-TX", "demux buffer empty\n");
+ ccu_descr->er_ccu_empty_cb(ccu_descr);
+}
+
+/* Function to handle outgoing E1 traffic */
+static void e1_send(struct e1inp_ts *ts)
+{
+ void *ctx = tall_ctx;
+ struct e1_ts_descr *ts_descr;
+
+ struct msgb *msg = msgb_alloc_c(ctx, E1_TS_BYTES, "E1-TX-timeslot-bytes");
+ uint8_t *ptr;
+
+ /* Note: The line number and ts number that arrives here should be clean. */
+ OSMO_ASSERT(ts->line->num < ARRAY_SIZE(e1_lines));
+ ts_descr = &e1_lines[ts->line->num].e1_ts[ts->num];
+
+ /* Get E1 frame from I.460 multiplexer */
+ ptr = msgb_put(msg, E1_TS_BYTES);
+ osmo_i460_mux_out(&ts_descr->i460_ts, ptr, E1_TS_BYTES);
+
+ LOGP(DE1, LOGL_DEBUG, "E1-TX: (line:%u, ts:%u) sending %u bytes: %s...\n",
ts->line->num, ts->num,
+ msgb_length(msg), osmo_hexdump_nospc(msgb_data(msg),
+ msgb_length(msg) >
+ DEBUG_BYTES_MAX ? DEBUG_BYTES_MAX : msgb_length(msg)));
+
+ /* Hand data over to the E1 stack */
+ msgb_enqueue(&ts->raw.tx_queue, msg);
+ return;
+}
+
+/* Callback function to handle incoming E1 traffic */
+static void e1_recv_cb(struct e1inp_ts *ts, struct msgb *msg)
+{
+ struct e1_ts_descr *ts_descr;
+
+ if (msg->len != E1_TS_BYTES) {
+ LOGP(DE1, LOGL_ERROR,
+ "E1-RX: (line:%u, ts:%u) receiving bad, expected length is %u, actual length
is %u!\n",
+ ts->line->num, ts->num, E1_TS_BYTES, msg->len);
+ msgb_free(msg);
+ return;
+ }
+
+ LOGP(DE1, LOGL_DEBUG, "E1-RX: (line:%u, ts:%u) receiving %u bytes: %s ...\n",
ts->line->num, ts->num,
+ msg->len, osmo_hexdump_nospc(msg->data, msg->len));
+
+ /* Note: The line number and ts number that arrives here should be clean. */
+ OSMO_ASSERT(ts->line->num < ARRAY_SIZE(e1_lines));
+ ts_descr = &e1_lines[ts->line->num].e1_ts[ts->num];
+
+ /* Hand data over to the I640 demultiplexer. */
+ osmo_i460_demux_in(&ts_descr->i460_ts, msg->data, msg->len);
+
+ /* Trigger sending of pending E1 traffic */
+ e1_send(ts);
+
+ /* e1inp_rx_ts() does not free() msgb */
+ msgb_free(msg);
+}
+
+static struct e1_ts_descr *ts_descr_from_ccu_descr(struct er_ccu_descr *ccu_descr)
+{
+ /* Make sure E1 line number is valid */
+ if (ccu_descr->e1_link.e1_nr >= ARRAY_SIZE(e1_lines)) {
+ LOGPCCU(ccu_descr, LOGL_ERROR, "SETUP", "Invalid E1 line
number!\n");
+ return NULL;
+ }
+
+ /* Make sure E1 timeslot number is valid */
+ if (ccu_descr->e1_link.e1_ts < 1 || ccu_descr->e1_link.e1_ts > NUM_E1_TS -
1) {
+ LOGPCCU(ccu_descr, LOGL_ERROR, "SETUP", "Invalid E1 timeslot
number!\n");
+ return NULL;
+ }
+
+ /* Timeslots are only initialized once and will stay open after that. */
+ return &e1_lines[ccu_descr->e1_link.e1_nr].e1_ts[ccu_descr->e1_link.e1_ts];
+}
+
+/* Configure an I.460 subslot and add it to the CCU descriptor */
+static int add_i460_subslot(void *ctx, struct er_ccu_descr *ccu_descr)
+{
+ struct e1_ts_descr *ts_descr;
+ enum osmo_tray_sync_pat_id sync_pattern;
+
+ /* NOTE: This is a serious error: subslots should be removed when l1if_close_pdch() is
called by the PCU. This
+ * log line points towards a problem with the PDCH management inside the PCU! */
+ if (ccu_descr->schan) {
+ LOGPCCU(ccu_descr, LOGL_ERROR, "SETUP", "I.460 subslot is already
configured -- will not touch it!\n");
+ return -EINVAL;
+ }
+
+ ts_descr = ts_descr_from_ccu_descr(ccu_descr);
+ if (!ts_descr)
+ return -EINVAL;
+ if (!ts_descr->in_use) {
+ LOGPCCU(ccu_descr, LOGL_ERROR, "SETUP", "E1 timeslot not
ready!\n");
+ return -EINVAL;
+ }
+
+ /* Set up I.460 subchannel and connect it to the MUX on the E1 timeslot */
+ if (ccu_descr->e1_link.e1_ts_ss == 255) {
+ LOGPCCU(ccu_descr, LOGL_INFO, "SETUP", "using 64k subslots\n");
+ if (ccu_descr->e1_link.e1_ts_ss > 3) {
+ LOGPCCU(ccu_descr, LOGL_ERROR, "SETUP", "Invalid I.460 subslot
number!\n");
+ return -EINVAL;
+ }
+ ccu_descr->scd.rate = OSMO_I460_RATE_64k;
+ ccu_descr->scd.demux.num_bits = E1_TS_BYTES * 8;
+ ccu_descr->scd.bit_offset = 0;
+ sync_pattern = OSMO_TRAU_SYNCP_64_ER_CCU;
+ } else {
+ LOGPCCU(ccu_descr, LOGL_INFO, "SETUP", "using 16k subslots\n");
+ if (ccu_descr->e1_link.e1_ts_ss != 0) {
+ LOGPCCU(ccu_descr, LOGL_ERROR, "SETUP", "Invalid I.460 subslot
number!\n");
+ return -EINVAL;
+ }
+ ccu_descr->scd.rate = OSMO_I460_RATE_16k;
+ ccu_descr->scd.demux.num_bits = E1_TS_BYTES / 4 * 8;
+ ccu_descr->scd.bit_offset = ccu_descr->e1_link.e1_ts_ss * 2;
+ sync_pattern = OSMO_TRAU_SYNCP_16_ER_CCU;
+ }
+
+ ccu_descr->scd.demux.out_cb_bits = e1_i460_demux_bits_cb;
+ ccu_descr->scd.demux.out_cb_bytes = NULL;
+ ccu_descr->scd.demux.user_data = ccu_descr;
+ ccu_descr->scd.mux.in_cb_queue_empty = e1_i460_mux_empty_cb;
+ ccu_descr->scd.mux.user_data = ccu_descr;
+
+ LOGPCCU(ccu_descr, LOGL_INFO, "SETUP", "adding I.460 subchannel:
bit_offset=%u, num_bits=%lu\n",
+ ccu_descr->scd.bit_offset, ccu_descr->scd.demux.num_bits);
+ ccu_descr->schan = osmo_i460_subchan_add(ctx, &ts_descr->i460_ts,
&ccu_descr->scd);
+ if (!ccu_descr->schan) {
+ LOGPCCU(ccu_descr, LOGL_ERROR, "SETUP", "adding I.460 subchannel:
failed!\n");
+ return -EINVAL;
+ }
+
+ /* Configure TRAU synchronizer */
+ ccu_descr->trau_sync_fi = osmo_trau_sync_alloc(NULL, "trau-sync",
sync_frame_out_cb, sync_pattern, ccu_descr);
+ if (!ccu_descr->trau_sync_fi) {
+ LOGPCCU(ccu_descr, LOGL_ERROR, "SETUP", "adding I.460 TRAU frame sync:
failed!\n");
+ }
+
+ return 0;
+}
+
+/* Remove an I.460 subslot from the CCU descriptor */
+static void del_i460_subslot(struct er_ccu_descr *ccu_descr)
+{
+ if (ccu_descr->schan)
+ osmo_i460_subchan_del(ccu_descr->schan);
+ ccu_descr->schan = NULL;
+ if (ccu_descr->trau_sync_fi)
+ osmo_fsm_inst_term(ccu_descr->trau_sync_fi, OSMO_FSM_TERM_REGULAR, NULL);
+ ccu_descr->trau_sync_fi = NULL;
+
+ memset(&ccu_descr->scd, 0, sizeof(ccu_descr->scd));
+}
+
+/* Configure an E1 timeslot according to the description in the ccu_descr */
+static int open_e1_timeslot(struct er_ccu_descr *ccu_descr)
+{
+ struct e1inp_line *e1_line;
+ struct e1_ts_descr *ts_descr;
+ int rc;
+
+ /* Timeslots are only initialized once and will stay open after that. */
+ ts_descr = ts_descr_from_ccu_descr(ccu_descr);
+ if (!ts_descr)
+ return -EINVAL;
+ if (ts_descr->in_use) {
+ LOGPCCU(ccu_descr, LOGL_DEBUG, "SETUP", "E1 timeslot already open --
using it as it is!\n");
+ return 0;
+ }
+
+ /* Find and set up E1 line */
+ e1_line = e1inp_line_find(ccu_descr->e1_link.e1_nr);
+ if (!e1_line) {
+ LOGPCCU(ccu_descr, LOGL_ERROR, "SETUP", "no such E1 line!\n");
+ return -EINVAL;
+ }
+ e1inp_line_bind_ops(e1_line, &dummy_e1_line_ops);
+
+ /* Set up E1 timeslot */
+ rc = e1inp_ts_config_raw(&e1_line->ts[ccu_descr->e1_link.e1_ts - 1], e1_line,
e1_recv_cb);
+ if (rc < 0) {
+ LOGPCCU(ccu_descr, LOGL_ERROR, "SETUP", "configuration of timeslot
failed!\n");
+ return -EINVAL;
+ }
+ e1inp_line_update(e1_line);
+ if (rc < 0) {
+ LOGPCCU(ccu_descr, LOGL_ERROR, "SETUP", "line update failed!\n");
+ return -EINVAL;
+ }
+
+ /* Make sure the i460 mux is always ready */
+ osmo_i460_ts_init(&ts_descr->i460_ts);
+
+ ts_descr->in_use = true;
+ return 0;
+}
+
+int er_ccu_if_open(struct er_ccu_descr *ccu_descr)
+{
+ if (open_e1_timeslot(ccu_descr) < 0)
+ return -EINVAL;
+
+ if (add_i460_subslot(tall_ctx, ccu_descr) < 0)
+ return -EINVAL;
+
+ ccu_descr->ccu_connected = true;
+ LOGPCCU(ccu_descr, LOGL_DEBUG, "SETUP", "CCU connected.\n");
+ return 0;
+}
+
+void er_ccu_if_close(struct er_ccu_descr *ccu_descr)
+{
+ del_i460_subslot(ccu_descr);
+ ccu_descr->ccu_connected = false;
+ LOGPCCU(ccu_descr, LOGL_DEBUG, "SETUP", "CCU disconnected.\n");
+}
+
+void er_ccu_if_tx(struct er_ccu_descr *ccu_descr, const ubit_t *bits, unsigned int
num_bits)
+{
+ struct msgb *msg;
+ uint8_t *ptr;
+ msg = msgb_alloc_c(tall_ctx, num_bits, "E1-I.460-PCU-IND-frame");
+ ptr = msgb_put(msg, num_bits);
+ memcpy(ptr, bits, num_bits);
+ LOGPCCU(ccu_descr, LOGL_DEBUG, "I.460-TX", "sending %u bits:
%s...\n", msgb_length(msg),
+ osmo_ubit_dump(msgb_data(msg), msgb_length(msg) > DEBUG_BITS_MAX ? DEBUG_BITS_MAX :
msgb_length(msg)));
+ osmo_i460_mux_enqueue(ccu_descr->schan, msg);
+}
+
+void er_ccu_if_init(void *ctx)
+{
+ libosmo_abis_init(ctx);
+ e1inp_vty_init();
+
+ tall_ctx = ctx;
+ memset(e1_lines, 0, sizeof(e1_lines));
+}
diff --git a/src/ericsson-rbs/er_ccu_if.h b/src/ericsson-rbs/er_ccu_if.h
new file mode 100644
index 0000000..0c92072
--- /dev/null
+++ b/src/ericsson-rbs/er_ccu_if.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include <stdint.h>
+#include <osmocom/abis/e1_input.h>
+#include "er_ccu_descr.h"
+
+int er_ccu_if_open(struct er_ccu_descr *ccu_descr);
+void er_ccu_if_close(struct er_ccu_descr *ccu_descr);
+void er_ccu_if_tx(struct er_ccu_descr *ccu_descr, const ubit_t * bits, unsigned int
num_bits);
+void er_ccu_if_init(void *ctx);
diff --git a/src/ericsson-rbs/er_ccu_l1_if.c b/src/ericsson-rbs/er_ccu_l1_if.c
new file mode 100644
index 0000000..87e8c5b
--- /dev/null
+++ b/src/ericsson-rbs/er_ccu_l1_if.c
@@ -0,0 +1,415 @@
+/*
+ * (C) 2022 by sysmocom s.f.m.c. GmbH <info(a)sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Philipp Maier
+ *
+ * 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 "er_ccu_descr.h"
+#include "er_ccu_l1_if.h"
+#include "er_ccu_if.h"
+
+#include <string.h>
+#include <errno.h>
+#include <pcu_l1_if.h>
+
+#include <osmocom/pcu/pcuif_proto.h>
+#include <osmocom/abis/e1_input.h>
+#include <osmocom/abis/abis.h>
+#include <osmocom/abis/e1_input.h>
+#include <osmocom/trau/trau_sync.h>
+#include <osmocom/trau/trau_pcu_ericsson.h>
+#include <osmocom/gsm/gsm0502.h>
+#include <osmocom/core/talloc.h>
+
+#include <bts.h>
+
+void *tall_ctx;
+
+const uint8_t fn_inc_table[4] = { 4, 4, 5, 0 };
+const uint8_t blk_nr_table[4] = { 4, 4, 5, 0 };
+
+#define SYNC_CHECK_INTERVAL 2000
+
+/* Subtrahend to convert Ericsson adjusted (block ending) fn to regular fn (uplink only)
*/
+#define AFN_SUBTRAHEND 3
+
+#define LOGPL1IF(ccu_descr, level, tag, fmt, args...) \
+ LOGP(DL1IF, level, "%s: (E1-line:%u, E1-TS:%u, E1-SS:%u, PDCH-TS:%u, TRX:%u) "
fmt, \
+ tag, ccu_descr->e1_link.e1_nr, ccu_descr->e1_link.e1_ts,
ccu_descr->e1_link.e1_ts_ss, \
+ ccu_descr->ts, ccu_descr->trx_no, \
+ ## args)
+
+/* Calculate GPRS block number from frame number */
+static uint8_t fn_to_block_nr(uint32_t fn)
+{
+ /* Note: See also 3GPP TS 03.64 6.5.7.2.1,
+ * Mapping on the multiframe structure */
+
+ uint8_t rel_fn;
+ uint8_t super_block;
+ uint8_t local_block;
+
+ rel_fn = fn % 52;
+
+ /* Warn in case of frames that do not belong to a block */
+ if (rel_fn == 12 || rel_fn == 25 || rel_fn == 38 || rel_fn == 51)
+ LOGP(DL1IF, LOGL_ERROR, "Frame number is referencing invalid block!\n");
+
+ super_block = (rel_fn / 13);
+ local_block = rel_fn % 13 / 4;
+ return super_block * 3 + local_block;
+}
+
+static uint32_t fn_dl_advance(uint32_t fn, uint32_t n_blocks)
+{
+ uint32_t i;
+
+ uint8_t inc_fn;
+
+ for (i = 0; i < n_blocks; i++) {
+ inc_fn = fn_inc_table[(fn % 13) / 4];
+ fn = GSM_TDMA_FN_SUM(fn, inc_fn);
+ }
+
+ return fn;
+}
+
+/* Receive block from CCU */
+static void er_ccu_rx_cb(struct er_ccu_descr *ccu_descr, const ubit_t *bits, unsigned int
num_bits)
+{
+ int rc;
+ struct er_gprs_trau_frame trau_frame;
+ uint8_t inc_ul;
+ uint8_t inc_dl;
+ uint32_t afn_ul;
+ uint32_t afn_dl;
+ uint32_t afn_ul_comp;
+ uint32_t afn_dl_comp;
+ struct pcu_l1_meas meas = { 0 };
+ struct gprs_rlcmac_bts *bts;
+ struct gprs_rlcmac_pdch *pdch;
+
+ /* Compute the current frame numbers from the last frame number */
+ inc_ul = fn_inc_table[(ccu_descr->last_afn_ul % 13) / 4];
+ inc_dl = fn_inc_table[(ccu_descr->last_afn_dl % 13) / 4];
+ afn_ul = GSM_TDMA_FN_SUM(ccu_descr->last_afn_ul, inc_ul);
+ afn_dl = GSM_TDMA_FN_SUM(ccu_descr->last_afn_dl, inc_dl);
+
+ /* Compute compensated frame numbers. This will be the framenumbers we
+ * will use to exchange blocks with the PCU code. The following applies:
+ *
+ * 1. The uplink related frame numbers sent by the ericsson CCU refer
+ * to the end of a block. This is compensated by subtracting three
+ * frames.
+ * 2. The CCU downlink frame number runs one block past the uplink
+ * frame number. This needs to be compesated as well (+1).
+ * 3. The difference between the local (PCU) and the returned (CCU)
+ * pseq counter value is the number of blocks that the PCU must
+ * shift its downlink alignment in order to compensate the link
+ * latency between PCU and CCU. */
+ afn_ul_comp = GSM_TDMA_FN_SUB(afn_ul, AFN_SUBTRAHEND); //<== prooven correct!
+ afn_dl_comp = afn_dl;
+ afn_dl_comp = fn_dl_advance(afn_dl_comp, GSM_TDMA_FN_DIFF(ccu_descr->pseq_pcu,
ccu_descr->pseq_ccu) + 1);
+
+ LOGPL1IF(ccu_descr, LOGL_DEBUG, "CCU-SYNC",
+ "afn_ul=%u/%u, afn_dl=%u/%u, afn_diff=%u => afn_ul_comp=%u/%u,
afn_dl_comp=%u/%u, afn_diff_comp=%u\n",
+ afn_ul, afn_ul % 52, afn_dl, afn_dl % 52, GSM_TDMA_FN_DIFF(afn_ul, afn_dl),
afn_ul_comp,
+ afn_ul_comp % 52, afn_dl_comp, afn_dl_comp % 52, GSM_TDMA_FN_DIFF(afn_ul_comp,
afn_dl_comp));
+
+ LOGPL1IF(ccu_descr, LOGL_DEBUG, "CCU-SYNC", "pseq_pcu=%u, pseq_ccu=%u,
pseq_diff=%u\n",
+ ccu_descr->pseq_pcu, ccu_descr->pseq_ccu,
GSM_TDMA_FN_DIFF(ccu_descr->pseq_pcu, ccu_descr->pseq_ccu));
+
+ /* Decode indication from CCU */
+ if (ccu_descr->e1_link.e1_ts_ss == 255)
+ rc = er_gprs_trau_frame_decode_64k(&trau_frame, bits);
+ else
+ rc = er_gprs_trau_frame_decode_16k(&trau_frame, bits);
+ if (rc < 0) {
+ LOGPL1IF(ccu_descr, LOGL_ERROR, "CCU-XXXX-IND",
+ "unable to decode uplink TRAU frame, afn_ul_comp=%u/%u\n", afn_ul_comp,
afn_ul_comp % 52);
+
+ /* Report to the CCU that there is an issue with downlink TRAU frames, the CCU will
then send
+ * a CCU-SYNC-IND within the next TRAU frame, so we can check if we are still in sync
and trigger
+ * synchronization procedure if necessary. */
+ ccu_descr->ul_frame_err = true;
+ goto skip;
+ }
+
+ switch (trau_frame.type) {
+ case ER_GPRS_TRAU_FT_SYNC:
+ if (trau_frame.u.ccu_sync_ind.pseq != 0x3FFFFF) {
+ LOGPL1IF(ccu_descr, LOGL_DEBUG, "CCU-SYNC-IND",
+ "tav=%u, dbe=%u, dfe=%u, pseq=%u, afn_ul=%u, afn_dl=%u\n",
+ trau_frame.u.ccu_sync_ind.tav, trau_frame.u.ccu_sync_ind.dbe,
+ trau_frame.u.ccu_sync_ind.dfe, trau_frame.u.ccu_sync_ind.pseq,
+ trau_frame.u.ccu_sync_ind.afn_ul, trau_frame.u.ccu_sync_ind.afn_dl);
+
+ /* Synchronize the current CCU PSEQ state */
+ ccu_descr->pseq_ccu = trau_frame.u.ccu_sync_ind.pseq;
+ } else {
+ LOGPL1IF(ccu_descr, LOGL_DEBUG, "CCU-SYNC-IND",
+ "tav=%u, dbe=%u, dfe=%u, pseq=(none), afn_ul=%u, afn_dl=%u\n",
+ trau_frame.u.ccu_sync_ind.tav, trau_frame.u.ccu_sync_ind.dbe,
+ trau_frame.u.ccu_sync_ind.dfe, trau_frame.u.ccu_sync_ind.afn_ul,
+ trau_frame.u.ccu_sync_ind.afn_dl);
+ }
+
+ ccu_descr->tav = trau_frame.u.ccu_sync_ind.tav;
+
+ /* Check if we are in sync with the CCU, if not trigger synchronization procedure */
+ if (afn_ul != trau_frame.u.ccu_sync_ind.afn_ul || afn_dl !=
trau_frame.u.ccu_sync_ind.afn_dl) {
+ if (afn_ul != trau_frame.u.ccu_sync_ind.afn_ul)
+ LOGPL1IF(ccu_descr, LOGL_DEBUG, "CCU-SYNC-IND",
+ "afn_ul=%u (computed) != afn_ul=%u (sync-ind) => delta=%u \n",
afn_ul,
+ trau_frame.u.ccu_sync_ind.afn_ul, GSM_TDMA_FN_DIFF(afn_ul,
+ trau_frame.u.ccu_sync_ind.
+ afn_ul));
+ if (afn_dl != trau_frame.u.ccu_sync_ind.afn_dl)
+ LOGPL1IF(ccu_descr, LOGL_DEBUG, "CCU-SYNC-IND",
+ "afn_dl=%u (computed) != afn_dl=%u (sync-ind) => delta=%u \n",
afn_dl,
+ trau_frame.u.ccu_sync_ind.afn_dl, GSM_TDMA_FN_DIFF(afn_dl,
+ trau_frame.u.ccu_sync_ind.
+ afn_dl));
+ LOGPL1IF(ccu_descr, LOGL_NOTICE, "CCU-SYNC-IND",
+ "FN jump detected, lost sync with CCU -- (re)synchronizing...\n");
+ ccu_descr->ccu_synced = false;
+ } else {
+ LOGPL1IF(ccu_descr, LOGL_NOTICE, "CCU-SYNC-IND", "in sync with
CCU\n");
+ ccu_descr->ccu_synced = true;
+ }
+
+ /* Overwrite calculated afn_ul and afn_dl with the actual values from the SYNC
indication */
+ afn_ul = trau_frame.u.ccu_sync_ind.afn_ul;
+ afn_dl = trau_frame.u.ccu_sync_ind.afn_dl;
+
+ break;
+ case ER_GPRS_TRAU_FT_DATA:
+
+ ccu_descr->tav = trau_frame.u.ccu_data_ind.tav;
+
+ /* Ignore all data indications that contain only noise */
+ if (!trau_frame.u.ccu_data_ind.u.gprs.parity_ok)
+ break;
+
+ LOGPL1IF(ccu_descr, LOGL_DEBUG, "CCU-DATA-IND",
+ "tav=%u, dbe=%u, cs_hdr=%u, rx_lev=%u, est_acc_del_dev=%u,"
+ "block_qual=%u, parity_ok=%u, data=%s<==, afn_ul_comp=%u/%u\n",
trau_frame.u.ccu_data_ind.tav,
+ trau_frame.u.ccu_data_ind.dbe, trau_frame.u.ccu_data_ind.cs_hdr,
+ trau_frame.u.ccu_data_ind.rx_lev, trau_frame.u.ccu_data_ind.est_acc_del_dev,
+ trau_frame.u.ccu_data_ind.u.gprs.block_qual,
trau_frame.u.ccu_data_ind.u.gprs.parity_ok,
+ osmo_hexdump_nospc(trau_frame.u.ccu_data_ind.data,
trau_frame.u.ccu_data_ind.data_len),
+ afn_ul_comp, afn_ul_comp % 52);
+
+ /* Hand received MAC block into PCU */
+ bts = llist_first_entry_or_null(&the_pcu->bts_list, struct gprs_rlcmac_bts,
list);
+ if (!bts)
+ break;
+ meas.have_rssi = 1;
+ meas.rssi = rxlev2dbm(trau_frame.u.ccu_data_ind.rx_lev);
+ meas.have_link_qual = 1;
+ meas.link_qual = trau_frame.u.ccu_data_ind.u.gprs.block_qual;
+ pdch = &bts->trx[ccu_descr->trx_no].pdch[ccu_descr->ts];
+ rc = pcu_rx_data_ind_pdtch(bts, pdch, trau_frame.u.ccu_data_ind.data,
+ trau_frame.u.ccu_data_ind.data_len, afn_ul_comp, &meas);
+ break;
+ default:
+ LOGPL1IF(ccu_descr, LOGL_ERROR, "CCU-XXXX-IND", "unhandled CCU
indication!\n");
+ }
+
+skip:
+ if (ccu_descr->ccu_synced) {
+ bts = llist_first_entry_or_null(&the_pcu->bts_list, struct gprs_rlcmac_bts,
list);
+ if (bts) {
+ /* The PCU timing is locked to the uplink fame number. The downlink frame number is
advanced
+ * into the future so that the line latency is compensated and the frame arrives at
the right
+ * point in time. */
+ pdch = &bts->trx[ccu_descr->trx_no].pdch[ccu_descr->ts];
+ pcu_rx_block_time(bts, pdch->trx->arfcn, afn_ul_comp, ccu_descr->ts);
+ rc = pcu_rx_rts_req_pdtch(bts, ccu_descr->trx_no, ccu_descr->ts, afn_dl_comp,
+ fn_to_block_nr(afn_dl_comp));
+ }
+ }
+
+ /* We do not receive sync indications in every cycle. When traffic is transfered we
won't get frame numbers
+ * from the CCU. In this case we must update the last_afn_ul/dl values from the computed
frame numbers
+ * (see above) */
+ ccu_descr->last_afn_ul = afn_ul;
+ ccu_descr->last_afn_dl = afn_dl;
+ ccu_descr->pseq_pcu++;
+ ccu_descr->pseq_ccu++;
+}
+
+static void er_ccu_empty_cb(struct er_ccu_descr *ccu_descr)
+{
+ struct er_gprs_trau_frame trau_frame;
+ ubit_t trau_frame_encoded[ER_GPRS_TRAU_FRAME_LEN_64K];
+ int rc;
+
+ memset(&trau_frame, 0, sizeof(trau_frame));
+ trau_frame.u.pcu_sync_ind.pseq = ccu_descr->pseq_pcu;
+ trau_frame.u.pcu_sync_ind.tav = ccu_descr->tav;
+ trau_frame.u.pcu_sync_ind.fn_ul = 0x3FFFFF;
+ trau_frame.u.pcu_sync_ind.fn_dl = 0x3FFFFF;
+ trau_frame.u.pcu_sync_ind.fn_ss = 0x3FFFFF;
+ trau_frame.u.pcu_sync_ind.ls = 0x3FFFFF;
+ trau_frame.u.pcu_sync_ind.ss = 0x3FFFFF;
+ trau_frame.type = ER_GPRS_TRAU_FT_SYNC;
+
+ if (ccu_descr->e1_link.e1_ts_ss == 255)
+ rc = er_gprs_trau_frame_encode_64k(trau_frame_encoded, &trau_frame);
+ else
+ rc = er_gprs_trau_frame_encode_16k(trau_frame_encoded, &trau_frame);
+ if (rc < 0) {
+ LOGPL1IF(ccu_descr, LOGL_ERROR, "PCU-SYNC-IND", "unable to encode TRAU
frame\n");
+ return;
+ }
+ LOGPL1IF(ccu_descr, LOGL_DEBUG, "PCU-SYNC-IND", "pseq=%u,
tav=%u\n",
+ trau_frame.u.pcu_sync_ind.pseq, trau_frame.u.pcu_sync_ind.tav);
+ er_ccu_if_tx(ccu_descr, trau_frame_encoded, rc);
+
+ /* Make sure timing adjustment value is reset after use */
+ ccu_descr->tav = TIME_ADJ_NONE;
+}
+
+/* use the length of the block to determine the coding scheme */
+static int cs_from_len(uint8_t len)
+{
+ switch (len) {
+ case 23:
+ return CS_OR_HDR_CS1;
+ case 34:
+ return CS_OR_HDR_CS2;
+ case 40:
+ return CS_OR_HDR_CS3;
+ case 54:
+ return CS_OR_HDR_CS4;
+ default:
+ return -EINVAL;
+ }
+}
+
+/* send packet data request to L1 */
+int l1if_pdch_req(void *obj, uint8_t ts, int is_ptcch, uint32_t fn,
+ uint16_t arfcn, uint8_t block_nr, uint8_t *data, uint8_t len)
+{
+ struct er_ccu_descr *ccu_descr = obj;
+ struct er_gprs_trau_frame trau_frame;
+ ubit_t trau_frame_encoded[ER_GPRS_TRAU_FRAME_LEN_64K];
+ int rc;
+
+ /* Make sure that the CCU is synchronized and connected. */
+ if (!ccu_descr->ccu_connected) {
+ LOGPL1IF(ccu_descr, LOGL_NOTICE, "PCU-DATA-IND", "CCU not connected,
tossing MAC block...\n");
+ return -EINVAL;
+ }
+ if (!ccu_descr->ccu_synced) {
+ LOGPL1IF(ccu_descr, LOGL_NOTICE, "PCU-DATA-IND", "CCU not synchronized,
tossing MAC block...\n");
+ return -EINVAL;
+ }
+
+ memset(&trau_frame, 0, sizeof(trau_frame));
+ trau_frame.type = ER_GPRS_TRAU_FT_DATA;
+
+ rc = cs_from_len(len);
+ if (rc < 0) {
+ LOGPL1IF(ccu_descr, LOGL_ERROR, "PCU-DATA-IND",
+ "unable to encode TRAU frame, invalid CS or MCS value set\n");
+ return -EINVAL;
+ }
+ trau_frame.u.pcu_data_ind.cs_hdr = (enum er_cs_or_hdr)rc;
+ trau_frame.u.pcu_data_ind.tav = ccu_descr->tav;
+ trau_frame.u.pcu_data_ind.ul_frame_err = ccu_descr->ul_frame_err;
+ trau_frame.u.pcu_data_ind.ul_chan_mode = ER_UL_CHMOD_NB_GMSK; /* <- TODO: select this
depending on the CS (EGPRS) */
+ OSMO_ASSERT(len < sizeof(trau_frame.u.pcu_data_ind.data));
+ memcpy(trau_frame.u.pcu_data_ind.data, data, len);
+
+ /* Regulary ignore one MAC block in uplink. The CCU will then send one CCU-SYNC-IND
instead. We use this
+ * indication to check whether we are still in sync with the CCU. */
+ if (fn % SYNC_CHECK_INTERVAL == 0)
+ trau_frame.u.pcu_data_ind.ul_chan_mode = ER_UL_CHMOD_VOID;
+
+ if (ccu_descr->e1_link.e1_ts_ss == 255)
+ rc = er_gprs_trau_frame_encode_64k(trau_frame_encoded, &trau_frame);
+ else
+ rc = er_gprs_trau_frame_encode_16k(trau_frame_encoded, &trau_frame);
+ if (rc < 0) {
+ LOGPL1IF(ccu_descr, LOGL_ERROR, "PCU-DATA-IND", "unable to encode TRAU
frame\n");
+ return -EINVAL;
+ }
+ LOGPL1IF(ccu_descr, LOGL_DEBUG, "PCU-DATA-IND",
+ "tav=%u, ul_frame_err=%u, cs_hdr=%u, ul_chan_mode=%u, atten_db=%u,
timing_offset=%u,"
+ " data=%s==>, fn=%u/%u (comp)\n", trau_frame.u.pcu_data_ind.tav,
+ trau_frame.u.pcu_data_ind.ul_frame_err, trau_frame.u.pcu_data_ind.cs_hdr,
+ trau_frame.u.pcu_data_ind.ul_chan_mode, trau_frame.u.pcu_data_ind.atten_db,
+ trau_frame.u.pcu_data_ind.timing_offset,
osmo_hexdump_nospc(trau_frame.u.pcu_data_ind.data, len), fn,
+ fn % 52);
+ er_ccu_if_tx(ccu_descr, trau_frame_encoded, rc);
+
+ /* Make sure timing adjustment value is reset after use */
+ ccu_descr->tav = TIME_ADJ_NONE;
+ ccu_descr->ul_frame_err = false;
+
+ return 0;
+}
+
+void *l1if_open_pdch(uint8_t trx_no, uint32_t hlayer1)
+{
+ struct er_ccu_descr *ccu_descr;
+
+ /* Note: We do not have enough information to really open anything at
+ * this point. We will just create the CCU context. */
+
+ ccu_descr = talloc_zero(tall_ctx, struct er_ccu_descr);
+ OSMO_ASSERT(ccu_descr);
+ ccu_descr->er_ccu_rx_cb = er_ccu_rx_cb;
+ ccu_descr->er_ccu_empty_cb = er_ccu_empty_cb;
+ ccu_descr->trx_no = trx_no;
+
+ return ccu_descr;
+}
+
+int l1if_close_pdch(void *obj)
+{
+ struct er_ccu_descr *ccu_descr = obj;
+ er_ccu_if_close(ccu_descr);
+ talloc_free(ccu_descr);
+ return 0;
+}
+
+int l1if_connect_pdch(void *obj, uint8_t ts)
+{
+ struct er_ccu_descr *ccu_descr = obj;
+ int rc;
+
+ ccu_descr->ts = ts;
+
+ rc = pcu_l1if_get_ccu_conn_pars(&ccu_descr->e1_link, ccu_descr->ts,
ccu_descr->trx_no);
+ if (rc < 0)
+ return -EINVAL;
+
+ rc = er_ccu_if_open(ccu_descr);
+ if (rc < 0)
+ return -EINVAL;
+
+ return 0;
+}
+
+void er_ccu_init(void *ctx)
+{
+ er_ccu_if_init(ctx);
+}
diff --git a/src/ericsson-rbs/er_ccu_l1_if.h b/src/ericsson-rbs/er_ccu_l1_if.h
new file mode 100644
index 0000000..a4599f0
--- /dev/null
+++ b/src/ericsson-rbs/er_ccu_l1_if.h
@@ -0,0 +1,3 @@
+#pragma once
+
+void er_ccu_init(void *ctx);
diff --git a/src/gprs_debug.c b/src/gprs_debug.c
index 03ef083..8aeca5b 100644
--- a/src/gprs_debug.c
+++ b/src/gprs_debug.c
@@ -128,6 +128,13 @@
.loglevel = LOGL_NOTICE,
.enabled = 1,
},
+ [DE1] = {
+ .name = "DE1",
+ .color = "\033[1;31m",
+ .description = "E1 line handling",
+ .loglevel = LOGL_NOTICE,
+ .enabled = 1,
+ },
};
static int filter_fn(const struct log_context *ctx,
diff --git a/src/gprs_debug.h b/src/gprs_debug.h
index 320c739..db9630c 100644
--- a/src/gprs_debug.h
+++ b/src/gprs_debug.h
@@ -41,6 +41,7 @@
DPCU,
DNACC,
DRIM,
+ DE1,
aDebug_LastEntry
};
diff --git a/src/pcu_main.cpp b/src/pcu_main.cpp
index 901ee6c..5738be5 100644
--- a/src/pcu_main.cpp
+++ b/src/pcu_main.cpp
@@ -48,6 +48,7 @@
#include <osmocom/core/stats.h>
#include <osmocom/core/gsmtap.h>
#include <osmocom/core/gsmtap_util.h>
+#include "ericsson-rbs/er_ccu_l1_if.h"
}
extern struct gprs_nsvc *nsvc;
@@ -250,6 +251,10 @@
osmo_cpu_sched_vty_init(tall_pcu_ctx);
logging_vty_add_deprecated_subsys(tall_pcu_ctx, "bssgp");
+#ifdef ENABLE_ER_E1_CCU
+ er_ccu_init(tall_pcu_ctx);
+#endif
+
handle_options(argc, argv);
if ((!!spoof_mcc) + (!!spoof_mnc) == 1) {
fprintf(stderr, "--mcc and --mnc must be specified "
--
To view, visit
https://gerrit.osmocom.org/c/osmo-pcu/+/31176
To unsubscribe, or for help writing mail filters, visit
https://gerrit.osmocom.org/settings
Gerrit-Project: osmo-pcu
Gerrit-Branch: master
Gerrit-Change-Id: I5c0a76667339ca984a12cbd2052f5d9e5b0f9c4d
Gerrit-Change-Number: 31176
Gerrit-PatchSet: 1
Gerrit-Owner: dexter <pmaier(a)sysmocom.de>
Gerrit-MessageType: newchange