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.
| Image | File | Purpose |
|---|---|---|
| icon.png | Icon 32 x 32 (for Sidebar & Header) | |
| thumbnail.png | Thumbnail 228 x 86 (for game profiles) | |
![]() | featured.png | Image 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 Type | Recommended Rate |
|---|---|
| LCD dashboard | 30-60 Hz |
| LEDs | 30-100 Hz |
| motion platform | 200-1000 Hz |
| telemetry logger | 10-60 Hz |
| network sender | 30-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:
| Setting | Value |
|---|---|
| Visual Studio | 2019 |
| Language Standard | C++17 |
| Runtime | Multi-threaded DLL |
| Configuration Type | Dynamic Library (.dll) |
| Character Set | Unicode |
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; }

