How to Develop Drivers and Streams

Implement a deterministic hardware driver, connect it to a stream, and register it with the engine.

This guide shows how to implement a driver and its matching stream, register the driver with the engine, and keep the data path deterministic and safe.

0. Send a Block From a Model (the common case)

Most model code never implements a new driver or stream - it just publishes an already-configured ICD block on a stream that already exists. This is unrelated to EngineSync: EngineSync only synchronizes engine frame timing against an external signal, it is not how you send or receive stream data. The rest of this guide (1 onward) is only for implementing a new driver/stream from scratch.

void CMyModel::SendStreamMessage(const char* blockName)
{
    uint64_t engSize, rawSize;

    // Get the block's Eng buffer and set the fields you want to send
    auto pEng = CLRUsProj::LRU_MyLru->GetBlockEng(blockName, engSize);
    // ... set fields on pEng (cast to the block's struct type) ...

    // Convert Eng -> Raw, then send the Raw bytes on the stream
    CLRUsProj::LRU_MyLru->Eng2RawByName(blockName);
    auto pRaw = CLRUsProj::LRU_MyLru->GetBlockRaw(blockName, rawSize);

    auto result = CDrivers::SendBlock("streamName", pRaw, rawSize);

    // Optional bookkeeping
    if (result > CDriverBase::EIOResult::eNoIO)
        CStreams::IncBlockMessageCounter(pEng);
    else if (result == CDriverBase::EIOResult::eFail)
        CStreams::IncBlockMessageErrCounter(pEng);
    CStreams::SetBlockTimeStamp(pEng);
}
CLRUsProj::LRU_MyLru is your project's generated LRU accessor (one per LRU, named after it) - it's how you reach a block's Eng/Raw buffers by name. CDrivers::SendBlock(streamName, rawPtr, rawSize) is the actual send call. The CStreams::... helpers are optional message/error counters and timestamp bookkeeping - not required to send data.

0b. Get a Block's Raw or Eng Data (the common case)

To just read a block's data - no sending involved - call the same LRU accessor directly. This is unrelated to implementing a driver's Recv() loop (section 4 below is only for building a new driver/stream from scratch):

uint64_t engSize, rawSize;
auto pEng = CLRUsProj::LRU_MyLru->GetBlockEng(blockName, engSize); // eng-side pointer + size
auto pRaw = CLRUsProj::LRU_MyLru->GetBlockRaw(blockName, rawSize); // raw-side pointer + size

If you need every block of the LRU converted at once (typically once per OnRun(), right after new raw data has arrived on a stream) use the whole-LRU conversion instead of a per-block call - see Raw ↔ Eng Conversion in the Model guide:

void CMyModel::OnRun()
{
    CLRUsProj::LRU_MyLru->Raw2Eng(); // converts every block of this LRU, raw -> eng
    // ... read the eng-side elements of any of this LRU's blocks here ...
}

1. Understand the Architecture

Drivers provide low-level I/O access to hardware. Streams translate the raw bytes drivers exchange into ICD-compliant blocks consumed by models and states. Data flows as follows:

Hardware Device → Driver → Stream → Models / States

2. Implement the Driver

A driver manages the low-level interface with the hardware device and exposes a thin, deterministic API to the simulation engine. Implement:

// driver.cpp – Sample implementation
CSampleDeviceDriver::CSampleDeviceDriver(const char* streamName)
    : CDriverBase(streamName)
{
    InitDriver();
}

void CSampleDeviceDriver::InitDriver()
{
    // Perform hardware initialization
}

CDriverBase::EIOResult CSampleDeviceDriver::Send(const uint8_t* raw, uint64_t size)
{
    // Send raw bytes to hardware
    return CDriverBase::EIOResult::eNoIO;
}

CDriverBase::EIOResult CSampleDeviceDriver::Recv(uint8_t* raw, uint64_t size)
{
    // Read raw bytes from hardware
    return CDriverBase::EIOResult::eNoIO;
}

3. Implement the Stream

Streams translate raw driver messages into structured ICD blocks — the bridge between hardware bytes and internal logic models. Implement:

// stream.cpp – Sample implementation
void CSampleDeviceStream::SetupStream(const CStreamDriver* pStreamDriver)
{
    // Configure max message sizes or block allocations here
}

bool CSampleDeviceStream::ReadMsg()
{
    auto pDriver = (CDriverBase*)pStreamDriver->pDriver;
    // Iterate ICD blocks and perform Recv() calls here
    return true;
}

4. Write the Stream Processing Loop

Poll the driver for each stream block and route results based on the I/O outcome:

for (auto& block : StreamBlocks)
{
    auto result = pDriver->Recv(block->rawPtr, block->rawSizeBytes);
    if (result > EIOResult::eNoIO)
    {
        // Successful receive
        // CallOnReceiveCB(block,...);
        // SetModelTrigger(block);
    }
    else if (result < EIOResult::eNoIO)
    {
        UpdateBlockPropertiesOnStreamError(block);
    }
}

5. Register the Driver

Register your new driver class so the engine can instantiate it:

// DriversProj.cpp
CDrivers::RegisterDriver<CSampleDeviceDriver>(CSampleDeviceDriver::getName());

6. Keep the Data Path Deterministic

Follow this checklist for time-critical driver and stream code:

Additional best practices:

7. Use Supporting Building Blocks

For lockless hand-off between threads, use a single-producer/single-consumer queue:

template <typename T, size_t N>
class SPSCQueue {
public:
    bool push(const T& v) {
        const auto next = (head_ + 1) % N;
        if (next == tail_) return false;
        buffer_[head_] = v;
        head_ = next;
        return true;
    }

    bool pop(T& out) {
        if (tail_ == head_) return false;
        out = buffer_[tail_];
        tail_ = (tail_ + 1) % N;
        return true;
    }

private:
    T buffer_[N];
    size_t head_ = 0;
    size_t tail_ = 0;
};

For running tasks at different rates from a common base tick, use a multi-rate scheduler pattern:

struct ScheduledTask {
    int rateHz;
    std::function<void()> callback;
    int accumulator = 0;
};

void RunScheduler(std::vector<ScheduledTask>& tasks, int baseHz) {
    const int step = 1000 / baseHz;
    for (auto& t : tasks) {
        t.accumulator += step;
        if (t.accumulator >= (1000 / t.rateHz)) {
            t.accumulator = 0;
            t.callback();
        }
    }
}

8. Verify the Full Flow

Confirm your driver and stream fit into the overall scheduling flow: Driver Layer (hardware I/O) → Stream Parser (validate & split) → ICD Block Models (model triggers).