Skip to content

E2SM-CCC — Cell Configuration and Control

This page is a self-contained guide to building xApps on top of the E2SM-CCC service model in airpuls. By the end you will understand what CCC is for, how the O-RAN specification structures it, and how to write both a monitoring xApp and a control xApp in Python, Go, or C.

Specification anchors

  • O-RAN.WG3.TS.E2SM-CCC-R004-v06.00 — the E2 Service Model for Cell Configuration and Control (the contract implemented here).
  • O-RAN.WG3.TS.E2AP — the E2 Application Protocol that carries the Subscription / Indication / Control procedures CCC rides on.
  • 3GPP TS 28.541 — 5G Network Resource Model (NRM); CCC's RAN Configuration Structures are the E2 projection of these managed objects (e.g. gNBDUFunction, the Cell Energy Saving management function, §4.4.1).
  • O-RAN.WG10 O1 Interface — the management-plane model CCC mirrors on the E2 (near-real-time) interface.
  • IETF RFC 5905 — the 64-bit NTP timestamp format used for eventTime / appliedTimestamp.

1. What CCC is for

A Near-RT RIC observes and acts on the RAN along three axes, one per service model:

Service model What it gives an xApp Example
E2SM-KPM Measurements — performance counters DL throughput, PRB usage
E2SM-RC Radio control — per-UE decisions Handover, QoS flow mapping
E2SM-CCC Configuration — observe and change managed-object state Cell energy saving, cell on/off, antenna config, RRM slice ratios, PRB blanking

CCC is the configuration axis. It does not report traffic and it does not steer individual UEs — it exposes the RAN's configuration state (the same objects an O1/NETCONF manager would touch, projected onto the near-real-time E2 interface) and lets an xApp read that state and write the writable parts of it.

Typical CCC use cases (E2SM-CCC R004 §1, §8.8): cell energy saving, cell DTX/DRX, O-RU user-plane configuration, RRM policy ratios for slicing, PRB blanking for interference coordination, and reading gNB-CU/DU function identity.


2. How the specification models CCC

Three ideas, in order.

2.1 RAN Configuration Structures

Everything in CCC is built from RAN Configuration Structures — named bundles of configuration attributes that map 1:1 onto 3GPP TS 28.541 managed objects. Each structure is identified by a RAN Configuration Structure Name (E2SM-CCC R004 §9.3.7) and lives at one of two scopes (§8.2):

  • Node-level — applies to the whole E2 node (the gNB function).
  • Cell-level — applies to one cell, addressed by its Cell Global Id (NR-CGI).
Scope RAN Configuration Structure (selection) Purpose
Node O-GnbCuCpFunction, O-GnbCuUpFunction, O-GnbDuFunction gNB function identity / config
Node O-RRMPolicyRatio RRM resource ratios (slicing)
Cell O-CESManagementFunction Cell energy saving
Cell O-NESPolicy, O-CellDTXDRXConfig Network/cell energy-saving policy, DTX/DRX
Cell O-NrCellCu, O-NrCellDu, O-Bwp NR cell + bandwidth-part config
Cell O-RUInfo, O-PRBBlankingPolicy O-RU config, PRB blanking

Note

A node advertises which structures it supports in its RAN Function Definition at E2 Setup (§9.2.2.1). An xApp inspects that advertisement before subscribing — you only ask for what a node offers.

2.2 Attributes — the O-CESManagementFunction example

A structure is a set of attributes (§9.3.8, §8.8). Each attribute has a 3GPP-defined type, a read/write flag, and a base spec. Take the cell energy-saving structure (E2SM-CCC R004 §8.8.2.5, mapping 3GPP TS 28.541 §4.4.1):

Attribute Access Type Meaning
cesSwitch read-only boolean Is the Cell Energy Saving feature enabled for this cell?
energySavingState read-only enum isNotEnergySaving / isEnergySaving What is — is the cell currently in an energy-saving state?
energySavingControl read-write enum toBeNotEnergySaving / toBeEnergySaving What is requested — the desired target state (the control knob)

The energySavingStateenergySavingControl distinction is the heart of CCC: one is the observed reality, the other is the requested target. A control writes energySavingControl; a subsequent report shows whether energySavingState followed. That is the loop an energy xApp closes.

2.3 Services, styles, and formats

CCC exposes its structures through RIC service styles (E2SM-CCC R004 §7). A style combines a scope with a direction:

Service Style 1 Style 2 INSERT / POLICY
Event Trigger E2 Node Configuration Change Periodic
REPORT Node-Level Configuration Cell-Level Configuration
CONTROL Node Configuration and Control Cell Configuration and Control
QUERY Node-Level Query Cell-Level Query

Scope of the airpuls implementation

REPORT (Styles 1 + 2) and CONTROL (Styles 1 + 2) are fully implemented end-to-end. QUERY is a separate E2AP elementary procedure (RIC Query) and is not yet implemented. INSERT and POLICY are Void in the spec (§7.5 / §7.7).

Each style pins format numbers for its messages (E2SM-CCC R004 §7.8):

Service style Action Def Ind. Header Ind. Message Control Header Control Message Control Outcome
REPORT Style 1 (node) 1 1 1
REPORT Style 2 (cell) 2 1 2
CONTROL Style 1 (node) 1 1 1
CONTROL Style 2 (cell) 1 2 2

Across the board: Format 1 = node-level, Format 2 = cell-level.

2.4 The one fact that shapes everything: CCC is JSON

Unlike KPM/RC/LLC, E2SM-CCC has no ASN.1 module. Spec §9.5 Message transfer syntax is Void; the normative data model is the §9.4 JSON Schema (OpenAPI 3.0.1). Every CCC information element — Action Definition, Event Trigger, Indication, Control Header/Message/Outcome, and even the RAN Function Definition — is a UTF-8 JSON document carried inside the generic E2AP OCTET STRINGs.

airpuls embraces this with a deliberately thin data plane: the C layers move CCC JSON as opaque bytes, and the xApp builds/parses it with its language's native JSON. Concretely, the Near-RT RIC never parses a byte of CCC content — it routes by RAN Function ID (4) and forwards the OCTET STRINGs untouched.

sequenceDiagram
    autonumber
    participant X as xApp (Py/Go/C)
    participant S as C SDK (libric-client)
    participant R as Near-RT RIC
    participant N as E2 Node (agent)

    X->>S: subscribe_ccc(node, actionDef, eventTrigger)
    Note over X,S: action/trigger built as JSON
    S->>R: RIC Subscription Request<br/>(JSON in OCTET STRINGs, ranFuncId=4)
    Note over R: routes by ranFuncId — never parses CCC JSON
    R->>N: E2AP RIC Subscription Request
    N->>N: parse Action Def + Event Trigger,<br/>arm reporting timer
    N-->>R: SubscribeAck
    R-->>S: ack
    S-->>X: sub_id

    loop every reporting period
        N->>N: build configuration snapshot (JSON)
        N-->>R: RIC Indication (JSON)
        R-->>S: forward opaque bytes
        S-->>X: on_indication(... data)
        Note over X: data.ccc_json() → parse natively
    end

3. OAI to E2 CCC Mapping Table

When the CCC service model runs embedded in an OAI gNB (rather than the emulator, which emits illustrative values), the agent populates each RAN Configuration Structure from the gNB's own running configuration — the same parameters the operator sets in the gNB .conf: served-cell identity, carrier and SSB, the initial bandwidth part, and the fronthaul antenna setup. All reporting is read-only — CCC never changes the gNB configuration.

This table is the contract: for every attribute an xApp can receive, it shows where the value comes from.

Mark Meaning
config Read directly from the gNB served-cell / BWP / fronthaul configuration
derived Computed from gNB config (an enum or RIV decoded to the spec value)
constant OAI has no equivalent concept; a fixed, spec-valid value is emitted
agent Owned by the CCC agent itself, not an OAI value

O-GnbDuFunction (node)

Attribute Source Reflects
gnbDuId config gNB-DU ID (.conf gNB_DU_ID)
gnbDuName config gNB-DU name (.conf gNB_name)
gnbId config gNB ID (.conf gNB_ID), as the gNB-ID bit string
gnbIdLength constant gNB-ID bit length — OAI does not store it; defaults to 22 (configurable)

O-NrCellDu (cell)

Attribute Source Reflects
cellLocalId derived Low bits of the NR Cell Identity (.conf nr_cellid) below the gNB-ID split
plmnInfoList[].plmnId config Cell PLMN (mcc / mnc)
plmnInfoList[].snssai config Configured network slice(s) (sst / sd)
nrPci config Physical cell ID (.conf physCellId)
nrTac config Tracking area code (.conf tracking_area_code)
arfcnDL / arfcnUL derived SSB-centre NR-ARFCN (.conf absoluteFrequencySSB); UL = DL for TDD
ssbSubCarrierSpacing config SSB subcarrier spacing in kHz (subcarrierSpacing enum → kHz)
operationalState constant ENABLED while the cell is up — OAI has no NRM state model
administrativeState constant UNLOCKED — OAI has no administrative lifecycle
cellState constant ACTIVE while serving — no IDLE/INACTIVE notion in OAI
arfcnSUL omitted No supplementary uplink configured
Attribute Source Reflects
bwpContext constant DL (the initial downlink BWP)
isInitialBwp derived INITIAL for the initial BWP
subCarrierSpacing config BWP numerology in kHz (subcarrierSpacing enum → kHz)
startRB / numberOfRBs derived PRB span, decoded from the BWP locationAndBandwidth (RIV)
cyclicPrefix constant §9.4.2 placeholder — see the note below

cyclicPrefix is a spec-defect placeholder

The E2SM-CCC R004 JSON Schema (§9.4.2) types CyclicPrefix as the string enum {"15","30","60","120"} (a copy of SubCarrierSpacing), which contradicts the §8.8.2.3 attribute table ({NORMAL,EXTENDED}). airpuls follows the normative JSON Schema, so this is a schema-valid placeholder, not a mapping of OAI's normal/extended cyclic prefix.

O-RUInfo (cell) — fronthaul antenna arrays

Attribute Source Reflects
tx/rxArrayList[].t/rBandNumber config NR band of the O-RU (.conf RUs.bands)
t/rNumberOf{Rows,Columns} derived Derived from the antenna count (nb_tx / nb_rx) under a dual-polarisation convention — an approximation of the panel geometry
t/rPolarizationList constant Dual-polarisation (±45°) assumption
element spacing, gain, layers constant Deployment defaults — the true panel geometry is an O-RU M-Plane property OAI does not expose

O-RUInfo geometry is a documented approximation

O-RUInfo carries the real band + antenna count today; the physical array geometry (element spacing, per-array gain, exact rows × columns) lives in the O-RU M-Plane, which OAI does not surface, so it is reported as a documented approximation until an M-Plane-managed fronthaul build supplies it. O-RUInfo is omitted entirely on a CU-only node (no radio unit).

O-CESManagementFunction (cell) — energy saving

Attribute Source Reflects
cesSwitch agent CES capability flag (constant true)
energySavingState agent Current energy-saving state — owned by the CCC agent, flipped by a CCC CONTROL; OAI has no energy-saving model
energySavingControl agent Requested energy-saving state

Energy saving is the one CCC value that is runtime-mutable: a CCC CONTROL that sets energySavingControl flips energySavingState, and the next REPORT carries the change.

What a "change" means in a report — changeType

Most cell configuration (PCI, TAC, PLMN, carrier, BWP, antenna setup) is fixed when the gNB starts and does not change while it runs, so those structures report changeType: none. The agent remembers the last value of each structure and reports changeType: modification — with the previous value in oldValuesOfAttributes — only when a value actually changes, which in practice is an energy-saving state flip. (Cell addition / deletion requires an O-RAN event-trigger path that is not yet wired.)

4. The subscription mechanic — what you actually ask for

A CCC subscription is two information elements:

  1. Action Definitionwhat to report: the RIC Style Type (1 = node, 2 = cell) + which RAN Configuration Structures (and, optionally, which attributes) and a reportType of all (every period) or change (only on change). (§9.2.1.2)
  2. Event Trigger Definitionwhen to report: periodic (every N ms, Format 3) or on configuration change (Format 1 node / Format 2 cell). (§9.2.1.1)

The SDK provides small builders that emit the §9.4-shaped JSON, so you work with native data structures, not hand-written JSON. For a cell-level energy-saving subscription the wire payloads are:

// Action Definition (the "what")
{ "ricStyleType": 2,
  "actionDefinitionFormat": {
    "listOfCellConfigurationsToBeReportedForADF": [
      { "listOfCellLevelRANConfigurationStructuresForADF": [
          { "reportType": "all",
            "ranConfigurationStructureName": "O-CESManagementFunction" } ] } ] } }
// Event Trigger Definition (the "when")
{ "eventTriggerDefinitionFormat": { "period": 1000 } }

You submit both with a single call; indications then stream to your on_indication handler until you unsubscribe.


5. Tutorial — a monitoring xApp

Goal: subscribe to every CCC-capable node and log each configuration snapshot as it arrives.

Step 1 — register the CCC plugin so the inbound indication path can dispatch CCC indications (keyed by RAN Function ID 4).

Step 2 — on each available node, capability-check for the CCC RAN function, then subscribe with an Action Definition + Event Trigger.

Step 3 — in the indication handler, read the raw JSON and parse it natively.

import json
from airpuls_ric_sdk import (
    CCC_RAN_FUNC_ID, ccc_plugin_get,
    ccc_action_definition_cell, ccc_action_definition_node,
    ccc_event_trigger_periodic,
)
from airpuls_ric_sdk.xapp import BaseXApp, setup_logging

class CccMonitor(BaseXApp):
    XAPP_TYPE = "ccc-monitor"

    # Step 1: advertise the CCC plugin to the SDK.
    def plugins(self):
        return [ccc_plugin_get()]

    # Step 2: subscribe to CCC-capable nodes.
    def on_e2_node_available(self, client, node):
        if node.find_ran_function(CCC_RAN_FUNC_ID) is None:
            return  # node does not advertise CCC
        # Cell-level (Style 2): energy-saving config, every 1 s.
        client.subscribe_ccc(
            node,
            ccc_action_definition_cell(["O-CESManagementFunction"]),
            ccc_event_trigger_periodic(1000),
        )
        # Node-level (Style 1): gNB-DU identity.
        client.subscribe_ccc(
            node,
            ccc_action_definition_node(["O-GnbDuFunction"]),
            ccc_event_trigger_periodic(1000),
        )

    # Step 3: parse the indication JSON natively.
    def on_indication(self, client, sub_id, node, ran_func_id, header, data):
        if ran_func_id != CCC_RAN_FUNC_ID:
            return
        msg = json.loads(data.ccc_json())
        fmt = msg["indicationMessageFormat"]
        if "listOfCellsReported" in fmt:                # Style 2
            for cell in fmt["listOfCellsReported"]:
                for cs in cell["listOfConfigurationStructuresReported"]:
                    vals = cs["valuesOfAttributes"]["ranConfigurationStructure"]
                    print(cs["ranConfigurationStructureName"], vals)
        elif "listOfConfigurationStructuresReported" in fmt:  # Style 1
            for cs in fmt["listOfConfigurationStructuresReported"]:
                vals = cs["valuesOfAttributes"]["ranConfigurationStructure"]
                print(cs["ranConfigurationStructureName"], vals)

if __name__ == "__main__":
    setup_logging()
    CccMonitor(config_path="/etc/airpuls/xapp.yml").run_sync()
package main

import (
    "encoding/json"
    "log"

    "github.com/airpuls/ric-client-go/ric"
)

type handler struct{ ric.DefaultEventHandler }

// Step 2: subscribe to CCC-capable nodes.
func (h *handler) OnE2NodeAvailable(c *ric.Client, node *ric.E2Node) {
    if node.FindRanFunction(ric.CccRanFuncID) == nil {
        return
    }
    // Cell-level (Style 2): energy-saving config, every 1 s.
    c.SubscribeCCC(node,
        ric.CccActionDefinitionCell([]string{"O-CESManagementFunction"}, "all"),
        ric.CccEventTriggerPeriodic(1000))
    // Node-level (Style 1): gNB-DU identity.
    c.SubscribeCCC(node,
        ric.CccActionDefinitionNode([]string{"O-GnbDuFunction"}, "all"),
        ric.CccEventTriggerPeriodic(1000))
}

// Step 3: parse the indication JSON natively.
func (h *handler) OnIndication(c *ric.Client, subID uint32, node *ric.E2Node,
    ranFuncID uint16, hdr *ric.IndicationHeader, data *ric.IndicationData) {
    if ranFuncID != ric.CccRanFuncID {
        return
    }
    var msg map[string]any
    if err := json.Unmarshal([]byte(data.CccJSON()), &msg); err != nil {
        return
    }
    log.Printf("CCC config: %v", msg["indicationMessageFormat"])
}

func main() {
    cfg := ric.NewXappConfig("/etc/airpuls/xapp.yml")
    defer cfg.Close()
    if err := cfg.Load(); err != nil {
        log.Fatal(err)
    }
    client, err := ric.Connect(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    client.RegisterSM(ric.CccPluginGet()) // Step 1
    client.SetEventHandler(&handler{})
    client.Run()
}
#include <stdio.h>
#include <string.h>
#include "ric-client.h"
#include "xapp-config.h"
#include "e2sm-ccc.h"     /* CCC_RAN_FUNC_ID, ccc_plugin_get, ccc_indication_json */

/* Step 2: subscribe when a CCC-capable node appears. */
static void on_node(ric_client_t *client, const ric_e2_node_t *node, void *ud) {
    (void)ud;
    /* E2SM-CCC is a JSON SM: build the Action Definition + Event
     * Trigger JSON directly (or with the bundled cJSON). */
    const char *action_def =
        "{\"ricStyleType\":2,\"actionDefinitionFormat\":{"
        "\"listOfCellConfigurationsToBeReportedForADF\":[{"
        "\"listOfCellLevelRANConfigurationStructuresForADF\":[{"
        "\"reportType\":\"all\","
        "\"ranConfigurationStructureName\":\"O-CESManagementFunction\"}]}]}}";
    const char *event_trigger =
        "{\"eventTriggerDefinitionFormat\":{\"period\":1000}}";

    uint32_t sub_id = 0;
    ric_client_subscribe_ccc(client, node,
        (const uint8_t *)action_def, strlen(action_def),
        (const uint8_t *)event_trigger, strlen(event_trigger),
        &sub_id);
}

/* Step 3: read the raw indication JSON. */
static void on_ind(ric_client_t *client, uint32_t sub_id,
                   const ric_e2_node_t *node, uint16_t ran_func_id,
                   const sm_indication_header_t *hdr,
                   const sm_indication_data_t *data, void *ud) {
    (void)client; (void)sub_id; (void)node; (void)hdr; (void)ud;
    if (ran_func_id != CCC_RAN_FUNC_ID) return;
    const char *json = ccc_indication_json(data);   /* borrowed */
    if (json) printf("CCC config: %s\n", json);      /* parse with cJSON, etc. */
}

int main(void) {
    ric_xapp_config_t *cfg = ric_xapp_config_new("/etc/airpuls/xapp.yml");
    ric_xapp_config_load(cfg);

    ric_client_t *client = ric_client_connect(cfg);
    ric_client_register_sm(client, ccc_plugin_get());       /* Step 1 */
    ric_client_set_on_e2_node_available(client, on_node, NULL);
    ric_client_set_on_indication(client, on_ind, NULL);
    ric_client_run(client);

    ric_client_destroy(client);
    ric_xapp_config_free(cfg);
    return 0;
}

What you will see

A cell-level indication carries, per reported cell, the structure name, a changeType (none / modification / addition / deletion), and the attribute values — e.g. for O-CESManagementFunction: {"cesSwitch": true, "energySavingState": "isNotEnergySaving", "energySavingControl": "toBeNotEnergySaving"}.


6. Tutorial — a control xApp

Goal: close the energy-saving loop — when a cell reports it is not energy-saving, request it to switch on, and confirm the change.

A CCC CONTROL carries two information elements: a Control Header (the RIC Style Type, §9.2.1.6) and a Control Message (the list of structures to write, each with its old values for optimistic concurrency and the new values, §9.2.1.7). The node replies with a structured Control Outcome (§9.2.1.8) — an Accepted list (old + current values + applied timestamp) and a Failed list (old + requested values + a cause).

sequenceDiagram
    autonumber
    participant X as xApp
    participant N as Cell (E2 Node)

    N-->>X: REPORT energySavingState = isNotEnergySaving
    X->>N: CONTROL energySavingControl → toBeEnergySaving
    N->>N: match old values, apply
    N-->>X: ControlOutcome (Accepted:<br/>current = isEnergySaving, appliedTimestamp)
    N-->>X: REPORT energySavingState = isEnergySaving

The control payloads on the wire:

// Control Header (Style 2)
{ "controlHeaderFormat": { "ricStyleType": 2 } }
// Control Message (Style 2 — write O-CESManagementFunction for one cell)
{ "controlMessageFormat": { "listOfCellsControlled": [ {
    "cellGlobalId": { "plmnIdentity": {"mcc":"001","mnc":"01"},
                      "nRCellIdentity": "000000010" },
    "listOfConfigurationStructures": [ {
      "ranConfigurationStructureName": "O-CESManagementFunction",
      "oldValuesOfAttributes": { "ranConfigurationStructure":
          { "energySavingControl": "toBeNotEnergySaving" } },
      "newValuesOfAttributes": { "ranConfigurationStructure":
          { "energySavingControl": "toBeEnergySaving" } } } ] } ] } }

The SDK builders assemble these; the call returns the parsed Control Outcome.

from airpuls_ric_sdk import (
    CCC_CONTROL_STYLE_CELL_LEVEL,
    ccc_control_header, ccc_control_message_cell, ccc_nr_cgi,
)

# `node` from on_e2_node_available; `cgi` from a prior cell indication
# (or built explicitly).  Issue the control:
cgi = ccc_nr_cgi("001", "01", "000000010")
header = ccc_control_header(CCC_CONTROL_STYLE_CELL_LEVEL)
message = ccc_control_message_cell(
    cgi,
    [("O-CESManagementFunction",
      {"energySavingControl": "toBeNotEnergySaving"},   # old
      {"energySavingControl": "toBeEnergySaving"})],     # new
)

outcome = client.control_ccc(node, header, message)      # blocks until Ack
fmt = outcome["controlOutcomeFormat"]
for cell in fmt.get("listOfCellsForControlOutcome", []):
    for acc in cell.get("ranConfigurationStructuresAcceptedList", []):
        cur = acc["currentValuesOfAttributes"]["ranConfigurationStructure"]
        print("accepted:", acc["ranConfigurationStructureName"], cur)
    for fail in cell.get("ranConfigurationStructuresFailedList", []):
        print("failed:", fail["ranConfigurationStructureName"], fail["cause"])
import "github.com/airpuls/ric-client-go/ric"

// `node` captured from OnE2NodeAvailable.
cgi := ric.CccNRCGI("001", "01", "000000010")
header := ric.CccControlHeader(ric.CccControlStyleCellLevel)
message := ric.CccControlMessageCell(cgi, []ric.CccWrite{{
    StructureName: "O-CESManagementFunction",
    Old:           map[string]any{"energySavingControl": "toBeNotEnergySaving"},
    New:           map[string]any{"energySavingControl": "toBeEnergySaving"},
}})

outcome, err := client.ControlCCC(node, header, message) // blocks until Ack
if err != nil {
    log.Printf("control failed: %v", err)
    return
}
log.Printf("control outcome: %v", outcome["controlOutcomeFormat"])
#include "e2sm-ccc.h"   /* ric_client_control_ccc, ccc_free */

const char *header =
    "{\"controlHeaderFormat\":{\"ricStyleType\":2}}";
const char *message =
    "{\"controlMessageFormat\":{\"listOfCellsControlled\":[{"
    "\"cellGlobalId\":{\"plmnIdentity\":{\"mcc\":\"001\",\"mnc\":\"01\"},"
    "\"nRCellIdentity\":\"000000010\"},"
    "\"listOfConfigurationStructures\":[{"
    "\"ranConfigurationStructureName\":\"O-CESManagementFunction\","
    "\"oldValuesOfAttributes\":{\"ranConfigurationStructure\":"
    "{\"energySavingControl\":\"toBeNotEnergySaving\"}},"
    "\"newValuesOfAttributes\":{\"ranConfigurationStructure\":"
    "{\"energySavingControl\":\"toBeEnergySaving\"}}}]}]}}";

uint8_t *outcome = NULL;
size_t   outcome_len = 0;
int rc = ric_client_control_ccc(client, node,
    (const uint8_t *)header, strlen(header),
    (const uint8_t *)message, strlen(message),
    /*ack_required=*/true, &outcome, &outcome_len);

if (rc == RIC_OK && outcome) {
    printf("control outcome: %.*s\n", (int)outcome_len, outcome);
    ccc_free(outcome);          /* caller owns the outcome buffer */
}

Optimistic concurrency

The Control Message carries both the old and new attribute values. The E2 node matches the old values against the live configuration before applying the new ones; a mismatch (or a non-writable attribute) yields a Failed outcome entry with a cause (NotSupported, IncompatibleState, …) rather than a silent no-op.


7. Reference

SDK surface

Concept Python Go C
Plugin ccc_plugin_get() ric.CccPluginGet() ccc_plugin_get()
RAN Function ID CCC_RAN_FUNC_ID (4) ric.CccRanFuncID CCC_RAN_FUNC_ID
Subscribe client.subscribe_ccc(node, ad, et) client.SubscribeCCC(node, ad, et) ric_client_subscribe_ccc(...)
Control client.control_ccc(node, hdr, msg) client.ControlCCC(node, hdr, msg) ric_client_control_ccc(...)
Read indication data.ccc_json() data.CccJSON() ccc_indication_json(data)
Action Def (node/cell) ccc_action_definition_node/_cell(...) ric.CccActionDefinitionNode/_Cell(...) build JSON
Event Trigger — periodic ccc_event_trigger_periodic(ms) ric.CccEventTriggerPeriodic(ms) build JSON
Event Trigger — on change ccc_event_trigger_node_change(names), ccc_event_trigger_cell_change(names) — (build the map by hand) build JSON
Control header/message ccc_control_header, ccc_control_message_cell/_node ric.CccControlHeader, ric.CccControlMessageCell/Node build JSON
NR-CGI ccc_nr_cgi(mcc, mnc, nci) ric.CccNRCGI(...) build JSON

The data plane is JSON, so in every language you ultimately work with your native JSON type (dict / map[string]any / your chosen C JSON parser; the MIT-licensed cJSON is bundled in libairpuls_common).

Running the bundled example

A complete reference xApp ships at xapps/ccc-monitor/ — it subscribes to both REPORT styles, demonstrates a CONTROL, and streams the configuration reports to the telemetry sinks. Run it natively or in Docker exactly like the other xApps (see Packaging):

./build_docker.sh ccc-monitor
docker run -v $(pwd)/xapps/ccc-monitor/xapp.yml:/etc/airpuls/xapp.yml \
    ccc-monitor-python:latest

Configuration

CCC xApps use the same xapp.yml schema as every other xApp — see SDK → Configuration. Nothing CCC-specific lives in YAML; the what/when of a subscription is expressed in code via the Action Definition + Event Trigger.