Skip to content

SDK Overview

The airpuls xApp SDK is how you build an xApp. It gives C, Python, and Go programs a callback-based surface to connect to the RIC, subscribe to E2 nodes, receive indications, and issue control — without ever touching E2AP, ASN.1, or the wire protocol. In Python it also lets an xApp enforce O-RAN A1 policies the RIC distributes — see A1 Policy Enforcement.

What the SDK is

libric-wire            Wire protocol (shared with the RIC): 10-byte framed
                       header + per-message TLVs; SM payloads ride as opaque
                       octet strings.

libric-client (C)      The FFI-clean public surface — opaque handles, plain-C
                       callbacks, int status codes. Owns the socket, decodes
                       frames, dispatches callbacks. This is libric_client.a.

lang_bindings/
  python/  (CFFI)      airpuls_ric_sdk — RicClient + BaseXApp + builders + sinks
  go/      (cgo)       github.com/airpuls/ric-client-go/ric — Client + EventHandler

Every binding wraps the same C core, so C, Python, and Go xApps share one behaviour and one wire contract. Each SM's encoders/decoders come from the same per-SM static archive (libe2sm_<sm>.a) the RIC links, so what an xApp can build is exactly what the RIC and agent understand.

The two-thread model

This is the one invariant to internalise before writing an xApp:

  • I/O thread (private, GLib) — owns the socket, decodes frames, and runs the registered SM plugin's decoder for REPORT indications. It deep-copies every payload into an event and posts it to an internal queue. It never invokes your callback.
  • Dispatch thread — whatever thread calls run() / dispatch(). All your callbacks fire here. Because callbacks are serialized onto this one thread, your xApp code needs no locking.
sequenceDiagram
    participant Net as RIC (socket)
    participant IO as SDK I/O thread
    participant Q as event queue
    participant App as your dispatch thread

    Net->>IO: framed indication bytes
    IO->>IO: decode frame + SM REPORT payload
    IO->>Q: deep-copy → event
    App->>Q: run() / dispatch() drains
    Q->>App: on_indication(...) fires here
    Note over App: no locks needed — single dispatch thread

Borrowed vs owned handles

The header / data handed to on_indication are borrowed — the SDK frees them the moment your callback returns. Copy out anything you need to keep. One-shot results you own (a control_rc outcome, a query snapshot) are released by you (Python with / __del__; Go finalizer; C explicit free).

How an xApp talks to an SM

Everything an xApp does with an SM follows one shape:

  1. Register the SM plugin for indication decode — register_sm(<sm>_plugin_get()) — before the first subscribe. (A CONTROL-only SM like EPI registers nothing.)
  2. Resolve the node's capability by OID, never by a hard-coded RAN Function ID: node.find_ran_function_by_oid(<SM>_OID).
  3. Subscribe with an SM builder (subscribe_kpm, subscribe_rc, …) or, for JSON SMs, borrowed JSON (subscribe_ccc). The call returns a subscription_id.
  4. Correlate indications by subscription_id, which every indication callback carries — route on it, not on ran_func_id.
  5. Control / query through the typed builder (control_rc, control_ccc, query_rc, query_air) or the generic control path (that is how EPI probes go out).

The three SM shapes differ only in how the payload is carried:

Shape SMs Subscribe REPORT decode
ASN.1 builder KPM, RC, LLC, AIR subscribe_<sm>(node, builder) Typed measurement blocks (or a typed struct for LLC/AIR)
JSON CCC subscribe_ccc(node, ad_json, et_json) Raw JSON, parsed natively by the xApp
CONTROL-only EPI — (no subscription)

What you can do, per service model

This is the whole E2 surface an xApp reaches through the SDK. Each cell names the Python call; the per-SM guide has the Go and C equivalents.

SM REPORT (subscribe) INSERT CONTROL QUERY (one-shot)
KPM Styles 1–5 — subscribe_kpm kpm_snapshot (client-side, not a wire procedure)
RC Styles 1–4 — subscribe_rc Style 3 / Action 1 — on_insert_indication + CPI-echoing control_rc Style 3 / Action 1 handover — control_rc Style 1 cells query_rc_cells, Style 2 UEs query_rc
CCC Node + cell styles — subscribe_ccc control_ccc (write RAN configuration structures)
LLC Style 1 LLI copy (SRS / CSI), Style 2 periodic — subscribe_llc
AIR Style 1 SRS UE estimate — subscribe_air Style 1 UE PHY config — query_air
EPI Echo probe over the generic control / control_async

Two calls are SM-agnostic and work against any RAN function: control(node, ran_func_id, request) for a control you encoded yourself, and unsubscribe(sub_id) to tear a subscription down.

Enforcing A1 policies

Beyond E2, a Python xApp can enforce O-RAN A1 policies. The Non-RT RIC (SMO) sends a policy to the Near-RT RIC; the RIC distributes it to the xApps that can carry it out; your xApp observes the RAN (with the same E2 subscriptions and control above), acts to meet the policy, and reports whether it is being enforced.

You implement a small A1Enforcer and pass it to BaseXApp(a1enforcers=[...]); the SDK owns the Redis policy seam, reconnect-safe re-subscription, indication routing, and durable status reporting. It is a Python capability today (Go and C are planned).

class MyEnforcer(A1Enforcer):
    policy_types = ["QoSTarget_6.0.1"]

    def on_policy_put(self, policy):
        self.ctx.observe(policy.ref, build=self._subscription_for)

    def on_policy_deleted(self, policy_type_id, policy_id, ref):
        self.ctx.forget(ref)

See A1 Policy Enforcement for the full guide.

Languages at a glance

Python Go C
Entry point BaseXApp subclass ric.EventHandler (embed DefaultEventHandler) ric_client_* + callbacks
Package airpuls_ric_sdk (CFFI) github.com/airpuls/ric-client-go/ric (cgo) ric-client.h + libric_client.a
Best for fast iteration, DSP/ISAC xApps throughput, in-container builds minimal footprint, the binding-overhead baseline
Service models all six all six (EPI via the separate epi package) all six
Telemetry sinks ✗ — no sink layer in libric-client
A1 policy enforcement

All three reach the same E2 surface — subscribe, INSERT, control, and query, on every SM. They differ above it: telemetry sinks exist in Python and Go but not in C (a C xApp that wants to publish writes the transport itself), and A1 policy enforcement is Python-only today. C is the lean baseline every binding wraps.

Next steps