Protocol
Version: v2.20260106.01
Status: Draft
This document describes the OTD-IPC V2 protocol used for communication between OpenTabletDriver and client applications.
Overview
The V2 protocol enables exclusive, real-time access to tablet state from OpenTabletDriver. When a client connects, the active OpenTabletDriver output mode is disabled, giving the client direct access to tablet input.
Transport Layer
Communication uses Unix domain sockets on all platforms (Windows, macOS, Linux). The socket path varies by implementation to support multiple servers, and is discovered through a file-based discovery mechanism (see Discovery Mechanism).
Legacy Note: The obsolete V1 protocol used Windows named pipes in message mode; later versions moved to Unix domain sockets on all platforms for consistency, as Windows gained support for Unix sockets in the Windows 10 October 2018 Update.
Connection Model
- Exclusive Access: Only one client may connect at a time
- Server-Initiated Messages: The server pushes state updates to the client as they occur
- Persistent Connection: Client maintains a long-lived connection to receive continuous updates
This is primarily intended for exclusive access while gaming; as such, while in-use, servers SHOULD:
- expose the full tablet area, ignoring any active area clipping or monitor mapping, unless the user has explicitly indicated they want this to affect OTD-IPC
- expose all buttons, suppressing their usual behavior, unless the user has explicitly indicated they want to use their usual bindings while OTD-IPC is active
Protocol Flow
1. Discovery Phase
Before connecting, the client must discover the socket path:
- Read
%LOCALAPPDATA%/otd-ipc/servers/v2/default.txtto get the default implementation ID - Read
%LOCALAPPDATA%/otd-ipc/servers/v2/available/{id}.txtto get server metadata including the socket path - Extract the
SOCKET=value from the metadata file
See Discovery Mechanism for details.
2. Connection Phase
The client connects to the Unix domain socket at the discovered path.
3. Handshake Phase
Upon client connection, the server sends:
- DebugMessage containing implementation identification (e.g.,
"OTD-IPC: 'OTDIPC' v1.0.0 running on 'OpenTabletDriver' v0.6.0") - DeviceInfo message if a tablet device is currently connected (optional, only sent if device is present)
4. Operational Phase
After handshake, the server continuously streams:
- State messages whenever tablet state changes (position, pressure, buttons, proximity)
- Ping messages periodically to verify connection health
- DeviceInfo messages when the tablet device changes (connect/disconnect/switch)
The client reads and processes messages in a continuous loop.
Message Flow Diagram
Client Server
| |
|-- Connect to socket --------->|
| |
|<-- DebugMessage --------------| (implementation ID and version)
|<-- DeviceInfo ----------------| (if device present)
| |
|<-- State ---------------------| (on tablet events)
|<-- State ---------------------|
|<-- Ping ----------------------| (periodic keepalive)
|<-- State ---------------------|
|<-- DeviceInfo ----------------| (on device change)
|<-- State ---------------------|
| ... |
The DebugMessage is optional, but strongly recommended for debugging and visbility.
Message Types
All messages follow the same structure: a header followed by message-specific data.
| Message Type | ID | Description | Direction |
|---|---|---|---|
| DeviceInfo | 1 | Tablet device information | Server → Client |
| State | 2 | Current tablet state | Server → Client |
| Ping | 3 | Connection keepalive | Server → Client |
| DebugMessage | 4 | Variable-length debug string | Server → Client |
| Experimental | 5 | GUID-identified experimental messages | Server ↔ Client |
Message Details
Header
Present in all messages. Contains message identification and routing information.
Fields:
messageType(uint32): Message type ID from the table abovesize(uint32): Total message size in bytes, including headernonPersistentTabletId(uint32): Tablet identifier (may change on disconnect/reconnect)
DeviceInfo (ID: 1)
Sent when a client connects (if device present) or when the tablet device changes.
Fields:
- All Header fields
maxX(float): Maximum X coordinate in device unitsmaxY(float): Maximum Y coordinate in device unitsmaxPressure(uint32): Maximum pressure valuepersistentId(char[256]): Unique device identifier string (UTF-8)- this will be null-terminated if the string is shorter than 256 bytes
name(char[256]): Human-readable device name (UTF-8)- this will be null-terminated if the string is shorter than 256 bytes
State (ID: 2)
Sent whenever tablet state changes. Contains position, pressure, button, and proximity information.
Fields:
- All Header fields
validBits(ValidMask): Bitmask indicating which fields contain valid datax(float): Horizontal distance from left edge in device unitsy(float): Vertical distance from top edge in device unitspressure(uint32): Pen pressurepenButtons(uint32): Pen button state (bitmask, bit N = button N). Button 0 (LSB) is reserved for the pen tipauxButtons(uint32): Auxiliary button state (bitmask, bit N = button N)hoverDistance(uint32): Distance above surface in device-specific unitspenIsNearSurface(bool): Whether pen is in proximity of tablet surface
Notes:
- (0, 0) is the top left corner; coordinates increase to the right and down.
- button 0 MUST be set if the pen is in contact with the surface
- button 0 MUST NOT be set if the pen is not in contact surface
- if there is no actual pen tip button, the server MUST synthesize button 0 from pressure data, and bit-shift the other buttons (if any) along
ValidMask values:
None= 0PositionX= 1 « 0,PositionY= 1 « 1,Pressure= 1 « 2,PenButtons= 1 « 3,AuxButtons= 1 « 4,PenIsNearSurface= 1 « 5,HoverDistance= 1 « 6,
Clients MUST check validBits before using each field. Only fields with corresponding bits set contain valid data.
Ping (ID: 3)
Sent periodically by the server to maintain connection and verify client responsiveness.
Fields:
- All Header fields
sequenceNumber(uint64): Incrementing sequence number
Clients should read and ignore Ping messages. If pings stop arriving, the connection may be broken.
DebugMessage (ID: 4)
Variable-length message containing UTF-8 text for debugging/logging purposes.
Structure:
- Header with
sizefield indicating total message size - UTF-8 text data (length =
size - sizeof(Header))
The text is NOT null-terminated. Use the size field to determine text length.
Experimental (ID: 5)
Reserved for private experimentation. Format:
- Header
- GUID (16 bytes in Win32/C# binary layout)
- Variable-length payload
The GUID identifies the actual type of the message, and the meaning of the payload.
Not recommended for production use. Consider contributing new message types to OTD-IPC instead.
Binary Format
Encoding Rules
- Structure Layout: All structures use natural alignment (C/C++ default, C#
StructLayout.LayoutKind.SequentialwithPack=0) - Byte Order: Native endianness (little-endian on x86/x64/ARM platforms)
- Integer Types:
uint16_t/UInt16: 16-bit unsigned integeruint32_t/UInt32: 32-bit unsigned integeruint64_t/UInt64: 64-bit unsigned integer
- Floating Point: IEEE 754 single-precision (32-bit)
- Boolean: Single byte (0 = false, non-zero = true)
- Strings: Fixed-size buffers containing null-terminated UTF-8 text
Size Validation
Implementations MUST:
- Verify that the message size from the header matches the number of bytes read
- Verify that the message size is AT LEAST as large as expected for the struct
- Accept messages larger than expected (forward compatibility - server may send extended messages)
Reference Implementations
| Type | C++ | C# |
|---|---|---|
enum MessageType | .hpp | .cs |
struct Header | .hpp | .cs |
✉️ struct DeviceInfo | .hpp | .cs |
✉️ struct Ping | .hpp | .cs |
✉️ struct State | .hpp | .cs |
Example Client: See OTDIPC-TestClient.cpp for a complete C++20 example implementation.
Discovery Mechanism
The V2 protocol uses a file-based discovery mechanism to locate available servers and their socket paths.
Cross-Platform Paths
All discovery files are located under {LocalApplicationData}/otd-ipc/servers/v2; {LocalApplicationData} is resolved as follows:
- Windows:
%LOCALAPPDATA% - Linux:
$XDG_DATA_DIR(typically~/.local/share) - macOS:
{ApplicationSupport}(typically~/Library/Application Support)
Directory Structure
{LocalApplicationData}/
└── otd-ipc/
└── servers/
└── v2/
├── default.txt # Contains default implementation ID
└── available/
└── {id}.txt # Metadata file per implementation
Discovery Process
Client Discovery
- Read default ID: Open
{LocalApplicationData}/otd-ipc/servers/v2/default.txt - Parse ID: Read the file contents and trim whitespace to get the implementation ID
- Read metadata: Open
{LocalApplicationData}/otd-ipc/servers/v2/available/{id}.txt - Extract socket path: Parse metadata file for the line beginning with
SOCKET= - Connect: Open Unix domain socket connection to the extracted path
Server Publishing
When the server starts:
- Choose a socket path::implementations should choose a socket path unique to their implementation; for example, while
OpenKneeboard/OTD-IPCusesotd-ipc/sock, *no other implementations should create their sockets inside theotd-ipcfolder - Create socket: Bind to Unix domain socket at chosen path
- Publish metadata: Write metadata file to
{LocalApplicationData}/otd-ipc/servers/v2/available/{implementation-id}.txt - Set default (if needed): If
default.txtdoesn’t exist, create it with this server’s implementation ID
Metadata File Format
The metadata file is a UTF-8 text file with \n line endings, containing KEY=VALUE pairs, one per line.
Required Fields:
ID: Implementation identifier (e.g.,otd-ipc.openkneeboard.com)SOCKET: Absolute path to Unix domain socket
Optional Fields:
NAME: Human-readable server nameSEMVER: Semantic version stringDEBUG_VERSION: Detailed version informationHOMEPAGE: URL to project homepage
Example:
ID=otd-ipc.openkneeboard.com
NAME=OpenTabletDriver OTD-IPC Plugin
SEMVER=1.0.0+revision.42
DEBUG_VERSION=OTD-IPC v1.0.0.42/OpenTabletDriver v0.6.4.0
HOMEPAGE=https://github.com/OpenKneeboard/OTD-IPC
SOCKET=/home/user/.local/share/otd-ipc/sock
Implementation Notes
- Servers MUST NOT replace an existing
default.txton regular startup - Servers MAY notify the user that they are not default, and provide an opt-in path
- Servers MUST NOT persist an opt-in choice to replace the default
-
Servers MAY replace an existing
default.txton installation, as long as ‘installation’ is not considered part of regular startup - Servers SHOULD clean up their metadata file on clean shutdown
- Clients SHOULD handle stale files gracefully
- Clients MUST handle strings that completely fill fixed-sized buffers gracefully; these strings only have a terminating null if they are shorter than the buffer size
- Servers SHOULD delete existing socket files before binding (Unix requirement)
- Clients SHOULD verify the socket is connectable before assuming a server is available
- Clients SHOULD attempt to use the default implementation, unless otherwise specified
- Clients MAY use other discoverable implementations, e.g. if the default is unreachable or due to explicit user selection
- Servers MUST make the
ID=field of the metadata file match the implementation ID component of the filename - Servers MUST use an absolute path for the
SOCKET=field - All implementations MUST ignore unrecognized message types
- All implementations MUST allow messages to be larger than expected for forwards compatibility
- implementations MUST ignore the additional bytes
- Implementations MUST NOT append additional data to the messages, unless either:
- the extensions have been accepted and merged into this protocol specification
- the message type is
Experimental(5), and the GUID is unique to the implementation
- Servers MUST write discovery text files using the UTF-8 encoding
- Servers MUST use
\n(LF) at the end of each line in discovery text files - Servers MUST NOT use
\r\n(CRLF) as line endings - Servers MAY include a newline (
\n) at the end of the file/the final line - Client SHOULD handle discovery files both with and without a trailing newline
- Multiple servers MAY publish different metadata files simultaneously, but only one default can exist
Implementation Checklist
Client Implementation
- Implement discovery mechanism to find socket path
- Connect to Unix domain socket
- Read messages in a loop
- Parse Header from each message
- Validate
sizefield matches bytes read - Validate
size>= expected struct size - Switch on
messageTypeto handle each message - Check
validBitsin State messages before using fields - Handle connection errors gracefully
- Ignore unknown message types (forward compatibility)
Server Implementation
- Create Unix domain socket at chosen path
- Clean up old socket file before binding
- Publish discovery metadata file
- Set default.txt if it doesn’t exist
- Accept client connection (one at a time)
- Send DebugMessage with implementation ID and version information on connect
- Send DeviceInfo on connect (if device present)
- Send State messages when tablet state changes
- Send Ping messages periodically
- Send DeviceInfo when device changes
- Handle client disconnect gracefully
- Accept next client after disconnect
Version History
- V2 (current): Unix domain sockets, discovery mechanism, improved device identification
- V1 (obsolete): Windows named pipes, fixed pipe name, limited to Windows
Applications should implement V2 only. V1 support is maintained for backward compatibility but not recommended for new implementations.