This guide is for developers who want to start writing APIC code as fast as possible. If you're looking for the broader system-engineering process instead (requirements → tests), see How to Build an APIC System for Software Engineers. This page assumes that process is already underway and focuses on where your code actually goes and what's available to you while writing it.
1. Your Logic Lives in Models and States
Almost all of your application logic belongs in two places: Models and States. Everything else — DBSim, drivers, streams — exists to feed data into and out of these two.
-
A Model is a logical unit representing a subsystem or functional domain. It runs at a configured
rate, reacts to engine lifecycle events (
OnInit,OnRun,OnReset, ...), owns one or more state controllers, and can depend on other models — declare those dependencies explicitly so the scheduler orders execution correctly. - A State is the smallest logical execution unit inside a model, grouped under a State Controller. Only one state per controller runs at a time; states switch to each other based on explicit conditions you define.
2. External Interfaces: Streams, Drivers, and Vendor APIs
Data gets in and out of your models through three distinct layers, each with a specific job:
| Layer | Job |
|---|---|
| Stream | The interface with the ICD structures — translates raw bytes into ICD-compliant blocks your models and states consume, and back again. |
| Driver | The bridge between a stream and hardware — the CDriverBase subclass a stream talks to (Send()/Recv()). |
| API layer | The actual low-level interface to the hardware vendor's SDK, built as its own shared library that the driver loads at runtime. |
Both streams and drivers are defined per project under interfaces/ and wired together in
streams.json. See Drivers and Streams for the full
folder layout and the driver/API-layer split, and streams.json for the configuration format.
3. Raw ↔ Eng Conversion and Bit/Byte Conventions
Every ICD block has a raw form (exact wire bytes) and an eng (engineering) form — the
plain C++ value your code reads and writes. The conversion call is made once per LRU (it converts
every block belonging to that LRU in one call), not per individual block. Convert explicitly in OnRun():
CLRUsProj::{{LRUName}}->Raw2Eng(); // after new raw data arrived, converts all of this LRU's blocks
// ... work with eng-side elements ...
CLRUsProj::{{LRUName}}->Eng2Raw(); // if you modified them and need to send
Which conversion is used for a given block is a per-block property set once in DBSimGenerator:
| Convention | Meaning |
|---|---|
NoRaw | No physical/binary raw representation — nothing to convert. |
LElsb | Little-Endian bytes, bits numbered from the LSB (natural C/C++ convention). |
LEmsb | Little-Endian bytes, bits numbered from the MSB. |
BElsb | Big-Endian bytes (wire order), bits numbered from the LSB. |
BEmsb | Big-Endian bytes, bits numbered from the MSB — common in avionics/MIL-STD ICDs. |
OnRun().
4. Command-Line Arguments
Read any name="value" argument passed to the executable from a model's OnInit():
// Usage: projectApp.exe myArg="value"
std::string myArgValue;
getCmdOption(SystemConfig.arguments, "myArg", myArgValue);
if (cmdOptionExists(SystemConfig.arguments, "?"))
{
// print usage/help and exit
}
config argument
(projectApp.exe config="[path]/config.json") — see Command-Line Argument Utilities for details.
5. Component Versioning
Every model and driver passes its own version string to its base constructor:
C{{ModelName}}::C{{ModelName}}() : CModelBase("{{ModelName}}", "1.00", {{{CoreAffinityArray}}}) {}
CSampleDeviceDriver::CSampleDeviceDriver(const char* streamName)
: CDriverBase(streamName, "1.00", "SampleDeviceAPI", "") {}
There's no separate build step — to "release" a new version of a model or driver, update that string literal. It's shown live per-component in DIPanel's Core States and System Layout, so mismatched versions across a deployment are easy to spot.
6. Fast-Start Toolkit
A quick map of the capabilities available from inside a model, and where to read more:
| Capability | Use it for | Details |
|---|---|---|
CMinorFrameGate | Run a shared code path at most once per minor frame, however many times it's called. | Utilities |
CValueChangeGate | Skip processing when an element hasn't changed since last check. | Utilities |
Async tasks + PhaseCoordinator | Fire-and-forget background work, or a thread synced to the frame cycle via a barrier. | Async Commands |
| Scope/manual profiling | Lightweight timing of a task in the hot path. | Profiling |
| Stream triggers & callbacks | Run a model only when specific data arrives; hook pre-send/post-receive/receive. | Streams, IO & Recording |
| Direct stream send/receive | CDrivers::SendBlock("StreamName", pRaw, rawSize) to send raw bytes straight to a stream's driver, or CStreams::GetStream("StreamName") to get the stream object itself and call its own send/receive methods directly. | Drivers and Streams |
| Recorder API | Set recording path, flush cadence, and per-element recording properties from code. | Recording |
| Custom keyboard handling | React to key presses inside the CoreEngine process itself. | Models guide |
| Memory monitor | Detect leaks/unusual allocations (enable from exactly one model). | Utilities |
| EngineSync | Align frame progression to an external hardware timing reference. | EngineSync |
| OGI init | Initialize the operator interface (call once from exactly one model). | OGI |