/* JSON helpers for OsmoBTS: stats, rate-counters, bts and transceiver * serialization API implementation * * Copyright (C) 2026 Timur Davydov * * 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 . * */ #include #include #include #include #include #include #include #include #include "stats_json.h" #include #include #include #include #include "l1_if.h" #include "trx_if.h" #include #include #include /* 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; }