Skip to main content

Overview

The IProtocol interface is the base class for all protocol plugins. Protocols are responsible for transmitting tracking data from OpenTrack to games and applications using various communication methods.

Interface Definition

struct IProtocol : module_status_mixin
{
    IProtocol();
    ~IProtocol() override;
    
    // Core methods (must implement)
    virtual void pose(const double* pose, const double* raw) = 0;
    virtual QString game_name() = 0;
    
    // From module_status_mixin
    virtual module_status initialize() = 0;
    
    // Deleted methods (non-copyable)
    IProtocol(const IProtocol&) = delete;
    IProtocol& operator=(const IProtocol&) = delete;
};

Methods

initialize()

virtual module_status initialize() = 0;
Initializes the protocol and prepares for data transmission.
return
module_status
Returns status_ok() on success, or error(message) on failure.
Description:
  • Called once when the protocol is started
  • Open network connections, shared memory, or other resources
  • Validate configuration settings
  • Register with game interfaces
  • Return error if initialization fails
Example implementations:
module_status freetrack::initialize()
{
    // Verify shared memory is available
    if (!shm.success())
        return error(tr("Can't load freetrack memory mapping"));

    // Set up protocol registry entries
    if (auto ret = set_protocols(); !ret.is_ok())
        return ret;

    // Initialize shared memory structure
    pMemData->data.DataID = 1;
    pMemData->data.CamWidth = 100;
    pMemData->data.CamHeight = 250;
    store(pMemData->GameID2, 0);
    
    for (unsigned k = 0; k < 2; k++)
        store(pMemData->table_ints[k], 0);

    // Start helper process if needed
    if (s.used_interface != settings::enable_freetrack)
        start_dummy();

    return status_ok();
}

pose()

virtual void pose(const double* pose, const double* raw) = 0;
Transmits filtered and raw tracking data to the target application.
pose
const double*
Filtered pose data as array: [TX, TY, TZ, Yaw, Pitch, Roll] in degrees/cm
raw
const double*
Raw pose data (before filtering) in same format as pose
This method is called 250 times per second. Keep it fast and avoid blocking operations.
Description:
  • Called at 250Hz from the tracking pipeline thread
  • Transform data to target format
  • Send data via network, shared memory, or other IPC
  • Use background threads for expensive operations
  • Don’t block the calling thread
Data format:
  • pose[TX] / raw[TX]: X translation in centimeters
  • pose[TY] / raw[TY]: Y translation in centimeters
  • pose[TZ] / raw[TZ]: Z translation in centimeters
  • pose[Yaw] / raw[Yaw]: Yaw rotation in degrees
  • pose[Pitch] / raw[Pitch]: Pitch rotation in degrees
  • pose[Roll] / raw[Roll]: Roll rotation in degrees
Example implementation:
void freetrack::pose(const double* headpose, const double* raw)
{
    constexpr double d2r = M_PI/180;  // Degrees to radians

    // Convert to radians and scale translations
    const float yaw = float(-headpose[Yaw] * d2r);
    const float roll = float(headpose[Roll] * d2r);
    const float tx = float(headpose[TX] * 10);  // cm to mm
    const float ty = float(headpose[TY] * 10);
    const float tz = float(headpose[TZ] * 10);

    // Handle pitch discontinuity at 90 degrees
    const bool is_crossing_90 = std::fabs(headpose[Pitch] - 90) < .15;
    const float pitch = float(-d2r * 
        (is_crossing_90 ? 89.86 : headpose[Pitch]));

    // Write to shared memory atomically
    FTHeap* const ft = pMemData;
    FTData* const data = &ft->data;

    store(data->X, tx);
    store(data->Y, ty);
    store(data->Z, tz);
    store(data->Yaw, yaw);
    store(data->Pitch, pitch);
    store(data->Roll, roll);

    // Also store raw data
    store(data->RawYaw, float(-raw[Yaw] * d2r));
    store(data->RawPitch, float(raw[Pitch] * d2r));
    store(data->RawRoll, float(raw[Roll] * d2r));
    store(data->RawX, float(raw[TX] * 10));
    store(data->RawY, float(raw[TY] * 10));
    store(data->RawZ, float(raw[TZ] * 10));

    // Update game detection
    const std::int32_t id = load(ft->GameID);
    if (intGameID != id)
    {
        // New game connected, update game name
        QString gamename;
        getGameData(id, gamename);
        
        QMutexLocker foo(&game_name_mutex);
        connected_game = gamename.isEmpty() ? 
            tr("Unknown game") : gamename;
        intGameID = id;
    }
    
    // Increment frame counter
    InterlockedAdd((LONG volatile*)&data->DataID, 1);
}

game_name()

virtual QString game_name() = 0;
Returns the name of the currently connected game or application.
return
QString
Name of connected game, or placeholder text if none detected
Description:
  • Called periodically from UI thread
  • Return actual game name if detected
  • Return generic text like “Game” if unknown
  • Use mutex protection if name is updated from pose()
Example implementations:
QString freetrack::game_name()
{
    QMutexLocker foo(&game_name_mutex);
    return connected_game;
}

Dialog Interface

struct IProtocolDialog : public BaseDialog
{
    virtual void register_protocol(IProtocol *protocol) = 0;
    virtual void unregister_protocol() = 0;
};

register_protocol()

virtual void register_protocol(IProtocol *protocol) = 0;
Receives a pointer to the active protocol instance.
protocol
IProtocol*
Pointer to the running protocol instance
Description:
  • Called from UI thread when protocol starts
  • Store pointer for runtime interaction (optional)
  • Can query protocol state or display status
  • Often implemented as empty function
Example:
class MyProtocolDialog : public IProtocolDialog
{
    IProtocol* protocol = nullptr;
    
public:
    void register_protocol(IProtocol* p) override
    {
        protocol = p;
        // Could start status update timer here
    }
    
    void unregister_protocol() override
    {
        protocol = nullptr;
    }
};

unregister_protocol()

virtual void unregister_protocol() = 0;
Called when protocol is about to be destroyed. Description:
  • Clear any stored protocol pointers
  • Stop accessing protocol data
  • Clean up any UI state

Complete Example

#pragma once
#include "api/plugin-api.hpp"
#include <QUdpSocket>
#include <QMutex>

class MyProtocol : public IProtocol
{
public:
    MyProtocol();
    ~MyProtocol() override;
    
    module_status initialize() override;
    void pose(const double* pose, const double* raw) override;
    QString game_name() override;
    
private:
    std::unique_ptr<QUdpSocket> socket;
    QHostAddress target_address;
    quint16 target_port;
    
    QMutex name_mutex;
    QString current_game;
};

class MyProtocolDialog : public IProtocolDialog
{
    Q_OBJECT
public:
    MyProtocolDialog();
    void register_protocol(IProtocol* p) override {}
    void unregister_protocol() override {}
};

class MyProtocolMetadata : public Metadata
{
    Q_OBJECT
    QString name() override { return tr("My Protocol"); }
    QIcon icon() override { return QIcon(":/images/icon.png"); }
};

Communication Patterns

Used by FreeTrack/TrackIR protocols for low-latency local communication.
class SharedMemoryProtocol : public IProtocol
{
    shm_wrapper shm{"SharedMemName", "MutexName", sizeof(Data)};
    Data* pData = (Data*)shm.ptr();
    
    module_status initialize() override
    {
        if (!shm.success())
            return error("Failed to create shared memory");
        return status_ok();
    }
    
    void pose(const double* pose, const double* raw) override
    {
        // Atomic writes to shared memory
        InterlockedExchange(&pData->x, pose[TX]);
        InterlockedExchange(&pData->y, pose[TY]);
        // ...
    }
};
Simple datagram-based protocol for network transmission.
void UDPProtocol::pose(const double* pose, const double* raw)
{
    // Format as string
    QString packet = QString("%1,%2,%3,%4,%5,%6")
        .arg(pose[TX]).arg(pose[TY]).arg(pose[TZ])
        .arg(pose[Yaw]).arg(pose[Pitch]).arg(pose[Roll]);
    
    // Send UDP datagram
    socket->writeDatagram(packet.toUtf8(), 
                         target_address, target_port);
}
Emulate joystick or other input device.
void VirtualDeviceProtocol::pose(const double* pose, 
                                  const double* raw)
{
    // Map to joystick axes
    joystick_state.x = map_to_axis(pose[TX], -50, 50);
    joystick_state.y = map_to_axis(pose[TY], -50, 50);
    joystick_state.z = map_to_axis(pose[TZ], -50, 50);
    joystick_state.rx = map_to_axis(pose[Yaw], -180, 180);
    joystick_state.ry = map_to_axis(pose[Pitch], -90, 90);
    joystick_state.rz = map_to_axis(pose[Roll], -180, 180);
    
    // Update virtual device
    device->update_state(joystick_state);
}

Performance Considerations

The pose() method is called 250 times per second. Optimize carefully.

Best Practices

  1. Use non-blocking I/O
    // Good: non-blocking UDP send
    socket->writeDatagram(data, addr, port);
    
    // Bad: blocking TCP send
    socket->write(data);
    socket->waitForBytesWritten();  // Blocks!
    
  2. Minimize allocations
    // Good: reuse buffer
    class MyProtocol {
        QByteArray buffer;  // Reused across calls
        
        void pose(const double* pose, const double* raw) override {
            buffer.clear();
            // Fill buffer...
        }
    };
    
  3. Batch updates if needed
    void pose(const double* pose, const double* raw) override
    {
        buffer_pose(pose);
        
        if (++frame_count % 10 == 0) {  // Send every 10th frame
            flush_buffer();
        }
    }
    
  4. Use atomic operations for shared memory
    // Good: atomic update
    InterlockedExchange(&pData->value, new_value);
    
    // Bad: non-atomic
    pData->value = new_value;  // Race condition!
    

See Also