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:
- Register the SM plugin for indication decode —
register_sm(<sm>_plugin_get())— before the first subscribe. (A CONTROL-only SM like EPI registers nothing.) - Resolve the node's capability by OID, never by a hard-coded RAN Function ID:
node.find_ran_function_by_oid(<SM>_OID). - Subscribe with an SM builder (
subscribe_kpm,subscribe_rc, …) or, for JSON SMs, borrowed JSON (subscribe_ccc). The call returns asubscription_id. - Correlate indications by
subscription_id, which every indication callback carries — route on it, not onran_func_id. - Control / query through the typed builder (
control_rc,control_ccc,query_rc,query_air) or the genericcontrolpath (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
- The
airpuls-sdkCLI — generate, run, package, and deploy an xApp project - Getting Started — install the SDK for your language
- Configuration — the
xapp.ymlschema - Building xApps — lifecycle, callbacks, the full skeleton
- A1 Policy Enforcement — enforce O-RAN A1 policies from your xApp
- Service Model Guides — per-SM subscription and control
- Telemetry Sinks — emit xApp data to Redis / InfluxDB / files
- Packaging — ship your xApp as a Docker image