From 6adc19214b41772336b89e0e9b1986b0e89789b5 Mon Sep 17 00:00:00 2001 From: Tristen Pierson Date: Tue, 2 Jun 2026 10:41:30 -0400 Subject: [PATCH] feat: add remote trace export, zbus integration, and Grafana metrics - REQ-ARCH-038: Remote trace export with CRC-8 binary framing and pluggable transport abstraction (arbiter_trace_export.h/.c) - REQ-ARCH-042: Zbus integration with facts input subscriber and result output publisher (arbiter_zbus.h/.c) - REQ-ARCH-042: Grafana metrics via Zephyr STATS subsystem with eval_count, latency, faults_active, etc. (arbiter_metrics.c) - Kconfig: CONFIG_ARBITER_TRACE_EXPORT, CONFIG_ARBITER_ZBUS, CONFIG_ARBITER_METRICS with proper dependency guards - CMakeLists.txt updates for conditional compilation Co-Authored-By: Oz --- CMakeLists.txt | 4 + include/arbiter/arbiter_trace_export.h | 67 ++++++++++++ include/arbiter/arbiter_zbus.h | 52 +++++++++ lib/arbiter_trace_export.c | 139 +++++++++++++++++++++++++ subsys/arbiter/CMakeLists.txt | 2 + subsys/arbiter/Kconfig | 31 ++++++ subsys/arbiter/arbiter_metrics.c | 102 ++++++++++++++++++ subsys/arbiter/arbiter_zbus.c | 97 +++++++++++++++++ 8 files changed, 494 insertions(+) create mode 100644 include/arbiter/arbiter_trace_export.h create mode 100644 include/arbiter/arbiter_zbus.h create mode 100644 lib/arbiter_trace_export.c create mode 100644 subsys/arbiter/arbiter_metrics.c create mode 100644 subsys/arbiter/arbiter_zbus.c diff --git a/CMakeLists.txt b/CMakeLists.txt index e9071f5..0f14156 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,6 +21,10 @@ if(CONFIG_ARBITER) ${CMAKE_CURRENT_LIST_DIR}/lib/arbiter_accel.c ) + zephyr_library_sources_ifdef(CONFIG_ARBITER_TRACE_EXPORT + ${CMAKE_CURRENT_LIST_DIR}/lib/arbiter_trace_export.c + ) + zephyr_include_directories(${CMAKE_CURRENT_LIST_DIR}/include) add_subdirectory_ifdef(CONFIG_ARBITER ${CMAKE_CURRENT_LIST_DIR}/subsys/arbiter) diff --git a/include/arbiter/arbiter_trace_export.h b/include/arbiter/arbiter_trace_export.h new file mode 100644 index 0000000..13219f2 --- /dev/null +++ b/include/arbiter/arbiter_trace_export.h @@ -0,0 +1,67 @@ +/* SPDX-License-Identifier: MIT */ + +#ifndef ARBITER_TRACE_EXPORT_H_ +#define ARBITER_TRACE_EXPORT_H_ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** Frame start marker for trace export protocol. */ +#define ARBITER_TRACE_EXPORT_MARKER 0xAB + +/** CRC-8 polynomial used by the trace export framing. */ +#define ARBITER_TRACE_EXPORT_CRC8_POLY 0x07 + +/** + * Transport abstraction for remote trace export. + * + * The engine serialises each trace entry into a binary frame and + * calls @c send to push it to a transport (UART, network, etc.). + */ +struct ARBITER_trace_transport { + /** + * @brief Send a binary frame. + * + * @param buf Serialized frame bytes. + * @param len Length of @p buf in bytes. + * @param user_data Opaque pointer passed through from the transport. + * @return 0 on success, negative errno on failure. + */ + int (*send)(const uint8_t *buf, size_t len, void *user_data); + + /** Opaque user data forwarded to @c send. */ + void *user_data; +}; + +/** + * @brief Initialize the remote trace exporter. + * + * Registers the transport and resets the internal sequence counter. + * + * @param transport Transport to use for sending frames. + * @return 0 on success, -EINVAL if @p transport or its send callback is NULL. + */ +int ARBITER_trace_export_init(const struct ARBITER_trace_transport *transport); + +/** + * @brief Export all entries from a trace buffer. + * + * Each entry is serialised into a binary frame: + * [0xAB][len_le16][seq_u16][rule_id_u16][fired_u8] + * [action_id_u16][n_facts_u8][fact_ids...][crc8] + * + * @param trace Trace buffer to export. + * @return 0 on success, negative errno on transport failure. + */ +int ARBITER_trace_export(const struct ARBITER_trace *trace); + +#ifdef __cplusplus +} +#endif + +#endif /* ARBITER_TRACE_EXPORT_H_ */ diff --git a/include/arbiter/arbiter_zbus.h b/include/arbiter/arbiter_zbus.h new file mode 100644 index 0000000..65840d5 --- /dev/null +++ b/include/arbiter/arbiter_zbus.h @@ -0,0 +1,52 @@ +/* SPDX-License-Identifier: MIT */ + +#ifndef ARBITER_ZBUS_H_ +#define ARBITER_ZBUS_H_ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** Inbound message: set a fact value with timestamp. */ +struct arbiter_facts_msg { + arbiter_index_t fact_id; + int32_t value; + uint32_t timestamp_ms; +}; + +/** Outbound message: evaluation result summary. */ +struct arbiter_result_msg { + uint16_t mode; + uint32_t faults; + uint16_t action_count; + uint32_t op_count; +}; + +/* Channel declarations (defined in arbiter_zbus.c). */ +ZBUS_CHAN_DECLARE(arbiter_facts_chan, arbiter_result_chan); + +/** + * @brief Initialize the arbiter zbus integration. + * + * @param ctx Initialized arbiter context for fact updates. + * @return 0 on success, -EINVAL if @p ctx is NULL. + */ +int arbiter_zbus_init(struct ARBITER_ctx *ctx); + +/** + * @brief Publish an evaluation result to the result channel. + * + * @param result Evaluation result to publish. + * @return 0 on success, negative errno on failure. + */ +int arbiter_zbus_publish_result(const struct ARBITER_result *result); + +#ifdef __cplusplus +} +#endif + +#endif /* ARBITER_ZBUS_H_ */ diff --git a/lib/arbiter_trace_export.c b/lib/arbiter_trace_export.c new file mode 100644 index 0000000..daf487d --- /dev/null +++ b/lib/arbiter_trace_export.c @@ -0,0 +1,139 @@ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +LOG_MODULE_DECLARE(arbiter, CONFIG_ARBITER_LOG_LEVEL); + +/* ── Static state (no dynamic allocation) ─────────────────────── */ + +static const struct ARBITER_trace_transport *active_transport; +static uint16_t seq_counter; + +/* ── CRC-8 (polynomial 0x07) ─────────────────────────────────── */ + +static uint8_t crc8_update(uint8_t crc, const uint8_t *__restrict data, + size_t len) +{ + for (size_t i = 0; i < len; i++) { + crc ^= data[i]; + for (uint8_t bit = 0; bit < 8; bit++) { + if (crc & 0x80) { + crc = (uint8_t)((crc << 1) ^ ARBITER_TRACE_EXPORT_CRC8_POLY); + } else { + crc = (uint8_t)(crc << 1); + } + } + } + return crc; +} + +/* ── Helpers ──────────────────────────────────────────────────── */ + +static void put_le16(uint8_t *__restrict buf, uint16_t val) +{ + buf[0] = (uint8_t)(val & 0xFF); + buf[1] = (uint8_t)((val >> 8) & 0xFF); +} + +/* ── Public API ───────────────────────────────────────────────── */ + +int ARBITER_trace_export_init(const struct ARBITER_trace_transport *transport) +{ + if (transport == NULL || transport->send == NULL) { + return ARBITER_EINVAL; + } + + active_transport = transport; + seq_counter = 0; + + LOG_INF("Trace export initialized"); + return ARBITER_OK; +} + +int ARBITER_trace_export(const struct ARBITER_trace *trace) +{ + if (trace == NULL) { + return ARBITER_EINVAL; + } + + if (active_transport == NULL || active_transport->send == NULL) { + return ARBITER_EINVAL; + } + + for (uint16_t i = 0; i < trace->count; i++) { + const struct ARBITER_trace_entry *e = &trace->entries[i]; + + /* + * Frame layout: + * [marker 1B][len_le16 2B][seq_le16 2B] + * [rule_id_le16 2B][fired 1B][action_id_le16 2B] + * [n_facts 1B][fact_ids n*2B][crc8 1B] + * + * Payload length = everything after len field, including crc. + */ + uint8_t n_facts = (uint8_t)e->input_fact_count; + size_t payload_len = 2 + 2 + 1 + 2 + 1 + + ((size_t)n_facts * 2) + 1; + size_t frame_len = 1 + 2 + payload_len; + + /* + * Stack-allocated frame buffer. Maximum frame size with + * CONFIG_ARBITER_MAX_TRACE_INPUTS = 8 is: + * 1 + 2 + 2 + 2 + 1 + 2 + 1 + 16 + 1 = 28 bytes. + * Use a generous upper bound. + */ + uint8_t frame[3 + 2 + 2 + 1 + 2 + 1 + + (CONFIG_ARBITER_MAX_TRACE_INPUTS * 2) + 1]; + size_t pos = 0; + + /* Marker */ + frame[pos++] = ARBITER_TRACE_EXPORT_MARKER; + + /* Payload length (LE16) */ + put_le16(&frame[pos], (uint16_t)payload_len); + pos += 2; + + /* Sequence number (LE16) — wraps at UINT16_MAX */ + put_le16(&frame[pos], seq_counter); + pos += 2; + seq_counter++; + + /* Rule ID (LE16) */ + put_le16(&frame[pos], e->rule_id); + pos += 2; + + /* Fired flag */ + frame[pos++] = e->condition_result ? 1U : 0U; + + /* Action ID (LE16) */ + put_le16(&frame[pos], e->action_id); + pos += 2; + + /* Number of input facts */ + frame[pos++] = n_facts; + + /* Input fact IDs (LE16 each) */ + for (uint8_t f = 0; f < n_facts; f++) { + put_le16(&frame[pos], e->input_facts[f]); + pos += 2; + } + + /* CRC-8 over everything after the marker */ + uint8_t crc = crc8_update(0x00, &frame[1], pos - 1); + + frame[pos++] = crc; + + int ret = active_transport->send(frame, pos, + active_transport->user_data); + if (unlikely(ret < 0)) { + LOG_ERR("Trace export send failed: %d", ret); + return ret; + } + } + + return ARBITER_OK; +} diff --git a/subsys/arbiter/CMakeLists.txt b/subsys/arbiter/CMakeLists.txt index a62c8d3..bb70aa7 100644 --- a/subsys/arbiter/CMakeLists.txt +++ b/subsys/arbiter/CMakeLists.txt @@ -5,3 +5,5 @@ zephyr_library() zephyr_library_sources_ifdef(CONFIG_ARBITER_SHELL arbiter_shell.c) zephyr_library_sources_ifdef(CONFIG_ARBITER_RUNTIME_THREAD arbiter_runtime_thread.c) zephyr_library_sources_ifdef(CONFIG_ARBITER_WATCHDOG arbiter_watchdog.c) +zephyr_library_sources_ifdef(CONFIG_ARBITER_ZBUS arbiter_zbus.c) +zephyr_library_sources_ifdef(CONFIG_ARBITER_METRICS arbiter_metrics.c) diff --git a/subsys/arbiter/Kconfig b/subsys/arbiter/Kconfig index 9f66d17..237b8a6 100644 --- a/subsys/arbiter/Kconfig +++ b/subsys/arbiter/Kconfig @@ -284,4 +284,35 @@ config ARBITER_FPGA_OFFLOAD to offload condition evaluation and expression execution to FPGA fabric. No implementation is shipped in v1. +# ── Remote Trace Export (REQ-ARCH-038) ─────────────────────── + +config ARBITER_TRACE_EXPORT + bool "Enable remote trace export" + default n + help + Serialize evaluation trace entries into binary frames and + send them over a pluggable transport (UART, network, etc.) + for remote analysis and debugging. + +# ── Zbus Integration (REQ-ARCH-042) ───────────────────────── + +config ARBITER_ZBUS + bool "Enable zbus integration" + depends on ZBUS + default n + help + Publish evaluation results and subscribe to fact updates + via Zephyr zbus channels. + +# ── Grafana Metrics (REQ-ARCH-042) ────────────────────────── + +config ARBITER_METRICS + bool "Enable runtime metrics (Zephyr stats)" + depends on STATS + default n + help + Expose evaluation metrics (count, latency, faults, etc.) + through the Zephyr stats subsystem for Grafana or other + monitoring dashboards. + endif # ARBITER diff --git a/subsys/arbiter/arbiter_metrics.c b/subsys/arbiter/arbiter_metrics.c new file mode 100644 index 0000000..4d03a49 --- /dev/null +++ b/subsys/arbiter/arbiter_metrics.c @@ -0,0 +1,102 @@ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +LOG_MODULE_DECLARE(arbiter, CONFIG_ARBITER_LOG_LEVEL); + +/* ── STATS section definition ─────────────────────────────────── */ + +STATS_SECT_START(arbiter_stats) +STATS_SECT_ENTRY(eval_count) +STATS_SECT_ENTRY(eval_latency_us) +STATS_SECT_ENTRY(eval_max_latency_us) +STATS_SECT_ENTRY(rules_fired) +STATS_SECT_ENTRY(faults_active) +STATS_SECT_ENTRY(op_count_last) +STATS_SECT_END; + +STATS_NAME_START(arbiter_stats) +STATS_NAME(arbiter_stats, eval_count) +STATS_NAME(arbiter_stats, eval_latency_us) +STATS_NAME(arbiter_stats, eval_max_latency_us) +STATS_NAME(arbiter_stats, rules_fired) +STATS_NAME(arbiter_stats, faults_active) +STATS_NAME(arbiter_stats, op_count_last) +STATS_NAME_END(arbiter_stats); + +STATS_SECT_DECL(arbiter_stats) arbiter_stats_inst; + +/* ── Helpers ──────────────────────────────────────────────────── */ + +/** + * Count the number of bits set in a 32-bit word (fault bitmap). + */ +static uint32_t popcount32(uint32_t v) +{ + uint32_t c = 0; + + for (; v; v &= v - 1) { + c++; + } + return c; +} + +/* ── Public API ───────────────────────────────────────────────── */ + +/** + * @brief Update arbiter metrics after an evaluation. + * + * @param result Evaluation result. + * @param latency_us Wall-clock evaluation latency in microseconds. + */ +void arbiter_metrics_update(const struct ARBITER_result *__restrict result, + uint32_t latency_us) +{ + if (result == NULL) { + return; + } + + STATS_INC(arbiter_stats_inst, eval_count); + STATS_SET(arbiter_stats_inst, eval_latency_us, latency_us); + + if (latency_us > arbiter_stats_inst.eval_max_latency_us) { + STATS_SET(arbiter_stats_inst, eval_max_latency_us, latency_us); + } + + STATS_SET(arbiter_stats_inst, rules_fired, + result->requested_action_count); + STATS_SET(arbiter_stats_inst, faults_active, + popcount32(result->raised_faults)); + STATS_SET(arbiter_stats_inst, op_count_last, result->eval_op_count); +} + +/** + * @brief Register arbiter stats with the Zephyr stats subsystem. + * + * Called automatically at boot via SYS_INIT. + */ +static int arbiter_metrics_init(void) +{ + int ret = stats_init(&arbiter_stats_inst.s_hdr, + STATS_SIZE_INIT_PARMS(arbiter_stats_inst, + STATS_SECT_DECL(arbiter_stats))); + + if (ret < 0) { + LOG_ERR("stats_init failed: %d", ret); + return ret; + } + + ret = stats_register("arbiter", &arbiter_stats_inst.s_hdr); + if (ret < 0) { + LOG_ERR("stats_register failed: %d", ret); + return ret; + } + + LOG_INF("arbiter metrics registered"); + return 0; +} + +SYS_INIT(arbiter_metrics_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); diff --git a/subsys/arbiter/arbiter_zbus.c b/subsys/arbiter/arbiter_zbus.c new file mode 100644 index 0000000..cd1650f --- /dev/null +++ b/subsys/arbiter/arbiter_zbus.c @@ -0,0 +1,97 @@ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +LOG_MODULE_DECLARE(arbiter, CONFIG_ARBITER_LOG_LEVEL); + +/* ── Channel definitions ──────────────────────────────────────── */ + +ZBUS_CHAN_DEFINE(arbiter_facts_chan, + struct arbiter_facts_msg, + NULL, NULL, + ZBUS_OBSERVERS(arbiter_facts_sub), + ZBUS_MSG_INIT(.fact_id = 0, .value = 0, .timestamp_ms = 0)); + +ZBUS_CHAN_DEFINE(arbiter_result_chan, + struct arbiter_result_msg, + NULL, NULL, + ZBUS_OBSERVERS_EMPTY, + ZBUS_MSG_INIT(.mode = 0, .faults = 0, + .action_count = 0, .op_count = 0)); + +/* ── Static state ─────────────────────────────────────────────── */ + +static struct ARBITER_ctx *zbus_ctx; + +/* ── Subscriber callback ──────────────────────────────────────── */ + +static void facts_cb(const struct zbus_channel *chan) +{ + if (zbus_ctx == NULL) { + return; + } + + struct arbiter_facts_msg msg; + int ret = zbus_chan_read(chan, &msg, K_NO_WAIT); + + if (unlikely(ret < 0)) { + LOG_ERR("zbus facts read failed: %d", ret); + return; + } + + ret = ARBITER_set_i32(zbus_ctx, msg.fact_id, msg.value); + if (unlikely(ret != ARBITER_OK)) { + LOG_WRN("set_i32 fact %u failed: %d", + (unsigned int)msg.fact_id, ret); + return; + } + + ret = ARBITER_set_timestamp(zbus_ctx, msg.fact_id, msg.timestamp_ms); + if (unlikely(ret != ARBITER_OK)) { + LOG_WRN("set_timestamp fact %u failed: %d", + (unsigned int)msg.fact_id, ret); + } +} + +ZBUS_LISTENER_DEFINE(arbiter_facts_sub, facts_cb); + +/* ── Public API ───────────────────────────────────────────────── */ + +int arbiter_zbus_init(struct ARBITER_ctx *ctx) +{ + if (ctx == NULL) { + return ARBITER_EINVAL; + } + + zbus_ctx = ctx; + LOG_INF("arbiter zbus integration initialized"); + + return ARBITER_OK; +} + +int arbiter_zbus_publish_result(const struct ARBITER_result *result) +{ + if (result == NULL) { + return ARBITER_EINVAL; + } + + struct arbiter_result_msg msg = { + .mode = result->current_mode, + .faults = result->raised_faults, + .action_count = result->requested_action_count, + .op_count = result->eval_op_count, + }; + + int ret = zbus_chan_pub(&arbiter_result_chan, &msg, K_NO_WAIT); + + if (unlikely(ret < 0)) { + LOG_ERR("zbus result publish failed: %d", ret); + return ret; + } + + return ARBITER_OK; +}