fixeria has uploaded this change for review.

View Change

libosmo-trx/client: add TRXC client (command queue) API

Add the osmo_trxc_client module: a transport-agnostic generalization
of the TRXC command handling logic in osmo-bts-trx (trx_if.c):

* command queue with a single command in flight;
* retransmission on response timeout (default: 2 s);
* suppression of consecutive duplicate commands;
* RSP<->CMD matching, optionally including the parameters;
* filtering of duplicate responses caused by retransmissions;
* escalation of NACKed critical commands (OSMO_TRXC_F_CRITICAL)
to the fatal_error call-back;
* TRXD PDU version negotiation (SETFORMAT), incl. the fallback to
version 0 for old transceivers rejecting it with 'RSP ERR 1'.

The client neither opens nor owns a socket: the application transmits
serialized messages in the tx_msg call-back and feeds received
datagrams into osmo_trxc_client_rx(). The response call-back may
return N > 0 to request a re-transmission of the same command after
N seconds (e.g. POWERON retry, as implemented in osmo-bts-trx).

Change-Id: I817e394f74a10e3adae4a0b58342c82acdf0794e
---
M .gitignore
M libosmo-trx/include/Makefile.am
A libosmo-trx/include/osmocom/trx/trxc_client.h
M libosmo-trx/src/Makefile.am
A libosmo-trx/src/trxc_client.c
M tests/libosmo-trx/Makefile.am
A tests/libosmo-trx/trxc_client_test.c
A tests/libosmo-trx/trxc_client_test.err
A tests/libosmo-trx/trxc_client_test.ok
M tests/testsuite.at
10 files changed, 1,064 insertions(+), 0 deletions(-)

git pull ssh://gerrit.osmocom.org:29418/osmo-trx refs/changes/08/43108/1
diff --git a/.gitignore b/.gitignore
index 26f7fab..03f345b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,7 @@

# tests
tests/libosmo-trx/trxc_test
+tests/libosmo-trx/trxc_client_test
tests/libosmo-trx/trxd_test
tests/CommonLibs/BitVectorTest
tests/CommonLibs/F16Test
diff --git a/libosmo-trx/include/Makefile.am b/libosmo-trx/include/Makefile.am
index 993f430..b4553be 100644
--- a/libosmo-trx/include/Makefile.am
+++ b/libosmo-trx/include/Makefile.am
@@ -1,5 +1,6 @@
nobase_include_HEADERS = \
osmocom/trx/trxc.h \
+ osmocom/trx/trxc_client.h \
osmocom/trx/trxd.h \
$(NULL)

diff --git a/libosmo-trx/include/osmocom/trx/trxc_client.h b/libosmo-trx/include/osmocom/trx/trxc_client.h
new file mode 100644
index 0000000..7984b93
--- /dev/null
+++ b/libosmo-trx/include/osmocom/trx/trxc_client.h
@@ -0,0 +1,98 @@
+/*! \file osmocom/trx/trxc_client.h
+ * TRXC client command queue engine: queue, retransmission, RSP matching. */
+#pragma once
+
+#include <stdint.h>
+#include <stddef.h>
+
+#include <osmocom/trx/trxc.h>
+
+/*! TRXC client engine, driving the ctrl connection towards a transceiver:
+ * command queue with a single command in flight, retransmit timer,
+ * consecutive-duplicate suppression, RSP<->CMD matching and duplicate-RSP
+ * filtering. This structure is opaque.
+ *
+ * The engine is transport-agnostic: it neither opens nor owns a socket.
+ * The application transmits serialized messages in the tx_msg call-back
+ * and feeds received datagrams into osmo_trxc_client_rx(). */
+struct osmo_trxc_client;
+
+/*! Response call-back, invoked when a response to a command is received.
+ * \param[in] client TRXC client instance
+ * \param[in] rsp received response message
+ * \param[in] cb_data opaque data passed to osmo_trxc_client_send_cmd()
+ * \returns 0 when done (the command gets dequeued);
+ * N > 0 to re-send the same command after N seconds;
+ * negative to indicate a fatal error (like a NACKed critical
+ * command, ends up in the fatal_error call-back) */
+typedef int osmo_trxc_client_rsp_cb(struct osmo_trxc_client *client,
+ const struct osmo_trxc_msg *rsp,
+ void *cb_data);
+
+struct osmo_trxc_client_ops {
+ /*! transmit a serialized TRXC message (mandatory).
+ * E.g. write() / osmo_iofd_write_msgb() on the app's ctrl socket. */
+ int (*tx_msg)(struct osmo_trxc_client *client, const char *buf, size_t len);
+ /*! a critical command definitively failed (optional; default: log).
+ * \param[in] rsp the offending response (may be NULL) */
+ void (*fatal_error)(struct osmo_trxc_client *client,
+ const struct osmo_trxc_msg *rsp);
+ /*! opaque application-private data (see osmo_trxc_client_get_priv()) */
+ void *priv;
+};
+
+struct osmo_trxc_client *osmo_trxc_client_alloc(void *ctx,
+ const struct osmo_trxc_client_ops *ops);
+void osmo_trxc_client_free(struct osmo_trxc_client *client);
+void *osmo_trxc_client_get_priv(const struct osmo_trxc_client *client);
+int osmo_trxc_client_set_name(struct osmo_trxc_client *client, const char *fmt, ...);
+void osmo_trxc_client_set_log_cat(struct osmo_trxc_client *client, int log_cat);
+void osmo_trxc_client_set_retrans(struct osmo_trxc_client *client, unsigned int sec);
+
+/*! escalate to the fatal_error call-back on NACK */
+#define OSMO_TRXC_F_CRITICAL (1 << 0)
+/*! RSP params must echo CMD params (e.g. SETSLOT, SETFORMAT) */
+#define OSMO_TRXC_F_MATCH_PARAMS (1 << 1)
+
+int osmo_trxc_client_send_cmd(struct osmo_trxc_client *client, uint32_t flags,
+ osmo_trxc_client_rsp_cb *cb, void *cb_data,
+ const char *cmd, const char *fmt, ...);
+void osmo_trxc_client_flush(struct osmo_trxc_client *client);
+
+int osmo_trxc_client_rx(struct osmo_trxc_client *client, const char *buf, size_t len);
+
+/*! TRXD PDU version negotiation result call-back.
+ * \param[in] ver_use the negotiated version to be used */
+typedef void osmo_trxc_setformat_cb(struct osmo_trxc_client *client,
+ uint8_t ver_use, void *cb_data);
+int osmo_trxc_client_negotiate_format(struct osmo_trxc_client *client,
+ uint8_t ver_max,
+ osmo_trxc_setformat_cb *cb, void *cb_data);
+
+/* Convenience wrappers for the common command set (thin, optional) */
+static inline int osmo_trxc_client_poweron(struct osmo_trxc_client *client,
+ osmo_trxc_client_rsp_cb *cb, void *cb_data)
+{
+ return osmo_trxc_client_send_cmd(client, OSMO_TRXC_F_CRITICAL, cb, cb_data,
+ OSMO_TRXC_CMD_POWERON, NULL);
+}
+static inline int osmo_trxc_client_poweroff(struct osmo_trxc_client *client,
+ osmo_trxc_client_rsp_cb *cb, void *cb_data)
+{
+ return osmo_trxc_client_send_cmd(client, OSMO_TRXC_F_CRITICAL, cb, cb_data,
+ OSMO_TRXC_CMD_POWEROFF, NULL);
+}
+static inline int osmo_trxc_client_rxtune(struct osmo_trxc_client *client,
+ unsigned int freq_khz,
+ osmo_trxc_client_rsp_cb *cb, void *cb_data)
+{
+ return osmo_trxc_client_send_cmd(client, OSMO_TRXC_F_CRITICAL, cb, cb_data,
+ OSMO_TRXC_CMD_RXTUNE, "%u", freq_khz);
+}
+static inline int osmo_trxc_client_txtune(struct osmo_trxc_client *client,
+ unsigned int freq_khz,
+ osmo_trxc_client_rsp_cb *cb, void *cb_data)
+{
+ return osmo_trxc_client_send_cmd(client, OSMO_TRXC_F_CRITICAL, cb, cb_data,
+ OSMO_TRXC_CMD_TXTUNE, "%u", freq_khz);
+}
diff --git a/libosmo-trx/src/Makefile.am b/libosmo-trx/src/Makefile.am
index 63eab2f..95f1fc3 100644
--- a/libosmo-trx/src/Makefile.am
+++ b/libosmo-trx/src/Makefile.am
@@ -18,6 +18,7 @@

libosmotrx_la_SOURCES = \
trxc.c \
+ trxc_client.c \
trxd.c \
$(NULL)

diff --git a/libosmo-trx/src/trxc_client.c b/libosmo-trx/src/trxc_client.c
new file mode 100644
index 0000000..39b2a43
--- /dev/null
+++ b/libosmo-trx/src/trxc_client.c
@@ -0,0 +1,490 @@
+/*! \file src/trxc_client.c
+ * TRXC client command queue engine: queue, retransmission, RSP matching.
+ * Based on the TRXC command queue logic in osmo-bts-trx (trx_if.c). */
+
+/*
+ * (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 <stdbool.h>
+
+#include <osmocom/core/talloc.h>
+#include <osmocom/core/timer.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/utils.h>
+
+#include <osmocom/trx/trxc.h>
+#include <osmocom/trx/trxc_client.h>
+
+/*! Default retransmit timeout (in seconds) */
+#define TRXC_CLIENT_RETRANS_SEC 2
+
+/*! A single command in the queue */
+struct trxc_cmd_entry {
+ struct llist_head list;
+ struct osmo_trxc_msg msg; /* type == OSMO_TRXC_MT_CMD */
+ uint32_t flags; /* OSMO_TRXC_F_* */
+ osmo_trxc_client_rsp_cb *rsp_cb;
+ void *cb_data;
+};
+
+struct osmo_trxc_client {
+ const struct osmo_trxc_client_ops *ops;
+ char *name; /* log prefix */
+ int log_cat; /* logging category (default DLGLOBAL) */
+ unsigned int retrans_sec; /* retransmit timeout */
+
+ struct llist_head cmd_queue; /* list of struct trxc_cmd_entry */
+ struct trxc_cmd_entry *last_acked;
+ struct osmo_timer_list retrans_timer;
+
+ /* guards for osmo_trxc_client_flush() from within a rsp_cb */
+ bool in_rx;
+ bool flushed_in_rx;
+
+ /* TRXD PDU version negotiation state */
+ uint8_t setformat_ver_req;
+ osmo_trxc_setformat_cb *setformat_cb;
+ void *setformat_cb_data;
+};
+
+#define LOGCL(client, level, fmt, args...) \
+ LOGP((client)->log_cat, level, "%s: " fmt, (client)->name, ## args)
+
+#define CMD_NAME_FMT "CMD %s%s%s"
+#define CMD_NAME_ARGS(e) \
+ (e)->msg.cmd, (e)->msg.params[0] != '\0' ? " " : "", (e)->msg.params
+
+/* Transmit the first command in the queue (if any), (re)start the timer */
+static void trxc_client_send_next(struct osmo_trxc_client *client)
+{
+ char buf[OSMO_TRXC_MSG_BUF_SIZE];
+ struct trxc_cmd_entry *e;
+ int rc;
+
+ if (llist_empty(&client->cmd_queue))
+ return;
+ e = llist_first_entry(&client->cmd_queue, struct trxc_cmd_entry, list);
+
+ rc = osmo_trxc_msg_build(buf, sizeof(buf), &e->msg);
+ OSMO_ASSERT(rc > 0); /* validated in osmo_trxc_client_send_cmd() */
+
+ LOGCL(client, LOGL_DEBUG, "Tx '%s'\n", buf);
+ rc = client->ops->tx_msg(client, buf, rc);
+ if (rc < 0)
+ LOGCL(client, LOGL_ERROR, "tx_msg() failed with rc=%d\n", rc);
+
+ osmo_timer_schedule(&client->retrans_timer, client->retrans_sec, 0);
+}
+
+static void trxc_client_retrans_timer_cb(void *data)
+{
+ struct osmo_trxc_client *client = data;
+ struct trxc_cmd_entry *e;
+
+ OSMO_ASSERT(!llist_empty(&client->cmd_queue));
+ e = llist_first_entry(&client->cmd_queue, struct trxc_cmd_entry, list);
+
+ LOGCL(client, LOGL_NOTICE, "No response from transceiver for '" CMD_NAME_FMT "'\n",
+ CMD_NAME_ARGS(e));
+
+ trxc_client_send_next(client);
+}
+
+/*! Allocate a TRXC client instance.
+ * \param[in] ctx talloc context to allocate from
+ * \param[in] ops call-backs (with ops->tx_msg being mandatory) and
+ * application-private data; must remain valid throughout
+ * the lifetime of the instance
+ * \returns pointer to the allocated instance; NULL on error */
+struct osmo_trxc_client *osmo_trxc_client_alloc(void *ctx,
+ const struct osmo_trxc_client_ops *ops)
+{
+ struct osmo_trxc_client *client;
+
+ if (ops == NULL || ops->tx_msg == NULL)
+ return NULL;
+
+ client = talloc_zero(ctx, struct osmo_trxc_client);
+ if (client == NULL)
+ return NULL;
+
+ client->ops = ops;
+ client->name = talloc_strdup(client, "trxc_client");
+ client->log_cat = DLGLOBAL;
+ client->retrans_sec = TRXC_CLIENT_RETRANS_SEC;
+
+ INIT_LLIST_HEAD(&client->cmd_queue);
+ osmo_timer_setup(&client->retrans_timer, &trxc_client_retrans_timer_cb, client);
+
+ return client;
+}
+
+/*! Free the given TRXC client instance (flushes the command queue).
+ * Must not be called from within a response call-back. */
+void osmo_trxc_client_free(struct osmo_trxc_client *client)
+{
+ if (client == NULL)
+ return;
+ OSMO_ASSERT(!client->in_rx);
+ osmo_trxc_client_flush(client);
+ talloc_free(client);
+}
+
+/*! Obtain the application-private data (ops->priv) */
+void *osmo_trxc_client_get_priv(const struct osmo_trxc_client *client)
+{
+ return client->ops->priv;
+}
+
+/*! Set the name (log prefix) of the given instance, e.g. "phy0.trx0" */
+int osmo_trxc_client_set_name(struct osmo_trxc_client *client, 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(client, &client->name, name);
+
+ return 0;
+}
+
+/*! Set the logging category (e.g. DTRX in osmo-bts; default: DLGLOBAL) */
+void osmo_trxc_client_set_log_cat(struct osmo_trxc_client *client, int log_cat)
+{
+ client->log_cat = log_cat;
+}
+
+/*! Set the retransmit timeout in seconds (default: 2) */
+void osmo_trxc_client_set_retrans(struct osmo_trxc_client *client, unsigned int sec)
+{
+ client->retrans_sec = sec;
+}
+
+/*! Enqueue a new command for transmission.
+ *
+ * The new command is added to the end of the queue; there's at most one
+ * command in flight at any time. Consecutive duplicate commands are not
+ * enqueued. Commands are retransmitted until a matching response is
+ * received (see osmo_trxc_client_rx()).
+ *
+ * \param[in] client TRXC client instance
+ * \param[in] flags OSMO_TRXC_F_*
+ * \param[in] cb call-back invoked on the response (optional); without it,
+ * a NACKed OSMO_TRXC_F_CRITICAL command is escalated to the
+ * fatal_error call-back, other responses are just logged
+ * \param[in] cb_data opaque data for the response call-back
+ * \param[in] cmd command verb, e.g. "POWERON"
+ * \param[in] fmt format string for the parameters (optional, may be NULL)
+ * \returns 0 on success; negative on error */
+int osmo_trxc_client_send_cmd(struct osmo_trxc_client *client, uint32_t flags,
+ osmo_trxc_client_rsp_cb *cb, void *cb_data,
+ const char *cmd, const char *fmt, ...)
+{
+ struct trxc_cmd_entry *e, *prev = NULL;
+ va_list ap;
+ int rc;
+
+ e = talloc_zero(client, struct trxc_cmd_entry);
+ if (e == NULL)
+ return -ENOMEM;
+
+ e->msg.type = OSMO_TRXC_MT_CMD;
+ if (osmo_strlcpy(e->msg.cmd, cmd, sizeof(e->msg.cmd)) >= sizeof(e->msg.cmd)) {
+ talloc_free(e);
+ return -EMSGSIZE;
+ }
+ if (fmt != NULL && fmt[0] != '\0') {
+ va_start(ap, fmt);
+ rc = vsnprintf(e->msg.params, sizeof(e->msg.params), fmt, ap);
+ va_end(ap);
+ if (rc < 0 || rc >= (int)sizeof(e->msg.params)) {
+ talloc_free(e);
+ return -EMSGSIZE;
+ }
+ }
+
+ e->flags = flags;
+ e->rsp_cb = cb;
+ e->cb_data = cb_data;
+
+ /* avoid enqueueing consecutive duplicates, e.g. two POWEROFF */
+ if (!llist_empty(&client->cmd_queue))
+ prev = llist_last_entry(&client->cmd_queue, struct trxc_cmd_entry, list);
+ if (prev != NULL && strcmp(prev->msg.cmd, e->msg.cmd) == 0
+ && strcmp(prev->msg.params, e->msg.params) == 0) {
+ LOGCL(client, LOGL_DEBUG, "Not enqueueing duplicate '" CMD_NAME_FMT "'\n",
+ CMD_NAME_ARGS(e));
+ talloc_free(e);
+ return 0;
+ }
+
+ LOGCL(client, LOGL_INFO, "Enqueueing '" CMD_NAME_FMT "'\n", CMD_NAME_ARGS(e));
+ llist_add_tail(&e->list, &client->cmd_queue);
+
+ /* transmit, unless we already have a command in flight.
+ * If we are in the rx code path, skip transmitting: it's done
+ * when returning from the response handling. */
+ if (prev == NULL && !client->in_rx)
+ trxc_client_send_next(client);
+
+ return 0;
+}
+
+/*! Flush (drop) all pending commands. May be called from within
+ * a response call-back. */
+void osmo_trxc_client_flush(struct osmo_trxc_client *client)
+{
+ struct trxc_cmd_entry *e, *e2;
+
+ llist_for_each_entry_safe(e, e2, &client->cmd_queue, list) {
+ llist_del(&e->list);
+ talloc_free(e);
+ }
+
+ TALLOC_FREE(client->last_acked);
+
+ /* the queue is empty now, no point in keeping the timer armed */
+ osmo_timer_del(&client->retrans_timer);
+
+ /* if we are in the rx code path, signal to the returning code path */
+ if (client->in_rx)
+ client->flushed_in_rx = true;
+}
+
+static bool cmd_matches_rsp(const struct trxc_cmd_entry *e,
+ const struct osmo_trxc_msg *rsp)
+{
+ if (strcmp(e->msg.cmd, rsp->cmd) != 0)
+ return false;
+ /* Some commands (e.g. SETSLOT) may be pending for different params,
+ * so the response shall additionally be matched by the params. */
+ if ((e->flags & OSMO_TRXC_F_MATCH_PARAMS) && strcmp(e->msg.params, rsp->params) != 0)
+ return false;
+ return true;
+}
+
+/* Default response handling, when no rsp_cb was given */
+static int trxc_client_default_rsp_cb(struct osmo_trxc_client *client,
+ const struct trxc_cmd_entry *e,
+ const struct osmo_trxc_msg *rsp)
+{
+ if (rsp->status == 0)
+ return 0;
+
+ LOGCL(client, (e->flags & OSMO_TRXC_F_CRITICAL) ? LOGL_FATAL : LOGL_NOTICE,
+ "Transceiver rejected '" CMD_NAME_FMT "' with response '%s'\n",
+ CMD_NAME_ARGS(e), osmo_trxc_msg_name(rsp));
+
+ if (e->flags & OSMO_TRXC_F_CRITICAL)
+ return -EINVAL;
+ return 0;
+}
+
+static int trxc_client_fatal(struct osmo_trxc_client *client,
+ const struct osmo_trxc_msg *rsp)
+{
+ if (client->ops->fatal_error != NULL) {
+ client->ops->fatal_error(client, rsp);
+ } else {
+ LOGCL(client, LOGL_FATAL, "A critical command failed ('%s'), "
+ "and no fatal_error call-back is given\n",
+ rsp ? osmo_trxc_msg_name(rsp) : "timeout");
+ }
+
+ /* keep the command queue frozen, so the processing is stopped */
+ return -EINVAL;
+}
+
+/*! Feed a datagram received on the ctrl socket into the engine.
+ *
+ * To be called by the application for every datagram read from the TRXC
+ * socket. The engine parses the message, filters duplicate responses
+ * caused by retransmissions, matches the response against the command
+ * in flight, invokes its response call-back and transmits the next
+ * queued command (if any).
+ *
+ * \param[in] client TRXC client instance
+ * \param[in] buf received datagram (not necessarily zero-terminated)
+ * \param[in] len length of the datagram
+ * \returns 0 on success; negative on error */
+int osmo_trxc_client_rx(struct osmo_trxc_client *client, const char *buf, size_t len)
+{
+ struct osmo_trxc_msg rsp;
+ struct trxc_cmd_entry *e;
+ bool flushed;
+ int rc;
+
+ rc = osmo_trxc_msg_parse(&rsp, buf, len);
+ if (rc < 0) {
+ LOGCL(client, LOGL_NOTICE, "Rx malformed TRXC message (rc=%d)\n", rc);
+ return rc;
+ }
+ if (rsp.type != OSMO_TRXC_MT_RSP) {
+ LOGCL(client, LOGL_NOTICE, "Rx unexpected TRXC message '%s'\n",
+ osmo_trxc_msg_name(&rsp));
+ return -EINVAL;
+ }
+
+ LOGCL(client, LOGL_INFO, "Rx '%s'\n", osmo_trxc_msg_name(&rsp));
+
+ /* abort the retransmit timer */
+ osmo_timer_del(&client->retrans_timer);
+
+ if (llist_empty(&client->cmd_queue)) {
+ /* a response from a retransmission, skip it */
+ if (client->last_acked != NULL && cmd_matches_rsp(client->last_acked, &rsp)) {
+ LOGCL(client, LOGL_NOTICE, "Discarding duplicate response '%s'\n",
+ osmo_trxc_msg_name(&rsp));
+ return 0;
+ }
+ LOGCL(client, LOGL_NOTICE, "Rx response without a pending command\n");
+ return -ENOENT;
+ }
+
+ e = llist_first_entry(&client->cmd_queue, struct trxc_cmd_entry, list);
+
+ if (!cmd_matches_rsp(e, &rsp)) {
+ /* a response from a retransmission, skip it */
+ if (client->last_acked != NULL && cmd_matches_rsp(client->last_acked, &rsp)) {
+ LOGCL(client, LOGL_NOTICE, "Discarding duplicate response '%s'\n",
+ osmo_trxc_msg_name(&rsp));
+ /* the command in flight still awaits its response */
+ osmo_timer_schedule(&client->retrans_timer, client->retrans_sec, 0);
+ return 0;
+ }
+
+ LOGCL(client, (e->flags & OSMO_TRXC_F_CRITICAL) ? LOGL_FATAL : LOGL_NOTICE,
+ "Response '%s' does not match pending '" CMD_NAME_FMT "'\n",
+ osmo_trxc_msg_name(&rsp), CMD_NAME_ARGS(e));
+
+ if (e->flags & OSMO_TRXC_F_CRITICAL)
+ return trxc_client_fatal(client, &rsp);
+
+ /* We may get 'RSP ERR 1' for non-critical commands not
+ * supported by the transceiver. Deliver such responses to
+ * the call-back of the command in flight, so that it can
+ * implement a fallback (see the SETFORMAT negotiation). */
+ }
+
+ client->in_rx = true;
+ if (e->rsp_cb != NULL)
+ rc = e->rsp_cb(client, &rsp, e->cb_data);
+ else
+ rc = trxc_client_default_rsp_cb(client, e, &rsp);
+ flushed = client->flushed_in_rx;
+ client->flushed_in_rx = false;
+ client->in_rx = false;
+
+ if (rc < 0)
+ return trxc_client_fatal(client, &rsp);
+
+ /* the call-back requested a re-transmission in rc seconds */
+ if (rc > 0) {
+ /* the queue may have been flushed by the call-back */
+ if (!flushed && !llist_empty(&client->cmd_queue))
+ osmo_timer_schedule(&client->retrans_timer, rc, 0);
+ return 0;
+ }
+
+ if (!flushed) {
+ /* dequeue the command, keep it for duplicate-RSP filtering */
+ llist_del(&e->list);
+ talloc_free(client->last_acked);
+ client->last_acked = e;
+ } /* else: e was freed by osmo_trxc_client_flush(), do not access it */
+
+ /* transmit the next command waiting in the queue */
+ trxc_client_send_next(client);
+
+ return 0;
+}
+
+/***********************************************************************
+ * TRXD PDU version negotiation (SETFORMAT)
+ ***********************************************************************/
+
+static int setformat_rsp_cb(struct osmo_trxc_client *client,
+ const struct osmo_trxc_msg *rsp, void *cb_data)
+{
+ /* Old transceivers reject 'SETFORMAT' with 'RSP ERR 1' */
+ if (strcmp(rsp->cmd, OSMO_TRXC_CMD_SETFORMAT) != 0) {
+ LOGCL(client, LOGL_NOTICE, "Transceiver rejected the format "
+ "negotiation command, using TRXD PDU version 0\n");
+ if (client->setformat_cb != NULL)
+ client->setformat_cb(client, 0, client->setformat_cb_data);
+ return 0;
+ }
+
+ /* Status shall indicate a proper version supported by the transceiver */
+ if (rsp->status < 0 || rsp->status > client->setformat_ver_req) {
+ LOGCL(client, LOGL_ERROR, "Transceiver indicated an out of range "
+ "TRXD PDU version %d (requested %u)\n",
+ rsp->status, client->setformat_ver_req);
+ return -EINVAL;
+ }
+
+ LOGCL(client, LOGL_INFO, "Using TRXD PDU version %d\n", rsp->status);
+ if (client->setformat_cb != NULL)
+ client->setformat_cb(client, rsp->status, client->setformat_cb_data);
+
+ return 0;
+}
+
+/*! Negotiate the TRXD PDU version with the transceiver (SETFORMAT).
+ *
+ * If the transceiver does not support the format negotiation at all,
+ * it rejects the command with 'RSP ERR 1' and version 0 is assumed.
+ * If the requested version is not supported by the transceiver, the
+ * status code of the response indicates a preferred lower version.
+ *
+ * \param[in] client TRXC client instance
+ * \param[in] ver_max the maximum (desired) TRXD PDU version
+ * \param[in] cb call-back invoked with the negotiated version
+ * \param[in] cb_data opaque data for the call-back
+ * \returns 0 on success; negative on error */
+int osmo_trxc_client_negotiate_format(struct osmo_trxc_client *client,
+ uint8_t ver_max,
+ osmo_trxc_setformat_cb *cb, void *cb_data)
+{
+ client->setformat_ver_req = ver_max;
+ client->setformat_cb = cb;
+ client->setformat_cb_data = cb_data;
+
+ LOGCL(client, LOGL_INFO, "Requesting TRXD PDU version %u\n", ver_max);
+
+ return osmo_trxc_client_send_cmd(client, OSMO_TRXC_F_MATCH_PARAMS,
+ &setformat_rsp_cb, NULL,
+ OSMO_TRXC_CMD_SETFORMAT, "%u", ver_max);
+}
diff --git a/tests/libosmo-trx/Makefile.am b/tests/libosmo-trx/Makefile.am
index 7ed034b..aeaab33 100644
--- a/tests/libosmo-trx/Makefile.am
+++ b/tests/libosmo-trx/Makefile.am
@@ -21,14 +21,19 @@

EXTRA_DIST = \
trxc_test.ok \
+ trxc_client_test.ok \
+ trxc_client_test.err \
trxd_test.ok \
$(NULL)

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

trxc_test_SOURCES = trxc_test.c

+trxc_client_test_SOURCES = trxc_client_test.c
+
trxd_test_SOURCES = trxd_test.c
diff --git a/tests/libosmo-trx/trxc_client_test.c b/tests/libosmo-trx/trxc_client_test.c
new file mode 100644
index 0000000..e042e10
--- /dev/null
+++ b/tests/libosmo-trx/trxc_client_test.c
@@ -0,0 +1,319 @@
+/*! \file tests/trxc_client_test.c
+ * Regression test for the TRXC client command queue engine. */
+
+/*
+ * (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/application.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/core/talloc.h>
+#include <osmocom/core/timer.h>
+#include <osmocom/core/timer_compat.h>
+#include <osmocom/core/utils.h>
+
+#include <osmocom/trx/trxc.h>
+#include <osmocom/trx/trxc_client.h>
+
+static void *test_ctx = NULL;
+
+/* feed a response into the engine, printing what happens */
+static void rx_rsp(struct osmo_trxc_client *client, const char *rsp)
+{
+ int rc;
+
+ printf("rx_rsp: '%s'\n", rsp);
+ rc = osmo_trxc_client_rx(client, rsp, strlen(rsp));
+ if (rc != 0)
+ printf("\trc=%d\n", rc);
+}
+
+/* advance the (overridden) time and fire expired timers */
+static void fake_time_passes(time_t sec)
+{
+ printf("(time passes: %ld s)\n", (long)sec);
+ osmo_gettimeofday_override_add(sec, 0);
+ osmo_timers_prepare();
+ osmo_timers_update();
+}
+
+static int tx_msg_cb(struct osmo_trxc_client *client, const char *buf, size_t len)
+{
+ printf("tx_msg: '%s'\n", buf);
+ return 0;
+}
+
+static void fatal_error_cb(struct osmo_trxc_client *client,
+ const struct osmo_trxc_msg *rsp)
+{
+ printf("fatal_error: '%s'\n", rsp ? osmo_trxc_msg_name(rsp) : "(null)");
+}
+
+static const struct osmo_trxc_client_ops test_ops = {
+ .tx_msg = &tx_msg_cb,
+ .fatal_error = &fatal_error_cb,
+ .priv = "test-priv",
+};
+
+static struct osmo_trxc_client *client_alloc(void)
+{
+ struct osmo_trxc_client *client;
+
+ client = osmo_trxc_client_alloc(test_ctx, &test_ops);
+ OSMO_ASSERT(client != NULL);
+ OSMO_ASSERT(strcmp(osmo_trxc_client_get_priv(client), "test-priv") == 0);
+ osmo_trxc_client_set_name(client, "phy%u.trx%u", 0, 0);
+
+ return client;
+}
+
+static int rsp_cb(struct osmo_trxc_client *client,
+ const struct osmo_trxc_msg *rsp, void *cb_data)
+{
+ printf("rsp_cb(%s): '%s'\n", (const char *)cb_data, osmo_trxc_msg_name(rsp));
+ return 0;
+}
+
+static void test_basic(void)
+{
+ struct osmo_trxc_client *client = client_alloc();
+
+ printf("=== %s ===\n", __func__);
+
+ /* a command is transmitted immediately when the queue is empty */
+ osmo_trxc_client_poweron(client, &rsp_cb, "poweron");
+ rx_rsp(client, "RSP POWERON 0");
+
+ osmo_trxc_client_free(client);
+}
+
+static void test_queueing(void)
+{
+ struct osmo_trxc_client *client = client_alloc();
+
+ printf("=== %s ===\n", __func__);
+
+ /* only the first command is transmitted... */
+ osmo_trxc_client_rxtune(client, 890000, &rsp_cb, "rxtune");
+ osmo_trxc_client_txtune(client, 935000, &rsp_cb, "txtune");
+ osmo_trxc_client_send_cmd(client, 0, &rsp_cb, "setslot",
+ OSMO_TRXC_CMD_SETSLOT, "%u %u", 0, 1);
+ /* ... consecutive duplicates are not enqueued at all */
+ osmo_trxc_client_send_cmd(client, 0, &rsp_cb, "setslot",
+ OSMO_TRXC_CMD_SETSLOT, "%u %u", 0, 1);
+
+ /* each response triggers transmission of the next command */
+ rx_rsp(client, "RSP RXTUNE 0 890000");
+ rx_rsp(client, "RSP TXTUNE 0 935000");
+ rx_rsp(client, "RSP SETSLOT 0 0 1");
+
+ osmo_trxc_client_free(client);
+}
+
+static void test_dup_rsp(void)
+{
+ struct osmo_trxc_client *client = client_alloc();
+
+ printf("=== %s ===\n", __func__);
+
+ osmo_trxc_client_poweron(client, &rsp_cb, "poweron");
+ rx_rsp(client, "RSP POWERON 0");
+ /* a duplicate response (e.g. caused by retransmission) is discarded */
+ rx_rsp(client, "RSP POWERON 0");
+ /* an unexpected response is reported */
+ rx_rsp(client, "RSP POWEROFF 0");
+
+ osmo_trxc_client_free(client);
+}
+
+static void test_retrans(void)
+{
+ struct osmo_trxc_client *client = client_alloc();
+
+ printf("=== %s ===\n", __func__);
+
+ osmo_trxc_client_poweron(client, &rsp_cb, "poweron");
+ /* no response: the command is retransmitted (default: every 2 s) */
+ fake_time_passes(2);
+ fake_time_passes(2);
+ rx_rsp(client, "RSP POWERON 0");
+ /* no pending commands anymore, the timer shall be inactive */
+ fake_time_passes(10);
+
+ osmo_trxc_client_free(client);
+}
+
+static int rsp_retry_cb(struct osmo_trxc_client *client,
+ const struct osmo_trxc_msg *rsp, void *cb_data)
+{
+ printf("rsp_retry_cb: '%s'\n", osmo_trxc_msg_name(rsp));
+
+ /* POWERON failed: re-send it after 5 seconds */
+ if (rsp->status != 0)
+ return 5;
+ return 0;
+}
+
+static void test_rsp_cb_retry(void)
+{
+ struct osmo_trxc_client *client = client_alloc();
+
+ printf("=== %s ===\n", __func__);
+
+ osmo_trxc_client_poweron(client, &rsp_retry_cb, NULL);
+ /* transceiver is not ready yet, the call-back requests a retry */
+ rx_rsp(client, "RSP POWERON 1");
+ fake_time_passes(5);
+ rx_rsp(client, "RSP POWERON 0");
+
+ osmo_trxc_client_free(client);
+}
+
+static void test_fatal_error(void)
+{
+ struct osmo_trxc_client *client = client_alloc();
+
+ printf("=== %s ===\n", __func__);
+
+ /* no rsp_cb given: a NACKed critical command is escalated */
+ osmo_trxc_client_rxtune(client, 890000, NULL, NULL);
+ rx_rsp(client, "RSP RXTUNE 1 890000");
+
+ osmo_trxc_client_free(client);
+}
+
+static void setformat_cb(struct osmo_trxc_client *client,
+ uint8_t ver_use, void *cb_data)
+{
+ printf("setformat_cb: ver_use=%u\n", ver_use);
+}
+
+static void test_negotiate_format(void)
+{
+ struct osmo_trxc_client *client = client_alloc();
+
+ printf("=== %s ===\n", __func__);
+
+ /* case a) the transceiver confirms the requested version */
+ osmo_trxc_client_negotiate_format(client, 2, &setformat_cb, NULL);
+ rx_rsp(client, "RSP SETFORMAT 2 2");
+
+ /* case b) the transceiver indicates a lower version */
+ osmo_trxc_client_negotiate_format(client, 2, &setformat_cb, NULL);
+ rx_rsp(client, "RSP SETFORMAT 1 2");
+
+ /* case c) an old transceiver rejects the command ('RSP ERR 1') */
+ osmo_trxc_client_negotiate_format(client, 2, &setformat_cb, NULL);
+ rx_rsp(client, "RSP ERR 1");
+
+ /* case d) the transceiver indicates an out of range version */
+ osmo_trxc_client_negotiate_format(client, 2, &setformat_cb, NULL);
+ rx_rsp(client, "RSP SETFORMAT 5 2");
+
+ osmo_trxc_client_free(client);
+}
+
+static void test_flush(void)
+{
+ struct osmo_trxc_client *client = client_alloc();
+
+ printf("=== %s ===\n", __func__);
+
+ osmo_trxc_client_rxtune(client, 890000, &rsp_cb, "rxtune");
+ osmo_trxc_client_txtune(client, 935000, &rsp_cb, "txtune");
+ osmo_trxc_client_flush(client);
+
+ /* a late response finds no pending command */
+ rx_rsp(client, "RSP RXTUNE 0 890000");
+ /* the retransmit timer shall be inactive */
+ fake_time_passes(10);
+
+ osmo_trxc_client_free(client);
+}
+
+static int rsp_flush_cb(struct osmo_trxc_client *client,
+ const struct osmo_trxc_msg *rsp, void *cb_data)
+{
+ printf("rsp_flush_cb: '%s', flushing the queue\n", osmo_trxc_msg_name(rsp));
+ osmo_trxc_client_flush(client);
+ return 0;
+}
+
+static void test_flush_in_rsp_cb(void)
+{
+ struct osmo_trxc_client *client = client_alloc();
+
+ printf("=== %s ===\n", __func__);
+
+ /* flushing the queue from within a response call-back */
+ osmo_trxc_client_poweroff(client, &rsp_flush_cb, NULL);
+ osmo_trxc_client_rxtune(client, 890000, &rsp_cb, "rxtune");
+ rx_rsp(client, "RSP POWEROFF 0");
+ fake_time_passes(10);
+
+ osmo_trxc_client_free(client);
+}
+
+static void test_malformed(void)
+{
+ struct osmo_trxc_client *client = client_alloc();
+
+ printf("=== %s ===\n", __func__);
+
+ rx_rsp(client, "MALFORMED MESSAGE");
+ rx_rsp(client, "IND CLOCK 1234"); /* not a RSP */
+
+ osmo_trxc_client_free(client);
+}
+
+int main(int argc, char **argv)
+{
+ test_ctx = talloc_named_const(NULL, 0, "trxc_client_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_DEBUG);
+
+ /* take control over the clock */
+ osmo_gettimeofday_override = true;
+ osmo_gettimeofday_override_time = (struct timeval){ 1000000, 0 };
+
+ test_basic();
+ test_queueing();
+ test_dup_rsp();
+ test_retrans();
+ test_rsp_cb_retry();
+ test_fatal_error();
+ test_negotiate_format();
+ test_flush();
+ test_flush_in_rsp_cb();
+ test_malformed();
+
+ printf("Done\n");
+ return 0;
+}
diff --git a/tests/libosmo-trx/trxc_client_test.err b/tests/libosmo-trx/trxc_client_test.err
new file mode 100644
index 0000000..e280bf1
--- /dev/null
+++ b/tests/libosmo-trx/trxc_client_test.err
@@ -0,0 +1,69 @@
+DLGLOBAL INFO phy0.trx0: Enqueueing 'CMD POWERON'
+DLGLOBAL DEBUG phy0.trx0: Tx 'CMD POWERON'
+DLGLOBAL INFO phy0.trx0: Rx 'RSP POWERON 0'
+DLGLOBAL INFO phy0.trx0: Enqueueing 'CMD RXTUNE 890000'
+DLGLOBAL DEBUG phy0.trx0: Tx 'CMD RXTUNE 890000'
+DLGLOBAL INFO phy0.trx0: Enqueueing 'CMD TXTUNE 935000'
+DLGLOBAL INFO phy0.trx0: Enqueueing 'CMD SETSLOT 0 1'
+DLGLOBAL DEBUG phy0.trx0: Not enqueueing duplicate 'CMD SETSLOT 0 1'
+DLGLOBAL INFO phy0.trx0: Rx 'RSP RXTUNE 0 890000'
+DLGLOBAL DEBUG phy0.trx0: Tx 'CMD TXTUNE 935000'
+DLGLOBAL INFO phy0.trx0: Rx 'RSP TXTUNE 0 935000'
+DLGLOBAL DEBUG phy0.trx0: Tx 'CMD SETSLOT 0 1'
+DLGLOBAL INFO phy0.trx0: Rx 'RSP SETSLOT 0 0 1'
+DLGLOBAL INFO phy0.trx0: Enqueueing 'CMD POWERON'
+DLGLOBAL DEBUG phy0.trx0: Tx 'CMD POWERON'
+DLGLOBAL INFO phy0.trx0: Rx 'RSP POWERON 0'
+DLGLOBAL INFO phy0.trx0: Rx 'RSP POWERON 0'
+DLGLOBAL NOTICE phy0.trx0: Discarding duplicate response 'RSP POWERON 0'
+DLGLOBAL INFO phy0.trx0: Rx 'RSP POWEROFF 0'
+DLGLOBAL NOTICE phy0.trx0: Rx response without a pending command
+DLGLOBAL INFO phy0.trx0: Enqueueing 'CMD POWERON'
+DLGLOBAL DEBUG phy0.trx0: Tx 'CMD POWERON'
+DLGLOBAL NOTICE phy0.trx0: No response from transceiver for 'CMD POWERON'
+DLGLOBAL DEBUG phy0.trx0: Tx 'CMD POWERON'
+DLGLOBAL NOTICE phy0.trx0: No response from transceiver for 'CMD POWERON'
+DLGLOBAL DEBUG phy0.trx0: Tx 'CMD POWERON'
+DLGLOBAL INFO phy0.trx0: Rx 'RSP POWERON 0'
+DLGLOBAL INFO phy0.trx0: Enqueueing 'CMD POWERON'
+DLGLOBAL DEBUG phy0.trx0: Tx 'CMD POWERON'
+DLGLOBAL INFO phy0.trx0: Rx 'RSP POWERON 1'
+DLGLOBAL NOTICE phy0.trx0: No response from transceiver for 'CMD POWERON'
+DLGLOBAL DEBUG phy0.trx0: Tx 'CMD POWERON'
+DLGLOBAL INFO phy0.trx0: Rx 'RSP POWERON 0'
+DLGLOBAL INFO phy0.trx0: Enqueueing 'CMD RXTUNE 890000'
+DLGLOBAL DEBUG phy0.trx0: Tx 'CMD RXTUNE 890000'
+DLGLOBAL INFO phy0.trx0: Rx 'RSP RXTUNE 1 890000'
+DLGLOBAL FATAL phy0.trx0: Transceiver rejected 'CMD RXTUNE 890000' with response 'RSP RXTUNE 1 890000'
+DLGLOBAL INFO phy0.trx0: Requesting TRXD PDU version 2
+DLGLOBAL INFO phy0.trx0: Enqueueing 'CMD SETFORMAT 2'
+DLGLOBAL DEBUG phy0.trx0: Tx 'CMD SETFORMAT 2'
+DLGLOBAL INFO phy0.trx0: Rx 'RSP SETFORMAT 2 2'
+DLGLOBAL INFO phy0.trx0: Using TRXD PDU version 2
+DLGLOBAL INFO phy0.trx0: Requesting TRXD PDU version 2
+DLGLOBAL INFO phy0.trx0: Enqueueing 'CMD SETFORMAT 2'
+DLGLOBAL DEBUG phy0.trx0: Tx 'CMD SETFORMAT 2'
+DLGLOBAL INFO phy0.trx0: Rx 'RSP SETFORMAT 1 2'
+DLGLOBAL INFO phy0.trx0: Using TRXD PDU version 1
+DLGLOBAL INFO phy0.trx0: Requesting TRXD PDU version 2
+DLGLOBAL INFO phy0.trx0: Enqueueing 'CMD SETFORMAT 2'
+DLGLOBAL DEBUG phy0.trx0: Tx 'CMD SETFORMAT 2'
+DLGLOBAL INFO phy0.trx0: Rx 'RSP ERR 1'
+DLGLOBAL NOTICE phy0.trx0: Response 'RSP ERR 1' does not match pending 'CMD SETFORMAT 2'
+DLGLOBAL NOTICE phy0.trx0: Transceiver rejected the format negotiation command, using TRXD PDU version 0
+DLGLOBAL INFO phy0.trx0: Requesting TRXD PDU version 2
+DLGLOBAL INFO phy0.trx0: Enqueueing 'CMD SETFORMAT 2'
+DLGLOBAL DEBUG phy0.trx0: Tx 'CMD SETFORMAT 2'
+DLGLOBAL INFO phy0.trx0: Rx 'RSP SETFORMAT 5 2'
+DLGLOBAL ERROR phy0.trx0: Transceiver indicated an out of range TRXD PDU version 5 (requested 2)
+DLGLOBAL INFO phy0.trx0: Enqueueing 'CMD RXTUNE 890000'
+DLGLOBAL DEBUG phy0.trx0: Tx 'CMD RXTUNE 890000'
+DLGLOBAL INFO phy0.trx0: Enqueueing 'CMD TXTUNE 935000'
+DLGLOBAL INFO phy0.trx0: Rx 'RSP RXTUNE 0 890000'
+DLGLOBAL NOTICE phy0.trx0: Rx response without a pending command
+DLGLOBAL INFO phy0.trx0: Enqueueing 'CMD POWEROFF'
+DLGLOBAL DEBUG phy0.trx0: Tx 'CMD POWEROFF'
+DLGLOBAL INFO phy0.trx0: Enqueueing 'CMD RXTUNE 890000'
+DLGLOBAL INFO phy0.trx0: Rx 'RSP POWEROFF 0'
+DLGLOBAL NOTICE phy0.trx0: Rx malformed TRXC message (rc=-22)
+DLGLOBAL NOTICE phy0.trx0: Rx unexpected TRXC message 'IND CLOCK 1234'
diff --git a/tests/libosmo-trx/trxc_client_test.ok b/tests/libosmo-trx/trxc_client_test.ok
new file mode 100644
index 0000000..5e908ce
--- /dev/null
+++ b/tests/libosmo-trx/trxc_client_test.ok
@@ -0,0 +1,73 @@
+=== test_basic ===
+tx_msg: 'CMD POWERON'
+rx_rsp: 'RSP POWERON 0'
+rsp_cb(poweron): 'RSP POWERON 0'
+=== test_queueing ===
+tx_msg: 'CMD RXTUNE 890000'
+rx_rsp: 'RSP RXTUNE 0 890000'
+rsp_cb(rxtune): 'RSP RXTUNE 0 890000'
+tx_msg: 'CMD TXTUNE 935000'
+rx_rsp: 'RSP TXTUNE 0 935000'
+rsp_cb(txtune): 'RSP TXTUNE 0 935000'
+tx_msg: 'CMD SETSLOT 0 1'
+rx_rsp: 'RSP SETSLOT 0 0 1'
+rsp_cb(setslot): 'RSP SETSLOT 0 0 1'
+=== test_dup_rsp ===
+tx_msg: 'CMD POWERON'
+rx_rsp: 'RSP POWERON 0'
+rsp_cb(poweron): 'RSP POWERON 0'
+rx_rsp: 'RSP POWERON 0'
+rx_rsp: 'RSP POWEROFF 0'
+ rc=-2
+=== test_retrans ===
+tx_msg: 'CMD POWERON'
+(time passes: 2 s)
+tx_msg: 'CMD POWERON'
+(time passes: 2 s)
+tx_msg: 'CMD POWERON'
+rx_rsp: 'RSP POWERON 0'
+rsp_cb(poweron): 'RSP POWERON 0'
+(time passes: 10 s)
+=== test_rsp_cb_retry ===
+tx_msg: 'CMD POWERON'
+rx_rsp: 'RSP POWERON 1'
+rsp_retry_cb: 'RSP POWERON 1'
+(time passes: 5 s)
+tx_msg: 'CMD POWERON'
+rx_rsp: 'RSP POWERON 0'
+rsp_retry_cb: 'RSP POWERON 0'
+=== test_fatal_error ===
+tx_msg: 'CMD RXTUNE 890000'
+rx_rsp: 'RSP RXTUNE 1 890000'
+fatal_error: 'RSP RXTUNE 1 890000'
+ rc=-22
+=== test_negotiate_format ===
+tx_msg: 'CMD SETFORMAT 2'
+rx_rsp: 'RSP SETFORMAT 2 2'
+setformat_cb: ver_use=2
+tx_msg: 'CMD SETFORMAT 2'
+rx_rsp: 'RSP SETFORMAT 1 2'
+setformat_cb: ver_use=1
+tx_msg: 'CMD SETFORMAT 2'
+rx_rsp: 'RSP ERR 1'
+setformat_cb: ver_use=0
+tx_msg: 'CMD SETFORMAT 2'
+rx_rsp: 'RSP SETFORMAT 5 2'
+fatal_error: 'RSP SETFORMAT 5 2'
+ rc=-22
+=== test_flush ===
+tx_msg: 'CMD RXTUNE 890000'
+rx_rsp: 'RSP RXTUNE 0 890000'
+ rc=-2
+(time passes: 10 s)
+=== test_flush_in_rsp_cb ===
+tx_msg: 'CMD POWEROFF'
+rx_rsp: 'RSP POWEROFF 0'
+rsp_flush_cb: 'RSP POWEROFF 0', flushing the queue
+(time passes: 10 s)
+=== test_malformed ===
+rx_rsp: 'MALFORMED MESSAGE'
+ rc=-22
+rx_rsp: 'IND CLOCK 1234'
+ rc=-22
+Done
diff --git a/tests/testsuite.at b/tests/testsuite.at
index 6e72c81..090906b 100644
--- a/tests/testsuite.at
+++ b/tests/testsuite.at
@@ -56,6 +56,13 @@
AT_CHECK([$abs_top_builddir/tests/libosmo-trx/trxc_test], [], [expout], [])
AT_CLEANUP

+AT_SETUP([trxc_client_test])
+AT_KEYWORDS([trxc_client_test])
+cat $abs_srcdir/libosmo-trx/trxc_client_test.ok > expout
+cat $abs_srcdir/libosmo-trx/trxc_client_test.err > experr
+AT_CHECK([$abs_top_builddir/tests/libosmo-trx/trxc_client_test], [], [expout], [experr])
+AT_CLEANUP
+
AT_SETUP([trxd_test])
AT_KEYWORDS([trxd_test])
cat $abs_srcdir/libosmo-trx/trxd_test.ok > expout

To view, visit change 43108. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-MessageType: newchange
Gerrit-Project: osmo-trx
Gerrit-Branch: master
Gerrit-Change-Id: I817e394f74a10e3adae4a0b58342c82acdf0794e
Gerrit-Change-Number: 43108
Gerrit-PatchSet: 1
Gerrit-Owner: fixeria <vyanitskiy@sysmocom.de>