Quickstart
This program connects to Odyssey Desktop and prints the aimpoint of every device it tracks. Start Odyssey Desktop first, with a device connected and the screen calibrated.
using System.Threading.Channels;using Ohc = OdysseyArm.HubClient;
using var client = new Ohc.Client();await client.Connect();
var events = Channel.CreateUnbounded<(Ohc.uniffi.Event?, Ohc.uniffi.ClientException?)>();_ = client.SubscribeEvents(events.Writer);
await foreach (var (ev, error) in events.Reader.ReadAllAsync()){ if (error != null) { Console.WriteLine($"Stream ended: {error.Message}"); break; } if (ev is Ohc.uniffi.Event.DeviceEvent deviceEvent && deviceEvent.V1.Kind is Ohc.uniffi.DeviceEventKind.TrackingEvent tracking) { var aim = tracking.V1.Aimpoint; Console.WriteLine($"{deviceEvent.V1.Device.Uuid:X}: aimpoint ({aim.X:F3}, {aim.Y:F3})"); }}import asyncioimport ohc_uniffi as ohc
async def main(): client = ohc.Client() await client.connect() stream = await client.subscribe_events() while True: event = await stream.next() if not event.is_device_event(): continue device_event = event[0] if device_event.kind.is_tracking_event(): tracking = device_event.kind[0] uuid = ohc.Device(device_event.device).uuid() print(f"{uuid:x}: aimpoint ({tracking.aimpoint.x:.3f}, {tracking.aimpoint.y:.3f})")
asyncio.run(main())#ifndef _WIN32#define _POSIX_C_SOURCE 199309L /* for nanosleep */#endif#include <ohc.h>
#include <stdatomic.h>#include <stdio.h>#ifdef _WIN32#include <windows.h>static void sleep_ms(unsigned ms) { Sleep(ms); }#else#include <time.h>static void sleep_ms(unsigned ms) { struct timespec ts = {ms / 1000, (ms % 1000) * 1000000L}; nanosleep(&ts, NULL);}#endif
static atomic_bool done = false;
struct State { struct ohc_Handle *handle; struct ohc_Client *client;};
static void on_event(struct ohc_UserObj userdata, enum ohc_ClientError error, struct ohc_Event event) { (void)userdata; if (error != OHC_CLIENT_ERROR_NONE) { atomic_store(&done, true); return; } if (event.tag != OHC_EVENT_DEVICE_EVENT) { return; } const struct ohc_DeviceEvent *device_event = &event.device_event; if (device_event->kind.tag == OHC_DEVICE_EVENT_KIND_TRACKING_EVENT) { const struct ohc_TrackingEvent *tracking = &device_event->kind.tracking_event; printf("aimpoint (%.3f, %.3f)\n", tracking->aimpoint.x, tracking->aimpoint.y); }}
static void on_connect(struct ohc_UserObj userdata, enum ohc_ClientError error) { const struct State *state = userdata.ptr; if (error != OHC_CLIENT_ERROR_NONE) { puts("Could not connect to Odyssey Desktop"); atomic_store(&done, true); return; } client_start_stream(state->handle, state->client, (struct ohc_UserObj){NULL}, on_event);}
int main(void) { struct ohc_Handle *handle = rt_init(); struct ohc_Client *client = client_new(); struct State state = {handle, client}; client_connect(handle, client, (struct ohc_UserObj){&state}, on_connect); while (!atomic_load(&done)) { sleep_ms(10); } client_free(client); rt_free(handle); return 0;}The SDK calls your callbacks on its own threads; hand data to your main thread yourself.
#include <ohc.hpp>
#include <atomic>#include <chrono>#include <cstdio>#include <memory>#include <thread>
static std::atomic_bool done{false};
struct State { ohc::Handle *handle; ohc::Client *client;};
static void onEvent(ohc::UserObj, ohc::ClientError error, ohc::Event event) { if (error != ohc::ClientError::NONE) { done = true; return; } if (event.tag != ohc::Event::Tag::DEVICE_EVENT) { return; } const ohc::DeviceEvent &device_event = event.DEVICE_EVENT._0; if (device_event.kind.tag == ohc::DeviceEventKind::Tag::TRACKING_EVENT) { const ohc::TrackingEvent &tracking = device_event.kind.TRACKING_EVENT._0; std::printf("aimpoint (%.3f, %.3f)\n", tracking.aimpoint.x, tracking.aimpoint.y); }}
static void onConnect(ohc::UserObj userdata, ohc::ClientError error) { auto *state = static_cast<const State *>(userdata.ptr); if (error != ohc::ClientError::NONE) { std::puts("Could not connect to Odyssey Desktop"); done = true; return; } ohc::client_start_stream(state->handle, state->client, ohc::UserObj{nullptr}, onEvent);}
int main() { std::unique_ptr<ohc::Handle, decltype(&ohc::rt_free)> handle(ohc::rt_init(), ohc::rt_free); std::unique_ptr<ohc::Client, decltype(&ohc::client_free)> client(ohc::client_new(), ohc::client_free); State state{handle.get(), client.get()}; ohc::client_connect(handle.get(), client.get(), ohc::UserObj{&state}, onConnect); while (!done) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); }}The SDK calls your callbacks on its own threads; hand data to your main thread yourself.
use futures::StreamExt as _;use odyssey_hub_client::client::Client;use odyssey_hub_common::events::{DeviceEvent, DeviceEventKind, Event};
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let client = Client::connect().await?; let mut events = client.subscribe_events().await?; while let Some(event) = events.next().await { if let Event::DeviceEvent(DeviceEvent(device, DeviceEventKind::TrackingEvent(tracking))) = event? { let aim = tracking.aimpoint; println!("{:02x?}: aimpoint ({:.3}, {:.3})", device.uuid, aim.x, aim.y); } } Ok(())}This also needs tokio (with the macros and rt-multi-thread features) and
futures.
Aim a device around the screen and watch the numbers change: (0, 0) is the
top-left corner of the screen and (1, 1) the bottom-right.
These programs follow the samples in the repository, which show more of the API: Python, C#, C, C++ and Rust.
- Coordinate system: what the aimpoint and pose mean.
- Tracking events: every other event and field.

