fixeria has uploaded this change for review.
libosmo-trx: add TRXC/TRXD message codec
Introduce libosmo-trx: a shared implementation of the TRXC/TRXD
(OpenBTS-style TRX) protocol, to be used by osmo-trx, osmo-bts,
trxcon, and the upcoming C rewrite of fake_trx.
This initial version provides two I/O-free codec modules:
* trxd: TRXD PDU codec for BURST.ind and BURST.req
* trxc: verb-agnostic TRXC message codec (CMD/RSP/IND)
The codec logic is based on the existing implementations in
osmo-bts (f0ee51997470e1c3020ddb85082385ed43ba0c68).
Change-Id: I933fc417a67d0043f74b04626b7643c79e381492
---
M .gitignore
M Makefile.am
M configure.ac
A libosmo-trx/Makefile.am
A libosmo-trx/include/Makefile.am
A libosmo-trx/include/osmocom/trx/trxc.h
A libosmo-trx/include/osmocom/trx/trxd.h
A libosmo-trx/include/osmocom/trx/trxd_int.h
A libosmo-trx/libosmotrx.pc.in
A libosmo-trx/src/Makefile.am
A libosmo-trx/src/trxc.c
A libosmo-trx/src/trxd.c
M tests/Makefile.am
A tests/libosmo-trx/Makefile.am
A tests/libosmo-trx/trxc_test.c
A tests/libosmo-trx/trxc_test.ok
A tests/libosmo-trx/trxd_test.c
A tests/libosmo-trx/trxd_test.ok
M tests/testsuite.at
19 files changed, 1,909 insertions(+), 1 deletion(-)
git pull ssh://gerrit.osmocom.org:29418/osmo-trx refs/changes/04/43104/1
diff --git a/.gitignore b/.gitignore
index fc567db..26f7fab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
*.o
*.lo
*.la
+libosmo-trx/libosmotrx.pc
Transceiver52M/osmo-trx-uhd
Transceiver52M/osmo-trx-usrp1
Transceiver52M/osmo-trx-lms
@@ -19,6 +20,8 @@
.clang-format
# tests
+tests/libosmo-trx/trxc_test
+tests/libosmo-trx/trxd_test
tests/CommonLibs/BitVectorTest
tests/CommonLibs/F16Test
tests/CommonLibs/InterthreadTest
diff --git a/Makefile.am b/Makefile.am
index d4c3c93..844d6e7 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -34,6 +34,7 @@
# Order must be preserved
SUBDIRS += \
+ libosmo-trx \
CommonLibs \
GSM \
Transceiver52M \
diff --git a/configure.ac b/configure.ac
index a384495..34afa0b 100644
--- a/configure.ac
+++ b/configure.ac
@@ -66,7 +66,11 @@
PKG_PROG_PKG_CONFIG([0.20])
AC_LIBTOOL_WIN32_DLL
-AC_DISABLE_SHARED dnl don't build shared libraries
+dnl Most libraries in this tree (CommonLibs, GSM, Transceiver52M/*) are
+dnl noinst convenience libraries and never installed, so static vs. shared
+dnl makes no difference for them. libosmo-trx/src/libosmotrx.la, however,
+dnl is installed and meant to be linked by other packages (osmo-bts,
+dnl osmocom-bb/trxcon), so shared library support must stay enabled.
AC_ENABLE_STATIC dnl do build static libraries
LT_INIT
@@ -358,6 +362,10 @@
dnl Output files
AC_CONFIG_FILES([\
Makefile \
+ libosmo-trx/Makefile \
+ libosmo-trx/include/Makefile \
+ libosmo-trx/src/Makefile \
+ libosmo-trx/libosmotrx.pc \
CommonLibs/Makefile \
GSM/Makefile \
Transceiver52M/Makefile \
@@ -373,6 +381,7 @@
Transceiver52M/device/ipc/Makefile \
Transceiver52M/device/bladerf/Makefile \
tests/Makefile \
+ tests/libosmo-trx/Makefile \
tests/CommonLibs/Makefile \
tests/Transceiver52M/Makefile \
utils/Makefile \
diff --git a/libosmo-trx/Makefile.am b/libosmo-trx/Makefile.am
new file mode 100644
index 0000000..a76bc8f
--- /dev/null
+++ b/libosmo-trx/Makefile.am
@@ -0,0 +1,7 @@
+SUBDIRS = \
+ include \
+ src \
+ $(NULL)
+
+pkgconfigdir = $(libdir)/pkgconfig
+pkgconfig_DATA = libosmotrx.pc
diff --git a/libosmo-trx/include/Makefile.am b/libosmo-trx/include/Makefile.am
new file mode 100644
index 0000000..993f430
--- /dev/null
+++ b/libosmo-trx/include/Makefile.am
@@ -0,0 +1,8 @@
+nobase_include_HEADERS = \
+ osmocom/trx/trxc.h \
+ osmocom/trx/trxd.h \
+ $(NULL)
+
+noinst_HEADERS = \
+ osmocom/trx/trxd_int.h \
+ $(NULL)
diff --git a/libosmo-trx/include/osmocom/trx/trxc.h b/libosmo-trx/include/osmocom/trx/trxc.h
new file mode 100644
index 0000000..ee7935a
--- /dev/null
+++ b/libosmo-trx/include/osmocom/trx/trxc.h
@@ -0,0 +1,59 @@
+/*! \file osmocom/trx/trxc.h
+ * TRXC (control) protocol and clock indications: I/O-free message codec. */
+#pragma once
+
+#include <stdint.h>
+#include <stddef.h>
+
+/*! Maximum length of a command verb (incl. '\0') */
+#define OSMO_TRXC_CMD_LEN_MAX 32
+/*! Maximum length of the parameters string (incl. '\0') */
+#define OSMO_TRXC_PARAMS_LEN_MAX 128
+/*! Recommended TRXC socket read/send buffer size */
+#define OSMO_TRXC_MSG_BUF_SIZE 1500
+
+enum osmo_trxc_msg_type {
+ OSMO_TRXC_MT_CMD, /*!< "CMD <verb> [<params>]" (L1 -> TRX) */
+ OSMO_TRXC_MT_RSP, /*!< "RSP <verb> <status> [<params>]" (TRX -> L1) */
+ OSMO_TRXC_MT_IND, /*!< "IND <verb> <params>" (TRX -> L1) */
+};
+
+/*! A single de-/serialized TRXC message.
+ * The command verb set is deliberately open: the codec is verb-agnostic,
+ * so dialect specific (e.g. trxcon's ECHO/MEASURE/SETTA) and custom
+ * (e.g. fake_trx's FAKE_*) commands need no library changes. */
+struct osmo_trxc_msg {
+ enum osmo_trxc_msg_type type;
+ char cmd[OSMO_TRXC_CMD_LEN_MAX]; /*!< verb, e.g. "POWERON" */
+ int status; /*!< RSP only */
+ char params[OSMO_TRXC_PARAMS_LEN_MAX]; /*!< raw parameters, may be "" */
+};
+
+int osmo_trxc_msg_parse(struct osmo_trxc_msg *msg, const char *buf, size_t len);
+int osmo_trxc_msg_build(char *buf, size_t buf_size, const struct osmo_trxc_msg *msg);
+int osmo_trxc_msg_params_scan(const struct osmo_trxc_msg *msg, const char *fmt, ...);
+const char *osmo_trxc_msg_name(const struct osmo_trxc_msg *msg);
+
+/* Well-known command verbs (the codec itself is verb-agnostic) */
+#define OSMO_TRXC_CMD_POWERON "POWERON"
+#define OSMO_TRXC_CMD_POWEROFF "POWEROFF"
+#define OSMO_TRXC_CMD_RXTUNE "RXTUNE"
+#define OSMO_TRXC_CMD_TXTUNE "TXTUNE"
+#define OSMO_TRXC_CMD_SETSLOT "SETSLOT"
+#define OSMO_TRXC_CMD_SETTSC "SETTSC"
+#define OSMO_TRXC_CMD_SETBSIC "SETBSIC"
+#define OSMO_TRXC_CMD_SETPOWER "SETPOWER"
+#define OSMO_TRXC_CMD_ADJPOWER "ADJPOWER"
+#define OSMO_TRXC_CMD_NOMTXPOWER "NOMTXPOWER"
+#define OSMO_TRXC_CMD_SETRXGAIN "SETRXGAIN"
+#define OSMO_TRXC_CMD_SETMAXDLY "SETMAXDLY"
+#define OSMO_TRXC_CMD_SETMAXDLYNB "SETMAXDLYNB"
+#define OSMO_TRXC_CMD_SETFORMAT "SETFORMAT"
+#define OSMO_TRXC_CMD_HANDOVER "HANDOVER"
+#define OSMO_TRXC_CMD_NOHANDOVER "NOHANDOVER"
+#define OSMO_TRXC_CMD_RFMUTE "RFMUTE"
+#define OSMO_TRXC_CMD_ERR "ERR" /*!< verb of a reject response */
+
+/* Clock socket: "IND CLOCK <fn>" */
+int osmo_trxc_clock_ind_parse(uint32_t *fn, const char *buf, size_t len);
+int osmo_trxc_clock_ind_build(char *buf, size_t buf_size, uint32_t fn);
diff --git a/libosmo-trx/include/osmocom/trx/trxd.h b/libosmo-trx/include/osmocom/trx/trxd.h
new file mode 100644
index 0000000..9ed8275
--- /dev/null
+++ b/libosmo-trx/include/osmocom/trx/trxd.h
@@ -0,0 +1,122 @@
+/*! \file osmocom/trx/trxd.h
+ * TRXD (burst data) protocol: I/O-free PDU codec. */
+#pragma once
+
+#include <stdint.h>
+#include <stddef.h>
+
+#include <osmocom/core/bits.h>
+
+struct msgb;
+
+/*! The highest TRXD PDU version supported by this library */
+#define OSMO_TRXD_PDU_VER_MAX 2
+
+/*! GMSK modulated burst: 1 bit per symbol */
+#define OSMO_TRXD_BURST_LEN_GMSK 148
+/*! 8-PSK modulated burst: 3 bits per symbol */
+#define OSMO_TRXD_BURST_LEN_8PSK 444
+/*! Maximum burst length in (soft-)bits */
+#define OSMO_TRXD_BURST_LEN_MAX OSMO_TRXD_BURST_LEN_8PSK
+
+/*! Maximum single-PDU header size, in bytes: the TRXDv2 header (largest of
+ * all versions, either direction) plus the TDMA FN present on the first
+ * PDU of a datagram. */
+#define OSMO_TRXD_HDR_LEN_MAX 12
+/*! Recommended Rx/Tx socket buffer size, in bytes, for a single
+ * (non-batched) TRXD datagram: largest possible header plus the largest
+ * possible burst payload. */
+#define OSMO_TRXD_MSG_BUF_SIZE (OSMO_TRXD_HDR_LEN_MAX + OSMO_TRXD_BURST_LEN_MAX)
+
+/*! Modulation types (defined by the TRXDv2 MTS encoding, 3GPP TS 45.002) */
+enum osmo_trxd_mod_type {
+ OSMO_TRXD_MOD_T_GMSK,
+ OSMO_TRXD_MOD_T_8PSK,
+ OSMO_TRXD_MOD_T_AQPSK,
+ /* room for 16QAM/32QAM */
+};
+
+/* Presence/meta flags for osmo_trxd_burst_{ind,req} */
+#define OSMO_TRXD_F_NOPE_IND (1 << 0) /*!< no burst detected / idle indication */
+#define OSMO_TRXD_F_MOD_TYPE (1 << 1) /*!< 'mod' is valid */
+#define OSMO_TRXD_F_TS_INFO (1 << 2) /*!< 'tsc_set'/'tsc' are valid */
+#define OSMO_TRXD_F_CI_CB (1 << 3) /*!< 'ci_cb' is valid */
+#define OSMO_TRXD_F_TRX_NUM (1 << 4) /*!< 'trx_num' is valid */
+#define OSMO_TRXD_F_BATCH_IND (1 << 5) /*!< more PDUs follow in this datagram */
+#define OSMO_TRXD_F_SHADOW_IND (1 << 6) /*!< burst received on a shadow (VAMOS) channel */
+#define OSMO_TRXD_F_ACCESS_BURST (1 << 7) /*!< an Access Burst (RACH) */
+
+/*! TRX -> L1: received burst indication (soft bits + measurements).
+ *
+ * ABI stability rule: mandatory fields come first, optional (flag-gated)
+ * fields after them; fields introduced by future TRXD revisions shall be
+ * appended at the end of the struct. */
+struct osmo_trxd_burst_ind {
+ uint32_t flags; /*!< see OSMO_TRXD_F_* */
+
+ /* Mandatory fields (all TRXD versions) */
+ uint32_t fn; /*!< TDMA frame number */
+ uint8_t tn; /*!< TDMA timeslot number */
+ int16_t toa256; /*!< Timing of Arrival in units of 1/256 of symbol */
+ int8_t rssi; /*!< Received Signal Strength Indication (dBm) */
+ sbit_t burst[OSMO_TRXD_BURST_LEN_MAX]; /*!< soft-bits buffer */
+ size_t burst_len; /*!< number of soft-bits (0 for NOPE.ind) */
+
+ /* Optional fields (presence indicated by flags) */
+ enum osmo_trxd_mod_type mod; /*!< Modulation type */
+ uint8_t tsc_set; /*!< Training Sequence Set */
+ uint8_t tsc; /*!< Training Sequence Code */
+ int16_t ci_cb; /*!< Carrier-to-Interference ratio (centiBels) */
+ uint8_t trx_num; /*!< TRX (RF channel) number */
+};
+
+/*! L1 -> TRX: burst transmit request (hard bits).
+ * Same ABI stability rule as for osmo_trxd_burst_ind. */
+struct osmo_trxd_burst_req {
+ uint32_t flags; /*!< see OSMO_TRXD_F_* */
+
+ /* Mandatory fields (all TRXD versions) */
+ uint32_t fn; /*!< TDMA frame number */
+ uint8_t tn; /*!< TDMA timeslot number */
+ uint8_t att; /*!< Tx power attenuation (BTS side) */
+ ubit_t burst[OSMO_TRXD_BURST_LEN_MAX]; /*!< hard-bits buffer */
+ size_t burst_len; /*!< number of hard-bits */
+
+ /* Optional fields (presence indicated by flags) */
+ enum osmo_trxd_mod_type mod; /*!< Modulation type */
+ uint8_t tsc_set; /*!< Training Sequence Set */
+ uint8_t tsc; /*!< Training Sequence Code */
+ int8_t scpir; /*!< SCPIR (AQPSK only, TRXDv2) */
+ uint8_t trx_num; /*!< TRX (RF channel) number */
+};
+
+/*! Parser state, keeping cross-PDU context for TRXDv2 batches.
+ * Initialize with osmo_trxd_parse_state_init() before parsing each datagram. */
+struct osmo_trxd_parse_state {
+ uint8_t pdu_ver; /*!< PDU version (parsed from the first PDU) */
+ unsigned int num_pdus; /*!< number of PDUs parsed so far */
+ uint32_t fn; /*!< TDMA fn of the first PDU (v2 batches omit it later) */
+};
+
+void osmo_trxd_parse_state_init(struct osmo_trxd_parse_state *st);
+
+int osmo_trxd_burst_ind_parse(struct osmo_trxd_parse_state *st,
+ struct osmo_trxd_burst_ind *bi,
+ const uint8_t *buf, size_t buf_len);
+int osmo_trxd_burst_req_parse(struct osmo_trxd_parse_state *st,
+ struct osmo_trxd_burst_req *br,
+ const uint8_t *buf, size_t buf_len);
+
+int osmo_trxd_burst_ind_build(struct msgb *msg, uint8_t pdu_ver,
+ const struct osmo_trxd_burst_ind *bi);
+int osmo_trxd_burst_req_build(struct msgb *msg, uint8_t pdu_ver,
+ const struct osmo_trxd_burst_req *br);
+void osmo_trxd_build_fin(struct msgb *msg, uint8_t pdu_ver);
+
+const char *osmo_trxd_burst_ind_name(const struct osmo_trxd_burst_ind *bi);
+const char *osmo_trxd_burst_req_name(const struct osmo_trxd_burst_req *br);
+extern const struct value_string osmo_trxd_mod_type_names[];
+static inline const char *osmo_trxd_mod_type_name(enum osmo_trxd_mod_type mod)
+{
+ return get_value_string(osmo_trxd_mod_type_names, mod);
+}
diff --git a/libosmo-trx/include/osmocom/trx/trxd_int.h b/libosmo-trx/include/osmocom/trx/trxd_int.h
new file mode 100644
index 0000000..1c2ff33
--- /dev/null
+++ b/libosmo-trx/include/osmocom/trx/trxd_int.h
@@ -0,0 +1,15 @@
+/*! \file osmocom/trx/trxd_int.h
+ * TRXD codec: internal declarations. */
+
+#pragma once
+
+#include <stdint.h>
+
+struct osmo_trxd_burst_ind;
+struct osmo_trxd_burst_req;
+
+/* MTS (Modulation and Training Sequence) field, for TRXDv1 and higher */
+int trxd_mts_parse_ind(struct osmo_trxd_burst_ind *bi, uint8_t mts);
+int trxd_mts_parse_req(struct osmo_trxd_burst_req *br, uint8_t mts);
+int trxd_mts_build_ind(uint8_t *mts, const struct osmo_trxd_burst_ind *bi);
+int trxd_mts_build_req(uint8_t *mts, const struct osmo_trxd_burst_req *br);
diff --git a/libosmo-trx/libosmotrx.pc.in b/libosmo-trx/libosmotrx.pc.in
new file mode 100644
index 0000000..11af1e8
--- /dev/null
+++ b/libosmo-trx/libosmotrx.pc.in
@@ -0,0 +1,11 @@
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+libdir=@libdir@
+includedir=@includedir@
+
+Name: Osmocom TRX protocol library
+Description: C Utility Library for the TRXC/TRXD (OpenBTS-style TRX) protocol
+Version: @VERSION@
+Requires: libosmocore libosmogsm
+Libs: -L${libdir} -losmotrx
+Cflags: -I${includedir}/
diff --git a/libosmo-trx/src/Makefile.am b/libosmo-trx/src/Makefile.am
new file mode 100644
index 0000000..63eab2f
--- /dev/null
+++ b/libosmo-trx/src/Makefile.am
@@ -0,0 +1,33 @@
+# This is _NOT_ the library release version, it's an API version.
+# Please read chapter "Library interface versions" of the libtool
+# documentation before making any modification
+LIBVERSION = 0:0:0
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/libosmo-trx/include \
+ -I$(top_builddir)/libosmo-trx/include \
+ $(NULL)
+
+AM_CFLAGS = \
+ -Wall \
+ $(LIBOSMOCORE_CFLAGS) \
+ $(LIBOSMOGSM_CFLAGS) \
+ $(NULL)
+
+lib_LTLIBRARIES = libosmotrx.la
+
+libosmotrx_la_SOURCES = \
+ trxc.c \
+ trxd.c \
+ $(NULL)
+
+libosmotrx_la_LDFLAGS = \
+ -version-info $(LIBVERSION) \
+ -no-undefined \
+ -export-symbols-regex '^osmo_' \
+ $(NULL)
+
+libosmotrx_la_LIBADD = \
+ $(LIBOSMOCORE_LIBS) \
+ $(LIBOSMOGSM_LIBS) \
+ $(NULL)
diff --git a/libosmo-trx/src/trxc.c b/libosmo-trx/src/trxc.c
new file mode 100644
index 0000000..bd038b5
--- /dev/null
+++ b/libosmo-trx/src/trxc.c
@@ -0,0 +1,195 @@
+/*! \file src/trxc.c
+ * TRXC (control) protocol and clock indications: I/O-free message codec.
+ * Based on the TRXC implementation in osmo-bts-trx and osmo-trx. */
+
+/*
+ * (C) 2013 Andreas Eversberg <jolly@eversberg.eu>
+ * (C) 2016-2017 Harald Welte <laforge@gnumonks.org>
+ * (C) 2019 Vadim Yanitskiy <axilirator@gmail.com>
+ * (C) 2021-2026 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ *
+ * 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 <errno.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdarg.h>
+#include <stdlib.h>
+
+#include <osmocom/core/utils.h>
+#include <osmocom/gsm/gsm0502.h>
+
+#include <osmocom/trx/trxc.h>
+
+static const struct value_string trxc_msg_type_names[] = {
+ { OSMO_TRXC_MT_CMD, "CMD" },
+ { OSMO_TRXC_MT_RSP, "RSP" },
+ { OSMO_TRXC_MT_IND, "IND" },
+ { 0, NULL }
+};
+
+/*! Parse a TRXC message ("CMD <verb> [<params>]", "RSP <verb> <status>
+ * [<params>]" or "IND <verb> <params>") from a zero-terminated buffer.
+ * \param[out] msg parsed message
+ * \param[in] buf message buffer (not necessarily zero-terminated)
+ * \param[in] len length of the message in the buffer
+ * \returns 0 on success; negative on error */
+int osmo_trxc_msg_parse(struct osmo_trxc_msg *msg, const char *buf, size_t len)
+{
+ char tmp[OSMO_TRXC_MSG_BUF_SIZE];
+ const char *p, *verb;
+ size_t vlen;
+
+ memset(msg, 0, sizeof(*msg));
+
+ if (len >= sizeof(tmp))
+ return -EMSGSIZE;
+ /* work on a zero-terminated copy */
+ memcpy(tmp, buf, len);
+ tmp[len] = '\0';
+
+ if (strncmp(tmp, "CMD ", 4) == 0)
+ msg->type = OSMO_TRXC_MT_CMD;
+ else if (strncmp(tmp, "RSP ", 4) == 0)
+ msg->type = OSMO_TRXC_MT_RSP;
+ else if (strncmp(tmp, "IND ", 4) == 0)
+ msg->type = OSMO_TRXC_MT_IND;
+ else
+ return -EINVAL;
+
+ /* the verb ends at the next space (or end of string) */
+ verb = tmp + 4;
+ p = strchr(verb, ' ');
+ vlen = (p != NULL) ? (size_t)(p - verb) : strlen(verb);
+ if (vlen == 0 || vlen >= sizeof(msg->cmd))
+ return -EINVAL;
+ memcpy(msg->cmd, verb, vlen);
+ msg->cmd[vlen] = '\0';
+
+ if (p == NULL) { /* no parameters at all */
+ if (msg->type == OSMO_TRXC_MT_RSP)
+ return -EINVAL; /* RSP requires a status code */
+ return 0;
+ }
+ p++;
+
+ /* RSP carries a status code before the parameters */
+ if (msg->type == OSMO_TRXC_MT_RSP) {
+ if (sscanf(p, "%d", &msg->status) != 1)
+ return -EINVAL;
+ p = strchr(p, ' ');
+ if (p == NULL) /* no parameters after the status */
+ return 0;
+ p++;
+ }
+
+ if (strlen(p) >= sizeof(msg->params))
+ return -EMSGSIZE;
+ strcpy(msg->params, p);
+
+ return 0;
+}
+
+/*! Serialize a TRXC message into the given buffer (zero-terminated).
+ * \returns length of the message (excl. '\0') on success; negative on error */
+int osmo_trxc_msg_build(char *buf, size_t buf_size, const struct osmo_trxc_msg *msg)
+{
+ int rc;
+
+ switch (msg->type) {
+ case OSMO_TRXC_MT_CMD:
+ case OSMO_TRXC_MT_IND:
+ rc = snprintf(buf, buf_size, "%s %s%s%s",
+ get_value_string(trxc_msg_type_names, msg->type),
+ msg->cmd,
+ msg->params[0] != '\0' ? " " : "", msg->params);
+ break;
+ case OSMO_TRXC_MT_RSP:
+ rc = snprintf(buf, buf_size, "RSP %s %d%s%s",
+ msg->cmd, msg->status,
+ msg->params[0] != '\0' ? " " : "", msg->params);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (rc < 0 || (size_t)rc >= buf_size)
+ return -EMSGSIZE;
+ return rc;
+}
+
+/*! Parse the parameters string of a TRXC message, sscanf() style.
+ * \returns number of successfully matched items (see sscanf) */
+int osmo_trxc_msg_params_scan(const struct osmo_trxc_msg *msg, const char *fmt, ...)
+{
+ va_list ap;
+ int rc;
+
+ va_start(ap, fmt);
+ rc = vsscanf(msg->params, fmt, ap);
+ va_end(ap);
+
+ return rc;
+}
+
+/*! Compose a human-readable representation of the given message (for logging) */
+const char *osmo_trxc_msg_name(const struct osmo_trxc_msg *msg)
+{
+ static __thread char buf[OSMO_TRXC_MSG_BUF_SIZE];
+
+ if (osmo_trxc_msg_build(buf, sizeof(buf), msg) < 0)
+ return "(invalid)";
+ return buf;
+}
+
+/*! Parse a clock indication ("IND CLOCK <fn>").
+ * \param[out] fn TDMA frame number (validated to be < GSM_TDMA_HYPERFRAME)
+ * \returns 0 on success; negative on error */
+int osmo_trxc_clock_ind_parse(uint32_t *fn, const char *buf, size_t len)
+{
+ struct osmo_trxc_msg msg;
+ int rc;
+
+ rc = osmo_trxc_msg_parse(&msg, buf, len);
+ if (rc < 0)
+ return rc;
+ if (msg.type != OSMO_TRXC_MT_IND || strcmp(msg.cmd, "CLOCK") != 0)
+ return -EINVAL;
+ if (osmo_trxc_msg_params_scan(&msg, "%u", fn) != 1)
+ return -EINVAL;
+ if (*fn >= GSM_TDMA_HYPERFRAME)
+ return -ERANGE;
+
+ return 0;
+}
+
+/*! Serialize a clock indication ("IND CLOCK <fn>") into the given buffer.
+ * \returns length of the message (excl. '\0') on success; negative on error */
+int osmo_trxc_clock_ind_build(char *buf, size_t buf_size, uint32_t fn)
+{
+ int rc;
+
+ if (fn >= GSM_TDMA_HYPERFRAME)
+ return -ERANGE;
+
+ rc = snprintf(buf, buf_size, "IND CLOCK %u", fn);
+ if (rc < 0 || (size_t)rc >= buf_size)
+ return -EMSGSIZE;
+ return rc;
+}
diff --git a/libosmo-trx/src/trxd.c b/libosmo-trx/src/trxd.c
new file mode 100644
index 0000000..d8c6a89
--- /dev/null
+++ b/libosmo-trx/src/trxd.c
@@ -0,0 +1,697 @@
+/*! \file src/trxd.c
+ * TRXD (burst data) protocol: I/O-free PDU codec.
+ * Based on the TRXD implementation in osmo-bts-trx and osmo-trx. */
+
+/*
+ * (C) 2013 Andreas Eversberg <jolly@eversberg.eu>
+ * (C) 2016-2017 Harald Welte <laforge@gnumonks.org>
+ * (C) 2019 Vadim Yanitskiy <axilirator@gmail.com>
+ * (C) 2021-2026 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ *
+ * 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 <errno.h>
+#include <string.h>
+#include <stdint.h>
+
+#include <osmocom/core/bits.h>
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/gsm/gsm0502.h>
+
+#include <osmocom/trx/trxd.h>
+#include <osmocom/trx/trxd_int.h>
+
+/* Uplink TRXDv0 header length: TDMA TN + FN + RSSI + ToA256 */
+#define TRXD_IND_V0HDR_LEN (1 + 4 + 1 + 2)
+/* Uplink TRXDv1 header length: additional MTS + C/I */
+#define TRXD_IND_V1HDR_LEN (TRXD_IND_V0HDR_LEN + 1 + 2)
+/* Uplink TRXDv2 header length: TDMA TN + TRXN + MTS + RSSI + ToA256 + C/I */
+#define TRXD_IND_V2HDR_LEN (1 + 1 + 1 + 1 + 2 + 2)
+
+/* Downlink TRXDv0/v1 header length: TDMA TN + FN + Att */
+#define TRXD_REQ_V01HDR_LEN (1 + 4 + 1)
+/* Downlink TRXDv2 header length: TDMA TN + TRXN + MTS + Att + SCPIR + spare3 */
+#define TRXD_REQ_V2HDR_LEN (1 + 1 + 1 + 1 + 1 + 3)
+
+const struct value_string osmo_trxd_mod_type_names[] = {
+ { OSMO_TRXD_MOD_T_GMSK, "GMSK" },
+ { OSMO_TRXD_MOD_T_8PSK, "8-PSK" },
+ { OSMO_TRXD_MOD_T_AQPSK, "AQPSK" },
+ { 0, NULL }
+};
+
+/*! Initialize a parser state; call before parsing each datagram.
+ * \param[out] st parser state to be initialized */
+void osmo_trxd_parse_state_init(struct osmo_trxd_parse_state *st)
+{
+ memset(st, 0, sizeof(*st));
+}
+
+/* Expected burst length (in bits on the wire) for a given modulation */
+static int burst_len_by_mod(enum osmo_trxd_mod_type mod)
+{
+ switch (mod) {
+ case OSMO_TRXD_MOD_T_GMSK:
+ case OSMO_TRXD_MOD_T_AQPSK:
+ return OSMO_TRXD_BURST_LEN_GMSK;
+ case OSMO_TRXD_MOD_T_8PSK:
+ return OSMO_TRXD_BURST_LEN_8PSK;
+ default:
+ return -ENOTSUP;
+ }
+}
+
+static int mts_parse(uint32_t *flags, enum osmo_trxd_mod_type *mod,
+ uint8_t *tsc_set, uint8_t *tsc, uint8_t mts)
+{
+ if (mts & (1 << 7)) {
+ *flags |= OSMO_TRXD_F_NOPE_IND;
+ return 0;
+ }
+
+ /* | 7 6 5 4 3 2 1 0 | Bitmask / description
+ * | . 0 0 X X . . . | GMSK, 4 TSC sets (0..3)
+ * | . 0 1 0 X . . . | 8-PSK, 2 TSC sets (0..1)
+ * | . 0 1 1 0 . . . | GMSK, Access Burst
+ * | . 1 1 X X . . . | AQPSK, 2 TSC sets + SCPIR */
+ if ((mts >> 5) == 0x00) {
+ *mod = OSMO_TRXD_MOD_T_GMSK;
+ *tsc_set = (mts >> 3) & 0x03;
+ } else if ((mts >> 4) == 0x02) {
+ *mod = OSMO_TRXD_MOD_T_8PSK;
+ *tsc_set = (mts >> 3) & 0x01;
+ } else if ((mts >> 3) == 0x06) {
+ *flags |= OSMO_TRXD_F_ACCESS_BURST;
+ *mod = OSMO_TRXD_MOD_T_GMSK;
+ *tsc_set = 0;
+ } else if ((mts >> 5) == 0x03) {
+ *mod = OSMO_TRXD_MOD_T_AQPSK;
+ *tsc_set = (mts >> 3) & 0x01;
+ } else {
+ return -ENOTSUP;
+ }
+
+ *tsc = mts & 0x07;
+ *flags |= (OSMO_TRXD_F_MOD_TYPE | OSMO_TRXD_F_TS_INFO);
+
+ return 0;
+}
+
+int trxd_mts_parse_ind(struct osmo_trxd_burst_ind *bi, uint8_t mts)
+{
+ return mts_parse(&bi->flags, &bi->mod, &bi->tsc_set, &bi->tsc, mts);
+}
+
+int trxd_mts_parse_req(struct osmo_trxd_burst_req *br, uint8_t mts)
+{
+ return mts_parse(&br->flags, &br->mod, &br->tsc_set, &br->tsc, mts);
+}
+
+static int mts_build(uint8_t *mts, uint32_t flags,
+ enum osmo_trxd_mod_type mod,
+ uint8_t tsc_set, uint8_t tsc)
+{
+ if (flags & OSMO_TRXD_F_NOPE_IND) {
+ *mts = (1 << 7);
+ return 0;
+ }
+
+ if (flags & OSMO_TRXD_F_ACCESS_BURST) {
+ *mts = (0x06 << 3) | (tsc & 0x07);
+ return 0;
+ }
+
+ switch (mod) {
+ case OSMO_TRXD_MOD_T_GMSK:
+ *mts = ((tsc_set & 0x03) << 3);
+ break;
+ case OSMO_TRXD_MOD_T_8PSK:
+ *mts = (0x02 << 4) | ((tsc_set & 0x01) << 3);
+ break;
+ case OSMO_TRXD_MOD_T_AQPSK:
+ *mts = (0x03 << 5) | ((tsc_set & 0x01) << 3);
+ break;
+ default:
+ return -ENOTSUP;
+ }
+
+ *mts |= (tsc & 0x07);
+ return 0;
+}
+
+int trxd_mts_build_ind(uint8_t *mts, const struct osmo_trxd_burst_ind *bi)
+{
+ return mts_build(mts, bi->flags, bi->mod, bi->tsc_set, bi->tsc);
+}
+
+int trxd_mts_build_req(uint8_t *mts, const struct osmo_trxd_burst_req *br)
+{
+ return mts_build(mts, br->flags, br->mod, br->tsc_set, br->tsc);
+}
+
+/***********************************************************************
+ * burst indication (TRX -> L1): soft bits + measurements
+ ***********************************************************************/
+
+/* Convert unsigned soft-bits [254..0] to soft-bits [-127..127] */
+static void soft_bits_parse(sbit_t *out, const uint8_t *in, size_t len)
+{
+ size_t i;
+
+ for (i = 0; i < len; i++) {
+ if (in[i] == 255)
+ out[i] = -127;
+ else
+ out[i] = 127 - in[i];
+ }
+}
+
+/* Convert soft-bits [-127..127] to unsigned soft-bits [254..0] */
+static void soft_bits_build(uint8_t *out, const sbit_t *in, size_t len)
+{
+ size_t i;
+
+ for (i = 0; i < len; i++)
+ out[i] = 127 - in[i];
+}
+
+/* Common header part shared by TRXDv0 and TRXDv1 */
+static void trxd_burst_ind_parse_hdr_v0(struct osmo_trxd_burst_ind *bi,
+ const uint8_t *buf)
+{
+ bi->tn = buf[0] & 0x07;
+ bi->fn = osmo_load32be(buf + 1);
+ bi->rssi = -(int8_t)buf[5];
+ bi->toa256 = (int16_t)osmo_load16be(buf + 6);
+}
+
+/* TRXDv0: modulation is guessed by the burst length; a legacy transceiver
+ * may append two garbage bytes, which we tolerate (and consume). */
+static int trxd_burst_ind_parse_v0(struct osmo_trxd_burst_ind *bi,
+ const uint8_t *buf, size_t buf_len)
+{
+ size_t burst_len = buf_len - TRXD_IND_V0HDR_LEN;
+
+ trxd_burst_ind_parse_hdr_v0(bi, buf);
+
+ switch (burst_len) {
+ case OSMO_TRXD_BURST_LEN_GMSK:
+ case OSMO_TRXD_BURST_LEN_GMSK + 2:
+ bi->mod = OSMO_TRXD_MOD_T_GMSK;
+ bi->burst_len = OSMO_TRXD_BURST_LEN_GMSK;
+ break;
+ case OSMO_TRXD_BURST_LEN_8PSK:
+ case OSMO_TRXD_BURST_LEN_8PSK + 2:
+ bi->mod = OSMO_TRXD_MOD_T_8PSK;
+ bi->burst_len = OSMO_TRXD_BURST_LEN_8PSK;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ bi->flags |= OSMO_TRXD_F_MOD_TYPE;
+ soft_bits_parse(&bi->burst[0], buf + TRXD_IND_V0HDR_LEN, bi->burst_len);
+
+ /* consume everything, including the optional legacy padding */
+ return buf_len;
+}
+
+static int trxd_burst_ind_parse_v1(struct osmo_trxd_burst_ind *bi,
+ const uint8_t *buf, size_t buf_len)
+{
+ int rc;
+
+ trxd_burst_ind_parse_hdr_v0(bi, buf);
+
+ /* MTS (Modulation and Training Sequence) */
+ rc = trxd_mts_parse_ind(bi, buf[TRXD_IND_V0HDR_LEN + 0]);
+ if (rc < 0)
+ return rc;
+
+ /* C/I: Carrier-to-Interference ratio (in centiBels) */
+ bi->ci_cb = (int16_t)osmo_load16be(buf + TRXD_IND_V0HDR_LEN + 1);
+ bi->flags |= OSMO_TRXD_F_CI_CB;
+
+ return TRXD_IND_V1HDR_LEN;
+}
+
+static int trxd_burst_ind_parse_v2(struct osmo_trxd_parse_state *st,
+ struct osmo_trxd_burst_ind *bi,
+ const uint8_t *buf, size_t buf_len)
+{
+ int rc;
+
+ /* TDMA timeslot number (other bits are RFU) */
+ bi->tn = buf[0] & 0x07;
+
+ if (buf[1] & (1 << 7)) /* BATCH.ind */
+ bi->flags |= OSMO_TRXD_F_BATCH_IND;
+ if (buf[1] & (1 << 6)) /* VAMOS.ind */
+ bi->flags |= OSMO_TRXD_F_SHADOW_IND;
+
+ /* TRX (RF channel) number */
+ bi->trx_num = buf[1] & 0x3f;
+ bi->flags |= OSMO_TRXD_F_TRX_NUM;
+
+ /* MTS (Modulation and Training Sequence) */
+ rc = trxd_mts_parse_ind(bi, buf[2]);
+ if (rc < 0)
+ return rc;
+
+ bi->rssi = -(int8_t)buf[3];
+ bi->toa256 = (int16_t)osmo_load16be(buf + 4);
+ bi->ci_cb = (int16_t)osmo_load16be(buf + 6);
+ bi->flags |= OSMO_TRXD_F_CI_CB;
+
+ /* TDMA frame number is absent in batched PDUs */
+ if (st->num_pdus == 0) {
+ if (buf_len < TRXD_IND_V2HDR_LEN + sizeof(uint32_t))
+ return -EINVAL;
+ bi->fn = osmo_load32be(buf + TRXD_IND_V2HDR_LEN);
+ st->fn = bi->fn;
+ return TRXD_IND_V2HDR_LEN + sizeof(uint32_t);
+ }
+
+ bi->fn = st->fn;
+ return TRXD_IND_V2HDR_LEN;
+}
+
+/*! Parse one burst indication (TRX -> L1) PDU from the given buffer.
+ *
+ * The PDU version is parsed from the first PDU of a datagram and kept
+ * in \a st. Starting from TRXDv2, a datagram may batch multiple PDUs:
+ * call this function repeatedly on the remaining buffer as long as the
+ * parsed PDU has OSMO_TRXD_F_BATCH_IND set in bi->flags.
+ *
+ * \param[inout] st parser state (see osmo_trxd_parse_state_init())
+ * \param[out] bi parsed burst indication
+ * \param[in] buf buffer positioned at the beginning of a PDU
+ * \param[in] buf_len remaining length of the buffer
+ * \returns number of consumed bytes on success; negative on error */
+int osmo_trxd_burst_ind_parse(struct osmo_trxd_parse_state *st,
+ struct osmo_trxd_burst_ind *bi,
+ const uint8_t *buf, size_t buf_len)
+{
+ int hdr_len;
+ int burst_len;
+
+ if (buf_len == 0)
+ return -EINVAL;
+
+ /* PDU version is parsed from the first PDU of a datagram */
+ if (st->num_pdus == 0) {
+ st->pdu_ver = buf[0] >> 4;
+ if (st->pdu_ver > OSMO_TRXD_PDU_VER_MAX)
+ return -ENOTSUP;
+ }
+
+ memset(bi, 0, sizeof(*bi));
+
+ switch (st->pdu_ver) {
+ case 0:
+ if (buf_len < TRXD_IND_V0HDR_LEN)
+ return -EINVAL;
+ hdr_len = trxd_burst_ind_parse_v0(bi, buf, buf_len);
+ break;
+ case 1:
+ if (buf_len < TRXD_IND_V1HDR_LEN)
+ return -EINVAL;
+ hdr_len = trxd_burst_ind_parse_v1(bi, buf, buf_len);
+ break;
+ case 2:
+ if (buf_len < TRXD_IND_V2HDR_LEN)
+ return -EINVAL;
+ hdr_len = trxd_burst_ind_parse_v2(st, bi, buf, buf_len);
+ break;
+ default:
+ return -ENOTSUP;
+ }
+
+ if (hdr_len < 0)
+ return hdr_len;
+
+ if (bi->fn >= GSM_TDMA_HYPERFRAME)
+ return -EINVAL;
+
+ st->num_pdus++;
+
+ /* TRXDv0 consumes the whole datagram, incl. the burst bits */
+ if (st->pdu_ver == 0)
+ return hdr_len;
+
+ /* NOPE.ind contains no burst */
+ if (bi->flags & OSMO_TRXD_F_NOPE_IND) {
+ bi->burst_len = 0;
+ return hdr_len;
+ }
+
+ burst_len = burst_len_by_mod(bi->mod);
+ if (burst_len < 0)
+ return burst_len;
+ if (buf_len < (size_t)(hdr_len + burst_len))
+ return -EINVAL;
+
+ bi->burst_len = burst_len;
+ soft_bits_parse(&bi->burst[0], buf + hdr_len, bi->burst_len);
+
+ return hdr_len + burst_len;
+}
+
+/*! Append one encoded burst indication (TRX -> L1) PDU to the given msgb.
+ *
+ * For TRXDv2, PDU batching works by calling this function repeatedly on
+ * the same msgb; call osmo_trxd_build_fin() once the datagram is complete
+ * (it clears the BATCH.ind bit of the last PDU).
+ *
+ * \param[inout] msg destination message buffer
+ * \param[in] pdu_ver TRXD PDU version to encode
+ * \param[in] bi burst indication to be encoded
+ * \returns 0 on success; negative on error. Note that TRXDv0 cannot
+ * carry NOPE.ind PDUs: -ENOTSUP is returned and the caller
+ * shall skip (not send) them. */
+int osmo_trxd_burst_ind_build(struct msgb *msg, uint8_t pdu_ver,
+ const struct osmo_trxd_burst_ind *bi)
+{
+ bool first = (msgb_length(msg) == 0);
+ uint8_t *buf;
+ int rc;
+
+ switch (pdu_ver) {
+ case 0:
+ /* v0 doesn't support NOPE.ind, the caller shall skip it */
+ if (bi->flags & OSMO_TRXD_F_NOPE_IND)
+ return -ENOTSUP;
+ buf = msgb_put(msg, TRXD_IND_V0HDR_LEN);
+ buf[0] = ((pdu_ver & 0x0f) << 4) | (bi->tn & 0x07);
+ osmo_store32be(bi->fn, buf + 1);
+ buf[5] = (uint8_t)(-bi->rssi);
+ osmo_store16be(bi->toa256, buf + 6);
+ break;
+ case 1:
+ buf = msgb_put(msg, TRXD_IND_V1HDR_LEN);
+ buf[0] = ((pdu_ver & 0x0f) << 4) | (bi->tn & 0x07);
+ osmo_store32be(bi->fn, buf + 1);
+ buf[5] = (uint8_t)(-bi->rssi);
+ osmo_store16be(bi->toa256, buf + 6);
+ rc = trxd_mts_build_ind(&buf[8], bi);
+ if (rc < 0)
+ return rc;
+ osmo_store16be(bi->ci_cb, buf + 9);
+ break;
+ case 2:
+ /* l2h points to the last encoded PDU (for osmo_trxd_build_fin) */
+ msg->l2h = msg->tail;
+ buf = msgb_put(msg, TRXD_IND_V2HDR_LEN);
+ buf[0] = bi->tn & 0x07;
+ /* BATCH.ind; unset in the last PDU by osmo_trxd_build_fin() */
+ buf[1] = (bi->trx_num & 0x3f) | (1 << 7);
+ if (bi->flags & OSMO_TRXD_F_SHADOW_IND)
+ buf[1] |= (1 << 6);
+ rc = trxd_mts_build_ind(&buf[2], bi);
+ if (rc < 0)
+ return rc;
+ buf[3] = (uint8_t)(-bi->rssi);
+ osmo_store16be(bi->toa256, buf + 4);
+ osmo_store16be(bi->ci_cb, buf + 6);
+ /* Some fields are not present in batched PDUs */
+ if (first) {
+ buf[0] |= (pdu_ver & 0x0f) << 4;
+ msgb_put_u32(msg, bi->fn);
+ }
+ break;
+ default:
+ return -ENOTSUP;
+ }
+
+ if (~bi->flags & OSMO_TRXD_F_NOPE_IND) {
+ soft_bits_build(msgb_put(msg, bi->burst_len),
+ &bi->burst[0], bi->burst_len);
+ }
+
+ return 0;
+}
+
+/***********************************************************************
+ * burst transmit request (L1 -> TRX): hard bits
+ ***********************************************************************/
+
+static int trxd_burst_req_parse_v01(struct osmo_trxd_burst_req *br,
+ const uint8_t *buf, size_t buf_len)
+{
+ size_t burst_len = buf_len - TRXD_REQ_V01HDR_LEN;
+
+ br->tn = buf[0] & 0x07;
+ br->fn = osmo_load32be(&buf[1]);
+ br->att = buf[5];
+
+ switch (burst_len) {
+ case OSMO_TRXD_BURST_LEN_8PSK:
+ br->mod = OSMO_TRXD_MOD_T_8PSK;
+ break;
+ case OSMO_TRXD_BURST_LEN_GMSK:
+ br->mod = OSMO_TRXD_MOD_T_GMSK;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ br->flags |= OSMO_TRXD_F_MOD_TYPE;
+ br->burst_len = burst_len;
+ memcpy(&br->burst[0], buf + TRXD_REQ_V01HDR_LEN, burst_len);
+
+ return buf_len;
+}
+
+static int trxd_burst_req_parse_v2(struct osmo_trxd_parse_state *st,
+ struct osmo_trxd_burst_req *br,
+ const uint8_t *buf, size_t buf_len)
+{
+ size_t hdr_len = TRXD_REQ_V2HDR_LEN;
+ int burst_len;
+ int rc;
+
+ br->tn = buf[0] & 0x07;
+
+ if (buf[1] & (1 << 7)) /* BATCH.ind */
+ br->flags |= OSMO_TRXD_F_BATCH_IND;
+
+ br->trx_num = buf[1] & 0x3f;
+ br->flags |= OSMO_TRXD_F_TRX_NUM;
+
+ rc = trxd_mts_parse_req(br, buf[2]);
+ if (rc < 0)
+ return rc;
+
+ br->att = buf[3];
+ br->scpir = (int8_t)buf[4];
+ /* buf[5..7] is spare */
+
+ /* TDMA frame number is absent in batched PDUs */
+ if (st->num_pdus == 0) {
+ if (buf_len < hdr_len + sizeof(uint32_t))
+ return -EINVAL;
+ br->fn = osmo_load32be(buf + hdr_len);
+ st->fn = br->fn;
+ hdr_len += sizeof(uint32_t);
+ } else {
+ br->fn = st->fn;
+ }
+
+ burst_len = burst_len_by_mod(br->mod);
+ if (burst_len < 0)
+ return burst_len;
+ if (buf_len < hdr_len + burst_len)
+ return -EINVAL;
+
+ br->burst_len = burst_len;
+ memcpy(&br->burst[0], buf + hdr_len, burst_len);
+
+ return hdr_len + burst_len;
+}
+
+/*! Parse one burst transmit request (L1 -> TRX) PDU from the given buffer.
+ *
+ * The PDU version is parsed from the first PDU of a datagram and kept
+ * in \a st. Starting from TRXDv2, a datagram may batch multiple PDUs:
+ * call this function repeatedly on the remaining buffer as long as the
+ * parsed PDU has OSMO_TRXD_F_BATCH_IND set in br->flags.
+ *
+ * \param[inout] st parser state (see osmo_trxd_parse_state_init())
+ * \param[out] br parsed burst transmit request
+ * \param[in] buf buffer positioned at the beginning of a PDU
+ * \param[in] buf_len remaining length of the buffer
+ * \returns number of consumed bytes on success; negative on error */
+int osmo_trxd_burst_req_parse(struct osmo_trxd_parse_state *st,
+ struct osmo_trxd_burst_req *br,
+ const uint8_t *buf, size_t buf_len)
+{
+ int pdu_len;
+
+ if (buf_len == 0)
+ return -EINVAL;
+
+ if (st->num_pdus == 0) {
+ st->pdu_ver = buf[0] >> 4;
+ if (st->pdu_ver > OSMO_TRXD_PDU_VER_MAX)
+ return -ENOTSUP;
+ }
+
+ memset(br, 0, sizeof(*br));
+
+ switch (st->pdu_ver) {
+ case 0:
+ case 1:
+ if (buf_len < TRXD_REQ_V01HDR_LEN)
+ return -EINVAL;
+ pdu_len = trxd_burst_req_parse_v01(br, buf, buf_len);
+ break;
+ case 2:
+ if (buf_len < TRXD_REQ_V2HDR_LEN)
+ return -EINVAL;
+ pdu_len = trxd_burst_req_parse_v2(st, br, buf, buf_len);
+ break;
+ default:
+ return -ENOTSUP;
+ }
+
+ if (pdu_len < 0)
+ return pdu_len;
+
+ if (br->fn >= GSM_TDMA_HYPERFRAME)
+ return -EINVAL;
+
+ st->num_pdus++;
+ return pdu_len;
+}
+
+/*! Append one encoded burst transmit request (L1 -> TRX) PDU to the given msgb.
+ *
+ * For TRXDv2, PDU batching works by calling this function repeatedly on
+ * the same msgb; call osmo_trxd_build_fin() once the datagram is complete
+ * (it clears the BATCH.ind bit of the last PDU).
+ *
+ * \param[inout] msg destination message buffer
+ * \param[in] pdu_ver TRXD PDU version to encode
+ * \param[in] br burst transmit request to be encoded
+ * \returns 0 on success; negative on error */
+int osmo_trxd_burst_req_build(struct msgb *msg, uint8_t pdu_ver,
+ const struct osmo_trxd_burst_req *br)
+{
+ bool first = (msgb_length(msg) == 0);
+ uint8_t *buf;
+ int rc;
+
+ switch (pdu_ver) {
+ /* Both versions have the same PDU format */
+ case 0:
+ case 1:
+ buf = msgb_put(msg, TRXD_REQ_V01HDR_LEN);
+ buf[0] = ((pdu_ver & 0x0f) << 4) | (br->tn & 0x07);
+ osmo_store32be(br->fn, buf + 1);
+ buf[5] = br->att;
+ break;
+ case 2:
+ /* l2h points to the last encoded PDU (for osmo_trxd_build_fin) */
+ msg->l2h = msg->tail;
+ buf = msgb_put(msg, TRXD_REQ_V2HDR_LEN);
+ buf[0] = br->tn & 0x07;
+ /* BATCH.ind; unset in the last PDU by osmo_trxd_build_fin() */
+ buf[1] = (br->trx_num & 0x3f) | (1 << 7);
+ rc = trxd_mts_build_req(&buf[2], br);
+ if (rc < 0)
+ return rc;
+ buf[3] = br->att;
+ buf[4] = (uint8_t)br->scpir;
+ buf[5] = buf[6] = buf[7] = 0x00; /* Spare */
+ /* Some fields are not present in batched PDUs */
+ if (first) {
+ buf[0] |= (pdu_ver & 0x0f) << 4;
+ msgb_put_u32(msg, br->fn);
+ }
+ break;
+ default:
+ return -ENOTSUP;
+ }
+
+ /* copy hard-bits {0,1} */
+ memcpy(msgb_put(msg, br->burst_len),
+ &br->burst[0], br->burst_len);
+
+ return 0;
+}
+
+/*! Finalize a datagram built by osmo_trxd_burst_{ind,req}_build().
+ * \param[inout] msg message buffer holding the encoded PDU(s)
+ * \param[in] pdu_ver TRXD PDU version in use */
+void osmo_trxd_build_fin(struct msgb *msg, uint8_t pdu_ver)
+{
+ /* TRXDv2: unset BATCH.ind in the last PDU */
+ if (pdu_ver >= 2 && msg->l2h != NULL)
+ msg->l2h[1] &= ~(1 << 7);
+}
+
+/***********************************************************************
+ * logging helpers
+ ***********************************************************************/
+
+/*! Compose a human-readable representation of the given burst indication
+ * (for logging). \returns pointer to a thread-local static buffer */
+const char *osmo_trxd_burst_ind_name(const struct osmo_trxd_burst_ind *bi)
+{
+ static __thread char buf[256];
+ struct osmo_strbuf sb = { .buf = buf, .len = sizeof(buf) };
+
+ OSMO_STRBUF_PRINTF(sb, "%s tn=%u fn=%u",
+ (bi->flags & OSMO_TRXD_F_NOPE_IND) ? "NOPE.ind" : "BURST.ind",
+ bi->tn, bi->fn);
+ if (bi->flags & OSMO_TRXD_F_TRX_NUM)
+ OSMO_STRBUF_PRINTF(sb, " trx_num=%u", bi->trx_num);
+ OSMO_STRBUF_PRINTF(sb, " rssi=%d toa256=%d", bi->rssi, bi->toa256);
+ if (bi->flags & OSMO_TRXD_F_CI_CB)
+ OSMO_STRBUF_PRINTF(sb, " C/I=%d cB", bi->ci_cb);
+ if (bi->flags & OSMO_TRXD_F_NOPE_IND)
+ return buf;
+ if (bi->flags & OSMO_TRXD_F_MOD_TYPE)
+ OSMO_STRBUF_PRINTF(sb, " mod=%s", osmo_trxd_mod_type_name(bi->mod));
+ if (bi->flags & OSMO_TRXD_F_TS_INFO)
+ OSMO_STRBUF_PRINTF(sb, " set=%u tsc=%u", bi->tsc_set, bi->tsc);
+ OSMO_STRBUF_PRINTF(sb, " burst_len=%zu", bi->burst_len);
+
+ return buf;
+}
+
+/*! Compose a human-readable representation of the given burst transmit
+ * request (for logging). \returns pointer to a thread-local static buffer */
+const char *osmo_trxd_burst_req_name(const struct osmo_trxd_burst_req *br)
+{
+ static __thread char buf[256];
+ struct osmo_strbuf sb = { .buf = buf, .len = sizeof(buf) };
+
+ OSMO_STRBUF_PRINTF(sb, "BURST.req tn=%u fn=%u att=%u", br->tn, br->fn, br->att);
+ if (br->flags & OSMO_TRXD_F_TRX_NUM)
+ OSMO_STRBUF_PRINTF(sb, " trx_num=%u", br->trx_num);
+ if (br->flags & OSMO_TRXD_F_MOD_TYPE)
+ OSMO_STRBUF_PRINTF(sb, " mod=%s", osmo_trxd_mod_type_name(br->mod));
+ if (br->flags & OSMO_TRXD_F_TS_INFO)
+ OSMO_STRBUF_PRINTF(sb, " set=%u tsc=%u", br->tsc_set, br->tsc);
+ OSMO_STRBUF_PRINTF(sb, " burst_len=%zu", br->burst_len);
+
+ return buf;
+}
diff --git a/tests/Makefile.am b/tests/Makefile.am
index d4589a4..60fd0b0 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -1,4 +1,5 @@
SUBDIRS = \
+ libosmo-trx \
CommonLibs \
Transceiver52M \
$(NULL)
diff --git a/tests/libosmo-trx/Makefile.am b/tests/libosmo-trx/Makefile.am
new file mode 100644
index 0000000..7ed034b
--- /dev/null
+++ b/tests/libosmo-trx/Makefile.am
@@ -0,0 +1,34 @@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/libosmo-trx/include \
+ -I$(top_builddir)/libosmo-trx/include \
+ $(NULL)
+
+AM_CFLAGS = \
+ -Wall -g \
+ $(LIBOSMOCORE_CFLAGS) \
+ $(LIBOSMOGSM_CFLAGS) \
+ $(NULL)
+
+# Link statically, so that the tests can also call symbols hidden
+# from the public library ABI (-export-symbols-regex).
+AM_LDFLAGS = -static
+
+LDADD = \
+ $(top_builddir)/libosmo-trx/src/libosmotrx.la \
+ $(LIBOSMOCORE_LIBS) \
+ $(LIBOSMOGSM_LIBS) \
+ $(NULL)
+
+EXTRA_DIST = \
+ trxc_test.ok \
+ trxd_test.ok \
+ $(NULL)
+
+check_PROGRAMS = \
+ trxc_test \
+ trxd_test \
+ $(NULL)
+
+trxc_test_SOURCES = trxc_test.c
+
+trxd_test_SOURCES = trxd_test.c
diff --git a/tests/libosmo-trx/trxc_test.c b/tests/libosmo-trx/trxc_test.c
new file mode 100644
index 0000000..82676a7
--- /dev/null
+++ b/tests/libosmo-trx/trxc_test.c
@@ -0,0 +1,162 @@
+/*! \file tests/trxc_test.c
+ * Regression test for the TRXC message codec. */
+
+/*
+ * (C) 2026 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ * Author: Vadim Yanitskiy <vyanitskiy@sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ *
+ * 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 <stdio.h>
+#include <string.h>
+
+#include <osmocom/core/utils.h>
+
+#include <osmocom/trx/trxc.h>
+
+static void test_msg_parse(void)
+{
+ static const char * const messages[] = {
+ "CMD POWERON",
+ "CMD POWEROFF",
+ "CMD RXTUNE 890000",
+ "CMD SETSLOT 0 7 C5/S1",
+ "CMD ECHO", /* MS dialect */
+ "CMD FAKE_TOA 128 0", /* fake_trx custom command */
+ "RSP POWERON 0",
+ "RSP SETSLOT 0 0 7",
+ "RSP SETFORMAT 1 2",
+ "RSP NOMTXPOWER 0 23",
+ "RSP ERR 1",
+ "RSP MEASURE 0 890000 -85",
+ "IND CLOCK 402312",
+ /* malformed messages */
+ "NDI KCOLC 123456",
+ "RSP NOSTATUS",
+ "CMD ",
+ "",
+ };
+
+ printf("=== %s ===\n", __func__);
+
+ for (unsigned int i = 0; i < ARRAY_SIZE(messages); i++) {
+ struct osmo_trxc_msg msg;
+ int rc;
+
+ rc = osmo_trxc_msg_parse(&msg, messages[i], strlen(messages[i]));
+ if (rc < 0) {
+ printf("'%s' -> rc=%d\n", messages[i], rc);
+ continue;
+ }
+ printf("'%s' -> type=%d cmd='%s' status=%d params='%s'\n",
+ messages[i], msg.type, msg.cmd, msg.status, msg.params);
+ /* re-encode and compare against the original */
+ printf("\tre-encoded: '%s'\n", osmo_trxc_msg_name(&msg));
+ OSMO_ASSERT(strcmp(osmo_trxc_msg_name(&msg), messages[i]) == 0);
+ }
+}
+
+static void test_msg_build(void)
+{
+ static const struct osmo_trxc_msg messages[] = {
+ { .type = OSMO_TRXC_MT_CMD, .cmd = "POWERON" },
+ { .type = OSMO_TRXC_MT_CMD, .cmd = "SETSLOT", .params = "0 7" },
+ { .type = OSMO_TRXC_MT_RSP, .cmd = "POWERON", .status = 0 },
+ { .type = OSMO_TRXC_MT_RSP, .cmd = "SETSLOT", .status = 1, .params = "0 7" },
+ { .type = OSMO_TRXC_MT_IND, .cmd = "CLOCK", .params = "402312" },
+ };
+ char buf[OSMO_TRXC_MSG_BUF_SIZE];
+ int rc;
+
+ printf("=== %s ===\n", __func__);
+
+ for (unsigned int i = 0; i < ARRAY_SIZE(messages); i++) {
+ rc = osmo_trxc_msg_build(buf, sizeof(buf), &messages[i]);
+ OSMO_ASSERT(rc > 0 && rc == (int)strlen(buf));
+ printf("'%s' (rc=%d)\n", buf, rc);
+ }
+
+ /* buffer too small */
+ rc = osmo_trxc_msg_build(buf, 8, &messages[1]);
+ printf("build into a too small buffer: rc=%d\n", rc);
+ OSMO_ASSERT(rc < 0);
+}
+
+static void test_params_scan(void)
+{
+ struct osmo_trxc_msg msg;
+ unsigned int tn, ts_type;
+ int rc;
+
+ printf("=== %s ===\n", __func__);
+
+ rc = osmo_trxc_msg_parse(&msg, "RSP SETSLOT 0 3 7", 17);
+ OSMO_ASSERT(rc == 0);
+
+ rc = osmo_trxc_msg_params_scan(&msg, "%u %u", &tn, &ts_type);
+ printf("'%s' -> rc=%d tn=%u ts_type=%u\n", msg.params, rc, tn, ts_type);
+ OSMO_ASSERT(rc == 2 && tn == 3 && ts_type == 7);
+}
+
+static void test_clk_ind(void)
+{
+ static const char * const messages[] = {
+ "IND CLOCK 402312",
+ "IND CLOCK 0",
+ "IND CLOCK 2715647", /* GSM_TDMA_HYPERFRAME - 1 */
+ "IND CLOCK 2715648", /* GSM_TDMA_HYPERFRAME */
+ "IND CLOCK",
+ "IND KCOLC 123",
+ "CMD CLOCK 123",
+ };
+ char buf[OSMO_TRXC_MSG_BUF_SIZE];
+ uint32_t fn;
+ int rc;
+
+ printf("=== %s ===\n", __func__);
+
+ for (unsigned int i = 0; i < ARRAY_SIZE(messages); i++) {
+ rc = osmo_trxc_clock_ind_parse(&fn, messages[i], strlen(messages[i]));
+ if (rc < 0) {
+ printf("'%s' -> rc=%d\n", messages[i], rc);
+ continue;
+ }
+ printf("'%s' -> fn=%u\n", messages[i], fn);
+ /* re-encode and compare against the original */
+ rc = osmo_trxc_clock_ind_build(buf, sizeof(buf), fn);
+ OSMO_ASSERT(rc > 0);
+ OSMO_ASSERT(strcmp(buf, messages[i]) == 0);
+ }
+
+ /* out of range TDMA fn */
+ rc = osmo_trxc_clock_ind_build(buf, sizeof(buf), 2715648);
+ printf("build with out of range fn: rc=%d\n", rc);
+ OSMO_ASSERT(rc < 0);
+}
+
+int main(int argc, char **argv)
+{
+ test_msg_parse();
+ test_msg_build();
+ test_params_scan();
+ test_clk_ind();
+
+ printf("Done\n");
+ return 0;
+}
diff --git a/tests/libosmo-trx/trxc_test.ok b/tests/libosmo-trx/trxc_test.ok
new file mode 100644
index 0000000..9422d19
--- /dev/null
+++ b/tests/libosmo-trx/trxc_test.ok
@@ -0,0 +1,50 @@
+=== test_msg_parse ===
+'CMD POWERON' -> type=0 cmd='POWERON' status=0 params=''
+ re-encoded: 'CMD POWERON'
+'CMD POWEROFF' -> type=0 cmd='POWEROFF' status=0 params=''
+ re-encoded: 'CMD POWEROFF'
+'CMD RXTUNE 890000' -> type=0 cmd='RXTUNE' status=0 params='890000'
+ re-encoded: 'CMD RXTUNE 890000'
+'CMD SETSLOT 0 7 C5/S1' -> type=0 cmd='SETSLOT' status=0 params='0 7 C5/S1'
+ re-encoded: 'CMD SETSLOT 0 7 C5/S1'
+'CMD ECHO' -> type=0 cmd='ECHO' status=0 params=''
+ re-encoded: 'CMD ECHO'
+'CMD FAKE_TOA 128 0' -> type=0 cmd='FAKE_TOA' status=0 params='128 0'
+ re-encoded: 'CMD FAKE_TOA 128 0'
+'RSP POWERON 0' -> type=1 cmd='POWERON' status=0 params=''
+ re-encoded: 'RSP POWERON 0'
+'RSP SETSLOT 0 0 7' -> type=1 cmd='SETSLOT' status=0 params='0 7'
+ re-encoded: 'RSP SETSLOT 0 0 7'
+'RSP SETFORMAT 1 2' -> type=1 cmd='SETFORMAT' status=1 params='2'
+ re-encoded: 'RSP SETFORMAT 1 2'
+'RSP NOMTXPOWER 0 23' -> type=1 cmd='NOMTXPOWER' status=0 params='23'
+ re-encoded: 'RSP NOMTXPOWER 0 23'
+'RSP ERR 1' -> type=1 cmd='ERR' status=1 params=''
+ re-encoded: 'RSP ERR 1'
+'RSP MEASURE 0 890000 -85' -> type=1 cmd='MEASURE' status=0 params='890000 -85'
+ re-encoded: 'RSP MEASURE 0 890000 -85'
+'IND CLOCK 402312' -> type=2 cmd='CLOCK' status=0 params='402312'
+ re-encoded: 'IND CLOCK 402312'
+'NDI KCOLC 123456' -> rc=-22
+'RSP NOSTATUS' -> rc=-22
+'CMD ' -> rc=-22
+'' -> rc=-22
+=== test_msg_build ===
+'CMD POWERON' (rc=11)
+'CMD SETSLOT 0 7' (rc=15)
+'RSP POWERON 0' (rc=13)
+'RSP SETSLOT 1 0 7' (rc=17)
+'IND CLOCK 402312' (rc=16)
+build into a too small buffer: rc=-90
+=== test_params_scan ===
+'3 7' -> rc=2 tn=3 ts_type=7
+=== test_clk_ind ===
+'IND CLOCK 402312' -> fn=402312
+'IND CLOCK 0' -> fn=0
+'IND CLOCK 2715647' -> fn=2715647
+'IND CLOCK 2715648' -> rc=-34
+'IND CLOCK' -> rc=-22
+'IND KCOLC 123' -> rc=-22
+'CMD CLOCK 123' -> rc=-22
+build with out of range fn: rc=-34
+Done
diff --git a/tests/libosmo-trx/trxd_test.c b/tests/libosmo-trx/trxd_test.c
new file mode 100644
index 0000000..7cb3d94
--- /dev/null
+++ b/tests/libosmo-trx/trxd_test.c
@@ -0,0 +1,407 @@
+/*! \file tests/trxd_test.c
+ * Regression test for the TRXD PDU codec. */
+
+/*
+ * (C) 2026 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ * Author: Vadim Yanitskiy <vyanitskiy@sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ *
+ * 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 <stdio.h>
+#include <string.h>
+#include <errno.h>
+
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/utils.h>
+
+#include <osmocom/trx/trxd.h>
+#include <osmocom/trx/trxd_int.h>
+
+static void fill_burst_ind(struct osmo_trxd_burst_ind *bi, size_t burst_len)
+{
+ *bi = (struct osmo_trxd_burst_ind){
+ .flags = OSMO_TRXD_F_MOD_TYPE
+ | OSMO_TRXD_F_TS_INFO
+ | OSMO_TRXD_F_CI_CB,
+ .fn = 1234567,
+ .tn = 5,
+ .toa256 = -512,
+ .rssi = -63,
+ .mod = (burst_len == OSMO_TRXD_BURST_LEN_8PSK) ?
+ OSMO_TRXD_MOD_T_8PSK : OSMO_TRXD_MOD_T_GMSK,
+ .tsc_set = 1,
+ .tsc = 7,
+ .ci_cb = -150,
+ .burst_len = burst_len,
+ };
+
+ for (size_t i = 0; i < burst_len; i++)
+ bi->burst[i] = (i & 1) ? -100 : 100;
+}
+
+static void fill_burst_req(struct osmo_trxd_burst_req *br, size_t burst_len)
+{
+ *br = (struct osmo_trxd_burst_req){
+ .flags = OSMO_TRXD_F_MOD_TYPE
+ | OSMO_TRXD_F_TS_INFO,
+ .fn = 2654321,
+ .tn = 2,
+ .att = 10,
+ .mod = (burst_len == OSMO_TRXD_BURST_LEN_8PSK) ?
+ OSMO_TRXD_MOD_T_8PSK : OSMO_TRXD_MOD_T_GMSK,
+ .tsc_set = 0,
+ .tsc = 3,
+ .burst_len = burst_len,
+ };
+
+ for (size_t i = 0; i < burst_len; i++)
+ br->burst[i] = (i & 1);
+}
+
+static void test_burst_ind(uint8_t pdu_ver, size_t burst_len)
+{
+ struct osmo_trxd_parse_state st;
+ struct osmo_trxd_burst_ind bi, bi2;
+ struct msgb *msg = msgb_alloc(4096, "ind");
+ int rc;
+
+ printf("=== %s(v%u, burst_len=%zu) ===\n",
+ __func__, pdu_ver, burst_len);
+
+ fill_burst_ind(&bi, burst_len);
+ rc = osmo_trxd_burst_ind_build(msg, pdu_ver, &bi);
+ OSMO_ASSERT(rc == 0);
+ osmo_trxd_build_fin(msg, pdu_ver);
+
+ printf("build: %s\n", osmo_trxd_burst_ind_name(&bi));
+ printf("datagram (%u bytes): %s...\n", msgb_length(msg),
+ osmo_hexdump_nospc(msgb_data(msg), OSMO_MIN(16, msgb_length(msg))));
+
+ osmo_trxd_parse_state_init(&st);
+ rc = osmo_trxd_burst_ind_parse(&st, &bi2, msgb_data(msg), msgb_length(msg));
+ OSMO_ASSERT(rc == (int)msgb_length(msg));
+ printf("parse: %s\n", osmo_trxd_burst_ind_name(&bi2));
+
+ OSMO_ASSERT(bi2.fn == bi.fn && bi2.tn == bi.tn);
+ OSMO_ASSERT(bi2.rssi == bi.rssi && bi2.toa256 == bi.toa256);
+ OSMO_ASSERT(bi2.burst_len == bi.burst_len);
+ OSMO_ASSERT(memcmp(bi2.burst, bi.burst, bi.burst_len) == 0);
+ if (pdu_ver >= 1) {
+ OSMO_ASSERT(bi2.mod == bi.mod);
+ OSMO_ASSERT(bi2.tsc_set == bi.tsc_set && bi2.tsc == bi.tsc);
+ OSMO_ASSERT(bi2.ci_cb == bi.ci_cb);
+ }
+
+ msgb_free(msg);
+}
+
+static void test_burst_ind_nope(uint8_t pdu_ver)
+{
+ struct osmo_trxd_parse_state st;
+ struct osmo_trxd_burst_ind bi, bi2;
+ struct msgb *msg = msgb_alloc(4096, "nope");
+ int rc;
+
+ printf("=== %s(v%u) ===\n", __func__, pdu_ver);
+
+ fill_burst_ind(&bi, 0);
+ bi.flags = OSMO_TRXD_F_NOPE_IND | OSMO_TRXD_F_CI_CB;
+
+ rc = osmo_trxd_burst_ind_build(msg, pdu_ver, &bi);
+ if (pdu_ver == 0) {
+ /* TRXDv0 cannot carry NOPE.ind */
+ printf("build: rc=%d (expected -ENOTSUP)\n", rc);
+ OSMO_ASSERT(rc == -ENOTSUP);
+ msgb_free(msg);
+ return;
+ }
+ OSMO_ASSERT(rc == 0);
+ osmo_trxd_build_fin(msg, pdu_ver);
+ printf("build: %s\n", osmo_trxd_burst_ind_name(&bi));
+
+ osmo_trxd_parse_state_init(&st);
+ rc = osmo_trxd_burst_ind_parse(&st, &bi2, msgb_data(msg), msgb_length(msg));
+ OSMO_ASSERT(rc == (int)msgb_length(msg));
+ printf("parse: %s\n", osmo_trxd_burst_ind_name(&bi2));
+
+ OSMO_ASSERT(bi2.flags & OSMO_TRXD_F_NOPE_IND);
+ OSMO_ASSERT(bi2.burst_len == 0);
+
+ msgb_free(msg);
+}
+
+static void test_burst_ind_v0_legacy_padding(void)
+{
+ struct osmo_trxd_parse_state st;
+ struct osmo_trxd_burst_ind bi, bi2;
+ struct msgb *msg = msgb_alloc(4096, "legacy");
+ int rc;
+
+ printf("=== %s ===\n", __func__);
+
+ fill_burst_ind(&bi, OSMO_TRXD_BURST_LEN_GMSK);
+ rc = osmo_trxd_burst_ind_build(msg, 0, &bi);
+ OSMO_ASSERT(rc == 0);
+
+ /* a legacy transceiver may append two garbage bytes */
+ msgb_put_u16(msg, 0xdead);
+
+ osmo_trxd_parse_state_init(&st);
+ rc = osmo_trxd_burst_ind_parse(&st, &bi2, msgb_data(msg), msgb_length(msg));
+ printf("parse: rc=%d (%u bytes incl. padding)\n", rc, msgb_length(msg));
+ OSMO_ASSERT(rc == (int)msgb_length(msg));
+ OSMO_ASSERT(bi2.burst_len == OSMO_TRXD_BURST_LEN_GMSK);
+
+ msgb_free(msg);
+}
+
+static void test_burst_ind_batch(uint8_t pdu_ver)
+{
+ struct osmo_trxd_parse_state st;
+ struct osmo_trxd_burst_ind bi, bi2;
+ struct msgb *msg;
+ const uint8_t *buf;
+ size_t buf_len;
+ unsigned int i;
+ int rc;
+
+ /* PDU batching is only supported by TRXDv2 and higher */
+ if (pdu_ver < 2)
+ return;
+
+ printf("=== %s(v%u) ===\n", __func__, pdu_ver);
+
+ msg = msgb_alloc(4096, "batch");
+
+ /* batch a normal burst + a NOPE.ind for another timeslot */
+ fill_burst_ind(&bi, OSMO_TRXD_BURST_LEN_GMSK);
+ bi.trx_num = 1;
+ bi.flags |= OSMO_TRXD_F_TRX_NUM;
+ rc = osmo_trxd_burst_ind_build(msg, pdu_ver, &bi);
+ OSMO_ASSERT(rc == 0);
+
+ bi.tn = 6;
+ bi.flags = OSMO_TRXD_F_NOPE_IND | OSMO_TRXD_F_TRX_NUM;
+ bi.burst_len = 0;
+ rc = osmo_trxd_burst_ind_build(msg, pdu_ver, &bi);
+ OSMO_ASSERT(rc == 0);
+
+ osmo_trxd_build_fin(msg, pdu_ver);
+ printf("datagram (%u bytes)\n", msgb_length(msg));
+
+ osmo_trxd_parse_state_init(&st);
+ buf = msgb_data(msg);
+ buf_len = msgb_length(msg);
+ for (i = 0; buf_len > 0; i++) {
+ rc = osmo_trxd_burst_ind_parse(&st, &bi2, buf, buf_len);
+ OSMO_ASSERT(rc > 0);
+ printf("parse[%u]: %s\n", i, osmo_trxd_burst_ind_name(&bi2));
+ /* the batched PDU inherits the TDMA fn of the first one */
+ OSMO_ASSERT(bi2.fn == 1234567);
+ buf += rc;
+ buf_len -= rc;
+ /* BATCH.ind shall be set on all PDUs but the last one */
+ OSMO_ASSERT(!!(bi2.flags & OSMO_TRXD_F_BATCH_IND) == (buf_len > 0));
+ }
+ OSMO_ASSERT(i == 2);
+
+ msgb_free(msg);
+}
+
+static void test_burst_req(uint8_t pdu_ver, size_t burst_len)
+{
+ struct osmo_trxd_parse_state st;
+ struct osmo_trxd_burst_req br, br2;
+ struct msgb *msg = msgb_alloc(4096, "req");
+ int rc;
+
+ printf("=== %s(v%u, burst_len=%zu) ===\n", __func__, pdu_ver, burst_len);
+
+ fill_burst_req(&br, burst_len);
+ rc = osmo_trxd_burst_req_build(msg, pdu_ver, &br);
+ OSMO_ASSERT(rc == 0);
+ osmo_trxd_build_fin(msg, pdu_ver);
+
+ printf("build: %s\n", osmo_trxd_burst_req_name(&br));
+ printf("datagram (%u bytes): %s...\n", msgb_length(msg),
+ osmo_hexdump_nospc(msgb_data(msg), OSMO_MIN(16, msgb_length(msg))));
+
+ osmo_trxd_parse_state_init(&st);
+ rc = osmo_trxd_burst_req_parse(&st, &br2, msgb_data(msg), msgb_length(msg));
+ OSMO_ASSERT(rc == (int)msgb_length(msg));
+ printf("parse: %s\n", osmo_trxd_burst_req_name(&br2));
+
+ OSMO_ASSERT(br2.fn == br.fn && br2.tn == br.tn);
+ OSMO_ASSERT(br2.att == br.att);
+ OSMO_ASSERT(br2.burst_len == br.burst_len);
+ OSMO_ASSERT(memcmp(br2.burst, br.burst, br.burst_len) == 0);
+ if (pdu_ver >= 2) {
+ OSMO_ASSERT(br2.mod == br.mod);
+ OSMO_ASSERT(br2.tsc_set == br.tsc_set && br2.tsc == br.tsc);
+ }
+
+ msgb_free(msg);
+}
+
+static void test_burst_req_batch(uint8_t pdu_ver)
+{
+ struct osmo_trxd_parse_state st;
+ struct osmo_trxd_burst_req br, br2;
+ struct msgb *msg;
+ const uint8_t *buf;
+ size_t buf_len;
+ unsigned int i;
+ int rc;
+
+ /* PDU batching is only supported by TRXDv2 and higher */
+ if (pdu_ver < 2)
+ return;
+
+ printf("=== %s(v%u) ===\n", __func__, pdu_ver);
+
+ msg = msgb_alloc(4096, "batch");
+
+ for (i = 0; i < 3; i++) {
+ fill_burst_req(&br, OSMO_TRXD_BURST_LEN_GMSK);
+ br.tn = i;
+ rc = osmo_trxd_burst_req_build(msg, pdu_ver, &br);
+ OSMO_ASSERT(rc == 0);
+ }
+ osmo_trxd_build_fin(msg, pdu_ver);
+ printf("datagram (%u bytes)\n", msgb_length(msg));
+
+ osmo_trxd_parse_state_init(&st);
+ buf = msgb_data(msg);
+ buf_len = msgb_length(msg);
+ for (i = 0; buf_len > 0; i++) {
+ rc = osmo_trxd_burst_req_parse(&st, &br2, buf, buf_len);
+ OSMO_ASSERT(rc > 0);
+ printf("parse[%u]: %s\n", i, osmo_trxd_burst_req_name(&br2));
+ OSMO_ASSERT(br2.tn == i && br2.fn == 2654321);
+ buf += rc;
+ buf_len -= rc;
+ OSMO_ASSERT(!!(br2.flags & OSMO_TRXD_F_BATCH_IND) == (buf_len > 0));
+ }
+ OSMO_ASSERT(i == 3);
+
+ msgb_free(msg);
+}
+
+static void test_mts(void)
+{
+ static const uint8_t mts_bytes[] = {
+ 0x80, /* NOPE.ind */
+ 0x07, /* GMSK, set 0, tsc 7 */
+ 0x1d, /* GMSK, set 3, tsc 5 */
+ 0x2b, /* 8-PSK, set 1, tsc 3 */
+ 0x31, /* GMSK, Access Burst, tsc 1 */
+ 0x66, /* AQPSK, set 0, tsc 6 */
+ 0x51, /* invalid */
+ };
+
+ printf("=== %s ===\n", __func__);
+
+ for (unsigned int i = 0; i < ARRAY_SIZE(mts_bytes); i++) {
+ struct osmo_trxd_burst_ind bi = { };
+ uint8_t mts = mts_bytes[i];
+
+ if (trxd_mts_parse_ind(&bi, mts) < 0) {
+ printf("trxd_mts_parse_ind(0x%02x) failed\n", mts);
+ continue;
+ }
+ printf("trxd_mts_parse_ind(0x%02x): flags=0x%02x mod=%s set=%u tsc=%u",
+ mts, bi.flags, osmo_trxd_mod_type_name(bi.mod),
+ bi.tsc_set, bi.tsc);
+
+ /* re-encode and compare */
+ OSMO_ASSERT(trxd_mts_build_ind(&mts, &bi) == 0);
+ printf(" (re-encoded: 0x%02x)\n", mts);
+ OSMO_ASSERT(mts == mts_bytes[i]);
+ }
+}
+
+static void test_parse_errors(void)
+{
+ struct osmo_trxd_parse_state st;
+ struct osmo_trxd_burst_ind bi;
+ struct osmo_trxd_burst_req br;
+ uint8_t buf[512];
+ int rc;
+
+ printf("=== %s ===\n", __func__);
+
+ /* empty buffer */
+ osmo_trxd_parse_state_init(&st);
+ rc = osmo_trxd_burst_ind_parse(&st, &bi, buf, 0);
+ printf("ind, empty buffer: rc=%d\n", rc);
+ OSMO_ASSERT(rc < 0);
+
+ /* unknown PDU version */
+ memset(buf, 0, sizeof(buf));
+ buf[0] = (0x0f << 4);
+ osmo_trxd_parse_state_init(&st);
+ rc = osmo_trxd_burst_ind_parse(&st, &bi, buf, sizeof(buf));
+ printf("ind, PDU version 15: rc=%d\n", rc);
+ OSMO_ASSERT(rc == -ENOTSUP);
+
+ /* TRXDv0 with odd burst length */
+ memset(buf, 0, sizeof(buf));
+ osmo_trxd_parse_state_init(&st);
+ rc = osmo_trxd_burst_ind_parse(&st, &bi, buf, 8 + 100);
+ printf("ind, v0 with odd burst length: rc=%d\n", rc);
+ OSMO_ASSERT(rc == -EINVAL);
+
+ /* TRXDv1 header, but no burst bits */
+ memset(buf, 0, sizeof(buf));
+ buf[0] = (1 << 4);
+ osmo_trxd_parse_state_init(&st);
+ rc = osmo_trxd_burst_ind_parse(&st, &bi, buf, 11);
+ printf("ind, v1 without burst bits: rc=%d\n", rc);
+ OSMO_ASSERT(rc == -EINVAL);
+
+ /* illegal TDMA fn */
+ memset(buf, 0, sizeof(buf));
+ osmo_store32be(0xffffffff, buf + 1);
+ osmo_trxd_parse_state_init(&st);
+ rc = osmo_trxd_burst_req_parse(&st, &br, buf, 6 + 148);
+ printf("req, illegal TDMA fn: rc=%d\n", rc);
+ OSMO_ASSERT(rc == -EINVAL);
+}
+
+int main(int argc, char **argv)
+{
+ for (uint8_t pdu_ver = 0; pdu_ver <= OSMO_TRXD_PDU_VER_MAX; pdu_ver++) {
+ test_burst_req(pdu_ver, OSMO_TRXD_BURST_LEN_GMSK);
+ test_burst_req(pdu_ver, OSMO_TRXD_BURST_LEN_8PSK);
+ test_burst_req_batch(pdu_ver);
+
+ test_burst_ind(pdu_ver, OSMO_TRXD_BURST_LEN_GMSK);
+ test_burst_ind(pdu_ver, OSMO_TRXD_BURST_LEN_8PSK);
+ test_burst_ind_batch(pdu_ver);
+ test_burst_ind_nope(pdu_ver);
+ }
+
+ test_burst_ind_v0_legacy_padding();
+
+ test_mts();
+ test_parse_errors();
+
+ printf("Done\n");
+ return 0;
+}
diff --git a/tests/libosmo-trx/trxd_test.ok b/tests/libosmo-trx/trxd_test.ok
new file mode 100644
index 0000000..e4f22da
--- /dev/null
+++ b/tests/libosmo-trx/trxd_test.ok
@@ -0,0 +1,82 @@
+=== test_burst_req(v0, burst_len=148) ===
+build: BURST.req tn=2 fn=2654321 att=10 mod=GMSK set=0 tsc=3 burst_len=148
+datagram (154 bytes): 02002880710a00010001000100010001...
+parse: BURST.req tn=2 fn=2654321 att=10 mod=GMSK burst_len=148
+=== test_burst_req(v0, burst_len=444) ===
+build: BURST.req tn=2 fn=2654321 att=10 mod=8-PSK set=0 tsc=3 burst_len=444
+datagram (450 bytes): 02002880710a00010001000100010001...
+parse: BURST.req tn=2 fn=2654321 att=10 mod=8-PSK burst_len=444
+=== test_burst_ind(v0, burst_len=148) ===
+build: BURST.ind tn=5 fn=1234567 rssi=-63 toa256=-512 C/I=-150 cB mod=GMSK set=1 tsc=7 burst_len=148
+datagram (156 bytes): 050012d6873ffe001be31be31be31be3...
+parse: BURST.ind tn=5 fn=1234567 rssi=-63 toa256=-512 mod=GMSK burst_len=148
+=== test_burst_ind(v0, burst_len=444) ===
+build: BURST.ind tn=5 fn=1234567 rssi=-63 toa256=-512 C/I=-150 cB mod=8-PSK set=1 tsc=7 burst_len=444
+datagram (452 bytes): 050012d6873ffe001be31be31be31be3...
+parse: BURST.ind tn=5 fn=1234567 rssi=-63 toa256=-512 mod=8-PSK burst_len=444
+=== test_burst_ind_nope(v0) ===
+build: rc=-95 (expected -ENOTSUP)
+=== test_burst_req(v1, burst_len=148) ===
+build: BURST.req tn=2 fn=2654321 att=10 mod=GMSK set=0 tsc=3 burst_len=148
+datagram (154 bytes): 12002880710a00010001000100010001...
+parse: BURST.req tn=2 fn=2654321 att=10 mod=GMSK burst_len=148
+=== test_burst_req(v1, burst_len=444) ===
+build: BURST.req tn=2 fn=2654321 att=10 mod=8-PSK set=0 tsc=3 burst_len=444
+datagram (450 bytes): 12002880710a00010001000100010001...
+parse: BURST.req tn=2 fn=2654321 att=10 mod=8-PSK burst_len=444
+=== test_burst_ind(v1, burst_len=148) ===
+build: BURST.ind tn=5 fn=1234567 rssi=-63 toa256=-512 C/I=-150 cB mod=GMSK set=1 tsc=7 burst_len=148
+datagram (159 bytes): 150012d6873ffe000fff6a1be31be31b...
+parse: BURST.ind tn=5 fn=1234567 rssi=-63 toa256=-512 C/I=-150 cB mod=GMSK set=1 tsc=7 burst_len=148
+=== test_burst_ind(v1, burst_len=444) ===
+build: BURST.ind tn=5 fn=1234567 rssi=-63 toa256=-512 C/I=-150 cB mod=8-PSK set=1 tsc=7 burst_len=444
+datagram (455 bytes): 150012d6873ffe002fff6a1be31be31b...
+parse: BURST.ind tn=5 fn=1234567 rssi=-63 toa256=-512 C/I=-150 cB mod=8-PSK set=1 tsc=7 burst_len=444
+=== test_burst_ind_nope(v1) ===
+build: NOPE.ind tn=5 fn=1234567 rssi=-63 toa256=-512 C/I=-150 cB
+parse: NOPE.ind tn=5 fn=1234567 rssi=-63 toa256=-512 C/I=-150 cB
+=== test_burst_req(v2, burst_len=148) ===
+build: BURST.req tn=2 fn=2654321 att=10 mod=GMSK set=0 tsc=3 burst_len=148
+datagram (160 bytes): 2200030a000000000028807100010001...
+parse: BURST.req tn=2 fn=2654321 att=10 trx_num=0 mod=GMSK set=0 tsc=3 burst_len=148
+=== test_burst_req(v2, burst_len=444) ===
+build: BURST.req tn=2 fn=2654321 att=10 mod=8-PSK set=0 tsc=3 burst_len=444
+datagram (456 bytes): 2200230a000000000028807100010001...
+parse: BURST.req tn=2 fn=2654321 att=10 trx_num=0 mod=8-PSK set=0 tsc=3 burst_len=444
+=== test_burst_req_batch(v2) ===
+datagram (472 bytes)
+parse[0]: BURST.req tn=0 fn=2654321 att=10 trx_num=0 mod=GMSK set=0 tsc=3 burst_len=148
+parse[1]: BURST.req tn=1 fn=2654321 att=10 trx_num=0 mod=GMSK set=0 tsc=3 burst_len=148
+parse[2]: BURST.req tn=2 fn=2654321 att=10 trx_num=0 mod=GMSK set=0 tsc=3 burst_len=148
+=== test_burst_ind(v2, burst_len=148) ===
+build: BURST.ind tn=5 fn=1234567 rssi=-63 toa256=-512 C/I=-150 cB mod=GMSK set=1 tsc=7 burst_len=148
+datagram (160 bytes): 25000f3ffe00ff6a0012d6871be31be3...
+parse: BURST.ind tn=5 fn=1234567 trx_num=0 rssi=-63 toa256=-512 C/I=-150 cB mod=GMSK set=1 tsc=7 burst_len=148
+=== test_burst_ind(v2, burst_len=444) ===
+build: BURST.ind tn=5 fn=1234567 rssi=-63 toa256=-512 C/I=-150 cB mod=8-PSK set=1 tsc=7 burst_len=444
+datagram (456 bytes): 25002f3ffe00ff6a0012d6871be31be3...
+parse: BURST.ind tn=5 fn=1234567 trx_num=0 rssi=-63 toa256=-512 C/I=-150 cB mod=8-PSK set=1 tsc=7 burst_len=444
+=== test_burst_ind_batch(v2) ===
+datagram (168 bytes)
+parse[0]: BURST.ind tn=5 fn=1234567 trx_num=1 rssi=-63 toa256=-512 C/I=-150 cB mod=GMSK set=1 tsc=7 burst_len=148
+parse[1]: NOPE.ind tn=6 fn=1234567 trx_num=1 rssi=-63 toa256=-512 C/I=-150 cB
+=== test_burst_ind_nope(v2) ===
+build: NOPE.ind tn=5 fn=1234567 rssi=-63 toa256=-512 C/I=-150 cB
+parse: NOPE.ind tn=5 fn=1234567 trx_num=0 rssi=-63 toa256=-512 C/I=-150 cB
+=== test_burst_ind_v0_legacy_padding ===
+parse: rc=158 (158 bytes incl. padding)
+=== test_mts ===
+trxd_mts_parse_ind(0x80): flags=0x01 mod=GMSK set=0 tsc=0 (re-encoded: 0x80)
+trxd_mts_parse_ind(0x07): flags=0x06 mod=GMSK set=0 tsc=7 (re-encoded: 0x07)
+trxd_mts_parse_ind(0x1d): flags=0x06 mod=GMSK set=3 tsc=5 (re-encoded: 0x1d)
+trxd_mts_parse_ind(0x2b): flags=0x06 mod=8-PSK set=1 tsc=3 (re-encoded: 0x2b)
+trxd_mts_parse_ind(0x31): flags=0x86 mod=GMSK set=0 tsc=1 (re-encoded: 0x31)
+trxd_mts_parse_ind(0x66): flags=0x06 mod=AQPSK set=0 tsc=6 (re-encoded: 0x66)
+trxd_mts_parse_ind(0x51) failed
+=== test_parse_errors ===
+ind, empty buffer: rc=-22
+ind, PDU version 15: rc=-95
+ind, v0 with odd burst length: rc=-22
+ind, v1 without burst bits: rc=-22
+req, illegal TDMA fn: rc=-22
+Done
diff --git a/tests/testsuite.at b/tests/testsuite.at
index 0ac870d..6e72c81 100644
--- a/tests/testsuite.at
+++ b/tests/testsuite.at
@@ -49,3 +49,15 @@
cat $abs_srcdir/Transceiver52M/convolve_test.ok > expout
AT_CHECK([$abs_top_builddir/tests/Transceiver52M/convolve_test], [], [expout], [])
AT_CLEANUP
+
+AT_SETUP([trxc_test])
+AT_KEYWORDS([trxc_test])
+cat $abs_srcdir/libosmo-trx/trxc_test.ok > expout
+AT_CHECK([$abs_top_builddir/tests/libosmo-trx/trxc_test], [], [expout], [])
+AT_CLEANUP
+
+AT_SETUP([trxd_test])
+AT_KEYWORDS([trxd_test])
+cat $abs_srcdir/libosmo-trx/trxd_test.ok > expout
+AT_CHECK([$abs_top_builddir/tests/libosmo-trx/trxd_test], [], [expout], [])
+AT_CLEANUP
To view, visit change 43104. To unsubscribe, or for help writing mail filters, visit settings.