fixeria has uploaded this change for review. ( https://gerrit.osmocom.org/c/osmo-trx/+/43108?usp=email )
Change subject: libosmo-trx/client: add TRXC client (command queue) API ......................................................................
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