Timur Davydov has uploaded this change for review.

View Change

trx: add JSON stats export for WebSDR API

Introduce stats_json.c providing JSON serialization for:
- stats and counters
- rate counters
- BTS state
- TRX/transceiver state
- WebSDR runtime metrics

Add osmobts_get_stats() API to expose these via WebSDR
interface and export it in Emscripten build

This enables programmatic access to data previously available
via VTY (e.g. "show stats", "show bts")

Change-Id: Ifb82adfab879a65ecd222c45e06551983aa90a0f
---
M src/osmo-bts-trx/Makefile.am
M src/osmo-bts-trx/osmo-bts-trx-websdr.c
A src/osmo-bts-trx/stats_json.c
A src/osmo-bts-trx/stats_json.h
4 files changed, 752 insertions(+), 1 deletion(-)

git pull ssh://gerrit.osmocom.org:29418/osmo-bts refs/changes/04/42704/1
diff --git a/src/osmo-bts-trx/Makefile.am b/src/osmo-bts-trx/Makefile.am
index 83dd233..0092eee 100644
--- a/src/osmo-bts-trx/Makefile.am
+++ b/src/osmo-bts-trx/Makefile.am
@@ -103,7 +103,7 @@
-sENVIRONMENT="worker" \
-sLLD_REPORT_UNDEFINED=1 \
-sSINGLE_FILE=0 \
- -sEXPORTED_FUNCTIONS=[_osmobts_init,_osmobts_apply,_osmobts_getiqtx_short_vector,_osmobts_push_rx_short_vector,_ws_osmux_push_raw_data,_ws_ipa_push_raw_data,_on_sched_timer,_malloc,_free] \
+ -sEXPORTED_FUNCTIONS=[_osmobts_init,_osmobts_apply,_osmobts_get_stats,_osmobts_getiqtx_short_vector,_osmobts_push_rx_short_vector,_ws_osmux_push_raw_data,_ws_ipa_push_raw_data,_on_sched_timer,_malloc,_free] \
-sEXPORTED_RUNTIME_METHODS=[HEAPF32,HEAPU32,HEAP32,HEAPU16,HEAP16,HEAPU8,HEAP8,ccall,cwrap,stringToAscii,AsciiToString] \
-sINITIAL_MEMORY=256MB \
-sFORCE_FILESYSTEM \
@@ -123,6 +123,7 @@

osmo_bts_trx_websdr_js_SOURCES = \
osmo-bts-trx-websdr.c \
+ stats_json.c \
$(NULL)

osmo_bts_trx_websdr_js_LDADD = \
diff --git a/src/osmo-bts-trx/osmo-bts-trx-websdr.c b/src/osmo-bts-trx/osmo-bts-trx-websdr.c
index 0bf6bd8..33c4be3 100644
--- a/src/osmo-bts-trx/osmo-bts-trx-websdr.c
+++ b/src/osmo-bts-trx/osmo-bts-trx-websdr.c
@@ -128,6 +128,25 @@
return 0;
}

+int osmobts_get_stats(const char *group, char *buf, unsigned buflen)
+{
+ if (!strncmp(group, "stats", 5)) {
+ return stats_to_json(buf, buflen);
+ } else if (!strncmp(group, "rate-counters", 13)) {
+ return rate_counters_to_json(buf, buflen);
+ } else if (!strncmp(group, "bts", 3)) {
+ return bts_to_json(buf, buflen);
+ } else if (!strncmp(group, "trx", 3)) {
+ return trx_to_json(buf, buflen);
+ } else if (!strncmp(group, "transceiver", 11)) {
+ return transceiver_to_json(buf, buflen);
+ } else if (!strncmp(group, "websdr", 6)) {
+ return websdr_to_json(buf, buflen);
+ }
+
+ return -EINVAL;
+}
+
int apitrx_cmd_call(const char *command, char *response, size_t response_size)
{
int res = osmotrxlib_process_command(command, response, response_size);
diff --git a/src/osmo-bts-trx/stats_json.c b/src/osmo-bts-trx/stats_json.c
new file mode 100644
index 0000000..3452580
--- /dev/null
+++ b/src/osmo-bts-trx/stats_json.c
@@ -0,0 +1,689 @@
+/* JSON helpers for OsmoBTS: stats, rate-counters, bts and transceiver
+ * serialization API implementation
+ *
+ * Copyright (C) 2026 Timur Davydov <dtv.comp@gmail.com>
+ *
+ * All Rights Reserved
+ *
+ * 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 <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <stdbool.h>
+#include <stdarg.h>
+
+#include "stats_json.h"
+
+#include <osmo-bts/bts.h>
+#include <osmo-bts/abis.h>
+#include <osmo-bts/logging.h>
+#include <osmo-bts/scheduler.h>
+#include "l1_if.h"
+#include "trx_if.h"
+
+#include <osmocom/core/counter.h>
+#include <osmocom/core/rate_ctr.h>
+#include <osmocom/core/stat_item.h>
+
+/* g_bts is defined elsewhere */
+extern struct gsm_bts *g_bts;
+
+extern uint64_t sdr_send_ts;
+extern uint64_t g_last_ts;
+extern unsigned wptr;
+extern unsigned rptr;
+extern unsigned total_tx_late;
+extern unsigned start_fn;
+
+/* JSON stats context */
+struct stats_json_ctx {
+ char *buf;
+ size_t buflen;
+ size_t off;
+ bool first;
+ bool first_group;
+ bool first_ctr;
+ bool first_item;
+ const char *cur_group_desc;
+};
+
+/* append a single character into ctx->buf */
+static int sj_append_char(struct stats_json_ctx *ctx, char c)
+{
+ if (ctx->off + 1 >= ctx->buflen)
+ return -ENOSPC;
+
+ ctx->buf[ctx->off++] = c;
+ ctx->buf[ctx->off] = '\0';
+
+ return 0;
+}
+
+/* append a formatted string into ctx->buf */
+static int sj_append_fmt(struct stats_json_ctx *ctx, const char *fmt, ...)
+{
+ va_list ap;
+ int rc;
+ size_t rem;
+
+ if (ctx->off >= ctx->buflen)
+ return -ENOSPC;
+
+ rem = ctx->buflen - ctx->off;
+ va_start(ap, fmt);
+ rc = vsnprintf(ctx->buf + ctx->off, rem, fmt, ap);
+ va_end(ap);
+
+ if (rc < 0)
+ return -EIO;
+ if ((size_t)rc >= rem)
+ return -ENOSPC;
+
+ ctx->off += rc;
+ return 0;
+}
+
+/* simple JSON string escaper that appends into ctx->buf */
+static int sj_append_json_str(struct stats_json_ctx *ctx, const char *s)
+{
+ if (!s) s = "";
+
+ if (sj_append_char(ctx, '"'))
+ return -ENOSPC;
+ for (size_t j = 0; s[j] != '\0'; j++) {
+ char c = s[j];
+ if (c == '"' || c == '\\') {
+ int rc = sj_append_char(ctx, '\\');
+ if (rc)
+ return rc;
+ rc = sj_append_char(ctx, c);
+ if (rc)
+ return rc;
+ } else if ((unsigned char)c < 0x20) {
+ int rc = sj_append_char(ctx, ' ');
+ if (rc)
+ return rc;
+ } else {
+ int rc = sj_append_char(ctx, c);
+ if (rc)
+ return rc;
+ }
+ }
+
+ return sj_append_char(ctx, '"');
+}
+
+/* handle a single counter */
+static int sj_counters_handler(struct osmo_counter *counter, void *ctxv)
+{
+ struct stats_json_ctx *ctx = ctxv;
+ unsigned long val = counter->value;
+ int rc;
+
+ if (!ctx->first) {
+ rc = sj_append_char(ctx, ',');
+ if (rc)
+ return rc;
+ }
+ ctx->first = false;
+
+ rc = sj_append_fmt(ctx, "{\"name\":");
+ if (rc)
+ return rc;
+
+ rc = sj_append_json_str(ctx, counter->name);
+ if (rc)
+ return rc;
+
+ return sj_append_fmt(ctx, ",\"value\":%lu}", val);
+}
+
+/* Rate counters: produce entries with name + current + per_s
+ * per second doesn't work at the moment because osmo_fd_timer is not implemented
+ */
+static int sj_rate_ctr_counter_handler(struct rate_ctr_group *g, struct rate_ctr *ctr,
+ const struct rate_ctr_desc *desc, void *ctxv)
+{
+ struct stats_json_ctx *ctx = ctxv;
+ int rc;
+
+ (void)g;
+
+ if (!ctx->first_ctr) {
+ rc = sj_append_char(ctx, ',');
+ if (rc)
+ return rc;
+ }
+ ctx->first_ctr = false;
+
+ rc = sj_append_fmt(ctx, "{\"name\":");
+ if (rc)
+ return rc;
+
+ rc = sj_append_json_str(ctx, desc->name);
+ if (rc)
+ return rc;
+
+ return sj_append_fmt(ctx, ",\"current\":%" PRIu64 "}", ctr->current);
+#if 0
+ /* Per second rate doesn't work at the moment because osmo_fd_timer is not implemented */
+ return sj_append_fmt(ctx, ",\"current\":%" PRIu64 ",\"per_s\":%" PRIu64 "}",
+ ctr->current, ctr->intv[RATE_CTR_INTV_SEC].rate);
+#endif
+}
+
+/* Rate counter groups: produce entries with group + counters */
+static int sj_rate_ctr_group_handler(struct rate_ctr_group *ctrg, void *ctxv)
+{
+ struct stats_json_ctx *ctx = ctxv;
+ int rc;
+
+ if (!ctx->first_group) {
+ rc = sj_append_char(ctx, ',');
+ if (rc)
+ return rc;
+ }
+ ctx->first_group = false;
+
+ rc = sj_append_fmt(ctx, "{\"group_description\":");
+ if (rc)
+ return rc;
+
+ rc = sj_append_json_str(ctx, ctrg->desc->group_description);
+ if (rc)
+ return rc;
+
+ rc = sj_append_fmt(ctx, ",\"counters\":[");
+ if (rc)
+ return rc;
+
+ ctx->first_ctr = true;
+ rc = rate_ctr_for_each_counter(ctrg, sj_rate_ctr_counter_handler, ctx);
+ if (rc)
+ return rc;
+
+ return sj_append_fmt(ctx, "]}");
+}
+
+/* Flat rate-counters: produce entries with group + name + current + per_s
+ * per second doesn't work at the moment because osmo_fd_timer is not implemented
+ */
+static int sj_rate_ctr_flat_counter_handler(struct rate_ctr_group *g, struct rate_ctr *ctr,
+ const struct rate_ctr_desc *desc, void *ctxv)
+{
+ struct stats_json_ctx *ctx = ctxv;
+ int rc;
+
+ (void)g;
+
+ if (!ctx->first_ctr) {
+ rc = sj_append_char(ctx, ',');
+ if (rc)
+ return rc;
+ }
+ ctx->first_ctr = false;
+
+ rc = sj_append_fmt(ctx, "{\"group\":");
+ if (rc)
+ return rc;
+
+ rc = sj_append_json_str(ctx, ctx->cur_group_desc);
+ if (rc)
+ return rc;
+
+ rc = sj_append_fmt(ctx, ",\"name\":");
+ if (rc)
+ return rc;
+
+ rc = sj_append_json_str(ctx, desc->name);
+ if (rc)
+ return rc;
+
+ return sj_append_fmt(ctx, ",\"current\":%" PRIu64 "}", ctr->current);
+#if 0
+ /* Per second rate doesn't work at the moment because osmo_fd_timer is not implemented */
+ return sj_append_fmt(ctx, ",\"current\":%" PRIu64 ",\"per_s\":%" PRIu64 "}",
+ ctr->current, ctr->intv[RATE_CTR_INTV_SEC].rate);
+#endif
+}
+
+/* Rate counter groups: produce entries with group + counters */
+static int sj_rate_ctr_group_flat_handler(struct rate_ctr_group *ctrg, void *ctxv)
+{
+ struct stats_json_ctx *ctx = ctxv;
+ int rc;
+
+ ctx->cur_group_desc = ctrg->desc->group_description ? ctrg->desc->group_description : NULL;
+ rc = rate_ctr_for_each_counter(ctrg, sj_rate_ctr_flat_counter_handler, ctx);
+
+ ctx->cur_group_desc = NULL;
+
+ return rc;
+}
+
+/* TRX / Transceiver timeslot JSON */
+static int sj_transceiver_timeslot(struct gsm_bts_trx *trx, unsigned int tn, struct stats_json_ctx *ctx)
+{
+ const struct gsm_bts_trx_ts *ts = &trx->ts[tn];
+ const struct l1sched_ts *l1ts = ts->priv;
+ int rc;
+
+ if (!l1ts)
+ return 0;
+
+ if (!ctx->first_item) {
+ rc = sj_append_char(ctx, ',');
+ if (rc)
+ return rc;
+ }
+ ctx->first_item = false;
+
+ rc = sj_append_fmt(ctx, "{\"tn\":%u,\"mf\":", tn);
+ if (rc)
+ return rc;
+
+ rc = sj_append_json_str(ctx, (const char *)trx_sched_multiframes[l1ts->mf_index].name);
+ if (rc)
+ return rc;
+
+ return sj_append_fmt(ctx, ",\"pending_dl_prims\":%u,\"interference\":%d,\"unit\":\"dBm\"}",
+ (unsigned)llist_count(&l1ts->dl_prims),
+ l1ts->chan_state[TRXC_IDLE].meas.interf_avg);
+}
+
+/* TRX / Transceiver JSON */
+static int sj_transceiver_handler_ll(struct stats_json_ctx *ctx)
+{
+ struct gsm_bts_trx *trx;
+ bool first_trx = true;
+ int rc;
+
+ if (!g_bts)
+ return 0;
+
+ llist_for_each_entry(trx, &g_bts->trx_list, list) {
+ struct phy_instance *pinst = trx_phy_instance(trx);
+ struct phy_link *plink = pinst->phy_link;
+ struct trx_l1h *l1h = pinst->u.osmotrx.hdl;
+
+ if (!first_trx) {
+ rc = sj_append_char(ctx, ',');
+ if (rc)
+ return rc;
+ }
+ first_trx = false;
+
+ rc = sj_append_fmt(ctx, "{\"nr\":%u,\"source\":", trx->nr);
+ if (rc)
+ return rc;
+
+ const char *sname = plink->u.osmotrx.trx_clk_iofd
+ ? osmo_iofd_get_name(plink->u.osmotrx.trx_clk_iofd) : "";
+ rc = sj_append_json_str(ctx, sname);
+ if (rc)
+ return rc;
+
+ rc = sj_append_fmt(ctx, ",\"poweron\":%s,\"phy_link_state\":",
+ trx_if_powered(l1h) ? "true" : "false");
+ if (rc)
+ return rc;
+
+ rc = sj_append_json_str(ctx, phy_link_state_name(phy_link_state_get(plink)));
+ if (rc)
+ return rc;
+
+ /* arfcn/tsc/bsic */
+ if (l1h->config.arfcn_valid) {
+ rc = sj_append_fmt(ctx, ",\"arfcn\":%d", l1h->config.arfcn & ~ARFCN_PCS);
+ } else {
+ rc = sj_append_fmt(ctx, ",\"arfcn\":null");
+ }
+ if (rc)
+ return rc;
+
+ if (l1h->config.tsc_valid) {
+ rc = sj_append_fmt(ctx, ",\"tsc\":%d", l1h->config.tsc);
+ } else {
+ rc = sj_append_fmt(ctx, ",\"tsc\":null");
+ }
+ if (rc)
+ return rc;
+
+ if (l1h->config.bsic_valid) {
+ rc = sj_append_fmt(ctx, ",\"bsic\":%d", l1h->config.bsic);
+ } else {
+ rc = sj_append_fmt(ctx, ",\"bsic\":null");
+ }
+ if (rc)
+ return rc;
+
+ /* timeslots */
+ rc = sj_append_fmt(ctx, ",\"timeslots\":[");
+ if (rc)
+ return rc;
+
+ ctx->first_item = true;
+ for (unsigned int tn = 0; tn < ARRAY_SIZE(trx->ts); tn++) {
+ rc = sj_transceiver_timeslot(trx, tn, ctx);
+ if (rc)
+ return rc;
+ }
+
+ rc = sj_append_fmt(ctx, "]}");
+ if (rc)
+ return rc;
+ }
+
+ return 0;
+}
+
+/* Stat items: produce entries with name + value + unit */
+static int sj_stat_item_handler(struct osmo_stat_item_group *g, struct osmo_stat_item *item, void *ctxv)
+{
+ struct stats_json_ctx *ctx = ctxv;
+ const struct osmo_stat_item_desc *desc = osmo_stat_item_get_desc(item);
+ int32_t value = osmo_stat_item_get_last(item);
+ int rc;
+
+ (void)g;
+
+ if (!ctx->first_item) {
+ rc = sj_append_char(ctx, ',');
+ if (rc)
+ return rc;
+ }
+ ctx->first_item = false;
+
+ rc = sj_append_fmt(ctx, "{\"name\":");
+ if (rc)
+ return rc;
+
+ rc = sj_append_json_str(ctx, desc->name);
+ if (rc)
+ return rc;
+
+ rc = sj_append_fmt(ctx, ",\"value\":%d,\"unit\":", value);
+ if (rc)
+ return rc;
+
+ rc = sj_append_json_str(ctx, desc->unit && desc->unit != OSMO_STAT_ITEM_NO_UNIT ? desc->unit : "");
+ if (rc)
+ return rc;
+
+ return sj_append_char(ctx, '}');
+}
+
+/* Stat item groups: produce entries with group + items */
+static int sj_stat_item_group_handler(struct osmo_stat_item_group *statg, void *ctxv)
+{
+ struct stats_json_ctx *ctx = ctxv;
+ int rc;
+
+ if (!ctx->first_group) {
+ rc = sj_append_char(ctx, ',');
+ if (rc)
+ return rc;
+ }
+ ctx->first_group = false;
+
+ rc = sj_append_fmt(ctx, "{\"group_description\":");
+ if (rc)
+ return rc;
+
+ rc = sj_append_json_str(ctx, statg->desc->group_description);
+ if (rc)
+ return rc;
+
+ rc = sj_append_fmt(ctx, ",\"items\":[");
+ if (rc)
+ return rc;
+
+ ctx->first_item = true;
+ rc = osmo_stat_item_for_each_item(statg, sj_stat_item_handler, ctx);
+ if (rc)
+ return rc;
+
+ return sj_append_fmt(ctx, "]}");
+}
+
+/* Build JSON for 'show bts' by enumerating fields of g_bts and related structures */
+int bts_to_json(char *buf, size_t buflen)
+{
+ struct stats_json_ctx ctx = {
+ .buf = buf,
+ .buflen = buflen,
+ };
+ int rc;
+
+ if (!buf || buflen == 0)
+ return -EINVAL;
+
+ if (g_bts) {
+ rc = sj_append_fmt(&ctx,
+ "{\"bts_nr\":%u,\"variant\":\"%s\",\"band\":\"%s\",\"cell_identity\":%u,\"lac\":%u"
+ ",\"bsic\":%u,\"num_trx\":%u,\"description\":",
+ g_bts->nr,
+ btsvariant2str(g_bts->variant),
+ gsm_band_name(g_bts->band),
+ g_bts->cell_identity,
+ g_bts->location_area_code,
+ g_bts->bsic,
+ g_bts->num_trx);
+ if (rc)
+ return rc;
+
+ rc = sj_append_json_str(&ctx, g_bts->description);
+ if (rc)
+ return rc;
+
+ rc = sj_append_fmt(&ctx,
+ ",\"unit_site_id\":%u,\"unit_bts_id\":%u,\"oml_connected\":%s,\"pcu_version\":",
+ g_bts->ip_access.site_id,
+ g_bts->ip_access.bts_id,
+ g_bts->oml_link ? "true" : "false");
+ if (rc)
+ return rc;
+
+ rc = sj_append_json_str(&ctx, g_bts->pcu_version);
+ if (rc)
+ return rc;
+
+ rc = sj_append_fmt(&ctx,
+ ",\"paging_queue_max\":%u,\"paging_queue_len\":%u,"
+ "\"agch_queue_max\":%u,\"agch_queue_len\":%u,\"agch_dropped\":%llu,\"agch_merged\":%llu"
+ ",\"agch_rejected\":%llu,\"agch_agch_msgs\":%llu,\"agch_pch_msgs\":%llu,\"smscb_tgt\":%d"
+ ",\"smscb_max\":%d,\"smscb_hyst\":%d,\"smscb_basic_len\":%u,\"smscb_ext_len\":%u,"
+ "\"ph_rts_fn_avg\":%d,\"ph_rts_fn_min\":%d,\"ph_rts_fn_max\":%d"
+ ",\"radio_link_timeout_current\":%d,\"radio_link_timeout_oml\":%d,\"c0_power_red_db\":%d}",
+ paging_get_queue_max(g_bts->paging_state),
+ paging_queue_length(g_bts->paging_state),
+ g_bts->agch_queue.max_length,
+ g_bts->agch_queue.length,
+ (unsigned long long)g_bts->agch_queue.dropped_msgs,
+ (unsigned long long)g_bts->agch_queue.merged_msgs,
+ (unsigned long long)g_bts->agch_queue.rejected_msgs,
+ (unsigned long long)g_bts->agch_queue.agch_msgs,
+ (unsigned long long)g_bts->agch_queue.pch_msgs,
+ g_bts->smscb_queue_tgt_len,
+ g_bts->smscb_queue_max_len,
+ g_bts->smscb_queue_hyst,
+ g_bts->smscb_basic.queue_len,
+ g_bts->smscb_extended.queue_len,
+ bts_get_avg_fn_advance(g_bts),
+ g_bts->fn_stats.min,
+ g_bts->fn_stats.max,
+ g_bts->radio_link_timeout.current,
+ g_bts->radio_link_timeout.oml,
+ g_bts->c0_power_red_db);
+ if (rc)
+ return rc;
+ }
+
+ return (int)ctx.off;
+}
+
+/* Build JSON for 'show stats' by enumerating counters, rate_ctr groups and stat items */
+int stats_to_json(char *buf, size_t buflen)
+{
+ struct stats_json_ctx ctx;
+ int rc;
+
+ if (!buf || buflen == 0)
+ return -EINVAL;
+
+ memset(&ctx, 0, sizeof(ctx));
+ ctx.buf = buf;
+ ctx.buflen = buflen;
+ ctx.first = true;
+ ctx.first_group = true;
+ ctx.first_ctr = true;
+ ctx.first_item = true;
+
+ rc = sj_append_char(&ctx, '{');
+ if (rc)
+ return rc;
+
+ /* Ungrouped counters */
+ rc = sj_append_fmt(&ctx, "\"ungrouped_counters\":[");
+ if (rc)
+ return rc;
+
+ rc = osmo_counters_for_each(sj_counters_handler, &ctx);
+ if (rc)
+ return rc;
+
+ rc = sj_append_char(&ctx, ']');
+ if (rc)
+ return rc;
+
+ /* Rate counter groups */
+ rc = sj_append_fmt(&ctx, ",\"rate_ctr_groups\":[");
+ if (rc)
+ return rc;
+
+ ctx.first_group = true;
+ rc = rate_ctr_for_each_group(sj_rate_ctr_group_handler, &ctx);
+ if (rc)
+ return rc;
+
+ rc = sj_append_char(&ctx, ']');
+ if (rc)
+ return rc;
+
+ /* Stat item groups */
+ rc = sj_append_fmt(&ctx, ",\"stat_item_groups\":[");
+ if (rc)
+ return rc;
+
+ ctx.first_group = true;
+ rc = osmo_stat_item_for_each_group(sj_stat_item_group_handler, &ctx);
+ if (rc)
+ return rc;
+
+ rc = sj_append_char(&ctx, ']');
+ if (rc)
+ return rc;
+
+ rc = sj_append_char(&ctx, '}');
+ if (rc)
+ return rc;
+
+ return (int)ctx.off;
+}
+
+/* Build JSON for 'show rate-counters' by enumerating rate counter groups and counters */
+int rate_counters_to_json(char *buf, size_t buflen)
+{
+ struct stats_json_ctx ctx;
+ int rc;
+
+ if (!buf || buflen == 0)
+ return -EINVAL;
+
+ memset(&ctx, 0, sizeof(ctx));
+ ctx.buf = buf;
+ ctx.buflen = buflen;
+ ctx.first_ctr = true;
+
+ rc = sj_append_fmt(&ctx, "{\"rate_counters\":[");
+ if (rc)
+ return rc;
+
+ rc = rate_ctr_for_each_group(sj_rate_ctr_group_flat_handler, &ctx);
+ if (rc)
+ return rc;
+
+ rc = sj_append_fmt(&ctx, "]}");
+ if (rc)
+ return rc;
+
+ return (int)ctx.off;
+}
+
+/* Build JSON for 'show transceivers' by enumerating transceivers */
+int transceiver_to_json(char *buf, size_t buflen)
+{
+ struct stats_json_ctx ctx;
+ int rc;
+
+ if (!buf || buflen == 0)
+ return -EINVAL;
+
+ memset(&ctx, 0, sizeof(ctx));
+ ctx.buf = buf;
+ ctx.buflen = buflen;
+
+ rc = sj_append_fmt(&ctx, "{\"transceivers\":[");
+ if (rc)
+ return rc;
+
+ rc = sj_transceiver_handler_ll(&ctx);
+ if (rc)
+ return rc;
+
+ rc = sj_append_fmt(&ctx, "]}");
+ if (rc)
+ return rc;
+
+ return (int)ctx.off;
+}
+
+/* Build JSON for WebSDR runtime counters */
+int websdr_to_json(char *buf, size_t buflen)
+{
+ struct stats_json_ctx ctx;
+ int rc;
+
+ if (!buf || buflen == 0)
+ return -EINVAL;
+
+ memset(&ctx, 0, sizeof(ctx));
+ ctx.buf = buf;
+ ctx.buflen = buflen;
+
+ rc = sj_append_fmt(&ctx,
+ "{\"sdr_send_ts\":%" PRIu64 ",\"g_last_ts\":%" PRIu64
+ ",\"wptr\":%u,\"rptr\":%u,\"total_tx_late\":%u,\"start_fn\":%u}",
+ sdr_send_ts, g_last_ts, wptr, rptr, total_tx_late, start_fn);
+ if (rc)
+ return rc;
+
+ return (int)ctx.off;
+}
diff --git a/src/osmo-bts-trx/stats_json.h b/src/osmo-bts-trx/stats_json.h
new file mode 100644
index 0000000..5e0e701
--- /dev/null
+++ b/src/osmo-bts-trx/stats_json.h
@@ -0,0 +1,42 @@
+/* JSON helpers for OsmoBTS: stats, rate-counters, bts and transceiver
+ * serialization API implementation
+ *
+ * Copyright (C) 2026 Timur Davydov <dtv.comp@gmail.com>
+ *
+ * All Rights Reserved
+ *
+ * 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/>.
+ *
+ */
+
+#ifndef STATS_JSON_H
+#define STATS_JSON_H
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+int bts_to_json(char *buf, size_t buflen);
+int stats_to_json(char *buf, size_t buflen);
+int rate_counters_to_json(char *buf, size_t buflen);
+int transceiver_to_json(char *buf, size_t buflen);
+int websdr_to_json(char *buf, size_t buflen);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* STATS_JSON_H */

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

Gerrit-MessageType: newchange
Gerrit-Project: osmo-bts
Gerrit-Branch: master
Gerrit-Change-Id: Ifb82adfab879a65ecd222c45e06551983aa90a0f
Gerrit-Change-Number: 42704
Gerrit-PatchSet: 1
Gerrit-Owner: Timur Davydov <dtv.comp@gmail.com>