Change in libosmocore[master]: Implement ITU-T I.460 multiplex / demultiplex

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
Thu May 28 13:08:52 UTC 2020


laforge has submitted this change. ( https://gerrit.osmocom.org/c/libosmocore/+/18247 )

Change subject: Implement ITU-T I.460 multiplex / demultiplex
......................................................................

Implement ITU-T I.460 multiplex / demultiplex

This implements a multiplexer and de-multiplexer for the ITU-T I.460
standard.  The latter covers the transmission of sub-slots of 32/16/8k
inside 64k timeslots.

Change-Id: Id522f06e73b77332b437b7a27e4966872da70eda
---
M include/Makefile.am
A include/osmocom/gsm/i460_mux.h
M src/gsm/Makefile.am
A src/gsm/i460_mux.c
M src/gsm/libosmogsm.map
M tests/Makefile.am
A tests/i460_mux/i460_mux_test.c
A tests/i460_mux/i460_mux_test.ok
M tests/testsuite.at
9 files changed, 999 insertions(+), 1 deletion(-)

Approvals:
  laforge: Looks good to me, approved
  tnt: Looks good to me, but someone else must approve
  Jenkins Builder: Verified



diff --git a/include/Makefile.am b/include/Makefile.am
index 572c880..456b8ef 100644
--- a/include/Makefile.am
+++ b/include/Makefile.am
@@ -103,6 +103,7 @@
                        osmocom/gsm/gsm_utils.h \
                        osmocom/gsm/gsup.h \
                        osmocom/gsm/gsup_sms.h \
+                       osmocom/gsm/i460_mux.h \
                        osmocom/gsm/ipa.h \
                        osmocom/gsm/lapd_core.h \
                        osmocom/gsm/lapdm.h \
diff --git a/include/osmocom/gsm/i460_mux.h b/include/osmocom/gsm/i460_mux.h
new file mode 100644
index 0000000..2e33b37
--- /dev/null
+++ b/include/osmocom/gsm/i460_mux.h
@@ -0,0 +1,104 @@
+/*! \file i460_mux.h
+ * ITU-T I.460 sub-channel multiplexer + demultiplexer */
+/*
+ * (C) 2020 by Harald Welte <laforge at gnumonks.org>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ *  MA  02110-1301, USA.
+ */
+
+#pragma once
+#include <stdint.h>
+#include <osmocom/core/bits.h>
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/msgb.h>
+
+/* I.460 sub-slot rate */
+enum osmo_i460_rate {
+	OSMO_I460_RATE_NONE,		/* disabled */
+	OSMO_I460_RATE_64k,
+	OSMO_I460_RATE_32k,
+	OSMO_I460_RATE_16k,
+	OSMO_I460_RATE_8k,
+};
+
+typedef void (*out_cb_bits_t)(void *user_data, const ubit_t *bits, unsigned int num_bits);
+typedef void (*out_cb_bytes_t)(void *user_data, const uint8_t *bytes, unsigned int num_bytes);
+
+struct osmo_i460_subchan_demux {
+	/*! bit-buffer for output bits */
+	uint8_t *out_bitbuf;
+	/*! size of out_bitbuf in bytes */
+	unsigned int out_bitbuf_size;
+	/*! offset of next bit to be written in out_bitbuf */
+	unsigned int out_idx;
+	/*! callback to be called once we have received out_bitbuf_size bits */
+	out_cb_bits_t out_cb_bits;
+	out_cb_bytes_t out_cb_bytes;
+	void *user_data;
+};
+
+struct osmo_i460_subchan_mux {
+	/*! list of to-be-transmitted message buffers */
+	struct llist_head tx_queue;
+};
+
+struct osmo_i460_subchan {
+	enum osmo_i460_rate rate;		/* 8/16/32/64k */
+	uint8_t bit_offset;		/* bit offset inside each byte of the B-channel */
+	struct osmo_i460_subchan_demux demux;
+	struct osmo_i460_subchan_mux mux;
+};
+
+struct osmo_i460_timeslot {
+	struct osmo_i460_subchan schan[8];
+};
+
+/*! description of a sub-channel; passed by caller */
+struct osmo_i460_schan_desc {
+	enum osmo_i460_rate rate;
+	uint8_t bit_offset;
+	struct {
+		/* size (in bits) of the internal buffer; determines granularity */
+		size_t num_bits;
+		/*! call-back function called whenever we received num_bits */
+		out_cb_bits_t out_cb_bits;
+		/*! out_cb_bytes call-back function called whenever we received num_bits.
+		 * The user is usually expected to provide either out_cb_bits or out_cb_bytes.  If only
+		 * out_cb_bits is provided, output data will always be provided as unpacked bits;  if only
+		 * out_cb_bytes is provided, output data will always be provided as packet bits (bytes).  If
+		 * both are provided, it is up to the I.460 multiplex to decide if it calls either of the two,
+		 * depending on what can be provided without extra conversion. */
+		out_cb_bytes_t out_cb_bytes;
+		/* opaque user data pointer to pass to out_cb */
+		void *user_data;
+	} demux;
+};
+
+void osmo_i460_demux_in(struct osmo_i460_timeslot *ts, const uint8_t *data, size_t data_len);
+
+void osmo_i460_mux_enqueue(struct osmo_i460_subchan *schan, struct msgb *msg);
+int osmo_i460_mux_out(struct osmo_i460_timeslot *ts, uint8_t *out, size_t out_len);
+
+void osmo_i460_ts_init(struct osmo_i460_timeslot *ts);
+
+struct osmo_i460_subchan *
+osmo_i460_subchan_add(void *ctx, struct osmo_i460_timeslot *ts, const struct osmo_i460_schan_desc *chd);
+
+void osmo_i460_subchan_del(struct osmo_i460_subchan *schan);
+
+/*! @} */
diff --git a/src/gsm/Makefile.am b/src/gsm/Makefile.am
index 6935eab..eeb1164 100644
--- a/src/gsm/Makefile.am
+++ b/src/gsm/Makefile.am
@@ -32,7 +32,7 @@
 			milenage/milenage.c gan.c ipa.c gsm0341.c apn.c \
 			gsup.c gsup_sms.c gprs_gea.c gsm0503_conv.c oap.c gsm0808_utils.c \
 			gsm23003.c mncc.c bts_features.c oap_client.c \
-			gsm29118.c gsm48_rest_octets.c cbsp.c gsm48049.c
+			gsm29118.c gsm48_rest_octets.c cbsp.c gsm48049.c i460_mux.c
 libgsmint_la_LDFLAGS = -no-undefined
 libgsmint_la_LIBADD = $(top_builddir)/src/libosmocore.la
 
diff --git a/src/gsm/i460_mux.c b/src/gsm/i460_mux.c
new file mode 100644
index 0000000..3fb63ec
--- /dev/null
+++ b/src/gsm/i460_mux.c
@@ -0,0 +1,363 @@
+/*! \file i460_mux.c
+ * ITU-T I.460 sub-channel multiplexer + demultiplexer */
+/*
+ * (C) 2020 by Harald Welte <laforge at gnumonks.org>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ *  MA  02110-1301, USA.
+ */
+
+#include <errno.h>
+
+#include <osmocom/core/bits.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/core/msgb.h>
+#include <osmocom/gsm/i460_mux.h>
+
+/* count the number of sub-channels in this I460 slot */
+static int osmo_i460_subchan_count(struct osmo_i460_timeslot *ts)
+{
+	int i, num_used = 0;
+
+	for (i = 0; i < ARRAY_SIZE(ts->schan); i++) {
+		if (ts->schan[i].rate != OSMO_I460_RATE_NONE)
+			num_used++;
+	}
+
+	return num_used;
+}
+
+/* does this channel have no sub-streams (single 64k subchannel)? */
+static bool osmo_i460_has_single_64k_schan(struct osmo_i460_timeslot *ts)
+{
+	if (osmo_i460_subchan_count(ts) != 1)
+		return false;
+
+	if (ts->schan[0].rate != OSMO_I460_RATE_64k)
+		return false;
+
+	return true;
+}
+
+/***********************************************************************
+ * Demultiplexer
+ ***********************************************************************/
+
+/* append a single bit to a sub-channel */
+static void demux_subchan_append_bit(struct osmo_i460_subchan *schan, uint8_t bit)
+{
+	struct osmo_i460_subchan_demux *demux = &schan->demux;
+
+	OSMO_ASSERT(demux->out_bitbuf);
+	OSMO_ASSERT(demux->out_idx < demux->out_bitbuf_size);
+
+	demux->out_bitbuf[demux->out_idx++] = bit ? 1 : 0;
+
+	if (demux->out_idx >= demux->out_bitbuf_size) {
+		if (demux->out_cb_bits)
+			demux->out_cb_bits(demux->user_data, demux->out_bitbuf, demux->out_idx);
+		else {
+			/* pack bits into bytes */
+			OSMO_ASSERT((demux->out_idx % 8) == 0);
+			unsigned int num_bytes = demux->out_idx / 8;
+			uint8_t bytes[num_bytes];
+			osmo_ubit2pbit(bytes, demux->out_bitbuf, demux->out_idx);
+			demux->out_cb_bytes(demux->user_data, bytes, num_bytes);
+		}
+		demux->out_idx = 0;
+	}
+}
+
+/* extract those bits relevant to this schan of each byte in 'data' */
+static void demux_subchan_extract_bits(struct osmo_i460_subchan *schan, const uint8_t *data, size_t data_len)
+{
+	int i;
+
+	for (i = 0; i < data_len; i++) {
+		uint8_t inbyte = data[i];
+		uint8_t inbits = inbyte >> schan->bit_offset;
+
+		/* extract the bits relevant to the given schan */
+		switch (schan->rate) {
+		case OSMO_I460_RATE_8k:
+			demux_subchan_append_bit(schan, inbits & 0x01);
+			break;
+		case OSMO_I460_RATE_16k:
+			demux_subchan_append_bit(schan, inbits & 0x01);
+			demux_subchan_append_bit(schan, inbits & 0x02);
+			break;
+		case OSMO_I460_RATE_32k:
+			demux_subchan_append_bit(schan, inbits & 0x01);
+			demux_subchan_append_bit(schan, inbits & 0x02);
+			demux_subchan_append_bit(schan, inbits & 0x04);
+			demux_subchan_append_bit(schan, inbits & 0x08);
+			break;
+		case OSMO_I460_RATE_64k:
+			demux_subchan_append_bit(schan, inbits & 0x01);
+			demux_subchan_append_bit(schan, inbits & 0x02);
+			demux_subchan_append_bit(schan, inbits & 0x04);
+			demux_subchan_append_bit(schan, inbits & 0x08);
+			demux_subchan_append_bit(schan, inbits & 0x10);
+			demux_subchan_append_bit(schan, inbits & 0x20);
+			demux_subchan_append_bit(schan, inbits & 0x40);
+			demux_subchan_append_bit(schan, inbits & 0x80);
+			break;
+		default:
+			OSMO_ASSERT(0);
+		}
+	}
+}
+
+/*! Data from E1 timeslot into de-multiplexer
+ *  \param[in] ts timeslot state
+ *  \param[in] data input data bytes as received from E1/T1
+ *  \param[in] data_len length of data in bytes */
+void osmo_i460_demux_in(struct osmo_i460_timeslot *ts, const uint8_t *data, size_t data_len)
+{
+	struct osmo_i460_subchan *schan;
+	struct osmo_i460_subchan_demux *demux;
+	int i;
+
+	/* fast path if entire 64k slot is used */
+	if (osmo_i460_has_single_64k_schan(ts)) {
+		schan = &ts->schan[0];
+		demux = &schan->demux;
+		if (demux->out_cb_bytes)
+			demux->out_cb_bytes(demux->user_data, data, data_len);
+		else {
+			ubit_t bits[data_len*8];
+			osmo_pbit2ubit(bits, data, data_len*8);
+			demux->out_cb_bits(demux->user_data, bits, data_len*8);
+		}
+		return;
+	}
+
+	/* Slow path iterating over all lchans */
+	for (i = 0; i < ARRAY_SIZE(ts->schan); i++) {
+		schan = &ts->schan[i];
+		if (schan->rate == OSMO_I460_RATE_NONE)
+			continue;
+		demux_subchan_extract_bits(schan, data, data_len);
+	}
+}
+
+
+/***********************************************************************
+ * Multiplexer
+ ***********************************************************************/
+
+/*! enqueue a to-be-transmitted message buffer containing unpacked bits */
+void osmo_i460_mux_enqueue(struct osmo_i460_subchan *schan, struct msgb *msg)
+{
+	OSMO_ASSERT(msgb_length(msg) > 0);
+	msgb_enqueue(&schan->mux.tx_queue, msg);
+}
+
+/* mux: pull the next bit out of the given sub-channel */
+static ubit_t mux_schan_provide_bit(struct osmo_i460_subchan *schan)
+{
+	struct osmo_i460_subchan_mux *mux = &schan->mux;
+	struct msgb *msg;
+	ubit_t bit;
+
+	/* if we don't have anything to transmit, return '1' bits */
+	if (llist_empty(&mux->tx_queue))
+		return 0x01;
+
+	msg = llist_entry(mux->tx_queue.next, struct msgb, list);
+	bit = msgb_pull_u8(msg);
+
+	/* free msgb if we have pulled the last bit */
+	if (msgb_length(msg) <= 0) {
+		llist_del(&msg->list);
+		talloc_free(msg);
+	}
+
+	return bit;
+}
+
+/*! provide one byte with the subchan-specific bits of given sub-channel.
+ *  \param[in] schan sub-channel that is to provide bits
+ *  \parma[out] mask bitmask of those bits filled in
+ *  \returns bits of given sub-channel */
+static uint8_t mux_subchan_provide_bits(struct osmo_i460_subchan *schan, uint8_t *mask)
+{
+	uint8_t outbits = 0;
+	uint8_t outmask;
+
+	switch (schan->rate) {
+	case OSMO_I460_RATE_8k:
+		outbits = mux_schan_provide_bit(schan);
+		outmask = 0x01;
+		break;
+	case OSMO_I460_RATE_16k:
+		outbits |= mux_schan_provide_bit(schan) << 1;
+		outbits |= mux_schan_provide_bit(schan) << 0;
+		outmask = 0x03;
+		break;
+	case OSMO_I460_RATE_32k:
+		outbits |= mux_schan_provide_bit(schan) << 3;
+		outbits |= mux_schan_provide_bit(schan) << 2;
+		outbits |= mux_schan_provide_bit(schan) << 1;
+		outbits |= mux_schan_provide_bit(schan) << 0;
+		outmask = 0x0F;
+		break;
+	case OSMO_I460_RATE_64k:
+		outbits |= mux_schan_provide_bit(schan) << 7;
+		outbits |= mux_schan_provide_bit(schan) << 6;
+		outbits |= mux_schan_provide_bit(schan) << 5;
+		outbits |= mux_schan_provide_bit(schan) << 4;
+		outbits |= mux_schan_provide_bit(schan) << 3;
+		outbits |= mux_schan_provide_bit(schan) << 2;
+		outbits |= mux_schan_provide_bit(schan) << 1;
+		outbits |= mux_schan_provide_bit(schan) << 0;
+		outmask = 0xFF;
+		break;
+	default:
+		OSMO_ASSERT(0);
+	}
+	*mask = outmask << schan->bit_offset;
+	return outbits << schan->bit_offset;
+}
+
+/* provide one byte of multiplexed I.460 bits */
+static uint8_t mux_timeslot_provide_bits(struct osmo_i460_timeslot *ts)
+{
+	int i, count = 0;
+	uint8_t ret = 0xff; /* unused bits must be '1' as per I.460 */
+
+	for (i = 0; i < ARRAY_SIZE(ts->schan); i++) {
+		struct osmo_i460_subchan *schan = &ts->schan[i];
+		uint8_t bits, mask;
+
+		if (schan->rate == OSMO_I460_RATE_NONE)
+			continue;
+		count++;
+		bits = mux_subchan_provide_bits(schan, &mask);
+		ret &= ~mask;
+		ret |= bits;
+	}
+
+	return ret;
+}
+
+
+/*! Data from E1 timeslot into de-multiplexer
+ *  \param[in] ts timeslot state
+ *  \param[out] out caller-provided buffer where to store generated output bytes
+ *  \param[in] out_len number of bytes to be stored at out
+ */
+int osmo_i460_mux_out(struct osmo_i460_timeslot *ts, uint8_t *out, size_t out_len)
+{
+	int i;
+
+	/* fast path if entire 64k slot is used */
+	//if (osmo_i460_has_single_64k_schan(ts)) { }
+
+	for (i = 0; i < out_len; i++)
+		out[i] = mux_timeslot_provide_bits(ts);
+
+	return out_len;
+}
+
+
+/***********************************************************************
+ * Initialization / Control
+ ***********************************************************************/
+
+
+static int alloc_bitbuf(void *ctx, struct osmo_i460_subchan *schan, size_t num_bits)
+{
+	struct osmo_i460_subchan_demux *demux = &schan->demux;
+
+	talloc_free(demux->out_bitbuf);
+	demux->out_bitbuf = talloc_zero_size(ctx, num_bits);
+	if (!demux->out_bitbuf)
+		return -ENOMEM;
+	demux->out_bitbuf_size = num_bits;
+
+	return 0;
+}
+
+
+static int find_unused_subchan_idx(const struct osmo_i460_timeslot *ts)
+{
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(ts->schan); i++) {
+		const struct osmo_i460_subchan *schan = &ts->schan[i];
+		if (schan->rate == OSMO_I460_RATE_NONE)
+			return i;
+	}
+	return -1;
+}
+
+/*! initialize an I.460 timeslot */
+void osmo_i460_ts_init(struct osmo_i460_timeslot *ts)
+{
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(ts->schan); i++) {
+		struct osmo_i460_subchan *schan = &ts->schan[i];
+
+		memset(schan, 0, sizeof(*schan));
+		schan->rate = OSMO_I460_RATE_NONE;
+		INIT_LLIST_HEAD(&schan->mux.tx_queue);
+	}
+}
+
+/*! add a new sub-channel to the given timeslot
+ *  \param[in] ctx talloc context from where to allocate the internal buffer
+ *  \param[in] ts timeslot to which to add a sub-channel
+ *  \param[in] chd description of the sub-channel to be added
+ *  \return pointer to sub-channel on success, NULL on error */
+struct osmo_i460_subchan *
+osmo_i460_subchan_add(void *ctx, struct osmo_i460_timeslot *ts, const struct osmo_i460_schan_desc *chd)
+{
+	struct osmo_i460_subchan *schan;
+	int idx, rc;
+
+	idx = find_unused_subchan_idx(ts);
+	if (idx < 0)
+		return NULL;
+
+	schan = &ts->schan[idx];
+
+	schan->rate = chd->rate;
+	schan->bit_offset = chd->bit_offset;
+
+	schan->demux.out_cb_bits = chd->demux.out_cb_bits;
+	schan->demux.out_cb_bytes = chd->demux.out_cb_bytes;
+	schan->demux.user_data = chd->demux.user_data;
+	rc = alloc_bitbuf(ctx, schan, chd->demux.num_bits);
+	if (rc < 0) {
+		memset(schan, 0, sizeof(*schan));
+		return NULL;
+	}
+
+	/* return number of schan in use */
+	return schan;
+}
+
+/* remove a su-channel from the multiplex */
+void osmo_i460_subchan_del(struct osmo_i460_subchan *schan)
+{
+	talloc_free(schan->demux.out_bitbuf);
+	memset(schan, 0, sizeof(*schan));
+}
+
+/*! @} */
diff --git a/src/gsm/libosmogsm.map b/src/gsm/libosmogsm.map
index 70b3916..2000e6c 100644
--- a/src/gsm/libosmogsm.map
+++ b/src/gsm/libosmogsm.map
@@ -667,5 +667,12 @@
 osmo_cbsp_recv_buffered;
 osmo_cbsp_errstr;
 
+osmo_i460_demux_in;
+osmo_i460_mux_enqueue;
+osmo_i460_mux_out;
+osmo_i460_subchan_add;
+osmo_i460_subchan_del;
+osmo_i460_ts_init;
+
 local: *;
 };
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 0d0327a..5e810e6 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -35,6 +35,7 @@
 		 context/context_test					\
                  gsm0502/gsm0502_test					\
                  dtx/dtx_gsm0503_test					\
+                 i460_mux/i460_mux_test					\
 		 $(NULL)
 
 if ENABLE_MSGFILE
@@ -269,6 +270,9 @@
 exec_exec_test_SOURCES = exec/exec_test.c
 exec_exec_test_LDADD = $(LDADD)
 
+i460_mux_i460_mux_test_SOURCES = i460_mux/i460_mux_test.c
+i460_mux_i460_mux_test_LDADD = $(LDADD) $(top_builddir)/src/gsm/libosmogsm.la
+
 # The `:;' works around a Bash 3.2 bug when the output is not writeable.
 $(srcdir)/package.m4: $(top_srcdir)/configure.ac
 	:;{ \
@@ -346,6 +350,7 @@
 	     gsm0502/gsm0502_test.ok \
 	     dtx/dtx_gsm0503_test.ok \
 	     exec/exec_test.ok exec/exec_test.err \
+	     i460_mux/i460_mux_test.ok \
 	     $(NULL)
 
 DISTCLEANFILES = atconfig atlocal conv/gsm0503_test_vectors.c
diff --git a/tests/i460_mux/i460_mux_test.c b/tests/i460_mux/i460_mux_test.c
new file mode 100644
index 0000000..53144fd
--- /dev/null
+++ b/tests/i460_mux/i460_mux_test.c
@@ -0,0 +1,397 @@
+
+#include <osmocom/core/utils.h>
+
+#include <osmocom/gsm/i460_mux.h>
+
+static void bits_cb(void *user_data, const ubit_t *bits, unsigned int num_bits)
+{
+	char *str = user_data;
+	printf("demux_bits_cb '%s': %s\n", str, osmo_ubit_dump(bits, num_bits));
+}
+
+
+const struct osmo_i460_schan_desc scd64 = {
+	.rate = OSMO_I460_RATE_64k,
+	.bit_offset = 0,
+	.demux = {
+		.num_bits = 40,
+		.out_cb_bits = bits_cb,
+		.out_cb_bytes = NULL,
+		.user_data = "64k",
+	},
+};
+
+const struct osmo_i460_schan_desc scd32_0 = {
+	.rate = OSMO_I460_RATE_32k,
+	.bit_offset = 0,
+	.demux = {
+		.num_bits = 40,
+		.out_cb_bits = bits_cb,
+		.out_cb_bytes = NULL,
+		.user_data = "32k_0",
+	},
+};
+const struct osmo_i460_schan_desc scd32_4 = {
+	.rate = OSMO_I460_RATE_32k,
+	.bit_offset = 4,
+	.demux = {
+		.num_bits = 40,
+		.out_cb_bits = bits_cb,
+		.out_cb_bytes = NULL,
+		.user_data = "32k_4",
+	},
+};
+
+const struct osmo_i460_schan_desc scd16_0 = {
+	.rate = OSMO_I460_RATE_16k,
+	.bit_offset = 0,
+	.demux = {
+		.num_bits = 40,
+		.out_cb_bits = bits_cb,
+		.out_cb_bytes = NULL,
+		.user_data = "16k_0",
+	},
+};
+const struct osmo_i460_schan_desc scd16_2 = {
+	.rate = OSMO_I460_RATE_16k,
+	.bit_offset = 2,
+	.demux = {
+		.num_bits = 40,
+		.out_cb_bits = bits_cb,
+		.out_cb_bytes = NULL,
+		.user_data = "16k_2",
+	},
+};
+const struct osmo_i460_schan_desc scd16_4 = {
+	.rate = OSMO_I460_RATE_16k,
+	.bit_offset = 4,
+	.demux = {
+		.num_bits = 40,
+		.out_cb_bits = bits_cb,
+		.out_cb_bytes = NULL,
+		.user_data = "16k_4",
+	},
+};
+const struct osmo_i460_schan_desc scd16_6 = {
+	.rate = OSMO_I460_RATE_16k,
+	.bit_offset = 6,
+	.demux = {
+		.num_bits = 40,
+		.out_cb_bits = bits_cb,
+		.out_cb_bytes = NULL,
+		.user_data = "16k_6",
+	},
+};
+
+const struct osmo_i460_schan_desc scd8_0 = {
+	.rate = OSMO_I460_RATE_8k,
+	.bit_offset = 0,
+	.demux = {
+		.num_bits = 40,
+		.out_cb_bits = bits_cb,
+		.out_cb_bytes = NULL,
+		.user_data = "8k_0",
+	},
+};
+const struct osmo_i460_schan_desc scd8_1 = {
+	.rate = OSMO_I460_RATE_8k,
+	.bit_offset = 1,
+	.demux = {
+		.num_bits = 40,
+		.out_cb_bits = bits_cb,
+		.out_cb_bytes = NULL,
+		.user_data = "8k_1",
+	},
+};
+const struct osmo_i460_schan_desc scd8_2 = {
+	.rate = OSMO_I460_RATE_8k,
+	.bit_offset = 2,
+	.demux = {
+		.num_bits = 40,
+		.out_cb_bits = bits_cb,
+		.out_cb_bytes = NULL,
+		.user_data = "8k_2",
+	},
+};
+const struct osmo_i460_schan_desc scd8_3 = {
+	.rate = OSMO_I460_RATE_8k,
+	.bit_offset = 3,
+	.demux = {
+		.num_bits = 40,
+		.out_cb_bits = bits_cb,
+		.out_cb_bytes = NULL,
+		.user_data = "8k_3",
+	},
+};
+const struct osmo_i460_schan_desc scd8_4 = {
+	.rate = OSMO_I460_RATE_8k,
+	.bit_offset = 4,
+	.demux = {
+		.num_bits = 40,
+		.out_cb_bits = bits_cb,
+		.out_cb_bytes = NULL,
+		.user_data = "8k_4",
+	},
+};
+const struct osmo_i460_schan_desc scd8_5 = {
+	.rate = OSMO_I460_RATE_8k,
+	.bit_offset = 5,
+	.demux = {
+		.num_bits = 40,
+		.out_cb_bits = bits_cb,
+		.out_cb_bytes = NULL,
+		.user_data = "8k_5",
+	},
+};
+const struct osmo_i460_schan_desc scd8_6 = {
+	.rate = OSMO_I460_RATE_8k,
+	.bit_offset = 6,
+	.demux = {
+		.num_bits = 40,
+		.out_cb_bits = bits_cb,
+		.out_cb_bytes = NULL,
+		.user_data = "8k_6",
+	},
+};
+const struct osmo_i460_schan_desc scd8_7 = {
+	.rate = OSMO_I460_RATE_8k,
+	.bit_offset = 7,
+	.demux = {
+		.num_bits = 40,
+		.out_cb_bits = bits_cb,
+		.out_cb_bytes = NULL,
+		.user_data = "8k_7",
+	},
+};
+
+static void test_no_subchan(void)
+{
+	struct osmo_i460_timeslot _ts, *ts = &_ts;
+
+	/* Initialization */
+	printf("\n==> %s\n", __func__);
+	osmo_i460_ts_init(ts);
+
+	/* feed in some data; expect nothing to happen */
+	const uint8_t nothing[128] = { 0, };
+	osmo_i460_demux_in(ts, nothing, sizeof(nothing));
+
+	/* pull bytes out of mux (should be all 0xff) */
+	uint8_t buf[128];
+	osmo_i460_mux_out(ts, buf, sizeof(buf));
+	printf("out: %s\n", osmo_hexdump(buf, sizeof(buf)));
+}
+
+static struct msgb *gen_alternating_bitmsg(unsigned int num_bits)
+{
+	struct msgb *msg = msgb_alloc(num_bits, "mux-in");
+	int i;
+	for (i = 0; i < num_bits; i++)
+		msgb_put_u8(msg, i & 1);
+	return msg;
+}
+
+static void test_64k_subchan(void)
+{
+	struct osmo_i460_timeslot _ts, *ts = &_ts;
+
+	/* Initialization */
+	printf("\n==> %s\n", __func__);
+	osmo_i460_ts_init(ts);
+	osmo_i460_subchan_add(NULL, ts, &scd64);
+
+	/* demux */
+	uint8_t sequence[128];
+	int i;
+	for (i = 0; i < sizeof(sequence); i++)
+		sequence[i] = i;
+	osmo_i460_demux_in(ts, sequence, sizeof(sequence));
+
+	/* mux */
+	struct msgb *msg = gen_alternating_bitmsg(128);
+	osmo_i460_mux_enqueue(&ts->schan[0], msg);
+
+	uint8_t buf[16];
+	osmo_i460_mux_out(ts, buf, sizeof(buf));
+	printf("mux_out: %s\n", osmo_hexdump(buf, sizeof(buf)));
+
+	osmo_i460_subchan_del(&ts->schan[0]);
+}
+
+static void test_32k_subchan(void)
+{
+	struct osmo_i460_timeslot _ts, *ts = &_ts;
+
+	/* Initialization */
+	printf("\n==> %s\n", __func__);
+	osmo_i460_ts_init(ts);
+	osmo_i460_subchan_add(NULL, ts, &scd32_0);
+	osmo_i460_subchan_add(NULL, ts, &scd32_4);
+
+	/* demux */
+	uint8_t sequence[10];
+	int i;
+	for (i = 0; i < sizeof(sequence); i++)
+		sequence[i] = 0;
+	sequence[0] = 0x0f;
+	sequence[1] = 0xf0;
+	sequence[2] = 0xff;
+	osmo_i460_demux_in(ts, sequence, sizeof(sequence));
+
+	/* mux */
+
+	/* test with only a single channel active */
+	for (i = 0; i < 2; i++) {
+		struct msgb *msg = gen_alternating_bitmsg(128);
+		osmo_i460_mux_enqueue(&ts->schan[i], msg);
+		printf("%s-single-%u\n", __func__, i);
+
+		uint8_t buf[16];
+		int j;
+		for (j = 0; j < 3; j++) {
+			osmo_i460_mux_out(ts, buf, sizeof(buf));
+			printf("mux_out: %s\n", osmo_hexdump(buf, sizeof(buf)));
+		}
+	}
+
+	for (i = 0; i < 4; i++)
+		osmo_i460_subchan_del(&ts->schan[i]);
+}
+
+
+
+static void test_16k_subchan(void)
+{
+	struct osmo_i460_timeslot _ts, *ts = &_ts;
+
+	/* Initialization */
+	printf("\n==> %s\n", __func__);
+	osmo_i460_ts_init(ts);
+	osmo_i460_subchan_add(NULL, ts, &scd16_0);
+	osmo_i460_subchan_add(NULL, ts, &scd16_2);
+	osmo_i460_subchan_add(NULL, ts, &scd16_4);
+	osmo_i460_subchan_add(NULL, ts, &scd16_6);
+
+	/* demux */
+	uint8_t sequence[20];
+	int i;
+	for (i = 0; i < sizeof(sequence); i++)
+		sequence[i] = 0;
+	sequence[0] = 0x03;
+	sequence[1] = 0x0c;
+	sequence[2] = 0x30;
+	sequence[3] = 0xc0;
+	sequence[4] = 0xff;
+	osmo_i460_demux_in(ts, sequence, sizeof(sequence));
+
+	/* mux */
+
+	/* test with only a single channel active */
+	for (i = 0; i < 4; i++) {
+		struct msgb *msg = gen_alternating_bitmsg(128);
+		osmo_i460_mux_enqueue(&ts->schan[i], msg);
+		printf("%s-single-%u\n", __func__, i);
+
+		uint8_t buf[16];
+		int j;
+		for (j = 0; j < 5; j++) {
+			osmo_i460_mux_out(ts, buf, sizeof(buf));
+			printf("mux_out: %s\n", osmo_hexdump(buf, sizeof(buf)));
+		}
+	}
+
+	for (i = 0; i < 4; i++)
+		osmo_i460_subchan_del(&ts->schan[i]);
+}
+
+
+static void test_8k_subchan(void)
+{
+	struct osmo_i460_timeslot _ts, *ts = &_ts;
+
+	/* Initialization */
+	printf("\n==> %s\n", __func__);
+	osmo_i460_ts_init(ts);
+	osmo_i460_subchan_add(NULL, ts, &scd8_0);
+	osmo_i460_subchan_add(NULL, ts, &scd8_1);
+	osmo_i460_subchan_add(NULL, ts, &scd8_2);
+	osmo_i460_subchan_add(NULL, ts, &scd8_3);
+	osmo_i460_subchan_add(NULL, ts, &scd8_4);
+	osmo_i460_subchan_add(NULL, ts, &scd8_5);
+	osmo_i460_subchan_add(NULL, ts, &scd8_6);
+	osmo_i460_subchan_add(NULL, ts, &scd8_7);
+
+	/* demux */
+	uint8_t sequence[40];
+	int i;
+	for (i = 0; i < sizeof(sequence); i++)
+		sequence[i] = 0;
+	i = 0;
+	sequence[i++] = 0x01;
+	sequence[i++] = 0x02;
+	sequence[i++] = 0x04;
+	sequence[i++] = 0x08;
+	sequence[i++] = 0x0f;
+	sequence[i++] = 0x10;
+	sequence[i++] = 0x20;
+	sequence[i++] = 0x40;
+	sequence[i++] = 0x80;
+	sequence[i++] = 0xf0;
+	sequence[i++] = 0xff;
+	osmo_i460_demux_in(ts, sequence, sizeof(sequence));
+
+	/* mux */
+
+	/* test with only a single channel active */
+	for (i = 0; i < 8; i++) {
+		struct msgb *msg = gen_alternating_bitmsg(64);
+		osmo_i460_mux_enqueue(&ts->schan[i], msg);
+		printf("%s-single-%u\n", __func__, i);
+
+		uint8_t buf[16];
+		int j;
+		for (j = 0; j < 5; j++) {
+			osmo_i460_mux_out(ts, buf, sizeof(buf));
+			printf("mux_out: %s\n", osmo_hexdump(buf, sizeof(buf)));
+		}
+	}
+
+	for (i = 0; i < 8; i++)
+		osmo_i460_subchan_del(&ts->schan[i]);
+}
+
+/* activate only one sub-channel; expect unused bits to be '1' */
+static void test_unused_subchan(void)
+{
+	struct osmo_i460_timeslot _ts, *ts = &_ts;
+
+	/* Initialization */
+	printf("\n==> %s\n", __func__);
+	osmo_i460_ts_init(ts);
+	osmo_i460_subchan_add(NULL, ts, &scd16_0);
+
+	/* mux */
+	struct msgb *msg = gen_alternating_bitmsg(128);
+	memset(msgb_data(msg), 0, msgb_length(msg));
+	osmo_i460_mux_enqueue(&ts->schan[0], msg);
+	printf("%s-single\n", __func__);
+
+	uint8_t buf[16];
+	int j;
+	for (j = 0; j < 5; j++) {
+		osmo_i460_mux_out(ts, buf, sizeof(buf));
+		printf("mux_out: %s\n", osmo_hexdump(buf, sizeof(buf)));
+	}
+
+	osmo_i460_subchan_del(&ts->schan[0]);
+}
+
+int main(int argc, char **argv)
+{
+	test_no_subchan();
+	test_64k_subchan();
+	test_32k_subchan();
+	test_16k_subchan();
+	test_8k_subchan();
+	test_unused_subchan();
+}
diff --git a/tests/i460_mux/i460_mux_test.ok b/tests/i460_mux/i460_mux_test.ok
new file mode 100644
index 0000000..b94fb7b
--- /dev/null
+++ b/tests/i460_mux/i460_mux_test.ok
@@ -0,0 +1,115 @@
+
+==> test_no_subchan
+out: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 
+
+==> test_64k_subchan
+demux_bits_cb '64k': 0000000000000001000000100000001100000100000001010000011000000111000010000000100100001010000010110000110000001101000011100000111100010000000100010001001000010011000101000001010100010110000101110001100000011001000110100001101100011100000111010001111000011111001000000010000100100010001000110010010000100101001001100010011100101000001010010010101000101011001011000010110100101110001011110011000000110001001100100011001100110100001101010011011000110111001110000011100100111010001110110011110000111101001111100011111101000000010000010100001001000011010001000100010101000110010001110100100001001001010010100100101101001100010011010100111001001111010100000101000101010010010100110101010001010101010101100101011101011000010110010101101001011011010111000101110101011110010111110110000001100001011000100110001101100100011001010110011001100111011010000110100101101010011010110110110001101101011011100110111101110000011100010111001001110011011101000111010101110110011101110111100001111001011110100111101101111100011111010111111001111111
+mux_out: 55 55 55 55 55 55 55 55 55 55 55 55 55 55 55 55 
+
+==> test_32k_subchan
+demux_bits_cb '32k_0': 1111000011110000000000000000000000000000
+demux_bits_cb '32k_4': 0000111111110000000000000000000000000000
+test_32k_subchan-single-0
+mux_out: f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 
+mux_out: f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 f5 
+mux_out: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 
+test_32k_subchan-single-1
+mux_out: 5f 5f 5f 5f 5f 5f 5f 5f 5f 5f 5f 5f 5f 5f 5f 5f 
+mux_out: 5f 5f 5f 5f 5f 5f 5f 5f 5f 5f 5f 5f 5f 5f 5f 5f 
+mux_out: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 
+
+==> test_16k_subchan
+demux_bits_cb '16k_0': 1100000011000000000000000000000000000000
+demux_bits_cb '16k_2': 0011000011000000000000000000000000000000
+demux_bits_cb '16k_4': 0000110011000000000000000000000000000000
+demux_bits_cb '16k_6': 0000001111000000000000000000000000000000
+test_16k_subchan-single-0
+mux_out: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 
+mux_out: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 
+mux_out: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 
+mux_out: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 
+mux_out: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 
+test_16k_subchan-single-1
+mux_out: f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 
+mux_out: f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 
+mux_out: f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 
+mux_out: f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 
+mux_out: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 
+test_16k_subchan-single-2
+mux_out: df df df df df df df df df df df df df df df df 
+mux_out: df df df df df df df df df df df df df df df df 
+mux_out: df df df df df df df df df df df df df df df df 
+mux_out: df df df df df df df df df df df df df df df df 
+mux_out: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 
+test_16k_subchan-single-3
+mux_out: 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 
+mux_out: 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 
+mux_out: 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 
+mux_out: 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 7f 
+mux_out: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 
+
+==> test_8k_subchan
+demux_bits_cb '8k_0': 1000100000100000000000000000000000000000
+demux_bits_cb '8k_1': 0100100000100000000000000000000000000000
+demux_bits_cb '8k_2': 0010100000100000000000000000000000000000
+demux_bits_cb '8k_3': 0001100000100000000000000000000000000000
+demux_bits_cb '8k_4': 0000010001100000000000000000000000000000
+demux_bits_cb '8k_5': 0000001001100000000000000000000000000000
+demux_bits_cb '8k_6': 0000000101100000000000000000000000000000
+demux_bits_cb '8k_7': 0000000011100000000000000000000000000000
+test_8k_subchan-single-0
+mux_out: fe ff fe ff fe ff fe ff fe ff fe ff fe ff fe ff 
+mux_out: fe ff fe ff fe ff fe ff fe ff fe ff fe ff fe ff 
+mux_out: fe ff fe ff fe ff fe ff fe ff fe ff fe ff fe ff 
+mux_out: fe ff fe ff fe ff fe ff fe ff fe ff fe ff fe ff 
+mux_out: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 
+test_8k_subchan-single-1
+mux_out: fd ff fd ff fd ff fd ff fd ff fd ff fd ff fd ff 
+mux_out: fd ff fd ff fd ff fd ff fd ff fd ff fd ff fd ff 
+mux_out: fd ff fd ff fd ff fd ff fd ff fd ff fd ff fd ff 
+mux_out: fd ff fd ff fd ff fd ff fd ff fd ff fd ff fd ff 
+mux_out: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 
+test_8k_subchan-single-2
+mux_out: fb ff fb ff fb ff fb ff fb ff fb ff fb ff fb ff 
+mux_out: fb ff fb ff fb ff fb ff fb ff fb ff fb ff fb ff 
+mux_out: fb ff fb ff fb ff fb ff fb ff fb ff fb ff fb ff 
+mux_out: fb ff fb ff fb ff fb ff fb ff fb ff fb ff fb ff 
+mux_out: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 
+test_8k_subchan-single-3
+mux_out: f7 ff f7 ff f7 ff f7 ff f7 ff f7 ff f7 ff f7 ff 
+mux_out: f7 ff f7 ff f7 ff f7 ff f7 ff f7 ff f7 ff f7 ff 
+mux_out: f7 ff f7 ff f7 ff f7 ff f7 ff f7 ff f7 ff f7 ff 
+mux_out: f7 ff f7 ff f7 ff f7 ff f7 ff f7 ff f7 ff f7 ff 
+mux_out: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 
+test_8k_subchan-single-4
+mux_out: ef ff ef ff ef ff ef ff ef ff ef ff ef ff ef ff 
+mux_out: ef ff ef ff ef ff ef ff ef ff ef ff ef ff ef ff 
+mux_out: ef ff ef ff ef ff ef ff ef ff ef ff ef ff ef ff 
+mux_out: ef ff ef ff ef ff ef ff ef ff ef ff ef ff ef ff 
+mux_out: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 
+test_8k_subchan-single-5
+mux_out: df ff df ff df ff df ff df ff df ff df ff df ff 
+mux_out: df ff df ff df ff df ff df ff df ff df ff df ff 
+mux_out: df ff df ff df ff df ff df ff df ff df ff df ff 
+mux_out: df ff df ff df ff df ff df ff df ff df ff df ff 
+mux_out: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 
+test_8k_subchan-single-6
+mux_out: bf ff bf ff bf ff bf ff bf ff bf ff bf ff bf ff 
+mux_out: bf ff bf ff bf ff bf ff bf ff bf ff bf ff bf ff 
+mux_out: bf ff bf ff bf ff bf ff bf ff bf ff bf ff bf ff 
+mux_out: bf ff bf ff bf ff bf ff bf ff bf ff bf ff bf ff 
+mux_out: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 
+test_8k_subchan-single-7
+mux_out: 7f ff 7f ff 7f ff 7f ff 7f ff 7f ff 7f ff 7f ff 
+mux_out: 7f ff 7f ff 7f ff 7f ff 7f ff 7f ff 7f ff 7f ff 
+mux_out: 7f ff 7f ff 7f ff 7f ff 7f ff 7f ff 7f ff 7f ff 
+mux_out: 7f ff 7f ff 7f ff 7f ff 7f ff 7f ff 7f ff 7f ff 
+mux_out: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 
+
+==> test_unused_subchan
+test_unused_subchan-single
+mux_out: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc 
+mux_out: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc 
+mux_out: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc 
+mux_out: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc 
+mux_out: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 
diff --git a/tests/testsuite.at b/tests/testsuite.at
index bab5730..4ff6671 100644
--- a/tests/testsuite.at
+++ b/tests/testsuite.at
@@ -375,3 +375,9 @@
 cat $abs_srcdir/exec/exec_test.err > experr
 AT_CHECK([$abs_top_builddir/tests/exec/exec_test], [0], [expout], [experr])
 AT_CLEANUP
+
+AT_SETUP([i460_mux])
+AT_KEYWORDS([i460_mux])
+cat $abs_srcdir/i460_mux/i460_mux_test.ok > expout
+AT_CHECK([$abs_top_builddir/tests/i460_mux/i460_mux_test], [0], [expout], [ignore])
+AT_CLEANUP

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

Gerrit-Project: libosmocore
Gerrit-Branch: master
Gerrit-Change-Id: Id522f06e73b77332b437b7a27e4966872da70eda
Gerrit-Change-Number: 18247
Gerrit-PatchSet: 11
Gerrit-Owner: laforge <laforge at osmocom.org>
Gerrit-Reviewer: Jenkins Builder
Gerrit-Reviewer: laforge <laforge at osmocom.org>
Gerrit-Reviewer: tnt <tnt at 246tNt.com>
Gerrit-MessageType: merged
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.osmocom.org/pipermail/gerrit-log/attachments/20200528/c6cc09a0/attachment.htm>


More information about the gerrit-log mailing list