Creating an OUT Plugin

This chapter explains how to create an OpenSR OUT plugin in C++ using the IOpenSRPlugin interface.

Unlike GAME plugins, OUT plugins do not produce telemetry.

Instead, they consume telemetry already normalized by OpenSR and use it to:

  • drive external hardware
  • feed displays
  • control dashboards
  • send telemetry over network protocols
  • interface with motion systems
  • generate telemetry logs
  • monitor telemetry
  • forward telemetry to external applications
  • render UI
  • expose monitoring/debugging tools

This chapter uses the Monitor Telemetry Plugin from the OpenSR SDK as a production example.

The goal is NOT to teach how to create a telemetry monitor specifically.

The goal is to teach how to create a robust OUT plugin architecture that can later be adapted for:

  • LCD dashboards
  • motion platforms
  • LEDs
  • CAN devices
  • Arduino controllers
  • UDP forwarders
  • serial devices
  • telemetry analyzers
  • web telemetry services
  • custom hardware
  • stream overlays
  • debugging tools

1. OUT Plugin Role

A GAME plugin writes telemetry into OpenSR.

An OUT plugin reads telemetry from OpenSR.

Game Plugin
    ↓
OpenSR Core
    ↓
OUT Plugin

The OUT plugin never talks directly to the game.

It only consumes standardized OutSimData.

This abstraction is one of the most important OpenSR concepts.

It means your hardware/device/service plugin works with:

  • BeamNG
  • Assetto Corsa
  • iRacing
  • ACC
  • AMS2
  • RF2
  • custom games

without rewriting your device logic.


2. OUT Plugin File Extension

OUT plugins use:

.osro

Example:

OpenSRMonitorPlugin.osro

GAME plugins use .osri.


3. SDK Location

If the SDK component was installed from the OpenSR installer:

Documents/OpenSR/OpenSR_SDK1.0/

Typical structure:

OpenSR_SDK1.0/
│
├── includes/
├── examples/
└── docs/

4. Plugin Installation Directory

OUT plugins are installed inside:

Documents/OpenSR/Plugins/

Example:

Documents/OpenSR/Plugins/OpenSRMonitorPlugin/

Typical structure:

OpenSRMonitorPlugin/
│
├── OpenSRMonitorPlugin.osro
├── settings.xml
├── icon.png
├── thumbnail.png
├── featured.png
└── ui.dll (optional)

5. Mandatory Images

Every plugin should provide standardized artwork.

ImageFilePurpose
icon.pngIcon 32 x 32 (for Sidebar & Header)
thumbnail.pngThumbnail 228 x 86 (for game profiles)
featured.pngImage 512 x 282 (for Info page)

These images are used by the OpenSR dashboard and plugin manager.


6. OUT Plugin Architecture

The monitor plugin demonstrates several important production concepts:

  • OUT telemetry consumption
  • runtime threads
  • Win32 UI
  • telemetry rendering
  • XML settings
  • runtime settings reload
  • pause/resume synchronization
  • external windows
  • dashboard companion UI modules
  • telemetry dumping
  • plugin-managed resources

7. Basic OUT Plugin Skeleton

Minimal structure:

class MyOutPlugin : public osr::IOpenSRPlugin
{
public:

    bool Init(
        OpenSRContext* context,
        void* outBuf,
        const wchar_t* pluginPath) override;

    bool Start() override;

    void Stop() override;

    void Shutdown() override;

    void Pause() override;

    void Resume() override;

    bool IsRunning() const override;

    int GetType() override
    {
        return OUT_PLUGIN_TYPE;
    }
};

8. OUT Plugin Metadata

Metadata is mandatory.

Without metadata the dashboard cannot identify the plugin correctly.

Example from the monitor plugin:

void GetPluginName(
    char* buffer,
    size_t bufferSize) const override
{
    strcpy_s(
        buffer,
        bufferSize,
        "Monitor Telemetry (Example)");
}

Required metadata:

GetPluginName()
GetAuthor()
GetVersion()
GetLicenseType()
GetDescription()
GetPackageName()
GetType()

OUT plugins leave these empty:

GetTargetName()
GetTargetProcessName()

because they are not tied to a specific game executable.


9. Factory Exports

Every OUT plugin must export:

extern "C"
__declspec(dllexport)
osr::IOpenSRPlugin* CreatePlugin()
{
    return new MyOutPlugin();
}

extern "C"
__declspec(dllexport)
void DestroyPlugin(
    osr::IOpenSRPlugin* plugin)
{
    delete plugin;
}

These exports are mandatory.

OpenSR loads plugins dynamically through them.


10. Init()

Init() is called once after the plugin is loaded.

Typical responsibilities:

  • store context pointers
  • store shared buffers
  • store plugin path
  • create directories
  • prepare resources
  • build settings paths

Example:

bool OpenSRMonitorPlugin::Init(
    OpenSRContext* context,
    void* outBuf,
    const wchar_t* pluginPath)
{
    m_pContext = context;

    m_pBuffersOut =
        static_cast<OpenSRBuffersOUT*>(outBuf);

    m_pluginPath = pluginPath;

    return true;
}

11. Accessing Telemetry

OUT plugins read telemetry through:

m_pBuffersOut->outSimDataOUT

Example:

OutSimData* outSim =
    m_pBuffersOut->outSimDataOUT;

This structure contains normalized telemetry from all supported games.


12. Reading Telemetry

Example:

float speed =
    outSim->mVehicleData.speed;

float rpm =
    outSim->mVehicleData.rpm;

int gear =
    outSim->mVehicleData.gear;

This telemetry is already normalized by GAME plugins.

Your OUT plugin should NEVER care which game generated it.


13. Worker Threads

OUT plugins usually run one or more worker threads.

Examples:

  • serial writer thread
  • UDP sender thread
  • USB polling thread
  • display refresh thread
  • Win32 UI thread
  • telemetry logger thread

The monitor plugin uses a dedicated window thread:

windowThread_ =
    std::thread(
        &OpenSRMonitorPlugin::WindowThreadFunc,
        this);

14. Pause / Resume

OUT plugins must support pause/resume.

Example:

void Pause() override
{
    m_isPluginPaused = true;
}
void Resume() override
{
    m_isPluginPaused = false;

    m_pauseCv.notify_one();
}

This allows OpenSR to synchronize all plugins consistently.


15. Start()

Recommended responsibilities:

  • load settings
  • start threads
  • initialize devices
  • initialize sockets
  • open serial ports
  • create windows
  • initialize SDKs

Example:

bool OpenSRMonitorPlugin::Start()
{
    if (!m_pContext)
        return false;

    m_stopRequested = false;

    m_isPluginRunning = true;

    windowThread_ =
        std::thread(
            &OpenSRMonitorPlugin
                ::WindowThreadFunc,
            this);

    return true;
}

16. Stop()

Stop() must safely stop everything.

Typical responsibilities:

  • stop threads
  • close sockets
  • close serial devices
  • stop timers
  • destroy windows
  • stop SDKs
  • join worker threads

Example:

void OpenSRMonitorPlugin::Stop()
{
    m_stopRequested = true;

    if (windowThread_.joinable())
    {
        windowThread_.join();
    }

    m_isPluginRunning = false;
}

17. Runtime Settings

OUT plugins can provide optional:

settings.xml

Example:

<settings>
    <option
        name="device update delay (ms)"
        tooltip="update interval"
        type="slider"
        range="10,1000">200</option>
</settings>

18. Loading Settings

The monitor plugin uses TinyXML2.

Include:

#include "tinyxml2.h"

using namespace tinyxml2;

Example:

struct PluginSettings
{
    int deviceRate = 200;
};

Example loader:

bool LoadPluginSettings(
    const std::wstring& filepath,
    PluginSettings& s)
{
    XMLDocument doc;

    FILE* file =
        _wfsopen(
            filepath.c_str(),
            L"rb",
            _SH_DENYNO);

    if (!file)
        return false;

    if (doc.LoadFile(file) != XML_SUCCESS)
    {
        fclose(file);
        return false;
    }

    fclose(file);

    XMLElement* root =
        doc.FirstChildElement("settings");

    if (!root)
        return false;

    for (XMLElement* opt =
            root->FirstChildElement("option");
         opt;
         opt = opt->NextSiblingElement("option"))
    {
        const char* name =
            opt->Attribute("name");

        if (!name)
            continue;

        if (strcmp(
                name,
                "device update delay (ms)") == 0)
        {
            s.deviceRate =
                opt->IntText(200);
        }
    }

    return true;
}

19. Building Settings Path

Recommended pattern:

std::wstring spath =
    StringUtils::createWString(
        m_pContext->osrDocFolder,
        L"\\",
        m_pluginPath,
        L"\\settings.xml");

This guarantees compatibility with OpenSR folder conventions.


20. Runtime Settings Reload

Recommended approach:

void OnContextChanged(
    osr::OpenSRContextChange reason)
{
    if (reason ==
        osr::OpenSRContextChange
            ::SettingsChanged)
    {
        Stop();
        Start();
    }
}

Recommended practice:

  • reload settings inside Start()
  • restart runtime cleanly
  • recreate sockets/devices safely
  • avoid partial reload states

21. Rendering Windows

The monitor plugin demonstrates a full Win32 rendering loop.

This is optional.

Most OUT plugins do not need UI.

Typical OUT plugins instead:

  • send serial packets
  • send UDP packets
  • write HID reports
  • update LEDs
  • control displays

22. External UI vs Dashboard UI

The monitor plugin supports two UI approaches:

External Window

Standalone runtime window.

Dashboard UI Module

Separate ui.dll loaded directly inside the OpenSR dashboard.

This separation is recommended:

Runtime Device Logic
    ≠
Dashboard UI Logic

23. Telemetry Rates

OUT plugins should usually NOT run at unrestricted speed.

Typical device update rates:

Device TypeRecommended Rate
LCD dashboard30-60 Hz
LEDs30-100 Hz
motion platform200-1000 Hz
telemetry logger10-60 Hz
network sender30-120 Hz

The monitor plugin demonstrates configurable update timing.


24. Accessing All Telemetry Categories

The monitor plugin demonstrates access to:

  • packet header
  • players
  • vehicle data
  • wheel data
  • brakes
  • suspensions
  • environment
  • motion data

This makes it an excellent SDK reference.


25. OUT Plugins Can Produce Inputs

OUT plugins may also fill the Input structure.

Examples:

  • wheel buttons
  • custom button boxes
  • encoders
  • switches
  • axes

This is explained in the SDK architecture chapters.


26. Recommended OUT Plugin Architecture

Production recommendations:

Main Plugin
    ├── Device Thread
    ├── Telemetry Thread
    ├── UI Thread
    ├── Settings Loader
    ├── Protocol Encoder
    └── Device SDK Wrapper

Keep hardware/protocol logic separated from plugin lifecycle logic.


27. Recommended Visual Studio Settings

Recommended:

SettingValue
Visual Studio2019
Language StandardC++17
RuntimeMulti-threaded DLL
Configuration TypeDynamic Library (.dll)
Character SetUnicode

After build:

.dll → .osro

28. Final Recommendations

A robust OUT plugin should:

  • never block the OpenSR core
  • use worker threads
  • safely stop all threads
  • support pause/resume
  • support runtime settings reload
  • validate device communication
  • tolerate telemetry loss
  • tolerate game switching
  • avoid hardcoded game logic
  • rely only on normalized OpenSR telemetry

The monitor plugin is intentionally complex because it demonstrates many real production patterns used by advanced OUT plugins.

Out Plugin Template

MyPlugin.h

#pragma once

#include <string>
#include <thread>
#include <atomic>
#include <mutex>
#include <condition_variable>
#include <iostream> // For std::wcout
#include "IOpenSRPlugin.h"
#include "OutSimData.h"
#include "StringUtils.h" // Your string utility header

// -----------------------------------------------------------------------------
// Plugin Configuration Struct
// -----------------------------------------------------------------------------
struct PluginSettings {
    // Example: int ledBlinkRate = 100;
    // Example: std::string deviceId = "";
};

// -----------------------------------------------------------------------------
// Hardware Abstraction
// -----------------------------------------------------------------------------
class IHardwareManager {
public:
    virtual ~IHardwareManager() = default;
    virtual bool Initialize() = 0;
    virtual void SendReport(DeviceReport* report, bool forceClear) = 0;
    virtual void Close() = 0;
    virtual bool IsConnected() const = 0;
};

class DeviceReport {
public:
    float speed = 0.0f;
    float rpm = 0.0f;
    int32_t gear = 0;
    uint32_t leds = 0; 
};

class MyHardwareDriver : public IHardwareManager {
public:
    DeviceReport m_deviceState;

    bool Initialize() override {
        // Placeholder: Init hardware
        return true; 
    }
    
    void SendReport(DeviceReport* report, bool forceClear) override {
        if (forceClear) {
            m_deviceState.leds = 0; 
        } else {
            m_deviceState.speed = report->speed;
            m_deviceState.rpm = report->rpm;
            m_deviceState.leds = 0xFF; 
        }
    }
    
    void Close() override {}
    bool IsConnected() const override { return true; }
};

// -----------------------------------------------------------------------------
// Plugin Class
// -----------------------------------------------------------------------------
class MyPlugin : public osr::IOpenSRPlugin
{
public:
    MyPlugin();
    ~MyPlugin() override;

    // Lifecycle
    bool Init(osr::OpenSRContext* context, void* buffers, const wchar_t* pluginPath) override;
    bool Start() override;
    void Pause() override;
    void Resume() override;
    bool IsRunning() const override;
    void Stop() override;
    void Shutdown() override;
    
    // Logic Hooks
    void Idle();
    void OnContextChanged(osr::OpenSRContextChange reason) override;

	// Metadata
	
	// Package name must be unique
    inline void GetPackageName(char* buffer, size_t bufferSize) const override { strncpy_s(buffer, bufferSize, "com.dev.mydevice", bufferSize); }
    
	inline void GetPluginName(char* buffer, size_t bufferSize) const override { strncpy_s(buffer, bufferSize, "Device Plugin Template", bufferSize); }
    inline void GetAuthor(char* buffer, size_t bufferSize) const override { strncpy_s(buffer, bufferSize, "Developer", bufferSize); }
    inline void GetVersion(char* buffer, size_t bufferSize) const override { strncpy_s(buffer, bufferSize, "1.0.0", bufferSize); }
    inline void GetLicenseType(char* buffer, size_t bufferSize) const override { strncpy_s(buffer, bufferSize, "MIT", bufferSize); }
    inline void GetDescription(char* buffer, size_t bufferSize) const override { strncpy_s(buffer, bufferSize, "Generic Hardware Template Plugin", bufferSize); }
    
    // Not relevant for OUT Plugin (Default empty string)
	// name of target game for IN plugin
    inline void GetTargetName(char* buffer, size_t bufferSize) const override { strncpy_s(buffer, bufferSize, "", bufferSize); }
    // process exe name of target game for IN plugin
	inline void GetTargetProcessName(char* buffer, size_t bufferSize) const override { strncpy_s(buffer, bufferSize, "", bufferSize); }
    
	// Group name is the filename of all plugins that share the same game profile 
	// Example of group name IN plugin: Automobilista2 for plugins AMS2.exe, AMS2_AVX.exe and AMS2_Demo.exe
	// Example for out plugin: OpenSRVDash for plugins vdash1, vdash2, vdash3, vdash4
	inline void GetPluginGroupName(wchar_t* buffer, size_t bufferSize) const override { wcscpy_s(buffer, bufferSize, L""); }
    
	// Optional: Set UI TAB Name (Default 'Plugin')
    // inline void GetUITabName(char* buffer, size_t bufferSize) const override { strncpy_s(buffer, bufferSize, "My Plugin", bufferSize); }
    // Optional: Set Settings TAB Name (Default 'Settings')
    // inline void GetSettingsTabName(char* buffer, size_t bufferSize) const override { strncpy_s(buffer, bufferSize, "My Settings", bufferSize); }
    
    inline int GetType() override { return osr::OUT_PLUGIN_TYPE; }
    inline bool IsBackend() const override { return false; }

private:
    // Core State
    osr::OpenSRContext* m_pContext = nullptr;
    void* m_pBuffersOut = nullptr;
    std::wstring m_pluginPath;
    
    // Hardware
    MyHardwareDriver m_hardware;

    // Thread Management
    std::atomic<bool> m_isRunning = false;
    std::atomic<bool> m_stopRequested = false;
    std::thread m_workerThread;

    // Dashboard Pause / Kill Switch
    std::atomic<bool> m_isPluginPaused = false;
    std::mutex m_pauseMutex;
    std::condition_variable m_pauseCv;

    // Session State Machine
    std::atomic<bool> m_isSessionActive = false;

    // Report Struct
    DeviceReport m_report;
    PluginSettings m_pluginSettings;
    bool m_debugMode = false;

    // Worker Loop
    void WorkerLoop();

    // Logic
    void UpdateReport(const OutSimData* simData);
    void SendReport(bool forceClear);
    void EnterSession();
    void ExitSession();

    // Configuration Loading
    bool LoadPluginSettings(const std::wstring& path, PluginSettings& settings);
    void LoadConfiguration(const std::wstring& profilePath);
    void CheckDevice();
};

MyPlugin.cpp

#include "MyPlugin.h"
#include <cstring>

MyPlugin::MyPlugin() {
    m_isSessionActive = false;
}

MyPlugin::~MyPlugin() {
    m_stopRequested = true;
    m_pauseCv.notify_one();
    if (m_workerThread.joinable()) m_workerThread.join();
}

// -----------------------------------------------------------------------------
// Lifecycle
// -----------------------------------------------------------------------------

bool MyPlugin::Init(osr::OpenSRContext* context, void* buffers, const wchar_t* pluginPath) {
    m_pContext = context;
    m_pBuffersOut = buffers;
    m_pluginPath = pluginPath;
    return true;
}

bool MyPlugin::Start() {
    // 1. Initialize State Flags
    m_isSessionActive = false;
    m_report = DeviceReport(); 

    // 2. Initialize Hardware
    if (!m_hardware.Initialize()) {
        return false;
    }

    // 3. PLUGIN SETTINGS (not game profile)
    if (!m_pluginPath.empty()) {
        // Construct path: Documents/MyPluginFolder/settings.xml
        std::wstring settingsPath = StringUtils::createWString(m_pContext->osrDocFolder, L"\\", m_pluginPath, L"\\settings.xml");
        
        if (LoadPluginSettings(settingsPath, m_pluginSettings)) {
            std::wcout << "[TEMPLATE PLUGIN] Settings is loaded.\nSettings Path: " << settingsPath << std::endl;
            return true;
        }
    }
    
    // If path is empty or settings failed
    return false;
}

void MyPlugin::Pause() { m_isPluginPaused = true; }
void MyPlugin::Resume() {
    m_isPluginPaused = false;
    m_pauseCv.notify_one();
}
bool MyPlugin::IsRunning() const { return m_isRunning; }

void MyPlugin::Stop() {
    m_stopRequested = true;
    m_pauseCv.notify_one();
    
    if (m_isSessionActive) ExitSession(); 
    if (m_workerThread.joinable()) m_workerThread.join();
    m_isRunning = false;
    m_hardware.Close();
}

void MyPlugin::Shutdown() {}

// -----------------------------------------------------------------------------
// Context Change Handling
// -----------------------------------------------------------------------------

void MyPlugin::OnContextChanged(osr::OpenSRContextChange reason) {
    if (m_pContext && reason == osr::OpenSRContextChange::ProfilePathChanged) {
        std::wstring profilePath = m_pContext->currentProfilePath;
        if (!profilePath.empty()) {
            std::wcout << "[TEMPLATE PLUGIN] Profile needs to be reloaded.\nProfile Path: " << profilePath << std::endl;
            LoadConfiguration(profilePath);
        }
    }
    else if (m_pContext && reason == osr::OpenSRContextChange::SettingsChanged) {
        if (!m_pluginPath.empty()) {
            std::wstring settingsPath = StringUtils::createWString(m_pContext->osrDocFolder, L"\\", m_pluginPath, L"\\settings.xml");
            if (m_debugMode) std::wcout << "[TEMPLATE PLUGIN] Settings needs to be reloaded.\nSettings Path: " << settingsPath << std::endl;
            
            if (LoadPluginSettings(settingsPath, m_pluginSettings)) {
                // Settings updated successfully
            }
        }
    }
    else if (reason == osr::OpenSRContextChange::CheckDevice) {
        CheckDevice();
    }
}

// -----------------------------------------------------------------------------
// Logic
// -----------------------------------------------------------------------------

void MyPlugin::UpdateReport(const OutSimData* simData) {
    m_report.speed = simData->mVehicleData.speed;
    m_report.rpm = simData->mVehicleData.rpm;
    m_report.gear = simData->mVehicleData.gear;
}

void MyPlugin::SendReport(bool forceClear) {
    m_hardware.SendReport(&m_report, forceClear);
}

void MyPlugin::Idle() {
    // Called when no report is available or game is paused.
    // Force clear the device elements.
    SendReport(true); 
}

void MyPlugin::EnterSession() {
    std::wcout << "[TEMPLATE PLUGIN] Entering Session..." << std::endl;
}

void MyPlugin::ExitSession() {
    std::wcout << "[TEMPLATE PLUGIN] Exiting Session..." << std::endl;
}

void MyPlugin::WorkerLoop() {
    if (!m_pContext || !m_pBuffersOut) return;
    
    m_isRunning = true;

    while (!m_stopRequested && m_pContext->isAppRunning) {
        
        // 1. Dashboard Pause/Kill Switch
        {
            std::unique_lock<std::mutex> lock(m_pauseMutex);
            m_pauseCv.wait(lock, [this] { return !m_isPluginPaused; });
        }

        if (m_stopRequested || !m_pContext->isAppRunning) break;

        // 2. Cast Shared Memory
        const OutSimData* simData = static_cast<const OutSimData*>(m_pBuffersOut);
        
        const bool reportAvailable = (simData->mPacketHeader.reportAvailable == 1);
        const bool isPaused = (simData->mPacketHeader.isPaused == 1);

        // 3. State Machine: Active vs Idle
        if (!reportAvailable || isPaused) {
            // Game Paused / No Data
            Idle(); 
            
            // Exit Session if we were active
            if (m_isSessionActive) {
                ExitSession();
                m_isSessionActive = false;
            }
        } else {
            // Game Running
            if (!m_isSessionActive) {
                EnterSession();
                m_isSessionActive = true;
            }
            
            UpdateReport(simData);
            SendReport(false); 
        }

        // 4. Sleep (approx 60Hz)
        std::this_thread::sleep_for(std::chrono::milliseconds(16));
    }
    
    m_isRunning = false;
}

// -----------------------------------------------------------------------------
// Configuration & Device Logic
// -----------------------------------------------------------------------------

bool MyPlugin::LoadPluginSettings(const std::wstring& path, PluginSettings& settings) {
    // Stub: Load XML/JSON/INI file
    return true;
}

void MyPlugin::LoadConfiguration(const std::wstring& profilePath) {
    // Stub: Load specific profile data (e.g. Shift Light RPMs, Dashboard mappings)
}

void MyPlugin::CheckDevice() {
    // Stub: Verify hardware connection, ping device, etc.
    std::wcout << "[TEMPLATE PLUGIN] Checking device connection..." << std::endl;
}

// -----------------------------------------------------------------------------
// Factory
// -----------------------------------------------------------------------------
OpenSR_PLUGIN_API osr::IOpenSRPlugin* CreatePlugin() { return new MyPlugin(); }
OpenSR_PLUGIN_API void DestroyPlugin(osr::IOpenSRPlugin* plugin) { delete plugin; }