diff --git a/src/xrt/targets/CMakeLists.txt b/src/xrt/targets/CMakeLists.txt index 844f605c4..56306518c 100644 --- a/src/xrt/targets/CMakeLists.txt +++ b/src/xrt/targets/CMakeLists.txt @@ -41,3 +41,8 @@ if(XRT_FEATURE_SERVICE ) add_subdirectory(sdl_test) endif() + +# Monado management library +if(XRT_FEATURE_SERVICE AND XRT_HAVE_LINUX) + add_subdirectory(libmonado) +endif() diff --git a/src/xrt/targets/libmonado/CMakeLists.txt b/src/xrt/targets/libmonado/CMakeLists.txt new file mode 100644 index 000000000..682194875 --- /dev/null +++ b/src/xrt/targets/libmonado/CMakeLists.txt @@ -0,0 +1,16 @@ +# Copyright 2019-2023, Collabora, Ltd. +# SPDX-License-Identifier: BSL-1.0 + +add_library(monado SHARED monado.c libmonado.def) +set(LIBMONADO_HEADER_DIR ${CMAKE_INSTALL_INCLUDEDIR}/monado) +target_link_libraries(monado PRIVATE aux_util ipc_client) +target_include_directories( + monado INTERFACE $ + $ + ) + +install(TARGETS monado RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +install(FILES monado.h DESTINATION ${LIBMONADO_HEADER_DIR}) + +add_executable(libmonado-example example.c) +target_link_libraries(libmonado-example PRIVATE monado) diff --git a/src/xrt/targets/libmonado/example.c b/src/xrt/targets/libmonado/example.c new file mode 100644 index 000000000..37d4ec557 --- /dev/null +++ b/src/xrt/targets/libmonado/example.c @@ -0,0 +1,200 @@ +// Copyright 2020-2023, Collabora, Ltd. +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Small cli application to demonstrate use of libmonado. + * @author Ryan Pavlik + * @author Pete Black + */ + +#include "monado.h" + +#include +#include +#include +#include +#include +#include +#include + + +#define P(...) fprintf(stdout, __VA_ARGS__) +#define PE(...) fprintf(stderr, __VA_ARGS__) +#define CHECK_ID_EXIT(ID) \ + do { \ + if (ID < 0 || ID > INT_MAX) { \ + PE("Invalid client index %i.\n", s_val); \ + exit(1); \ + } \ + } while (false) + +typedef enum op_mode +{ + MODE_GET, + MODE_SET_PRIMARY, + MODE_SET_FOCUSED, + MODE_TOGGLE_IO, +} op_mode_t; + +int +get_mode(mnd_root_t *root) +{ + mnd_result_t mret = mnd_root_update_client_list(root); + if (mret != MND_SUCCESS) { + PE("Failed to get client list.\n"); + exit(1); + } + + uint32_t num_clients = 0; + mret = mnd_root_get_number_clients(root, &num_clients); + if (mret != MND_SUCCESS) { + PE("Failed to get client count.\n"); + exit(1); + } + + P("Clients: (%d)\n", num_clients); + for (uint32_t i = 0; i < num_clients; i++) { + uint32_t client_id = 0; + uint32_t flags = 0; + const char *name = NULL; + + mret = mnd_root_get_client_id_at_index(root, i, &client_id); + if (mret != MND_SUCCESS) { + PE("Failed to get client id for index %u", i); + continue; + } + + mret = mnd_root_get_client_state(root, client_id, &flags); + if (mret != MND_SUCCESS) { + PE("Failed to get client state for client id: %u (index: %u)", client_id, i); + continue; + } + + mret = mnd_root_get_client_name(root, client_id, &name); + if (mret != MND_SUCCESS) { + PE("Failed to get client name for client id: %u (index: %u)", client_id, i); + continue; + } + + P("\tid: % 8d" + "\tact: %d" + "\tdisp: %d" + "\tfoc: %d" + "\tio: %d" + "\tovly: %d" + "\t%s\n", + client_id, // + (flags & MND_CLIENT_SESSION_ACTIVE) != 0 ? 1 : 0, // + (flags & MND_CLIENT_SESSION_VISIBLE) != 0 ? 1 : 0, // + (flags & MND_CLIENT_SESSION_FOCUSED) != 0 ? 1 : 0, // + (flags & MND_CLIENT_IO_ACTIVE) != 0 ? 1 : 0, // + (flags & MND_CLIENT_SESSION_OVERLAY) != 0 ? 1 : 0, // + name); + } + + return 0; +} + +int +set_primary(mnd_root_t *root, int client_index) +{ + mnd_result_t mret = mnd_root_set_client_primary(root, client_index); + if (mret != MND_SUCCESS) { + PE("Failed to set active client to index %d.\n", client_index); + return 1; + } + + return 0; +} + +int +set_focused(mnd_root_t *root, int client_index) +{ + mnd_result_t mret = mnd_root_set_client_focused(root, client_index); + if (mret != MND_SUCCESS) { + PE("Failed to set focused client to index %d.\n", client_index); + return 1; + } + + return 0; +} + +int +toggle_io(mnd_root_t *root, int client_index) +{ + mnd_result_t mret = mnd_root_toggle_client_io_active(root, client_index); + if (mret != MND_SUCCESS) { + PE("Failed to toggle io for client index %d.\n", client_index); + return 1; + } + return 0; +} + +int +main(int argc, char *argv[]) +{ + op_mode_t op_mode = MODE_GET; + + // parse arguments + int c; + int s_val = 0; + + opterr = 0; + while ((c = getopt(argc, argv, "p:f:i:")) != -1) { + switch (c) { + case 'p': + s_val = atoi(optarg); + CHECK_ID_EXIT(s_val); + op_mode = MODE_SET_PRIMARY; + break; + case 'f': + s_val = atoi(optarg); + CHECK_ID_EXIT(s_val); + op_mode = MODE_SET_FOCUSED; + break; + case 'i': + s_val = atoi(optarg); + CHECK_ID_EXIT(s_val); + op_mode = MODE_TOGGLE_IO; + break; + case '?': + if (optopt == 's') { + PE("Option -s requires a client index to set.\n"); + } else if (isprint(optopt)) { + PE("Option `-%c' unknown. Usage:\n", optopt); + PE(" -f : Set focused client\n"); + PE(" -p : Set primary client\n"); + PE(" -i : Toggle whether client receives input\n"); + } else { + PE("Option `\\x%x' unknown.\n", optopt); + } + exit(1); + default: exit(0); + } + } + + mnd_root_t *root = NULL; + mnd_result_t mret; + + mret = mnd_root_create(&root); + if (mret != MND_SUCCESS) { + PE("Failed to connect."); + return 1; + } + + mret = mnd_root_update_client_list(root); + if (mret != MND_SUCCESS) { + PE("Failed to update client list."); + return 1; + } + + switch (op_mode) { + case MODE_GET: exit(get_mode(root)); break; + case MODE_SET_PRIMARY: exit(set_primary(root, s_val)); break; + case MODE_SET_FOCUSED: exit(set_focused(root, s_val)); break; + case MODE_TOGGLE_IO: exit(toggle_io(root, s_val)); break; + default: P("Unrecognised operation mode.\n"); exit(1); + } + + return 0; +} diff --git a/src/xrt/targets/libmonado/example.lua b/src/xrt/targets/libmonado/example.lua new file mode 100755 index 000000000..7294b5691 --- /dev/null +++ b/src/xrt/targets/libmonado/example.lua @@ -0,0 +1,189 @@ +#!/usr/bin/env luajit +-- Copyright 2020-2023, Collabora, Ltd. +-- SPDX-License-Identifier: BSL-1.0 +-- Author: Lubosz Sarnecki + +local ffi = require("ffi") + +ffi.cdef[[ +typedef enum mnd_result +{ + MND_SUCCESS = 0, + MND_ERROR_INVALID_VERSION = -1, + MND_ERROR_INVALID_VALUE = -2, + MND_ERROR_CONNECTING_FAILED = -3, + MND_ERROR_OPERATION_FAILED = -4, +} mnd_result_t; + +typedef enum mnd_client_flags +{ + MND_CLIENT_PRIMARY_APP = (1u << 0u), + MND_CLIENT_SESSION_ACTIVE = (1u << 1u), + MND_CLIENT_SESSION_VISIBLE = (1u << 2u), + MND_CLIENT_SESSION_FOCUSED = (1u << 3u), + MND_CLIENT_SESSION_OVERLAY = (1u << 4u), + MND_CLIENT_IO_ACTIVE = (1u << 5u), +} mnd_client_flags_t; + +typedef struct mnd_root mnd_root_t; + +void +mnd_api_get_version(uint32_t *out_major, uint32_t *out_minor, uint32_t *out_patch); + +mnd_result_t +mnd_root_create(mnd_root_t **out_root); + +void +mnd_root_destroy(mnd_root_t **root_ptr); + +mnd_result_t +mnd_root_update_client_list(mnd_root_t *root); + +mnd_result_t +mnd_root_get_number_clients(mnd_root_t *root, uint32_t *out_num); + +mnd_result_t +mnd_root_get_client_id_at_index(mnd_root_t *root, uint32_t index, uint32_t *out_client_id); + +mnd_result_t +mnd_root_get_client_name(mnd_root_t *root, uint32_t client_id, const char **out_name); + +mnd_result_t +mnd_root_get_client_state(mnd_root_t *root, uint32_t client_id, uint32_t *out_flags); + +mnd_result_t +mnd_root_set_client_primary(mnd_root_t *root, uint32_t client_id); + +mnd_result_t +mnd_root_set_client_focused(mnd_root_t *root, uint32_t client_id); + +mnd_result_t +mnd_root_toggle_client_io_active(mnd_root_t *root, uint32_t client_id); +]] + +local status, lib = pcall(ffi.load, "libmonado.so") +if not status then + print("Could not find an installed libmonado.so in your path.") + print("Add the Monado build directory to your LD_LIBRARY_PATH:") + print(string.format("LD_LIBRARY_PATH=/home/user/monado/build/src/xrt/targets/libmonado/ %s", arg[0])) + os.exit(1) +end + +-- Parse arguments +local args = {...} +for i=1, #args, 1 do + if args[i] == "-f" or args[i] == "--focused" then + args_focused = tonumber(args[i+1]) + elseif args[i] == "-p" or args[i] == "--primary" then + args_primary = tonumber(args[i+1]) + elseif args[i] == "-i" or args[i] == "--input" then + args_input = tonumber(args[i+1]) + end +end + +-- Using ** doesn't work here, it hits an assertion in moando due being NULL +local root_ptr = ffi.new("mnd_root_t*[1]") + +function create_root() + local ret = lib.mnd_root_create(root_ptr) + if ret ~= 0 then + error("Could not create root") + end + return root_ptr[0] +end + +local root = create_root() + +function update_clients() + local ret = lib.mnd_root_update_client_list(root) + if ret ~= 0 then + error("Could not update clients") + end +end + +function get_client_count() + client_count_ptr = ffi.new("uint32_t[1]") + ret = lib.mnd_root_get_number_clients(root, client_count_ptr) + if ret ~= 0 then + error("Could not get number of clients") + end + return client_count_ptr[0] +end + +function get_client_id_at_index(index) + client_id_ptr = ffi.new("uint32_t[1]") + local ret = lib.mnd_root_get_client_id_at_index(root, index, client_id_ptr) + if ret ~= 0 then + error("Could not get client id at index.") + end + return client_id_ptr[0] +end + +function get_client_name(client_id) + name_ptr = ffi.new("const char*[1]") + local ret = lib.mnd_root_get_client_name(root, client_id, name_ptr) + if ret ~= 0 then + error("Could not get client name.") + end + return name_ptr[0] +end + +function get_client_flags(client_id) + flags_ptr = ffi.new("uint32_t[1]") + local ret = lib.mnd_root_get_client_state(root, client_id, flags_ptr) + if ret ~= 0 then + error("Could not get client flags.") + end + return flags_ptr[0] +end + +function set_primary(client_id) + local ret = lib.mnd_root_set_client_primary(root, client_id) + if ret ~= 0 then + error("Failed to set primary client to client id", client_id) + end +end + +function set_focused(client_id) + ret = lib.mnd_root_set_client_focused(root, client_id) + if ret ~= 0 then + error("Failed to set focused client to client id", client_id) + end +end + +function toggle_io(client_id) + ret = lib.mnd_root_toggle_client_io_active(root, client_id) + if ret ~= 0 then + error("Failed to toggle io for client client id", client_id) + end +end + +if args_primary then + set_primary(args_primary) +end +if args_focused then + set_focused(args_focused) +end +if args_input then + toggle_io(args_input) +end + +update_clients(root) +print("Client count:", get_client_count()) + +for i = 0, get_client_count() - 1 do + local client_id = get_client_id_at_index(i) + local name = get_client_name(client_id) + local flags = get_client_flags(client_id) + local primary = bit.band(flags, lib.MND_CLIENT_PRIMARY_APP) ~= 0 + local active = bit.band(flags, lib.MND_CLIENT_SESSION_ACTIVE) ~= 0 + local visible = bit.band(flags, lib.MND_CLIENT_SESSION_VISIBLE) ~= 0 + local focused = bit.band(flags, lib.MND_CLIENT_SESSION_FOCUSED) ~= 0 + local overlay = bit.band(flags, lib.MND_CLIENT_SESSION_OVERLAY) ~= 0 + local io_active = bit.band(flags, lib.MND_CLIENT_IO_ACTIVE) ~= 0 + print(string.format("id: %d primary: %5s active: %5s visible: %5s focused: %5s io: %5s overlay: %5s name: %s", + client_id, tostring(primary), tostring(active), tostring(visible), tostring(focused), + tostring(io_active), tostring(overlay), ffi.string(name))) +end + +lib.mnd_root_destroy(root_ptr) diff --git a/src/xrt/targets/libmonado/example.py b/src/xrt/targets/libmonado/example.py new file mode 100755 index 000000000..1a4e59eb4 --- /dev/null +++ b/src/xrt/targets/libmonado/example.py @@ -0,0 +1,65 @@ +#!/bin/env python3 +# Copyright 2020-2023, Collabora, Ltd. +# SPDX-License-Identifier: BSL-1.0 +# Author: Jakob Bornecrantz +# Author: Lubosz Sarnecki +# Author: Korcan Hussein + +import argparse +from monado import Monado, MonadoLibraryNotFoundError, MonadoHeaderNotFoundError + + +def main(): + parser = argparse.ArgumentParser(description='libmonado Python example.') + parser.add_argument("-f", "--focused", type=int, metavar='CLIENT_ID', + help="Set focused client") + parser.add_argument("-p", "--primary", type=int, metavar='CLIENT_ID', + help="Set primary client") + parser.add_argument("-i", "--input", type=int, metavar='CLIENT_ID', + help="Toggle whether client receives input") + args = parser.parse_args() + + try: + m = Monado() + except MonadoLibraryNotFoundError: + print("Could not find an installed libmonado.so in your path.") + print("Add the Monado build directory to your LD_LIBRARY_PATH:") + print(f"LD_LIBRARY_PATH=/home/user/monado/build/src/xrt/targets/libmonado/ ./{parser.prog}") + return + except MonadoHeaderNotFoundError: + print("Could not find an installed monado.h.") + print("Try setting the MONADO_HEADER_PATH manually:") + print(f"MONADO_HEADER_PATH=/home/user/monado/src/xrt/targets/libmonado/monado.h ./{parser.prog}") + return + + if args.focused: + m.set_focused(args.focused) + if args.primary: + m.set_primary(args.primary) + if args.input: + m.toggle_io(args.input) + + m.update_clients() + + print(f"Clients: {m.client_count}") + for x in range(m.client_count): + c = m.snapshot_client(x) + print(f"\tid: {c.ident:4d}, primary: {c.primary:d}, active: {c.active:d}, " + f"visible: {c.visible:d}, focused: {c.focused:d}, io: {c.io_active:d}, " + f"overlay: {c.overlay:d}, name: {c.name}") + + devices = m.get_devices() + print(f"Devices: {len(devices)}") + for dev in devices: + print(f"\tid: {dev.ident:4d}, name: {dev.name}") + + roles_map = m.get_device_roles() + print(f"Roles: {len(roles_map)}") + for role_name, dev_id in roles_map.items(): + print(f"\trole: {role_name},\tdevice-index: {dev_id:4d}") + + m.destroy() + + +if __name__ == "__main__": + main() diff --git a/src/xrt/targets/libmonado/libmonado.def b/src/xrt/targets/libmonado/libmonado.def new file mode 100644 index 000000000..ead47cd75 --- /dev/null +++ b/src/xrt/targets/libmonado/libmonado.def @@ -0,0 +1,15 @@ +; Copyright 2019-2023, Collabora, Ltd. +; SPDX-License-Identifier: BSL-1.0 +EXPORTS + mnd_root_create + mnd_root_destroy + mnd_root_update_client_list + mnd_root_get_number_clients + mnd_root_get_client_name + mnd_root_get_client_state + mnd_root_set_client_primary + mnd_root_set_client_focused + mnd_root_toggle_client_io_active + mnd_root_get_device_count + mnd_root_get_device_info + mnd_root_get_device_from_role diff --git a/src/xrt/targets/libmonado/monado.c b/src/xrt/targets/libmonado/monado.c new file mode 100644 index 000000000..ad999ee79 --- /dev/null +++ b/src/xrt/targets/libmonado/monado.c @@ -0,0 +1,336 @@ +// Copyright 2019-2023, Collabora, Ltd. +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Implementation of libmonado + * @author Jakob Bornecrantz + * @author Pete Black + * @author Ryan Pavlik + */ + +#include "monado.h" + +#include "xrt/xrt_results.h" + +#include "util/u_misc.h" +#include "util/u_file.h" +#include "util/u_logging.h" + +#include "shared/ipc_protocol.h" + +#include "client/ipc_client_connection.h" +#include "client/ipc_client.h" +#include "ipc_client_generated.h" + +#include +#include +#include + + +struct mnd_root +{ + struct ipc_connection ipc_c; + + //! List of clients. + struct ipc_client_list clients; + + /// State of most recent app asked about + struct ipc_app_state app_state; +}; + +#define P(...) fprintf(stdout, __VA_ARGS__) +#define PE(...) fprintf(stderr, __VA_ARGS__) + + +/* + * + * Helper functions. + * + */ + +#define CHECK_NOT_NULL(ARG) \ + do { \ + if (ARG == NULL) { \ + PE("Argument '" #ARG "' can not be null!"); \ + return MND_ERROR_INVALID_VALUE; \ + } \ + } while (false) + +#define CHECK_CLIENT_ID(ID) \ + do { \ + if (ID == 0 && ID > INT_MAX) { \ + PE("Invalid client id (%u)", ID); \ + return MND_ERROR_INVALID_VALUE; \ + } \ + } while (false) + +#define CHECK_CLIENT_INDEX(INDEX) \ + do { \ + if (INDEX >= root->clients.id_count) { \ + PE("Invalid client index, too large (%u)", INDEX); \ + return MND_ERROR_INVALID_VALUE; \ + } \ + } while (false) + +static int +get_client_info(mnd_root_t *root, uint32_t client_id) +{ + assert(root != NULL); + + xrt_result_t r = ipc_call_system_get_client_info(&root->ipc_c, client_id, &root->app_state); + if (r != XRT_SUCCESS) { + PE("Failed to get client info for client id: %u.\n", client_id); + return MND_ERROR_INVALID_VALUE; + } + + return MND_SUCCESS; +} + + +/* + * + * API API. + * + */ + +void +mnd_api_get_version(uint32_t *out_major, uint32_t *out_minor, uint32_t *out_patch) +{ + *out_major = MND_API_VERSION_MAJOR; + *out_minor = MND_API_VERSION_MINOR; + *out_patch = MND_API_VERSION_PATCH; +} + + +/* + * + * Root API + * + */ + +mnd_result_t +mnd_root_create(mnd_root_t **out_root) +{ + CHECK_NOT_NULL(out_root); + + mnd_root_t *r = U_TYPED_CALLOC(mnd_root_t); + + struct xrt_instance_info info = {0}; + snprintf(info.application_name, sizeof(info.application_name), "%s", "libmonado"); + + xrt_result_t xret = ipc_client_connection_init(&r->ipc_c, U_LOGGING_INFO, &info); + if (xret != XRT_SUCCESS) { + PE("Connection init error '%i'!\n", xret); + free(r); + return MND_ERROR_CONNECTING_FAILED; + } + + *out_root = r; + + return MND_SUCCESS; +} + +void +mnd_root_destroy(mnd_root_t **root_ptr) +{ + if (root_ptr == NULL) { + return; + } + + mnd_root_t *r = *root_ptr; + if (r == NULL) { + return; + } + + ipc_client_connection_fini(&r->ipc_c); + free(r); + + *root_ptr = NULL; + + return; +} + +mnd_result_t +mnd_root_update_client_list(mnd_root_t *root) +{ + CHECK_NOT_NULL(root); + + xrt_result_t r = ipc_call_system_get_clients(&root->ipc_c, &root->clients); + if (r != XRT_SUCCESS) { + PE("Failed to get client list.\n"); + return MND_ERROR_OPERATION_FAILED; + } + + return MND_SUCCESS; +} + +mnd_result_t +mnd_root_get_number_clients(mnd_root_t *root, uint32_t *out_num) +{ + CHECK_NOT_NULL(root); + CHECK_NOT_NULL(out_num); + + *out_num = root->clients.id_count; + + return MND_SUCCESS; +} + +mnd_result_t +mnd_root_get_client_id_at_index(mnd_root_t *root, uint32_t index, uint32_t *out_client_id) +{ + CHECK_NOT_NULL(root); + CHECK_CLIENT_INDEX(index); + + *out_client_id = root->clients.ids[index]; + + return MND_SUCCESS; +} + +mnd_result_t +mnd_root_get_client_name(mnd_root_t *root, uint32_t client_id, const char **out_name) +{ + CHECK_NOT_NULL(root); + CHECK_CLIENT_ID(client_id); + CHECK_NOT_NULL(out_name); + + mnd_result_t mret = get_client_info(root, client_id); + if (mret < 0) { + return mret; // Prints error. + } + + *out_name = &(root->app_state.info.application_name[0]); + + return MND_SUCCESS; +} + +mnd_result_t +mnd_root_get_client_state(mnd_root_t *root, uint32_t client_id, uint32_t *out_flags) +{ + CHECK_NOT_NULL(root); + CHECK_CLIENT_ID(client_id); + CHECK_NOT_NULL(out_flags); + + mnd_result_t mret = get_client_info(root, client_id); + if (mret < 0) { + return mret; // Prints error. + } + + uint32_t flags = 0; + flags |= (root->app_state.primary_application) ? MND_CLIENT_PRIMARY_APP : 0u; + flags |= (root->app_state.session_active) ? MND_CLIENT_SESSION_ACTIVE : 0u; + flags |= (root->app_state.session_visible) ? MND_CLIENT_SESSION_VISIBLE : 0u; + flags |= (root->app_state.session_focused) ? MND_CLIENT_SESSION_FOCUSED : 0u; + flags |= (root->app_state.session_overlay) ? MND_CLIENT_SESSION_OVERLAY : 0u; + flags |= (root->app_state.io_active) ? MND_CLIENT_IO_ACTIVE : 0u; + *out_flags = flags; + + return MND_SUCCESS; +} + +mnd_result_t +mnd_root_set_client_primary(mnd_root_t *root, uint32_t client_id) +{ + CHECK_NOT_NULL(root); + CHECK_CLIENT_ID(client_id); + + xrt_result_t r = ipc_call_system_set_primary_client(&root->ipc_c, client_id); + if (r != XRT_SUCCESS) { + PE("Failed to set primary to client id: %u.\n", client_id); + return MND_ERROR_OPERATION_FAILED; + } + + return MND_SUCCESS; +} + +mnd_result_t +mnd_root_set_client_focused(mnd_root_t *root, uint32_t client_id) +{ + CHECK_NOT_NULL(root); + CHECK_CLIENT_ID(client_id); + + xrt_result_t r = ipc_call_system_set_focused_client(&root->ipc_c, client_id); + if (r != XRT_SUCCESS) { + PE("Failed to set focused to client id: %u.\n", client_id); + return MND_ERROR_OPERATION_FAILED; + } + + return MND_SUCCESS; +} + +mnd_result_t +mnd_root_toggle_client_io_active(mnd_root_t *root, uint32_t client_id) +{ + CHECK_NOT_NULL(root); + CHECK_CLIENT_ID(client_id); + + xrt_result_t r = ipc_call_system_toggle_io_client(&root->ipc_c, client_id); + if (r != XRT_SUCCESS) { + PE("Failed to toggle io for client id: %u.\n", client_id); + return MND_ERROR_OPERATION_FAILED; + } + + return MND_SUCCESS; +} + +mnd_result_t +mnd_root_get_device_count(mnd_root_t *root, uint32_t *out_device_count) +{ + CHECK_NOT_NULL(root); + CHECK_NOT_NULL(out_device_count); + + *out_device_count = root->ipc_c.ism->isdev_count; + + return MND_SUCCESS; +} + +mnd_result_t +mnd_root_get_device_info(mnd_root_t *root, uint32_t device_index, uint32_t *out_device_id, const char **out_dev_name) +{ + CHECK_NOT_NULL(root); + CHECK_NOT_NULL(out_device_id); + CHECK_NOT_NULL(out_dev_name); + + if (device_index >= root->ipc_c.ism->isdev_count) { + PE("Invalid device index (%u)", device_index); + return MND_ERROR_INVALID_VALUE; + } + + const struct ipc_shared_device *shared_device = &root->ipc_c.ism->isdevs[device_index]; + *out_device_id = shared_device->name; + *out_dev_name = shared_device->str; + + return MND_SUCCESS; +} + +mnd_result_t +mnd_root_get_device_from_role(mnd_root_t *root, const char *role_name, int32_t *out_device_id) +{ + CHECK_NOT_NULL(root); + CHECK_NOT_NULL(role_name); + CHECK_NOT_NULL(out_device_id); + +#ifndef MND_PP_DEV_ID_FROM_ROLE +#define MND_PP_DEV_ID_FROM_ROLE(role) \ + if (strcmp(role_name, #role) == 0) { \ + *out_device_id = (int32_t)root->ipc_c.ism->roles.role; \ + return MND_SUCCESS; \ + } +#endif + MND_PP_DEV_ID_FROM_ROLE(head) + MND_PP_DEV_ID_FROM_ROLE(left) + MND_PP_DEV_ID_FROM_ROLE(right) + MND_PP_DEV_ID_FROM_ROLE(gamepad) + MND_PP_DEV_ID_FROM_ROLE(eyes) +#undef MND_PP_DEV_ID_FROM_ROLE + if (strcmp(role_name, "hand-tracking-left") == 0) { + *out_device_id = root->ipc_c.ism->roles.hand_tracking.left; + return MND_SUCCESS; + } + if (strcmp(role_name, "hand-tracking-right") == 0) { + *out_device_id = root->ipc_c.ism->roles.hand_tracking.right; + return MND_SUCCESS; + } + + PE("Invalid role name (%s)", role_name); + return MND_ERROR_INVALID_VALUE; +} diff --git a/src/xrt/targets/libmonado/monado.h b/src/xrt/targets/libmonado/monado.h new file mode 100644 index 000000000..782bb8f40 --- /dev/null +++ b/src/xrt/targets/libmonado/monado.h @@ -0,0 +1,247 @@ +// Copyright 2019-2023, Collabora, Ltd. +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Interface of libmonado + * @author Jakob Bornecrantz + * @author Ryan Pavlik + */ + +#include + + +#ifdef __cplusplus +extern "C" { +#endif + + +/* + * + * Enums, defines and objects. + * + */ + +//! Major version of the API. +#define MND_API_VERSION_MAJOR 1 +//! Minor version of the API. +#define MND_API_VERSION_MINOR 0 +//! Patch version of the API. +#define MND_API_VERSION_PATCH 0 + +/*! + * Result codes for operations, negative are errors, zero or positives are + * success. + */ +typedef enum mnd_result +{ + MND_SUCCESS = 0, + MND_ERROR_INVALID_VERSION = -1, + MND_ERROR_INVALID_VALUE = -2, + MND_ERROR_CONNECTING_FAILED = -3, + MND_ERROR_OPERATION_FAILED = -4, +} mnd_result_t; + +/*! + * Bitflags for client application state. + */ +typedef enum mnd_client_flags +{ + MND_CLIENT_PRIMARY_APP = (1u << 0u), + MND_CLIENT_SESSION_ACTIVE = (1u << 1u), + MND_CLIENT_SESSION_VISIBLE = (1u << 2u), + MND_CLIENT_SESSION_FOCUSED = (1u << 3u), + MND_CLIENT_SESSION_OVERLAY = (1u << 4u), + MND_CLIENT_IO_ACTIVE = (1u << 5u), +} mnd_client_flags_t; + +/*! + * Opaque type for libmonado state + */ +typedef struct mnd_root mnd_root_t; + + +/* + * + * Functions + * + */ + +/*! + * Returns the version of the API (not Monado itself), follows the versioning + * semantics of https://semver.org/ standard. In short if the major version + * mismatch then the interface is incompatible. + * + * @param[out] out_major Major version number, must be valid pointer. + * @param[out] out_minor Minor version number, must be valid pointer. + * @param[out] out_patch Patch version number, must be valid pointer. + * + * Always succeeds, or crashes if any pointer isn't valid. + */ +void +mnd_api_get_version(uint32_t *out_major, uint32_t *out_minor, uint32_t *out_patch); + +/*! + * Create libmonado state and connect to service + * + * @param[out] out_root Address to populate with the opaque state type. + * @return MND_SUCCESS on success + */ +mnd_result_t +mnd_root_create(mnd_root_t **out_root); + +/*! + * Destroy libmonado state, disconnecting from the service, and zeroing the + * pointer. + * + * @param root_ptr Pointer to your libmonado state. Null-checked, will be set to null. + */ +void +mnd_root_destroy(mnd_root_t **root_ptr); + +/*! + * Update our local cached copy of the client list + * + * @param root The libmonado state. + * @return MND_SUCCESS on success + */ +mnd_result_t +mnd_root_update_client_list(mnd_root_t *root); + +/*! + * Get the number of active clients + * + * This value only changes on calls to @ref mnd_root_update_client_list + * + * @param root The libmonado state. + * @param[out] out_num Pointer to value to populate with the number of clients. + * + * @pre Called @ref mnd_root_update_client_list at least once + * + * @return MND_SUCCESS on success + */ +mnd_result_t +mnd_root_get_number_clients(mnd_root_t *root, uint32_t *out_num); + +/*! + * Get the id from the current client list. + * + * @param root The libmonado state. + * @param index Index to retrieve id for. + * @param[out] out_client_id Pointer to value to populate with the id at the given index. + */ +mnd_result_t +mnd_root_get_client_id_at_index(mnd_root_t *root, uint32_t index, uint32_t *out_client_id); + +/*! + * Get the name of the client at the given index. + * + * The string returned is only valid until the next call into libmonado. + * + * @param root The libmonado state. + * @param client_id ID of client to retrieve name from. + * @param[out] out_name Pointer to populate with the client name. + * + * @pre Called @ref mnd_root_update_client_list at least once + * + * @return MND_SUCCESS on success + */ +mnd_result_t +mnd_root_get_client_name(mnd_root_t *root, uint32_t client_id, const char **out_name); + +/*! + * Get the state flags of the client at the given index. + * + * This result only changes on calls to @ref mnd_root_update_client_list + * + * @param root The libmonado state. + * @param client_id ID of client to retrieve flags from. + * @param[out] out_flags Pointer to populate with the flags, a bitwise combination of @ref mnd_client_flags. + * + * @pre Called @ref mnd_root_update_client_list at least once + * + * @return MND_SUCCESS on success + */ +mnd_result_t +mnd_root_get_client_state(mnd_root_t *root, uint32_t client_id, uint32_t *out_flags); + +/*! + * Set the client at the given index as "primary". + * + * @param root The libmonado state. + * @param client_id ID of the client set as primary. + * + * @pre Called @ref mnd_root_update_client_list at least once + * + * @return MND_SUCCESS on success + */ +mnd_result_t +mnd_root_set_client_primary(mnd_root_t *root, uint32_t client_id); + +/*! + * Set the client at the given index as "focused". + * + * @param root The libmonado state. + * @param client_id ID of the client set as focused. + * + * @pre Called @ref mnd_root_update_client_list at least once + * + * @return MND_SUCCESS on success + */ +mnd_result_t +mnd_root_set_client_focused(mnd_root_t *root, uint32_t client_id); + +/*! + * Toggle io activity for the client at the given index. + * + * @param root The libmonado state. + * @param client_id ID of the client to toggle IO for. + * + * @pre Called @ref mnd_root_update_client_list at least once + * + * @return MND_SUCCESS on success + */ +mnd_result_t +mnd_root_toggle_client_io_active(mnd_root_t *root, uint32_t client_id); + +/*! + * Get the number of devices + * + * @param root The libmonado state. + * @param[out] out_device_count Pointer to value to populate with the number of devices. + * + * @return MND_SUCCESS on success + */ +mnd_result_t +mnd_root_get_device_count(mnd_root_t *root, uint32_t *out_device_count); + +/*! + * Get device info at the given index. + * + * @param root The libmonado state. + * @param device_index Index of device to retrieve name from. + * @param[out] out_device_id Pointer to value to populate with the device id at the given index. + * @param[out] out_dev_name Pointer to populate with the device name. + * + * @return MND_SUCCESS on success + */ +mnd_result_t +mnd_root_get_device_info(mnd_root_t *root, uint32_t device_index, uint32_t *out_device_id, const char **out_dev_name); + +/*! + * Get the device index associated for a given role name. + * + * @param root The libmonado state. + * @param role_name Name of the role, one-of + * "head","left","right"."gamepad","eyes","hand-tracking-left","hand-tracking-right" + * @param[out] out_device_id Pointer to value to populate with the device id associated with given role name, -1 if not + * role is set. + * + * @return MND_SUCCESS on success + */ +mnd_result_t +mnd_root_get_device_from_role(mnd_root_t *root, const char *role_name, int32_t *out_device_id); + + +#ifdef __cplusplus +} +#endif diff --git a/src/xrt/targets/libmonado/monado.py b/src/xrt/targets/libmonado/monado.py new file mode 100644 index 000000000..8f882d6fa --- /dev/null +++ b/src/xrt/targets/libmonado/monado.py @@ -0,0 +1,211 @@ +# Copyright 2020-2023, Collabora, Ltd. +# SPDX-License-Identifier: BSL-1.0 +# Author: Jakob Bornecrantz +# Author: Lubosz Sarnecki +# Author: Korcan Hussein + +import os +from cffi import FFI +from pathlib import Path + + +def preprocess_ffi(lines: list[str]) -> str: + out_lines = [] + skipping = False + for line in lines: + if line.startswith("#ifdef"): + skipping = True + continue + elif line.startswith("#endif"): + skipping = False + continue + elif line.startswith("#include"): + continue + if skipping: + continue + out_lines.append(line) + + return "\n".join(out_lines) + + +class MonadoHeaderNotFoundError(Exception): + pass + + +def find_monado_header() -> Path: + search_paths = [ + # Try script directory first, in case running uninstalled + Path(__file__).parent / "monado.h", + # path from installed package + Path("/usr/include/monado/monado.h"), + # path from local install + Path("/usr/local/include/monado/monado.h") + ] + + # path from ENV + if os.getenv('MONADO_HEADER_PATH'): + search_paths.append(Path(os.getenv('MONADO_HEADER_PATH'))) + + for path in search_paths: + if path.is_file(): + return path + + # Give up + raise MonadoHeaderNotFoundError("Could not find monado.h. Define the path by setting MONADO_HEADER_PATH.") + + +def load_ffi() -> FFI: + ffi = FFI() + + header_path = find_monado_header() + with header_path.open("r") as f: + header_lines = f.readlines() + + header = preprocess_ffi(header_lines) + + ffi.cdef(header) + return ffi + + +class Device: + def __init__(self, ident, name): + self.ident = ident + self.name = name + + +class Client: + def __init__(self, ident, name, primary, focused, visible, active, overlay, io_active): + self.ident = ident + self.name = name + self.primary = primary + self.focused = focused + self.visible = visible + self.active = active + self.overlay = overlay + self.io_active = io_active + + +class MonadoLibraryNotFoundError(Exception): + pass + + +class Monado: + def __init__(self): + self.ffi = load_ffi() + try: + self.lib = self.ffi.dlopen("libmonado.so") + except OSError: + raise MonadoLibraryNotFoundError("Could not load libmonado.so.") + + self.root_ptr = self.ffi.new("mnd_root_t **") + + ret = self.lib.mnd_root_create(self.root_ptr) + if ret != 0: + raise Exception("Could not create root") + + self.root = self.root_ptr[0] + self.client_count = 0 + self.client_count_ptr = self.ffi.new("uint32_t *") + self.name_ptr = self.ffi.new("char **") + self.flags_ptr = self.ffi.new("uint32_t *") + self.client_id_ptr = self.ffi.new("uint32_t *") + self.device_id_ptr = self.ffi.new("uint32_t *") + self.device_name_ptr = self.ffi.new("char **") + self.device_count_ptr = self.ffi.new("uint32_t *") + + def update_clients(self): + ret = self.lib.mnd_root_update_client_list(self.root) + if ret != 0: + raise Exception("Could not update clients") + + ret = self.lib.mnd_root_get_number_clients(self.root, self.client_count_ptr) + if ret != 0: + raise Exception("Could not update clients") + + self.client_count = self.client_count_ptr[0] + + def get_client_id_at_index(self, index): + ret = self.lib.mnd_root_get_client_id_at_index(self.root, index, self.client_id_ptr) + if ret != 0: + raise Exception("Could not get client id at index") + + return self.client_id_ptr[0] + + def get_client_name(self, client_id): + ret = self.lib.mnd_root_get_client_name(self.root, client_id, self.name_ptr) + if ret != 0: + raise Exception("Could not get client name") + + return self.ffi.string(self.name_ptr[0]).decode("utf-8") + + def get_client_flags(self, client_id): + ret = self.lib.mnd_root_get_client_state(self.root, client_id, self.flags_ptr) + if ret != 0: + raise Exception("Could not get client state") + + return self.flags_ptr[0] + + def snapshot_client(self, index): + ident = self.get_client_id_at_index(index) + name = self.get_client_name(ident) + flags = self.get_client_flags(ident) + + primary = (flags & self.lib.MND_CLIENT_PRIMARY_APP) != 0 + active = (flags & self.lib.MND_CLIENT_SESSION_ACTIVE) != 0 + visible = (flags & self.lib.MND_CLIENT_SESSION_VISIBLE) != 0 + focused = (flags & self.lib.MND_CLIENT_SESSION_FOCUSED) != 0 + overlay = (flags & self.lib.MND_CLIENT_SESSION_OVERLAY) != 0 + io_active = (flags & self.lib.MND_CLIENT_IO_ACTIVE) != 0 + + return Client(ident, name, primary, focused, visible, active, overlay, io_active) + + def destroy(self): + self.lib.mnd_root_destroy(self.root_ptr) + self.root = self.root_ptr[0] + + def set_primary(self, client_id: int): + ret = self.lib.mnd_root_set_client_primary(self.root, client_id) + if ret != 0: + raise Exception(f"Failed to set primary client id to {client_id}.") + + def set_focused(self, client_id: int): + ret = self.lib.mnd_root_set_client_focused(self.root, client_id) + if ret != 0: + raise Exception(f"Failed to set focused client id to {client_id}.") + + def toggle_io(self, client_id: int): + ret = self.lib.mnd_root_toggle_client_io_active(self.root, client_id) + if ret != 0: + raise Exception(f"Failed to toggle io for client id {client_id}.") + + def get_device_count(self): + ret = self.lib.mnd_root_get_device_count(self.root, self.device_count_ptr) + if ret != 0: + raise Exception("Could not get device count") + return self.device_count_ptr[0] + + def get_device_at_index(self, index): + ret = self.lib.mnd_root_get_device_info(self.root, index, self.device_id_ptr, self.device_name_ptr) + if ret != 0: + raise Exception(f"Could not get device at index:{index}") + dev_id = self.device_id_ptr[0] + dev_name = self.ffi.string(self.device_name_ptr[0]).decode("utf-8") + return Device(dev_id, dev_name) + + def get_devices(self): + devices = [] + dev_count = self.get_device_count() + for i in range(dev_count): + devices.append(self.get_device_at_index(i)) + return devices + + def get_device_roles(self): + role_map = dict() + device_int_id_ptr = self.ffi.new("int32_t *") + for role_name in ["head", "left", "right", "gamepad", "eyes", "hand-tracking-left", "hand-tracking-right"]: + crole_name = role_name.encode('utf-8') + ret = self.lib.mnd_root_get_device_from_role(self.root, crole_name, device_int_id_ptr) + if ret != 0: + raise Exception(f"Could not get device role: {role_name}") + role_map[role_name] = device_int_id_ptr[0] + return role_map