fixeria has uploaded this change for review. ( 
https://gerrit.osmocom.org/c/osmo-trx/+/43111?usp=email )


Change subject: libosmo-trx/ep: add TRX endpoint module
......................................................................

libosmo-trx/ep: add TRX endpoint module

Add the osmo_trx_ep module, encapsulating the clock/ctrl/data UDP
socket management of the TRX protocol on top of osmo_io.  One endpoint
serves an (optional) clock socket plus N channels, each channel being
a ctrl + data socket pair.

The module is role-neutral: struct osmo_trx_ep_cfg carries a mode
(OSMO_TRX_EP_MODE_{L1,TRX}) selecting which side of the protocol the
endpoint implements.  The mode is needed to parse datagrams received
on the data sockets, as the wire format alone does not identify the
direction (BURST.ind vs BURST.req).  All local and remote ports are
derived from a single base port (default: 5700).

To avoid indirect calls on the hot path, the decoded Rx handlers
(osmo_trx_ep_rx_*) are plain function prototypes bound at link-time,
not function pointers.  The library provides weak default stubs to
stay link-complete (-no-undefined), so applications only implement
the handlers for the directions they consume, overriding the stubs.

The Tx path implements TRXDv2 PDU batching for burst requests:
osmo_trx_ep_send_burst_req() accumulates PDUs until it is called
with br == NULL (the batching breaker), like in osmo-bts-trx.

osmo-trx itself will not adopt this module: its per-channel/thread
socket ownership model (dedicated blocking-read threads per socket)
is incompatible with osmo_io's single-select-loop dispatch. It is
meant for osmo-bts, osmocom-bb/trxcon, and the upcoming C rewrite
of fake_trx.

Change-Id: I767fa43a9ca88be40c385f6dcb9de22891a6afc3
---
M .gitignore
M libosmo-trx/include/Makefile.am
A libosmo-trx/include/osmocom/trx/ep.h
M libosmo-trx/src/Makefile.am
A libosmo-trx/src/trx_ep.c
M tests/libosmo-trx/Makefile.am
A tests/libosmo-trx/trx_ep_test.c
A tests/libosmo-trx/trx_ep_test.err
A tests/libosmo-trx/trx_ep_test.ok
M tests/testsuite.at
10 files changed, 924 insertions(+), 0 deletions(-)



  git pull ssh://gerrit.osmocom.org:29418/osmo-trx refs/changes/11/43111/1

diff --git a/.gitignore b/.gitignore
index 03f345b..96a1d85 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,7 @@
 tests/libosmo-trx/trxc_test
 tests/libosmo-trx/trxc_client_test
 tests/libosmo-trx/trxd_test
+tests/libosmo-trx/trx_ep_test
 tests/CommonLibs/BitVectorTest
 tests/CommonLibs/F16Test
 tests/CommonLibs/InterthreadTest
diff --git a/libosmo-trx/include/Makefile.am b/libosmo-trx/include/Makefile.am
index b4553be..693daba 100644
--- a/libosmo-trx/include/Makefile.am
+++ b/libosmo-trx/include/Makefile.am
@@ -1,4 +1,5 @@
 nobase_include_HEADERS = \
+       osmocom/trx/ep.h \
        osmocom/trx/trxc.h \
        osmocom/trx/trxc_client.h \
        osmocom/trx/trxd.h \
diff --git a/libosmo-trx/include/osmocom/trx/ep.h 
b/libosmo-trx/include/osmocom/trx/ep.h
new file mode 100644
index 0000000..b1c9252
--- /dev/null
+++ b/libosmo-trx/include/osmocom/trx/ep.h
@@ -0,0 +1,101 @@
+/*! \file osmocom/trx/ep.h
+ * TRX protocol socket endpoint management (clock/ctrl/data UDP sockets). */
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#include <osmocom/trx/trxc.h>
+#include <osmocom/trx/trxd.h>
+
+/*! One TRX protocol endpoint: an (optional) clock socket + N channels,
+ * each channel being a ctrl + data UDP socket pair (osmo_io based).
+ * Role-neutral: serves the L1 side (osmo-bts), the TRX side (osmo-trx)
+ * and a bridge (fake_trx) alike. */
+struct osmo_trx_ep;
+
+/*! Endpoint mode: which side of the TRX protocol this endpoint implements.
+ * The mode determines how datagrams received on the data sockets are
+ * parsed: the wire format alone does not identify the direction. */
+enum osmo_trx_ep_mode {
+       /*! L1 side (e.g. osmo-bts): Rx BURST.ind, Tx BURST.req */
+       OSMO_TRX_EP_MODE_L1,
+       /*! TRX side (e.g. osmo-trx): Rx BURST.req, Tx BURST.ind */
+       OSMO_TRX_EP_MODE_TRX,
+};
+
+struct osmo_trx_ep_cfg {
+       enum osmo_trx_ep_mode mode;
+       const char *laddr, *raddr;      /*!< local/remote IP address */
+       /*! base UDP port (default: 5700).  All local and remote ports are
+        * derived from it, given that the L1 side uses a fixed offset of
+        * +100: the TRX side binds base + ofs and sends to base + 100 + ofs,
+        * while the L1 side binds base + 100 + ofs and sends to base + ofs,
+        * where ofs = 0 for the clock socket, and for each channel N:
+        * ofs = 2 * N + 1 (ctrl), ofs = 2 * N + 2 (data). */
+       uint16_t base_port;
+       unsigned int num_chans;
+       bool clock_socket;      /*!< open the clock socket (base port + 0) */
+};
+
+/* Decoded RX handlers: to avoid indirect calls on the hot path, these are
+ * plain function prototypes the API user implements (link-time binding,
+ * like bts_model_*() in osmo-bts), not function pointers.  The library
+ * provides weak default stubs (logging an error), so an application only
+ * needs to implement the handlers for the directions it consumes. */
+
+/*! Handle a received clock indication ("IND CLOCK <fn>").
+ *  Called for datagrams received on the clock socket (if configured).
+ *  \param[in] ep TRX endpoint instance
+ *  \param[in] fn indicated TDMA frame number (< GSM_TDMA_HYPERFRAME) */
+extern void osmo_trx_ep_rx_clck_ind(struct osmo_trx_ep *ep, uint32_t fn);
+
+/*! Handle a received TRXC message (CMD on the TRX side, RSP on the L1 side).
+ *  Called for datagrams received on the per-channel ctrl socket.  An L1 side
+ *  application would typically feed responses into the TRXC client engine
+ *  (see osmo_trxc_client_rx()).
+ *  \param[in] ep TRX endpoint instance
+ *  \param[in] chan channel index (0 .. num_chans - 1)
+ *  \param[in] msg parsed TRXC message */
+extern void osmo_trx_ep_rx_ctrl_msg(struct osmo_trx_ep *ep, unsigned int chan,
+                                   const struct osmo_trxc_msg *msg);
+
+/*! Handle a received burst indication (OSMO_TRX_EP_MODE_L1 only).
+ *  Called for each PDU parsed from a datagram received on the per-channel
+ *  data socket, also for each PDU of a TRXDv2 batch.
+ *  \param[in] ep TRX endpoint instance
+ *  \param[in] chan channel index (0 .. num_chans - 1)
+ *  \param[in] bi parsed burst indication */
+extern void osmo_trx_ep_rx_burst_ind(struct osmo_trx_ep *ep, unsigned int chan,
+                                    const struct osmo_trxd_burst_ind *bi);
+
+/*! Handle a received burst transmit request (OSMO_TRX_EP_MODE_TRX only).
+ *  Called for each PDU parsed from a datagram received on the per-channel
+ *  data socket, also for each PDU of a TRXDv2 batch.
+ *  \param[in] ep TRX endpoint instance
+ *  \param[in] chan channel index (0 .. num_chans - 1)
+ *  \param[in] br parsed burst transmit request */
+extern void osmo_trx_ep_rx_burst_req(struct osmo_trx_ep *ep, unsigned int chan,
+                                    const struct osmo_trxd_burst_req *br);
+
+struct osmo_trx_ep *osmo_trx_ep_alloc(void *ctx, const struct osmo_trx_ep_cfg 
*cfg,
+                                     void *priv);
+int osmo_trx_ep_open(struct osmo_trx_ep *ep);
+void osmo_trx_ep_close(struct osmo_trx_ep *ep);
+void osmo_trx_ep_free(struct osmo_trx_ep *ep);
+void *osmo_trx_ep_get_priv(const struct osmo_trx_ep *ep);
+int osmo_trx_ep_set_name(struct osmo_trx_ep *ep, const char *fmt, ...);
+void osmo_trx_ep_set_log_cat(struct osmo_trx_ep *ep, int log_cat);
+
+/*! per-channel TRXD PDU version in use (set after SETFORMAT negotiation) */
+void osmo_trx_ep_set_pdu_ver(struct osmo_trx_ep *ep, unsigned int chan, 
uint8_t ver);
+
+/* TX paths (encode + transmit) */
+int osmo_trx_ep_send_clck_ind(struct osmo_trx_ep *ep, uint32_t fn);
+int osmo_trx_ep_send_ctrl_msg(struct osmo_trx_ep *ep, unsigned int chan,
+                             const struct osmo_trxc_msg *msg);
+int osmo_trx_ep_send_burst_ind(struct osmo_trx_ep *ep, unsigned int chan,
+                              const struct osmo_trxd_burst_ind *bi);
+/*! br == NULL acts as the batching breaker (flush) for TRXDv2 */
+int osmo_trx_ep_send_burst_req(struct osmo_trx_ep *ep, unsigned int chan,
+                              const struct osmo_trxd_burst_req *br);
diff --git a/libosmo-trx/src/Makefile.am b/libosmo-trx/src/Makefile.am
index 95f1fc3..29462a9 100644
--- a/libosmo-trx/src/Makefile.am
+++ b/libosmo-trx/src/Makefile.am
@@ -20,6 +20,7 @@
        trxc.c \
        trxc_client.c \
        trxd.c \
+       trx_ep.c \
        $(NULL)

 libosmotrx_la_LDFLAGS = \
diff --git a/libosmo-trx/src/trx_ep.c b/libosmo-trx/src/trx_ep.c
new file mode 100644
index 0000000..77b6ca0
--- /dev/null
+++ b/libosmo-trx/src/trx_ep.c
@@ -0,0 +1,562 @@
+/*! \file src/trx_ep.c
+ * TRX protocol socket endpoint management (clock/ctrl/data UDP sockets).
+ * Based on the socket handling in osmo-bts-trx (trx_if.c) and the
+ * port numbering convention of trx_toolkit (transceiver.py). */
+
+/*
+ * (C) 2013 Andreas Eversberg <[email protected]>
+ * (C) 2016-2017 Harald Welte <[email protected]>
+ * (C) 2019 Vadim Yanitskiy <[email protected]>
+ * (C) 2021-2026 by sysmocom - s.f.m.c. GmbH <[email protected]>
+ *
+ * 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 <unistd.h>
+
+#include <netinet/in.h>
+
+#include <osmocom/core/talloc.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/osmo_io.h>
+#include <osmocom/core/socket.h>
+#include <osmocom/core/utils.h>
+
+#include <osmocom/trx/trxc.h>
+#include <osmocom/trx/trxd.h>
+#include <osmocom/trx/ep.h>
+
+/*! The L1 side uses a fixed base port offset (see trx_toolkit) */
+#define TRX_EP_L1_PORT_OFS     100
+
+/*! Maximum TRXD datagram size: 8 batched PDUs with the largest bursts */
+#define TRX_EP_DATA_BUF_SIZE   4096
+
+struct osmo_trx_ep_chan {
+       struct osmo_trx_ep *ep;         /* back-pointer */
+       unsigned int num;               /* channel index */
+       struct osmo_io_fd *ctrl_iofd;
+       struct osmo_io_fd *data_iofd;
+       uint8_t pdu_ver;                /* TRXD PDU version in use */
+       struct msgb *tx_msg;            /* pending TRXDv2 Tx batch */
+};
+
+struct osmo_trx_ep {
+       struct osmo_trx_ep_cfg cfg;     /* a copy of the config */
+       void *priv;
+       char *name;                     /* log prefix */
+       int log_cat;                    /* logging category (default DLGLOBAL) 
*/
+       struct osmo_io_fd *clck_iofd;
+       struct osmo_trx_ep_chan *chans; /* array of cfg.num_chans channels */
+};
+
+#define LOGEP(ep, level, fmt, args...) \
+       LOGP((ep)->log_cat, level, "%s: " fmt, (ep)->name, ## args)
+
+/* Default (weak) RX handler stubs.  Applications override the handlers
+ * for the directions they consume by defining their own (strong) versions;
+ * these weak defaults keep the library link-complete (-no-undefined) and
+ * cover the unused directions. */
+__attribute__((weak))
+void osmo_trx_ep_rx_clck_ind(struct osmo_trx_ep *ep, uint32_t fn)
+{
+       LOGEP(ep, LOGL_ERROR, "unhandled clock indication (fn=%u)\n", fn);
+}
+
+__attribute__((weak))
+void osmo_trx_ep_rx_ctrl_msg(struct osmo_trx_ep *ep, unsigned int chan,
+                            const struct osmo_trxc_msg *msg)
+{
+       LOGEP(ep, LOGL_ERROR, "chan=%u: unhandled TRXC message '%s'\n",
+             chan, osmo_trxc_msg_name(msg));
+}
+
+__attribute__((weak))
+void osmo_trx_ep_rx_burst_ind(struct osmo_trx_ep *ep, unsigned int chan,
+                             const struct osmo_trxd_burst_ind *bi)
+{
+       LOGEP(ep, LOGL_ERROR, "chan=%u: unhandled %s\n",
+             chan, osmo_trxd_burst_ind_name(bi));
+}
+
+__attribute__((weak))
+void osmo_trx_ep_rx_burst_req(struct osmo_trx_ep *ep, unsigned int chan,
+                             const struct osmo_trxd_burst_req *br)
+{
+       LOGEP(ep, LOGL_ERROR, "chan=%u: unhandled %s\n",
+             chan, osmo_trxd_burst_req_name(br));
+}
+
+/* Compute a local/remote UDP port: the L1 side uses a fixed offset of +100 */
+static uint16_t trx_ep_port(const struct osmo_trx_ep *ep, bool local, uint16_t 
ofs)
+{
+       const bool l1 = (ep->cfg.mode == OSMO_TRX_EP_MODE_L1);
+
+       if (local == l1) /* L1/local or TRX/remote */
+               return ep->cfg.base_port + TRX_EP_L1_PORT_OFS + ofs;
+       return ep->cfg.base_port + ofs;
+}
+
+/***********************************************************************
+ * RX paths
+ ***********************************************************************/
+
+static void trx_ep_clck_read_cb(struct osmo_io_fd *iofd, int res, struct msgb 
*msg)
+{
+       struct osmo_trx_ep *ep = osmo_iofd_get_data(iofd);
+       uint32_t fn;
+       int rc;
+
+       if (res <= 0)
+               goto ret_free_msg;
+
+       rc = osmo_trxc_clock_ind_parse(&fn, (const char *)msgb_data(msg), 
msgb_length(msg));
+       if (rc < 0) {
+               LOGEP(ep, LOGL_NOTICE, "Rx malformed clock indication 
(rc=%d)\n", rc);
+               goto ret_free_msg;
+       }
+
+       osmo_trx_ep_rx_clck_ind(ep, fn);
+
+ret_free_msg:
+       msgb_free(msg);
+}
+
+static void trx_ep_ctrl_read_cb(struct osmo_io_fd *iofd, int res, struct msgb 
*msg)
+{
+       struct osmo_trx_ep_chan *chan = osmo_iofd_get_data(iofd);
+       struct osmo_trx_ep *ep = chan->ep;
+       struct osmo_trxc_msg tmsg;
+       int rc;
+
+       if (res <= 0)
+               goto ret_free_msg;
+
+       rc = osmo_trxc_msg_parse(&tmsg, (const char *)msgb_data(msg), 
msgb_length(msg));
+       if (rc < 0) {
+               LOGEP(ep, LOGL_NOTICE, "chan=%u: Rx malformed TRXC message 
(rc=%d)\n",
+                     chan->num, rc);
+               goto ret_free_msg;
+       }
+
+       osmo_trx_ep_rx_ctrl_msg(ep, chan->num, &tmsg);
+
+ret_free_msg:
+       msgb_free(msg);
+}
+
+static void trx_ep_data_read_cb(struct osmo_io_fd *iofd, int res, struct msgb 
*msg)
+{
+       struct osmo_trx_ep_chan *chan = osmo_iofd_get_data(iofd);
+       struct osmo_trx_ep *ep = chan->ep;
+       struct osmo_trxd_parse_state st;
+       const uint8_t *buf = msgb_data(msg);
+       size_t buf_len = msgb_length(msg);
+       int rc;
+
+       if (res <= 0)
+               goto ret_free_msg;
+
+       osmo_trxd_parse_state_init(&st);
+
+       /* starting from TRXDv2, a datagram may batch multiple PDUs */
+       if (ep->cfg.mode == OSMO_TRX_EP_MODE_L1) {
+               while (buf_len > 0) {
+                       struct osmo_trxd_burst_ind bi;
+
+                       rc = osmo_trxd_burst_ind_parse(&st, &bi, buf, buf_len);
+                       if (rc < 0)
+                               goto parse_error;
+                       osmo_trx_ep_rx_burst_ind(ep, chan->num, &bi);
+                       buf += rc;
+                       buf_len -= rc;
+               }
+       } else {
+               while (buf_len > 0) {
+                       struct osmo_trxd_burst_req br;
+
+                       rc = osmo_trxd_burst_req_parse(&st, &br, buf, buf_len);
+                       if (rc < 0)
+                               goto parse_error;
+                       osmo_trx_ep_rx_burst_req(ep, chan->num, &br);
+                       buf += rc;
+                       buf_len -= rc;
+               }
+       }
+
+       msgb_free(msg);
+       return;
+
+parse_error:
+       LOGEP(ep, LOGL_NOTICE, "chan=%u: Rx malformed TRXD PDU (rc=%d)\n",
+             chan->num, rc);
+ret_free_msg:
+       msgb_free(msg);
+}
+
+static void trx_ep_write_cb(struct osmo_io_fd *iofd, int res, struct msgb *msg)
+{
+       /* nothing to do, but osmo_io requires a write call-back */
+}
+
+/***********************************************************************
+ * open/close
+ ***********************************************************************/
+
+static const struct osmo_io_ops trx_ep_clck_ioops = {
+       .read_cb = &trx_ep_clck_read_cb,
+       .write_cb = &trx_ep_write_cb,
+};
+
+static const struct osmo_io_ops trx_ep_ctrl_ioops = {
+       .read_cb = &trx_ep_ctrl_read_cb,
+       .write_cb = &trx_ep_write_cb,
+};
+
+static const struct osmo_io_ops trx_ep_data_ioops = {
+       .read_cb = &trx_ep_data_read_cb,
+       .write_cb = &trx_ep_write_cb,
+};
+
+/* Open a single UDP socket (base port + ofs) and set up osmo_io for it */
+static struct osmo_io_fd *trx_ep_open_iofd(struct osmo_trx_ep *ep, uint16_t 
ofs,
+                                          const struct osmo_io_ops *ioops,
+                                          unsigned int buf_size, void *data)
+{
+       char sock_name[OSMO_SOCK_NAME_MAXLEN];
+       struct osmo_io_fd *iofd;
+       int fd;
+
+       fd = osmo_sock_init2(AF_UNSPEC, SOCK_DGRAM, IPPROTO_UDP,
+                            ep->cfg.laddr, trx_ep_port(ep, true, ofs),
+                            ep->cfg.raddr, trx_ep_port(ep, false, ofs),
+                            OSMO_SOCK_F_BIND | OSMO_SOCK_F_CONNECT | 
OSMO_SOCK_F_NONBLOCK);
+       if (fd < 0) {
+               LOGEP(ep, LOGL_ERROR, "Failed to open a socket (ofs=%u): %d\n", 
ofs, fd);
+               return NULL;
+       }
+
+       osmo_sock_get_name_buf(sock_name, sizeof(sock_name), fd);
+       iofd = osmo_iofd_setup(ep, fd, sock_name, OSMO_IO_FD_MODE_READ_WRITE, 
ioops, data);
+       if (iofd == NULL) {
+               close(fd);
+               return NULL;
+       }
+
+       osmo_iofd_set_alloc_info(iofd, buf_size, 0);
+
+       if (osmo_iofd_register(iofd, -1) < 0) {
+               osmo_iofd_free(iofd);
+               return NULL;
+       }
+
+       return iofd;
+}
+
+/*! Allocate a TRX endpoint instance.
+ *  \param[in] ctx talloc context to allocate from
+ *  \param[in] cfg endpoint configuration (copied, incl. the addresses)
+ *  \param[in] priv opaque application-private data
+ *  \returns pointer to the allocated instance; NULL on error */
+struct osmo_trx_ep *osmo_trx_ep_alloc(void *ctx, const struct osmo_trx_ep_cfg 
*cfg,
+                                     void *priv)
+{
+       struct osmo_trx_ep *ep;
+
+       if (cfg == NULL || cfg->num_chans == 0)
+               return NULL;
+       if (cfg->laddr == NULL || cfg->raddr == NULL)
+               return NULL;
+
+       ep = talloc_zero(ctx, struct osmo_trx_ep);
+       if (ep == NULL)
+               return NULL;
+
+       ep->cfg = *cfg;
+       ep->cfg.laddr = talloc_strdup(ep, cfg->laddr);
+       ep->cfg.raddr = talloc_strdup(ep, cfg->raddr);
+       ep->priv = priv;
+       ep->name = talloc_strdup(ep, "trx_ep");
+       ep->log_cat = DLGLOBAL;
+
+       ep->chans = talloc_zero_array(ep, struct osmo_trx_ep_chan, 
cfg->num_chans);
+       if (ep->chans == NULL) {
+               talloc_free(ep);
+               return NULL;
+       }
+       for (unsigned int i = 0; i < cfg->num_chans; i++) {
+               ep->chans[i] = (struct osmo_trx_ep_chan){
+                       .ep = ep,
+                       .num = i,
+               };
+       }
+
+       return ep;
+}
+
+/*! Open the clock/ctrl/data sockets of the given endpoint.
+ *  \returns 0 on success; negative on error (all sockets closed) */
+int osmo_trx_ep_open(struct osmo_trx_ep *ep)
+{
+       LOGEP(ep, LOGL_NOTICE, "Opening TRXC/TRXD connections 
l=%s:%u<->r=%s:%u\n",
+             ep->cfg.laddr, trx_ep_port(ep, true, 0),
+             ep->cfg.raddr, trx_ep_port(ep, false, 0));
+
+       if (ep->cfg.clock_socket) {
+               ep->clck_iofd = trx_ep_open_iofd(ep, 0, &trx_ep_clck_ioops,
+                                                OSMO_TRXC_MSG_BUF_SIZE, ep);
+               if (ep->clck_iofd == NULL)
+                       goto ret_error;
+       }
+
+       for (unsigned int i = 0; i < ep->cfg.num_chans; i++) {
+               struct osmo_trx_ep_chan *chan = &ep->chans[i];
+
+               chan->ctrl_iofd = trx_ep_open_iofd(ep, 2 * i + 1, 
&trx_ep_ctrl_ioops,
+                                                  OSMO_TRXC_MSG_BUF_SIZE, 
chan);
+               if (chan->ctrl_iofd == NULL)
+                       goto ret_error;
+
+               chan->data_iofd = trx_ep_open_iofd(ep, 2 * i + 2, 
&trx_ep_data_ioops,
+                                                  TRX_EP_DATA_BUF_SIZE, chan);
+               if (chan->data_iofd == NULL)
+                       goto ret_error;
+       }
+
+       return 0;
+
+ret_error:
+       osmo_trx_ep_close(ep);
+       return -EIO;
+}
+
+/*! Close all sockets of the given endpoint (drops pending Tx batches) */
+void osmo_trx_ep_close(struct osmo_trx_ep *ep)
+{
+       LOGEP(ep, LOGL_NOTICE, "Closing TRXC/TRXD connections 
l=%s:%u<->r=%s:%u\n",
+             ep->cfg.laddr, trx_ep_port(ep, true, 0),
+             ep->cfg.raddr, trx_ep_port(ep, false, 0));
+
+       osmo_iofd_free(ep->clck_iofd);
+       ep->clck_iofd = NULL;
+
+       for (unsigned int i = 0; i < ep->cfg.num_chans; i++) {
+               struct osmo_trx_ep_chan *chan = &ep->chans[i];
+
+               osmo_iofd_free(chan->ctrl_iofd);
+               chan->ctrl_iofd = NULL;
+               osmo_iofd_free(chan->data_iofd);
+               chan->data_iofd = NULL;
+               msgb_free(chan->tx_msg);
+               chan->tx_msg = NULL;
+       }
+}
+
+/*! Free the given endpoint instance (closes all sockets) */
+void osmo_trx_ep_free(struct osmo_trx_ep *ep)
+{
+       if (ep == NULL)
+               return;
+       osmo_trx_ep_close(ep);
+       talloc_free(ep);
+}
+
+/*! Obtain the application-private data */
+void *osmo_trx_ep_get_priv(const struct osmo_trx_ep *ep)
+{
+       return ep->priv;
+}
+
+/*! Set the name (log prefix) of the given instance, e.g. "phy0" */
+int osmo_trx_ep_set_name(struct osmo_trx_ep *ep, const char *fmt, ...)
+{
+       char name[64];
+       va_list ap;
+       int rc;
+
+       va_start(ap, fmt);
+       rc = vsnprintf(name, sizeof(name), fmt, ap);
+       va_end(ap);
+
+       if (rc < 0 || rc >= (int)sizeof(name))
+               return -EMSGSIZE;
+       osmo_talloc_replace_string(ep, &ep->name, name);
+
+       return 0;
+}
+
+/*! Set the logging category (e.g. DTRX in osmo-bts; default: DLGLOBAL) */
+void osmo_trx_ep_set_log_cat(struct osmo_trx_ep *ep, int log_cat)
+{
+       ep->log_cat = log_cat;
+}
+
+/*! Set the TRXD PDU version in use for the given channel (default: 0),
+ *  usually after the negotiation (see osmo_trxc_client_negotiate_format()) */
+void osmo_trx_ep_set_pdu_ver(struct osmo_trx_ep *ep, unsigned int chan, 
uint8_t ver)
+{
+       OSMO_ASSERT(chan < ep->cfg.num_chans);
+       LOGEP(ep, LOGL_INFO, "chan=%u: using TRXD PDU version %u\n", chan, ver);
+       ep->chans[chan].pdu_ver = ver;
+}
+
+/***********************************************************************
+ * TX paths
+ ***********************************************************************/
+
+/*! Send a clock indication ("IND CLOCK <fn>") on the clock socket.
+ *  \returns 0 on success; negative on error */
+int osmo_trx_ep_send_clck_ind(struct osmo_trx_ep *ep, uint32_t fn)
+{
+       struct msgb *msg;
+       int rc;
+
+       if (ep->clck_iofd == NULL)
+               return -ENOTSUP;
+
+       msg = msgb_alloc_c(ep, OSMO_TRXC_MSG_BUF_SIZE, "trx_ep_clck_tx");
+       rc = osmo_trxc_clock_ind_build((char *)msgb_data(msg), 
msgb_tailroom(msg), fn);
+       if (rc < 0) {
+               msgb_free(msg);
+               return rc;
+       }
+       msgb_put(msg, rc);
+
+       rc = osmo_iofd_write_msgb(ep->clck_iofd, msg);
+       if (rc < 0)
+               msgb_free(msg);
+       return rc;
+}
+
+/*! Send a TRXC message on the ctrl socket of the given channel.
+ *  \returns 0 on success; negative on error */
+int osmo_trx_ep_send_ctrl_msg(struct osmo_trx_ep *ep, unsigned int chan,
+                             const struct osmo_trxc_msg *tmsg)
+{
+       struct msgb *msg;
+       int rc;
+
+       OSMO_ASSERT(chan < ep->cfg.num_chans);
+
+       msg = msgb_alloc_c(ep, OSMO_TRXC_MSG_BUF_SIZE, "trx_ep_ctrl_tx");
+       rc = osmo_trxc_msg_build((char *)msgb_data(msg), msgb_tailroom(msg), 
tmsg);
+       if (rc < 0) {
+               msgb_free(msg);
+               return rc;
+       }
+       msgb_put(msg, rc);
+
+       rc = osmo_iofd_write_msgb(ep->chans[chan].ctrl_iofd, msg);
+       if (rc < 0)
+               msgb_free(msg);
+       return rc;
+}
+
+/*! Send a burst indication on the data socket of the given channel
+ *  (OSMO_TRX_EP_MODE_TRX only).
+ *  \returns 0 on success; negative on error */
+int osmo_trx_ep_send_burst_ind(struct osmo_trx_ep *ep, unsigned int chan,
+                              const struct osmo_trxd_burst_ind *bi)
+{
+       struct osmo_trx_ep_chan *c;
+       struct msgb *msg;
+       int rc;
+
+       OSMO_ASSERT(chan < ep->cfg.num_chans);
+       c = &ep->chans[chan];
+
+       msg = msgb_alloc_c(ep, TRX_EP_DATA_BUF_SIZE, "trx_ep_data_tx");
+       rc = osmo_trxd_burst_ind_build(msg, c->pdu_ver, bi);
+       if (rc < 0) {
+               msgb_free(msg);
+               return rc;
+       }
+       osmo_trxd_build_fin(msg, c->pdu_ver);
+
+       rc = osmo_iofd_write_msgb(c->data_iofd, msg);
+       if (rc < 0)
+               msgb_free(msg);
+       return rc;
+}
+
+/*! Send a burst transmit request on the data socket of the given channel
+ *  (OSMO_TRX_EP_MODE_L1 only).
+ *
+ *  For TRXDv2, the PDUs are batched: they get accumulated until this
+ *  function is called with br == NULL (the batching breaker), which
+ *  transmits all accumulated PDUs in a single datagram.  For TRXDv0/v1,
+ *  each PDU is transmitted immediately; the breaker is a no-op.
+ *
+ *  \returns 0 on success; -ENOMSG for a breaker with nothing to send;
+ *          other negative on error */
+int osmo_trx_ep_send_burst_req(struct osmo_trx_ep *ep, unsigned int chan,
+                              const struct osmo_trxd_burst_req *br)
+{
+       struct osmo_trx_ep_chan *c;
+       struct msgb *msg;
+       int rc;
+
+       OSMO_ASSERT(chan < ep->cfg.num_chans);
+       c = &ep->chans[chan];
+
+       /* the batching breaker */
+       if (br == NULL) {
+               if (c->tx_msg == NULL)
+                       return -ENOMSG;
+               msg = c->tx_msg;
+               c->tx_msg = NULL;
+               /* unset BATCH.ind in the last accumulated PDU */
+               osmo_trxd_build_fin(msg, c->pdu_ver);
+               rc = osmo_iofd_write_msgb(c->data_iofd, msg);
+               if (rc < 0)
+                       msgb_free(msg);
+               return rc;
+       }
+
+       if (c->tx_msg != NULL) {
+               msg = c->tx_msg;
+       } else {
+               msg = msgb_alloc_c(ep, TRX_EP_DATA_BUF_SIZE, "trx_ep_data_tx");
+               if (msg == NULL)
+                       return -ENOMEM;
+       }
+
+       rc = osmo_trxd_burst_req_build(msg, c->pdu_ver, br);
+       if (rc < 0) {
+               if (c->tx_msg == NULL)
+                       msgb_free(msg);
+               return rc;
+       }
+
+       /* TRXDv2 and higher: wait for the batching breaker */
+       if (c->pdu_ver >= 2) {
+               c->tx_msg = msg;
+               return 0;
+       }
+
+       osmo_trxd_build_fin(msg, c->pdu_ver); /* no-op for TRXDv0/v1 */
+       rc = osmo_iofd_write_msgb(c->data_iofd, msg);
+       if (rc < 0)
+               msgb_free(msg);
+       return rc;
+}
diff --git a/tests/libosmo-trx/Makefile.am b/tests/libosmo-trx/Makefile.am
index aeaab33..88fb81b 100644
--- a/tests/libosmo-trx/Makefile.am
+++ b/tests/libosmo-trx/Makefile.am
@@ -24,12 +24,15 @@
        trxc_client_test.ok \
        trxc_client_test.err \
        trxd_test.ok \
+       trx_ep_test.ok \
+       trx_ep_test.err \
        $(NULL)

 check_PROGRAMS = \
        trxc_test \
        trxc_client_test \
        trxd_test \
+       trx_ep_test \
        $(NULL)

 trxc_test_SOURCES = trxc_test.c
@@ -37,3 +40,5 @@
 trxc_client_test_SOURCES = trxc_client_test.c

 trxd_test_SOURCES = trxd_test.c
+
+trx_ep_test_SOURCES = trx_ep_test.c
diff --git a/tests/libosmo-trx/trx_ep_test.c b/tests/libosmo-trx/trx_ep_test.c
new file mode 100644
index 0000000..7e79a3f
--- /dev/null
+++ b/tests/libosmo-trx/trx_ep_test.c
@@ -0,0 +1,222 @@
+/*! \file tests/trx_ep_test.c
+ * Regression test for the TRX endpoint module: two endpoints (L1 and TRX)
+ * talking to each other over UDP sockets on localhost. */
+
+/*
+ * (C) 2026 by sysmocom - s.f.m.c. GmbH <[email protected]>
+ * Author: Vadim Yanitskiy <[email protected]>
+ *
+ * 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/application.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/core/select.h>
+#include <osmocom/core/talloc.h>
+#include <osmocom/core/utils.h>
+
+#include <osmocom/trx/ep.h>
+
+#define TEST_BASE_PORT 16700
+
+static void *test_ctx = NULL;
+
+/* let the event loop deliver everything that is in flight */
+static void flush_io(void)
+{
+       for (unsigned int i = 0; i < 16; i++)
+               osmo_select_main(1);
+}
+
+static const char *ep_label(const struct osmo_trx_ep *ep)
+{
+       return (const char *)osmo_trx_ep_get_priv(ep);
+}
+
+/* RX handlers (strong symbols, overriding the library's weak stubs) */
+void osmo_trx_ep_rx_clck_ind(struct osmo_trx_ep *ep, uint32_t fn)
+{
+       printf("%s: rx_clck_ind: fn=%u\n", ep_label(ep), fn);
+}
+
+void osmo_trx_ep_rx_ctrl_msg(struct osmo_trx_ep *ep, unsigned int chan,
+                            const struct osmo_trxc_msg *msg)
+{
+       printf("%s: rx_ctrl_msg(chan=%u): '%s'\n",
+              ep_label(ep), chan, osmo_trxc_msg_name(msg));
+
+       /* the TRX side acknowledges all commands */
+       if (msg->type == OSMO_TRXC_MT_CMD) {
+               struct osmo_trxc_msg rsp = {
+                       .type = OSMO_TRXC_MT_RSP,
+                       .status = 0,
+               };
+
+               OSMO_STRLCPY_ARRAY(rsp.cmd, msg->cmd);
+               OSMO_STRLCPY_ARRAY(rsp.params, msg->params);
+               osmo_trx_ep_send_ctrl_msg(ep, chan, &rsp);
+       }
+}
+
+void osmo_trx_ep_rx_burst_ind(struct osmo_trx_ep *ep, unsigned int chan,
+                             const struct osmo_trxd_burst_ind *bi)
+{
+       printf("%s: rx_burst_ind(chan=%u): %s\n",
+              ep_label(ep), chan, osmo_trxd_burst_ind_name(bi));
+}
+
+void osmo_trx_ep_rx_burst_req(struct osmo_trx_ep *ep, unsigned int chan,
+                             const struct osmo_trxd_burst_req *br)
+{
+       printf("%s: rx_burst_req(chan=%u): %s\n",
+              ep_label(ep), chan, osmo_trxd_burst_req_name(br));
+}
+
+static void fill_burst_req(struct osmo_trxd_burst_req *br, uint32_t fn)
+{
+       *br = (struct osmo_trxd_burst_req){
+               .flags = OSMO_TRXD_F_MOD_TYPE | OSMO_TRXD_F_TS_INFO,
+               .fn = fn,
+               .tn = 3,
+               .att = 10,
+               .mod = OSMO_TRXD_MOD_T_GMSK,
+               .tsc = 7,
+               .burst_len = OSMO_TRXD_BURST_LEN_GMSK,
+       };
+
+       for (size_t i = 0; i < br->burst_len; i++)
+               br->burst[i] = (i & 1);
+}
+
+static void fill_burst_ind(struct osmo_trxd_burst_ind *bi, uint32_t fn)
+{
+       *bi = (struct osmo_trxd_burst_ind){
+               .flags = OSMO_TRXD_F_MOD_TYPE | OSMO_TRXD_F_TS_INFO | 
OSMO_TRXD_F_CI_CB,
+               .fn = fn,
+               .tn = 5,
+               .toa256 = -512,
+               .rssi = -63,
+               .mod = OSMO_TRXD_MOD_T_GMSK,
+               .tsc = 7,
+               .ci_cb = -150,
+               .burst_len = OSMO_TRXD_BURST_LEN_GMSK,
+       };
+
+       for (size_t i = 0; i < bi->burst_len; i++)
+               bi->burst[i] = (i & 1) ? -100 : 100;
+}
+
+int main(int argc, char **argv)
+{
+       struct osmo_trx_ep *ep_l1, *ep_trx;
+       struct osmo_trxd_burst_req br;
+       struct osmo_trxd_burst_ind bi;
+       int rc;
+
+       test_ctx = talloc_named_const(NULL, 0, "trx_ep_test");
+       osmo_init_logging2(test_ctx, NULL);
+       log_set_use_color(osmo_stderr_target, 0);
+       log_set_print_timestamp(osmo_stderr_target, 0);
+       log_set_print_filename2(osmo_stderr_target, LOG_FILENAME_NONE);
+       log_set_print_category(osmo_stderr_target, 1);
+       log_set_print_category_hex(osmo_stderr_target, 0);
+       log_set_print_level(osmo_stderr_target, 1);
+       log_set_category_filter(osmo_stderr_target, DLGLOBAL, 1, LOGL_INFO);
+
+       const struct osmo_trx_ep_cfg cfg_trx = {
+               .mode = OSMO_TRX_EP_MODE_TRX,
+               .laddr = "127.0.0.1",
+               .raddr = "127.0.0.1",
+               .base_port = TEST_BASE_PORT,
+               .num_chans = 2,
+               .clock_socket = true,
+       };
+       const struct osmo_trx_ep_cfg cfg_l1 = {
+               .mode = OSMO_TRX_EP_MODE_L1,
+               .laddr = "127.0.0.1",
+               .raddr = "127.0.0.1",
+               .base_port = TEST_BASE_PORT,
+               .num_chans = 2,
+               .clock_socket = true,
+       };
+
+       ep_trx = osmo_trx_ep_alloc(test_ctx, &cfg_trx, "trx");
+       ep_l1 = osmo_trx_ep_alloc(test_ctx, &cfg_l1, "l1");
+       OSMO_ASSERT(ep_trx != NULL && ep_l1 != NULL);
+       osmo_trx_ep_set_name(ep_trx, "ep_%s", "trx");
+       osmo_trx_ep_set_name(ep_l1, "ep_%s", "l1");
+
+       OSMO_ASSERT(osmo_trx_ep_open(ep_trx) == 0);
+       OSMO_ASSERT(osmo_trx_ep_open(ep_l1) == 0);
+
+       printf("=== clock indication (TRX -> L1) ===\n");
+       osmo_trx_ep_send_clck_ind(ep_trx, 402312);
+       flush_io();
+
+       printf("=== ctrl: CMD (L1 -> TRX), RSP (TRX -> L1) ===\n");
+       osmo_trx_ep_send_ctrl_msg(ep_l1, 0, &(struct osmo_trxc_msg){
+               .type = OSMO_TRXC_MT_CMD,
+               .cmd = "POWERON",
+       });
+       flush_io();
+
+       printf("=== burst req, TRXDv0 (L1 -> TRX) ===\n");
+       fill_burst_req(&br, 100000);
+       osmo_trx_ep_send_burst_req(ep_l1, 0, &br);
+       flush_io();
+
+       printf("=== burst ind, TRXDv0 (TRX -> L1), another channel ===\n");
+       fill_burst_ind(&bi, 100005);
+       osmo_trx_ep_send_burst_ind(ep_trx, 1, &bi);
+       flush_io();
+
+       printf("=== switch chan 0 to TRXDv2 ===\n");
+       osmo_trx_ep_set_pdu_ver(ep_l1, 0, 2);
+       osmo_trx_ep_set_pdu_ver(ep_trx, 0, 2);
+
+       printf("=== burst req batch, TRXDv2 (L1 -> TRX) ===\n");
+       /* an empty batch cannot be flushed */
+       rc = osmo_trx_ep_send_burst_req(ep_l1, 0, NULL);
+       printf("empty batch breaker: rc=%d\n", rc);
+       /* accumulate two PDUs, then flush them with the breaker */
+       fill_burst_req(&br, 200000);
+       br.tn = 1;
+       osmo_trx_ep_send_burst_req(ep_l1, 0, &br);
+       br.tn = 2;
+       osmo_trx_ep_send_burst_req(ep_l1, 0, &br);
+       flush_io(); /* nothing shall be delivered yet */
+       rc = osmo_trx_ep_send_burst_req(ep_l1, 0, NULL);
+       printf("batch breaker: rc=%d\n", rc);
+       flush_io();
+
+       printf("=== burst ind, TRXDv2 with NOPE (TRX -> L1) ===\n");
+       fill_burst_ind(&bi, 200005);
+       bi.flags = OSMO_TRXD_F_NOPE_IND | OSMO_TRXD_F_CI_CB;
+       bi.burst_len = 0;
+       osmo_trx_ep_send_burst_ind(ep_trx, 0, &bi);
+       flush_io();
+
+       osmo_trx_ep_free(ep_l1);
+       osmo_trx_ep_free(ep_trx);
+
+       printf("Done\n");
+       return 0;
+}
diff --git a/tests/libosmo-trx/trx_ep_test.err 
b/tests/libosmo-trx/trx_ep_test.err
new file mode 100644
index 0000000..fb3833d
--- /dev/null
+++ b/tests/libosmo-trx/trx_ep_test.err
@@ -0,0 +1,6 @@
+DLGLOBAL NOTICE ep_trx: Opening TRXC/TRXD connections 
l=127.0.0.1:16700<->r=127.0.0.1:16800
+DLGLOBAL NOTICE ep_l1: Opening TRXC/TRXD connections 
l=127.0.0.1:16800<->r=127.0.0.1:16700
+DLGLOBAL INFO ep_l1: chan=0: using TRXD PDU version 2
+DLGLOBAL INFO ep_trx: chan=0: using TRXD PDU version 2
+DLGLOBAL NOTICE ep_l1: Closing TRXC/TRXD connections 
l=127.0.0.1:16800<->r=127.0.0.1:16700
+DLGLOBAL NOTICE ep_trx: Closing TRXC/TRXD connections 
l=127.0.0.1:16700<->r=127.0.0.1:16800
diff --git a/tests/libosmo-trx/trx_ep_test.ok b/tests/libosmo-trx/trx_ep_test.ok
new file mode 100644
index 0000000..f3ad232
--- /dev/null
+++ b/tests/libosmo-trx/trx_ep_test.ok
@@ -0,0 +1,18 @@
+=== clock indication (TRX -> L1) ===
+l1: rx_clck_ind: fn=402312
+=== ctrl: CMD (L1 -> TRX), RSP (TRX -> L1) ===
+trx: rx_ctrl_msg(chan=0): 'CMD POWERON'
+l1: rx_ctrl_msg(chan=0): 'RSP POWERON 0'
+=== burst req, TRXDv0 (L1 -> TRX) ===
+trx: rx_burst_req(chan=0): BURST.req tn=3 fn=100000 att=10 mod=GMSK 
burst_len=148
+=== burst ind, TRXDv0 (TRX -> L1), another channel ===
+l1: rx_burst_ind(chan=1): BURST.ind tn=5 fn=100005 rssi=-63 toa256=-512 
mod=GMSK burst_len=148
+=== switch chan 0 to TRXDv2 ===
+=== burst req batch, TRXDv2 (L1 -> TRX) ===
+empty batch breaker: rc=-42
+batch breaker: rc=0
+trx: rx_burst_req(chan=0): BURST.req tn=1 fn=200000 att=10 trx_num=0 mod=GMSK 
set=0 tsc=7 burst_len=148
+trx: rx_burst_req(chan=0): BURST.req tn=2 fn=200000 att=10 trx_num=0 mod=GMSK 
set=0 tsc=7 burst_len=148
+=== burst ind, TRXDv2 with NOPE (TRX -> L1) ===
+l1: rx_burst_ind(chan=0): NOPE.ind tn=5 fn=200005 trx_num=0 rssi=-63 
toa256=-512 C/I=-150 cB
+Done
diff --git a/tests/testsuite.at b/tests/testsuite.at
index 090906b..5f4ca3f 100644
--- a/tests/testsuite.at
+++ b/tests/testsuite.at
@@ -68,3 +68,10 @@
 cat $abs_srcdir/libosmo-trx/trxd_test.ok > expout
 AT_CHECK([$abs_top_builddir/tests/libosmo-trx/trxd_test], [], [expout], [])
 AT_CLEANUP
+
+AT_SETUP([trx_ep_test])
+AT_KEYWORDS([trx_ep_test])
+cat $abs_srcdir/libosmo-trx/trx_ep_test.ok > expout
+cat $abs_srcdir/libosmo-trx/trx_ep_test.err > experr
+AT_CHECK([$abs_top_builddir/tests/libosmo-trx/trx_ep_test], [], [expout], 
[experr])
+AT_CLEANUP

--
To view, visit https://gerrit.osmocom.org/c/osmo-trx/+/43111?usp=email
To unsubscribe, or for help writing mail filters, visit 
https://gerrit.osmocom.org/settings?usp=email

Gerrit-MessageType: newchange
Gerrit-Project: osmo-trx
Gerrit-Branch: master
Gerrit-Change-Id: I767fa43a9ca88be40c385f6dcb9de22891a6afc3
Gerrit-Change-Number: 43111
Gerrit-PatchSet: 1
Gerrit-Owner: fixeria <[email protected]>

Reply via email to